首发于先知社区。
前言
WebAssembly的逆向工程在实际应用和CTF竞赛中愈发频繁。当前网络上的一些关于 Wasm 逆向的文章和教程往往显得过于繁琐或过时,因此本文旨在探讨 2024 年我们该如何更高效解决 Wasm 的逆向问题。
本文将对 Wasm 进行简要介绍,接着阐述几种常见的逆向方法及其优缺点,最后通过两道 2024 年的赛题进行实践演示。
关于wasm
WebAssembly(简称 Wasm)是一种新兴的低级字节码格式,旨在提升 Web 应用程序的性能和效率。Wasm 代码能够在现代浏览器中以接近原生的执行速度运行,此外,它还可以适用于服务器和嵌入式设备等多种环境。这种特性使得 Wasm 成为开发高性能 Web 应用的理想选择。
Wasm 的设计初衷是为了让开发者能够在不同平台上实现高效的代码执行,同时保持良好的跨平台特性。由于其低级特性,Wasm 也逐渐成为逆向工程师关注的焦点。
对于想要进行 Wasm 逆向工程的开发者和研究者,可以利用一些在线工具,比如https://mbebenita.github.io/WasmExplorer/ ,该网站允许用户进行简单的 Wasm 编译和优化,提供了一个直观的界面以便于快速实验和学习。
逆向wasm的几种方法
解包反编译wasm,配合ida
首先,可以使用 wasm2wat 工具将 .wasm 文件转换为文本格式的 .wat 文件。这一步骤有助于分析 Wasm 的结构和逻辑。
$ ./wasm2wat wasm.wasm -o wasm.wat
$ ./wasm2c wasm.wasm -o wasm.c
经过以上步骤后,将会生成 wasm.c 和 wasm.h 文件。这些文件可以与 IDA Pro 配合使用,进行更深入的静态分析。
虽然这种方法相对繁琐,但其优势在于利用了 IDA Pro 强大的分析能力,相比之下,Ghidra 的反编译效果往往不尽如人意。
ghidra插件一把梭
对于希望快速进行 Wasm 反编译的用户,可以使用 Ghidra 的 Wasm 插件,具体信息可参考 https://github.com/nneonneo/ghidra-wasm-plugin/
这种方法的优势在于其简单快捷,用户只需安装插件即可开始分析。然而,Ghidra 的反编译和操作体验有时会让人感到不够理想,尤其在面对复杂代码时。
IDA9.0
IDA 9.0 版本添加了对 Wasm 的反汇编支持,但目前尚未实现反编译功能。在处理 Wasm 文件时,可以考虑将 IDA 9.0 与 Ghidra 结合使用,以便更全面地解决逆向工程中的问题。
chrome 开发者工具 / VSCODE nodejs
Chrome 开发者工具和 VSCode 的 Node.js 插件提供了对 Wasm 的调试支持。结合 Ghidra 的插件,可以在 Chrome 中设置断点并进行调试。这种方法适合需要动态分析的场景,能够实时查看内存和调用栈的变化。
更多关于 Wasm 调试和内存查看的方法,可以参见参考文章:https://panda0s.top/2021/05/14/WebAssembly-Reverse/#%E8%B0%83%E8%AF%95%E6%96%B9%E6%B3%95
2024源鲁杯round2 wasm
解题分析
题目给了一个wasm文件,这里我们采用ghidra一把梭的方式。得到反编译的代码。
undefined4 unnamed_function_7(void)
{
int local_20 [4];
int local_10;
int local_c;
int local_8;
undefined4 local_4;
local_4 = 0;
local_8 = unnamed_function_14(s_GZCTF_FLAG_ram_00000429);
local_c = unnamed_function_20(local_8);
if ((local_c != 0) && (*(char *)(local_8 + local_c + -1) == '\n')) {
*(undefined *)(local_8 + local_c + -1) = 0;
}
for (local_10 = 0; *(char *)(local_8 + local_10) != '\0'; local_10 = local_10 + 1) {
if ((*(char *)(local_8 + local_10) < 'A') || ('Z' < *(char *)(local_8 + local_10))) {
if ((*(char *)(local_8 + local_10) < 'a') || ('z' < *(char *)(local_8 + local_10))) {
*(undefined *)(local_8 + local_10) = *(undefined *)(local_8 + local_10);
}
else {
*(char *)(local_8 + local_10) = *(char *)(local_8 + local_10) + -0x20;
*(char *)(local_8 + local_10) = (char)((*(char *)(local_8 + local_10) + -0x3a) % 0x1a) + 'A'
;
}
}
else {
*(char *)(local_8 + local_10) = *(char *)(local_8 + local_10) + ' ';
*(char *)(local_8 + local_10) = (char)((*(char *)(local_8 + local_10) + -0x5a) % 0x1a) + 'a ';
}
}
for (local_10 = 0; *(char *)(local_8 + local_10) != '\0'; local_10 = local_10 + 1) {
local_20[0] = (int)*(char *)(local_8 + local_10);
unnamed_function_15(0x43a,local_20);
}
return 0;
}
ghidra的反编译有些丑陋,我对他进行了一些优化。
undefined4 unnamed_function_7(void)
{
int local_20 [4];
int i;
int local_c;
int gzflag;
undefined4 local_4;
local_4 = 0;
gzflag = unnamed_function_14(s_GZCTF_FLAG_ram_00000429);
local_c = unnamed_function_20(gzflag);
if ((local_c != 0) && (*(char *)(gzflag + local_c + -1) == '\n')) {
*(undefined *)(gzflag + local_c + -1) = 0;
}
for (i = 0; *(char *)(gzflag[i]) != '\0'; i = i + 1) {
if ((*(char *)(gzflag[i]) < 'A') || ('Z' < *(char *)(gzflag[i]))) {
if ((*(char *)(gzflag[i]) < 'a') || ('z' < *(char *)(gzflag[i]))) {
*(undefined *)(gzflag[i]) = *(undefined *)(gzflag[i]);
}
else {
*(char *)(gzflag[i]) = *(char *)(gzflag[i]) + -0x20;
*(char *)(gzflag[i]) = (char)((*(char *)(gzflag[i]) + -0x3a) % 0x1a) + 'A'
;
}
}
else {
*(char *)(gzflag[i]) = *(char *)(gzflag[i]) + ' ';
*(char *)(gzflag[i]) = (char)((*(char *)(gzflag[i]) + -0x5a) % 0x1a) + 'a ';
}
}
for (i = 0; *(char *)(gzflag[i]) != '\0'; i = i + 1) {
local_20[0] = (int)*(char *)(gzflag[i]);
unnamed_function_15(0x43a,local_20);
}
return 0;
}
观察发现这是一个简单的类似于凯撒的加密,不过也不是一模一样,总之guess一下写出脚本就好了。
exp
def decrypt_flag(gzflag):
decrypted = []
for char in gzflag:
if 'A' <= chr(char) <= 'Z': # 大写字母
char = char - ord('A')
char = char - 0x20
char = (char - ord('A') + 0x5a) % 26 + ord('a') # 模运算
elif 'a' <= chr(char) <= 'z': # 小写字母
char = char - ord('a')
char = char + 0x20 # 加上0x20
char = (char - ord('a') + 0x3a) % 26 + ord('A') # 模运算
decrypted.append(chr(char))
return ''.join(decrypted)
# 测试解密函数
gzflag = [0x66, 0x73, 0x6a, 0x61, 0x6d, 0x7b, 0x48, 0x31, 0x30, 0x49, 0x38, 0x33,
0x30, 0x37, 0x2d, 0x36, 0x35, 0x4c, 0x4b, 0x2d, 0x34, 0x4c, 0x35, 0x36,
0x2d, 0x49, 0x30, 0x37, 0x39, 0x2d, 0x30, 0x31, 0x32, 0x37, 0x38, 0x4b,
0x4d, 0x49, 0x30, 0x32, 0x31, 0x4b, 0x7d]
decrypted_flag = decrypt_flag(gzflag)
print("Decrypted Flag:", decrypted_flag)
for i in gzflag:
print(chr(i),end="")
2024 N1junior wasm
解题分析
这是当时的一道0解题,现在来看并不是很难,同样的使用ghidra来解决。
我们找到关键的 main.silence函数进行分析。
undefined4 main.slice(int param1,undefined8 param_2,undefined8 param_3,undefined param_4)
{
undefined8 *puVar1;
undefined uVar2;
int iVar3;
ulonglong uVar4;
undefined8 uVar5;
uVar4 = 0;
uVar5 = 0;
code_r0x800ee613:
do {
while ((uVar2 = (undefined)uVar4, param1 != 0 && (param1 != 1))) {
if (param1 == 2) goto code_r0x800ee6bc;
if (param1 != 0x32) {
if (param1 == 0x33) {
uVar2 = 0;
}
else if (param1 != 0x34) {
if (param1 == 0x35) {
*(undefined *)((int)register0x00000008 + 0x38) = 0;
return 0;
}
do {
halt_trap();
} while( true );
}
*(undefined *)((int)register0x00000008 + 0x38) = uVar2;
return 0;
}
code_r0x800eeaba:
uVar4 = (ulonglong)(*(char *)((int)uVar5 + 0x2f) == '9');
param1 = 0x34;
}
puVar1 = (undefined8 *)register0x00000008;
if (register0x00000008 <= *(undefined8 **)((int)global_2 + 0x10)) {
*(undefined8 *)((int)register0x00000008 + -8) = 0x14170000;
iVar3 = runtime.morestack_noctxt(0);
puVar1 = (undefined8 *)((int)register0x00000008 + -8);
if (iVar3 != 0) {
return 1;
}
}
puVar1[-4] = puVar1[1];
puVar1[-3] = puVar1[2];
register0x00000008 = (BADSPACEBASE *)(puVar1 + -5);
*(undefined8 *)register0x00000008 = 0x14170002;
iVar3 = main.exit(0,puVar1[-4],puVar1[-3],puVar1[10],puVar1[0xb]);
if (iVar3 != 0) {
return 1;
}
code_r0x800ee6bc:
uVar4 = *(ulonglong *)((int)register0x00000008 + 0x18);
uVar5 = *(undefined8 *)((int)register0x00000008 + 0x10);
if (uVar4 != 0x30) {
param1 = 0x35;
goto code_r0x800ee613;
}
code_r0x800ee6de:
if (*(char *)uVar5 == 'Y') {
code_r0x800ee6f3:
if (*(char *)((int)uVar5 + 1) == '3') {
code_r0x800ee707:
if (*(char *)((int)uVar5 + 2) == 'R') {
code_r0x800ee71c:
if (*(char *)((int)uVar5 + 3) == 'm') {
code_r0x800ee731:
if (*(char *)((int)uVar5 + 4) == 'c') {
code_r0x800ee746:
if (*(char *)((int)uVar5 + 5) == 'H') {
code_r0x800ee75b:
if (*(char *)((int)uVar5 + 6) == 'V') {
code_r0x800ee770:
if (*(char *)((int)uVar5 + 7) == 'u') {
code_r0x800ee785:
if (*(char *)((int)uVar5 + 8) == 'a') {
code_r0x800ee79a:
if (*(char *)((int)uVar5 + 9) == '3') {
code_r0x800ee7ae:
if (*(char *)((int)uVar5 + 10) == 't') {
code_r0x800ee7c3:
if (*(char *)((int)uVar5 + 0xb) == 'X') {
code_r0x800ee7d8:
if (*(char *)((int)uVar5 + 0xc) == 'Q') {
code_r0x800ee7ed:
if (*(char *)((int)uVar5 + 0xd) == 'V') {
code_r0x800ee802:
if (*(char *)((int)uVar5 + 0xe) == 'N') {
code_r0x800ee817:
uVar4 = (ulonglong)*(byte *)((int)uVar5 + 0xf);
if (uVar4 == 0x4e) {
code_r0x800ee82e:
uVar4 = (ulonglong)*(byte *)((int)uVar5 + 0x10);
if (uVar4 == 0x58) {
code_r0x800ee845:
if (*(char *)((int)uVar5 + 0x11) == 'z') {
code_r0x800ee85a:
uVar4 = (ulonglong)*(byte *)((int)uVar5 + 0x12);
if (uVar4 == 0x59) {
code_r0x800ee871:
if (*(char *)((int)uVar5 + 0x13) == '2') {
code_r0x800ee885:
if (*(char *)((int)uVar5 + 0x14) == 'N') {
code_r0x800ee89a:
if (*(char *)((int)uVar5 + 0x15) == 'j') {
code_r0x800ee8af:
if (*(char *)((int)uVar5 + 0x16) == 'Y') {
code_r0x800ee8c4:
uVar4 = (ulonglong)*(byte *)((int)uVar5 + 0x17);
if (uVar4 == 0x32) {
code_r0x800ee8da:
if (*(char *)((int)uVar5 + 0x18) == 'N') {
code_r0x800ee8ef:
uVar4 = (ulonglong)
*(byte *)((int)uVar5 + 0x19);
if (uVar4 == 0x6a) {
code_r0x800ee906:
if (*(char *)((int)uVar5 + 0x1a) == 'Z') {
code_r0x800ee91b:
if (*(char *)((int)uVar5 + 0x1b) == 'f') {
code_r0x800ee930:
if (*(char *)((int)uVar5 + 0x1c) == 'R' )
{
code_r0x800ee945:
if (*(char *)((int)uVar5 + 0x1d) ==
'0') {
code_r0x800ee959:
if (*(char *)((int)uVar5 + 0x1e) ==
'9') {
code_r0x800ee96d:
if (*(char *)((int)uVar5 + 0x1f)
== 'B') {
code_r0x800ee982:
if (*(char *)((int)uVar5 + 0x20 )
== 'T') {
code_r0x800ee997:
if (*(char *)((int)uVar5 +
0x21) == 'E') {
code_r0x800ee9ac:
if (*(char *)((int)uVar5 +
0x22) == '5') {
code_r0x800ee9c0:
if (*(char *)((int)uVar5 +
0x23) == 'H' )
{
code_r0x800ee9d5:
if (*(char *)((int)uVar 5
+ 0x24) ==
'X') {
code_r0x800ee9ea:
if (*(char *)((int)
uVar5 + 0x25) == 'z') {
code_r0x800ee9ff:
if (*(char *)((int)uVar5 + 0x26) == 'Y') {
code_r0x800eea14:
if (*(char *)((int)uVar5 + 0x27) == '2') {
code_r0x800eea28:
if (*(char *)((int)uVar5 + 0x28) == 'N') {
code_r0x800eea3d:
if (*(char *)((int)uVar5 + 0x29) == 'j') {
code_r0x800eea52:
if (*(char *)((int)uVar5 + 0x2a) == 'Y' )
{
code_r0x800eea67:
if (*(char *)((int)uVar5 + 0x2b) ==
'2') {
code_r0x800eea7b:
if (*(char *)((int)uVar5 + 0x2c) ==
'N') {
code_r0x800eea90:
if (*(char *)((int)uVar5 + 0x2d)
== 'j') {
code_r0x800eeaa5:
if (*(char *)((int)uVar5 + 0x2e )
== 'Z')
goto code_r0x800eeaba;
param1 = 0x33;
}
// ....此处省略N行
} while( true );
}
我们发现他使用了一个嵌套判断来对flag进行判断,我们将它提取出来可以发现这很像一个base编码后的形式,于是尝试一下base64,成功!
很简单的一道题,但很多人因为畏惧wasm所以都没有去解这道题。同时另一个原因是网上绝大多数的教程都没有提到鸡爪插件来解决wasm。绝大多数都是wat工具然后在配合ida,流程过于繁琐。
exp
Y3RmcHVua3tXQVNNXzY2NjY2NjZfR09BTE5HXzY2NjY2NjZ9
base64解码
ctfpunk{WASM_6666666_GOALNG_6666666}
总结
WebAssembly的逆向越来越常见,本文通过介绍几种逆向方法,如使用wasm2wat和ida、Ghidra插件以及Chrome开发者工具,结合两道赛题实例,展示了如何有效解决Wasm逆向问题。
其中,Ghidra插件尤为显著地降低了Wasm逆向的门槛。
希望这篇文章能帮助到各位师傅更好的解决往后的wasm题目。