v1tCTF - cow clicker
0x01
页面要求点击1000000000次, 通过控制台查看,使用了flutter。 搜索核心字符串,发现在main.dart.wasm里 
尝试使用wasm cheat engine, 但是没有找到能用的(
下载main.dart.wasm,然后用ghidra wasm plugin或者wasm2wat,但都无法打开。
赛后有大佬解释了原因:
1 Yeah https://github.com/WebAssembly/wabt/issues/2348, basically use https://github.com/bytecodealliance/wasm-tools instead
接下来需要进一步分析wasm文件。
能否直接找flag相关逻辑? 如果最后的flag是向外部请求,能否直接搜索http关键字?(失败,而且最后可能是本地解密)
但是顺着这个思路,搜索关键词1000000000,大约有20个匹配的结果,但是大部分都是f64类型。重点关注其中的i64类型,找到了比较跳转逻辑: 
local.get $var2
struct.get $type281 $field7
i64.const 1000000000
i64.lt_s
if
local.get $var3
global.get $global13338
i32.const 333
i64.const 1000000000
local.get $var2
struct.get $type281 $field7
i64.sub
struct.new $type7
global.get $global13339
call $func727
ref.null none
global.get $global13340
ref.null none
call $func7022
call $func963
drop
end
local.get $var2
struct.get $type281 $field7
i64.const 1000000000
i64.ge_s
if
......
end
可以猜到click counter存在于 local $var2.$field7 其中struct.get $type281 $field7表示 将栈上的类型强转为$type281,然后取出某个值
可以猜测第一个 counter < x的逻辑用来显示下面“还剩几次”,而第二个couter >= x用来进一步的逻辑
尝试直接用chrome dev tools来修改值,但是似乎不会影响wasm运行时栈上的内容(可能是安全措施,禁止修改?但理论上应该能绕过)
既然无法直接用控制台改变已经固定的wasm相关逻辑,那就直接从源头修改wasm
0x02 使用Burpsuite拦截替换wasm response
最容易想到的是直接把第二个判断i64.ge_s改成i64.lt_s,
在Burpsuite尝试抓包的时候,需要清除前面的缓存,否则某些资源可能会从本地拿取。但是浏览器缓存也有好处,当自己完成一次缓存投毒后,接下来的访问就不用每次都进行修改,即可自动使用修改后的wasm
然而只是出现了一个flag checker:
输入失败会显示“doesn’t seem correct” 直接复制dev tool解析的wasm代码,然后全文搜索关键词:
1
2
3
4
5
6
......
(global $S.Good_job!_You_got_the_flag!_Mo0o0oo0oo! (;4082;) (import "S" "Good job! You got the flag! Mo0o0oo0oo!") externref)
(global $S.Nah__doesn't_seem_correct. (;4083;) (import "S" "Nah, doesn't seem correct.") externref)
......
(global $global13580 (ref $type560) (i32.const 1536) (i32.const 0) (ref.null none) (i32.const 1509) (i32.const 0) (ref.null none) (i32.const 4) (i32.const 0) (global.get $S.Good_job!_You_got_the_flag!_Mo0o0oo0oo!) (struct.new $type2) (ref.null none) (ref.null none) (ref.null none) (ref.null none) (struct.new $type559) (ref.null none) (struct.new $type560))
(global $global13581 (ref $type560) (i32.const 1536) (i32.const 0) (ref.null none) (i32.const 1509) (i32.const 0) (ref.null none) (i32.const 4) (i32.const 0) (global.get $S.Nah__doesn't_seem_correct.) (struct.new $type2) (ref.null none) (ref.null none) (ref.null none) (ref.null none) (struct.new $type559) (ref.null none) (struct.new $type560))
上面的代码应该是定义了一些常量,下面用常量来初始化一些object (可能是窗口函数的资源加载)
接着搜索 $global13580和$global13581:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
(func $func8329 (param $var0 (ref struct)) (result (ref null $type0))
(local $var1 (ref $type281))
(local $var2 (ref $type1595))
(local $var3 (ref $type2))
(local $var4 (ref $type2))
local.get $var0
ref.cast $type1595
local.tee $var2
struct.get $type1595 $field0
local.tee $var1
struct.get $type281 $field14
local.get $var2
struct.get $type1595 $field1
ref.as_non_null
call $func1520
call $strcat_func743
local.get $var1
struct.get $type281 $field15
call $strcat_func743
local.set $var3
block $label1 (result i32)
block $label0
local.get $var1
struct.get $type281 $field13
call $func4803
local.tee $var4
struct.get $type2 $field0
i32.const 4
i32.ne
br_if $label0
local.get $var3
struct.get $type2 $field2
local.get $var4
struct.get $type2 $field2
call $wasm:js-string.equals
i32.eqz
br_if $label0
i32.const 1
br $label1
end $label0
i32.const 0
end $label1
if (result (ref $type561))
local.get $var1
call $func4099
call $func8330
global.get $global13580 // object initialized with "good job"
call $func8332
else
local.get $var1
call $func4099
call $func8330
global.get $global13581 // object initialized with "doesn't correct"
call $func8332
end
wasm中的if会根据栈上的值来判断,通常由上一步来设置。这里显然有call $wasm:js-string.equals 调试后发现这里的数据v1t{xxxx},中间是乱码,猜测可能是数据解密和counter有关,验证发现确实如此.
TODO
0x03 二次patch
接下来尝试修改数据。 尝试寻找local $var2的源头,但是一层层向上追踪,实在不容易找到。 能不能直接修改var2来实现对counter的修改呢?毕竟为了效率,对于object的参数传递一般都是引用,可以一试。
最适合的地方就是i64.ge_s前面的一段代码,是struct.get,可以尝试置换成struct.set
把
1
2
3
4
5
6
7
local.get $var2
struct.get $type281 $field7
i64.const64 1000000000
i64.ge_s
if
...
end
修改成
1
2
3
4
5
6
7
8
local.get $var2
i64.const64 1000000000
struct.set $type281 $field7
nop
nop
nop(忘了几个nop了,反正就就是把if给覆盖掉)
...
nop
以前没怎么接触过wasm字节码,但是可以模仿其它的指令字节码,比如把 struct.get变成struct.set只需要修改指令的第二个字节。
另外发现wasm中的if指令没有指定跳转的目标地址,原来是使用if - end配对机制,所以需要把后面的end对应0x0B字节也给patch掉

