HackPack 2023 WasmSafe
- ctf
HackPack 2023 WasmSafe
一道 wasm 题,还是太年轻~
工具
网上流传比较多的方式是使用 WABT 转义成 C 然后链接成 .O 文件用 IDA 来分析。一开始就是用这个思路去做,IDA 分析的结果复杂度还是有点高,可以说是几乎没有可读性。参考外国友人的 WP ,还得是 Ghidra 对小众语言的支持好。
下面是 WASM Ghidra 插件。
Debug Chroium 内核的浏览器都可以,支持 WASM 比较完善。
Wasm Init
目前来说 wasm 都是作为一个模块挂载到环境里然后通过 js 来调用 wasm 中的导出函数。这里即是如此,windows.verify_flag = verify_flag
将函数挂载到全局中。那么无疑 verify_flag 就是逆向的关键。
Verify Logic
主要的验证逻辑都在 check 函数里,result 为 verify_flag 的返回值,而 verify_flag 实际上是对 wasm 内部函数的一个封装。
result 是一个 3 bit 的数据,当 result = 0b111 的时候会提示 Access granted。并且在每一部分验证成功的时候都会提示 Click On N 。那么接下来的分析就要进入 wasm 层。
VerifyFlag
使用 Ghidra 配合插件打开后在 export function 就可以看到 verify_flag 了。我们能够看到 if(Result&1)!=0
然后对返回值进行异或的代码,只要满足三个函数返回 1
就可以得到正确的结果。同时作者在这里留了类似 hint 的东西,但当时没有搞明白这是作者的善良,其实 verify_part_one/two/three 就告诉了这三个函数都是单独的 check 逻辑,不用太在乎这里的其他代码。
Part1
我们直接先定位到返回值的部分,函数的返回值由一个 value & 1
得到
从而可以确定第一部分的关键在这,当 local_3c
与 iRam00100010
相等时有可能有机会改变 local_55
的要想(local_55 ^ 0xff )&1 ==0
成立,那么需要 local_55 ^ 0xff == 0
,至少 local_55
末位为 1。local_55的值又由 iVar1 == 0
决定,回看循环体,iVar1 为两个变量的差值,当两个变量不等时则此值不为 1 。那么其实目的很明确了,我们只要找到对比的值就可以得到正确的 Part1。不过想要进入这个逻辑,需要确定 local_3c 的值,通过调试我们可以知道 IRam00100010 的值为 4 , S3Cret -> 6 , P4ssw0rd ->8 , 0Pen -> 4 。所以选择第三的是正确的。
WASM 的特性就是纯堆栈虚拟机,它每个函数获取变量的方式除了全局变量就只有通过寄存器来求偏移拿值,我们在对比的地方下断点。可以看到对比的值是 48(0x30,0)
和87(0x57,W)
, 在这之后可以直接通过寄存器值找到整个对比的字符串,当然也可以每次手动改动一点点来分布获取,WASM 还不能直接修改内存,如果可以的话,其实每次 Patch 一下就得到了。
于是我们得到 flag Part1 W4sm,实际上 html 没有给这个值,我们手动改一下 html 就可以验证了。
Part2
verify_flag_part_two 的主要逻辑都在 function_19 里,我们直接看 19 就行了。
先看第一个判断,最外层的 if-else
一般来说就是判断长度,这里我们可以构造几个输入测试一下。
和猜想一致,那么输入的长度 18 是确定的,我们构造一个 18 字长的字符串作为输入,“ABCDEFGHIJKLMNOPQR”
接着继续从结果出发,可以看到两条路径,分别是 1d7f 和 1d87。很明显我们要找到直接跳转到 1d87 的路子。
整个逻辑是一个 do while{ while True if-condition-break } 的结构,我尝试在 153 行对应的位置下断点来避免走到错误的循环,但这样实际上 do-while 就直接结束了,于是在 161 行对应的位置下断。这样就可以看到输入和 Check Value 进行对比了,接着我们一步一步替换 Input 来观察是否是变动的值以及其他检验。通过两次检查后,其实可以确定了这个值是不会变的。
既然如此,我们不妨观察一下这个值是怎么算的。可以很明显的看到这是数组值 ^0x79 得到,我们大胆的直接拿数组来异或试试看,没想到还真是。
不过问题在于第二个字符串,正常的结果应该是 ’s’ ,而 0x16 ^ 0x79 的结果并不是,看到之前有过异或 0x65 的操作,这里我直接大胆猜测这是根据奇偶进行判别的,当然这也有理可依。
答案很显然符合预期,得到 Part2 isamagicalb0xth4ts
我们可以看到代码里这里有个 if-break 的操作,local_c 承担了 index 的功能,x & 1
是一个判别奇偶的常见用房,当为偶数内部的 while True 就不再判别,否则交内部判别。
Part3
Part 3 与返回值直接相关的就在他的附近。Part3 的输入是一个整数,那么这里我们只要确定 local_4 是怎么来的就可以了。
通过调试可以直接确定这是输入的数值。
那么只要计算 0x100b/3-0x1f
就可以得到 Part3,得到 Part3 = 1338。
最后将 flag 拼接起来就是
flag{W4sm_isamagicalb0xth4ts_1338}
总结
这题总共 7 解,很遗憾自己没有成为比赛中的第 8 解,可能就是需要再多一点点的坚持吧,做了一半就去上班摆烂了。在复现的过程中时候,知道用 Ghidra 后实际上自己是完全独立的做出这道题目了,完全不需要看 Wp,怎么说呢,有时候多上 Github 找找工具少走很多弯路。