在逆向工程领域,IDA Pro 是一款广受赞誉的反汇编和调试工具,它支持多种主流的指令集,为开发者和安全研究人员提供了强大的分析能力。然而,一些特殊的指令集,如 VMP(Virtual Machine Protection)指令集,可能并不在 IDA 的支持列表中。目前,随着攻防对抗技术的发展,许多自定义的 VM 指令集被开发并应用在真实项目,迫切需要一种方法,扩展 IDA 的反编译器。本文目的是介绍一种让 IDA 反编译支持新指令集的简单方法,能够一定程度上缓解新指令集无反编译器的困境。该方法是并非笔者原创,而是笔者对许多前辈的文章和材料做出的一个总结,起到一个抛砖引玉的作用,旨在帮助想在 IDA 中实现未知指令集反编译的朋友。⚠️ 本文方法限定的指令集范围是一种 IDA 未知的指令集,读者应当具备该指令集的完整知识,至于如何获取该指令集的完整知识(对于 VM 来说就是 VM 指令集的逆向)的过程不在本文的讨论范围!IDA 处理器模块 与 Ghidra 插件
IDA Pro 虽然支持用户开发特定架构的处理器模块,却不能使用其反编译功能,并且编写特定架构的处理器模块过程复杂,参考资料少。Ghidra 是一款优秀的开源反编译器,用户通过插件的形式添加新指令的反编译器,例如有一个插件实现了 Ghidra 中反编译 WASM 模块[1],然而,编写 Ghidra 插件仍然是一项复杂的工作,参考资料少,并且不在本文限定的 “IDA” 范围。wasm 反编译
WebAssembly (WASM)是一种最近比较流行的底层指令集,主要运行在浏览器环境,也有部分运行在终端或嵌入式设备。一般,开发者使用编译型语言,例如 C/C++、Rust、Go 开发 WASM 上层程序,再使用 LLVM 将其编译为 WASM 模块。因此,WASM 的 IDA 反编译方法是一个很有代表性的方法。IDA Pro 8.4 仍然没有加入对 WASM 的反编译支持,然而有资料表明,IDA Pro 能够反编译 WASM 模块[2][3][2][3]。目前让 IDA Pro 支持 WASM 反编译的方法是使用 wasm2c[4][4]程序,将 WASM 模块转换成等价的 C 语言低级表示形式,再使用 GCC/CLANG 编译该代码,最后使用 IDA 分析最终产物。为什么不直接阅读 wasm2c 的结果呢?wasm2c 确实能够将 wasm 模块转换成等价的 C 语言形式,然而这种形式是其 wasm 汇编的等价表示形式,可读性极差。因此,需要使用 C 语言编译器将其优化再编译成一种 IDA 能够识别的指令集,最后使用 IDA 逆向分析最终产物。上图是一个 wasm 模块中 strlen 函数的反编译结果,strlen 所需的循环结构清晰可见,这种方法确实实现了让 IDA 反编译一种未知指令集的效果。然而,美中不足的是,wasm2c 并没有将内存访问 load 和 store 以一种原生 C 语言形式表示,这导致内存访问在 IDA 反编译结果中以函数形式呈现,这可能是由于 wasm2c 需要保证转换结果的正确性,然而,对于反编译来说,可以采用更加激进的内存访问行为,这将有利于提升 wasm IDA 反编译效果。笔者受到已有方法启发后,认为为了给新指令集快速实现一个 IDA 中的反编译器,可以先将未知指令集转换成一种 IDA 能够识别的指令集,例如 x86 或 ARM。然而直接转换成 x86 或 ARM 仍然有太多细节处理,因此,可以采取先将指令集,以函数单位,转换成等价的 C 语言表示形式的方法来完成未知指令集向已知指令集的转换。具体举例来说,如下图所示的例子,转换器输入未知指令集的汇编指令,将其转换成等价 C 语言低级层形式。对于寄存器,可以映射成 C 语言中的局部变量或全局变量,对于内存模型可以映射成 C 语言中的 memory 数组访问或指针访问,对于赋值指令、运算指令可以简单转换成 C 表达式,对于内存访问指令,可以转换成 C 语言的数组访问或指针访问的形式,对于标志寄存器,可以定义一组相关的宏来根据结果修改标志寄存器(C 编译器的优化功能会将未使用的标志寄存器相关的代码作为 dead code 删除!)。具体如何映射,还需要根据特定的指令集来设计,在编写转换器的时候,尽量保证转换的正确性,剩下的反编译效果,则交给 C 编译优化和 IDA 内部的反编译优化器来解决。笔者选择了 4 个比较有代表性的 VM 指令集例子,另外 wasm 的 IDA 逆向方法本身也是一个很好的例子,因为 wasm 的 IDA 反编译方法代表了 real world 大型复杂指令集。而笔者选择的指令集更加偏向于比赛场景,与 real world 中的指令集有一定程度差距,但是适合快速上手实践,同时笔者也希望读者能够在更多 real world 指令集上尝试这种方法。例子 1 直接转换成 x64 汇编
这个例子的指令集来自 QWB S5 vmnote ,笔者将该 VM 指令集直接转换成 x64 汇编,使用 IDA 反编译转换结果,最后成功发现程序中的漏洞。该VM指令集除了基础指令以外,还有一些特殊指令,例如getchar
从标准输入读取一个字符,putchar
向标准输出输出一个字符,putInt
向标准输出输出一个整数,这些指令都是 x64 指令集没有的。笔者的主要思想是将这些 x64 中不存在的功能性指令,替换成 IDA 能够正常识别且处理数据流的指令集。在反编译结果中,可以看到对内存 0xbbccdd 的访问,从而看出来 getchar 的值赋值给了哪个变量或者表达式。另外,还需要对调用约定进行一些转换,使 IDA 能够以 x64 的调用阅读处理函数调用。具体来说,vm 指令的寄存器经过映射后,使用 rdi 寄存器来传递返回值,然而,x64 调用一般使用 rax 传递返回值。只需要添加一条简单的 mov 指令即可完成转换。func:
_0x21: leave
_0x22: mov rax, rdi
ret_0x23: call func
mov rdi, rax
最后,在 IDA 中查看效果如下图,笔者将 getchar 指令映射成对 0xbbccdd 的内存访问,正如预期效果一样,IDA 正确分析出了 getchar 指令的数据流向,有了 vm 反编译器,我们团队迅速发现了 vm 程序内部的漏洞。(以下截图均为 VM 未知指令集 IDA 反编译)笔者实现该转换器的代码大概用时 3 小时,代码行数仅有三百行左右,并且代码都是重复片段,因此,这种转换方法确实能够在短时间内使 IDA 反编译未知指令集。具体的实践代码见https://github.com/P4nda0s/qwb_vmnote_recompiler例子 2 栈机反编译
这个实践例子来源于祥云杯 machine,这个例子独特在于,该 vm 是一个栈机(类似于 Java),栈机没有寄存器的概念,然而转换成 C 后再编译成二进制用 IDA 逆向,效果仍然不错。笔者直接将该例子的 vm 未知指令集转换成 C 语言等价形式。对于 machine vm 的逆向工程内容不再赘述,有能力的读者可以自行尝试逆向。由于篇幅有限,笔者仅展示部分指令的转换过程。对于栈机,在转换过程中需要维护一个栈(Stack),栈是一种先进后出的线性数据结构,而栈机没有寄存器,它的操作数就是存放在栈里面。具体来说,对于一条指令机指令 Add,它先从栈顶出栈两次,取出两个操作数 a、b,执行 a+b 运算,最后将结果压入栈。Add 转换成 C 语言的过程如下(提示:match 是 Python 的新语法)match self.instructions[pc]:
case Add(addr):
a = stack.pop()
b = stack.pop()
code.append("v{}={}+{};".format(variable, a, b))
int __cdecl main()
{
// ...... 变量定义 略
putc(73, stdout);
putc(110, stdout);
putc(112, stdout);
putc(117, stdout);
putc(116, stdout);
putc(58, stdout);
putc(32, stdout);
v3 = getc(stdin);
// ....
v4 = getc(stdin) + (v3 << 8);
v5 = getc(stdin) + (v4 << 8);
v6 = getc(stdin) + (v5 << 8);
v7 = getc(stdin) + (v6 << 8);
// ....
Check(v10 ^ '734f1698');
Check(v19 ^ 0x5606035104535A0ALL);
// ....
Check(v28 ^ v36 ^ 0x451505A50075C58LL);
// ....
putc(82, stdout);
putc(105, stdout);
putc(103, stdout);
putc(104, stdout);
}
从上面这段代码看出,IDA 识别出了输出字符和输入字符的数据流向,并且能够正确构造表达式。例子 3 控制流与正确性
这个例子来自 Google CTF 2021,笔者实现的转换器还原了控制流,同时还保证了程序转换后的正确性,转换后的程序甚至可以使用 angr 做符号执行,完成复杂的约束求解。 _1: R = ~R;
_2: Z = 1;
_3: R = R + Z;
_4: R = R + Z;
_5: if(!R) goto _38; else goto _6;
_6: R = R + Z;
_7: if(!R) goto _59; else goto _8;
_8: R = R + Z;
_9: if(!R) goto _59; else goto _10;
_10: bug();
_11: goto end;
_12: X = 1;
_13: Y = 0;
_14: if(!X) goto _22; else goto _15;
这个例子中,笔者将 vm 指令集的内存地址转换为 C 语言中的标号,以此来实现条件跳转。具体完整细节见附件https://panda0s.top/2021/07/19/Google-CTF-2021/#CPP例子 4 复杂的表达式
这个例子由三叶草技术小组 SYJ 完成,笔者在这里对他的内容做一个简单总结,并提供原文链接。原文链接:https://bbs.kanxue.com/thread-269591.htm这个例子主要处理了标号和跳转指令,并实现了对一种类 TEA 算法 的 VM 实现反编译。int __cdecl main()
{
a0 += (a1 + ((a1 >> 5) ^ (16 * a1))) ^ 0x7B7CC140;
a1 += (a0 + ((a0 >> 5) ^ (16 * a0))) ^ 0x2105B8C9;
a0 += (a1 + ((a1 >> 5) ^ (16 * a1))) ^ 0xFEA4FB66;
a1 += (a0 + ((a0 >> 5) ^ (16 * a0))) ^ 0x7D1AE6C0;
a0 += (a1 + ((a1 >> 5) ^ (16 * a1))) ^ 0x2775177E;
a1 += (a0 + ((a0 >> 5) ^ (16 * a0))) ^ 0x925FBD3;
// .....
本文在前人工作的基础上,总结了一种快速实现在 IDA Pro 中反编译未知指令集的方法,该方法适用于 real world 的 wasm 指令集逆向。同时也在一些小众指令集上取得成功,实现了“小语种”的IDA 反编译器。该方法能够在最短时间内编写一个未知指令集的反编译,能够缓解未知指令没有反编译器的窘境。尽管如此,该方法仍然有许多不足待改进,例如笔者还没有在 VMP 商业保护以及流行于 Android 应用程序加固领域的 Java 虚拟化保护上测试该方法,笔者认为,该方法仅适合用于对未知指令集的初步探索逆向,若要开发一个成熟的反编译器,仍然需要根据实际情况,编写 Ghidra 反编译插件或者从零开发反编译(例如脚本类字节码 Python、Lua 等)。https://github.com/nneonneo/ghidra-wasm-plugin)https://xz.aliyun.com/t/13474https://panda0s.top/2021/05/14/WebAssembly-Reverse/https://github.com/WebAssembly/wabt/blob/main/wasm2c/README.md如果各位老板有学习 IDA 基础操作的需求,给大家推荐我在看雪的 IDA 基础课程:
https://www.kanxue.com/book-section_list-156.htm若对本文有任何感想,欢迎移步原帖子(点击下方阅读原文)大家一起讨论。
看雪ID:无名侠
https://bbs.kanxue.com/user-home-617255.htm
*本文为看雪论坛优秀文章,由 无名侠 原创,转载请注明来自看雪社区
文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458552429&idx=1&sn=415d0ec99eeb9224171df3b40d32b0ae&chksm=b18db8e786fa31f16771aa01b1931942b36c595ca6eaedffb73b8bb2ebf021cfc2e6a459db10&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh