前言

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 的结构和逻辑。

1
2
$ ./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一把梭的方式。得到反编译的代码。

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
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的反编译有些丑陋,我对他进行了一些优化。

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
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

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
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函数进行分析。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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题目。