HackPack 2023 WasmSafe

- ctf

HackPack 2023 WasmSafe

一道 wasm 题,还是太年轻~

reference:Reversing WebAssembly — Write up Hackpack CTF 2023 WASM-safe | by Maulvi Alfansuri | Apr, 2023 | Medium

工具

网上流传比较多的方式是使用 WABT 转义成 C 然后链接成 .O 文件用 IDA 来分析。一开始就是用这个思路去做,IDA 分析的结果复杂度还是有点高,可以说是几乎没有可读性。参考外国友人的 WP ,还得是 Ghidra 对小众语言的支持好。

下面是 WASM Ghidra 插件。

nneonneo/ghidra-wasm-plugin: Ghidra Wasm plugin with disassembly and decompilation support (github.com)

Debug Chroium 内核的浏览器都可以,支持 WASM 比较完善。

Wasm Init

目前来说 wasm 都是作为一个模块挂载到环境里然后通过 js 来调用 wasm 中的导出函数。这里即是如此,windows.verify_flag = verify_flag 将函数挂载到全局中。那么无疑 verify_flag 就是逆向的关键。

image-20230428001233279

image-20230428001354805

Verify Logic

主要的验证逻辑都在 check 函数里,result 为 verify_flag 的返回值,而 verify_flag 实际上是对 wasm 内部函数的一个封装。

image-20230428001651125

image-20230428001754484

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 逻辑,不用太在乎这里的其他代码。

image-20230428002339148

Part1

我们直接先定位到返回值的部分,函数的返回值由一个 value & 1 得到

image-20230428004154048

从而可以确定第一部分的关键在这,当 local_3ciRam00100010 相等时有可能有机会改变 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 。所以选择第三的是正确的。

image-20230428004252990

image-20230428005035601

WASM 的特性就是纯堆栈虚拟机,它每个函数获取变量的方式除了全局变量就只有通过寄存器来求偏移拿值,我们在对比的地方下断点。可以看到对比的值是 48(0x30,0)87(0x57,W), 在这之后可以直接通过寄存器值找到整个对比的字符串,当然也可以每次手动改动一点点来分布获取,WASM 还不能直接修改内存,如果可以的话,其实每次 Patch 一下就得到了。

image-20230428005702195

于是我们得到 flag Part1 W4sm,实际上 html 没有给这个值,我们手动改一下 html 就可以验证了。

image-20230428010125949

Part2

verify_flag_part_two 的主要逻辑都在 function_19 里,我们直接看 19 就行了。

先看第一个判断,最外层的 if-else 一般来说就是判断长度,这里我们可以构造几个输入测试一下。

image-20230428011724487

和猜想一致,那么输入的长度 18 是确定的,我们构造一个 18 字长的字符串作为输入,“ABCDEFGHIJKLMNOPQR”

image-20230428011808788

接着继续从结果出发,可以看到两条路径,分别是 1d7f 和 1d87。很明显我们要找到直接跳转到 1d87 的路子。

image-20230428012203325

整个逻辑是一个 do while{ while True if-condition-break } 的结构,我尝试在 153 行对应的位置下断点来避免走到错误的循环,但这样实际上 do-while 就直接结束了,于是在 161 行对应的位置下断。这样就可以看到输入和 Check Value 进行对比了,接着我们一步一步替换 Input 来观察是否是变动的值以及其他检验。通过两次检查后,其实可以确定了这个值是不会变的。

既然如此,我们不妨观察一下这个值是怎么算的。可以很明显的看到这是数组值 ^0x79 得到,我们大胆的直接拿数组来异或试试看,没想到还真是。

image-20230428013831716

不过问题在于第二个字符串,正常的结果应该是 ’s’ ,而 0x16 ^ 0x79 的结果并不是,看到之前有过异或 0x65 的操作,这里我直接大胆猜测这是根据奇偶进行判别的,当然这也有理可依。

image-20230428014805701

答案很显然符合预期,得到 Part2 isamagicalb0xth4ts

image-20230428014919719

我们可以看到代码里这里有个 if-break 的操作,local_c 承担了 index 的功能,x & 1 是一个判别奇偶的常见用房,当为偶数内部的 while True 就不再判别,否则交内部判别。

image-20230428015052052

Part3

Part 3 与返回值直接相关的就在他的附近。Part3 的输入是一个整数,那么这里我们只要确定 local_4 是怎么来的就可以了。

image-20230428015430454

通过调试可以直接确定这是输入的数值。

image-20230428015746478

那么只要计算 0x100b/3-0x1f 就可以得到 Part3,得到 Part3 = 1338。

image-20230428015839685

最后将 flag 拼接起来就是

flag{W4sm_isamagicalb0xth4ts_1338}

image-20230428020010620

总结

这题总共 7 解,很遗憾自己没有成为比赛中的第 8 解,可能就是需要再多一点点的坚持吧,做了一半就去上班摆烂了。在复现的过程中时候,知道用 Ghidra 后实际上自己是完全独立的做出这道题目了,完全不需要看 Wp,怎么说呢,有时候多上 Github 找找工具少走很多弯路。