██╗ ███████╗███████╗████████╗
██║ ██╔════╝██╔════╝╚══██╔══╝
██║ █████╗ █████╗ ██║
██║ ██╔══╝ ██╔══╝ ██║
███████╗███████╗███████╗ ██║
╚══════╝╚══════╝╚══════╝ ╚═╝
██████╗ ██████╗ ███████╗██╗ ██╗███████╗ ██████╗ █████╗ ████████╗ ██████╗ ██████╗
██╔═══██╗██╔══██╗██╔════╝██║ ██║██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗
██║ ██║██████╔╝█████╗ ██║ ██║███████╗██║ ███████║ ██║ ██║ ██║██████╔╝
██║ ██║██╔══██╗██╔══╝ ██║ ██║╚════██║██║ ██╔══██║ ██║ ██║ ██║██╔══██╗
╚██████╔╝██████╔╝██║ ╚██████╔╝███████║╚██████╗██║ ██║ ██║ ╚██████╔╝██║ ██║
╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
A very simple obfuscator for C/C++ x64 and x86 code
这是一个 LLVM 分支,所以要混淆代码,你需要实际的源代码。它不是那种可对任意可执行文件进行混淆的工具,而是编译器的一个修改版本。
如果你想看看效果,我用它制作并混淆了 2 个 crackme。你可以从 releases 中获取它们,那里还有预编译好的 clang 二进制文件。
以下所有内容均使用 -O3 标志编译,所有截图均来自 IDA Pro 9.4。我还用 Binary Ninja 和 Ghidra 测试过,结果要么相同,要么更差。
在编译时加密字符串,并在每次使用加密字符串的位置插入一个解密函数。这完全禁用了在二进制文件中搜索任何字符串的能力。而且每个字符串的解密函数中都硬编码了各自唯一的密钥,这使得转储和解密它们要困难得多。
![]() 处理前 |
![]() 处理后 |
将算术运算替换为等价的 MBA 表达式。除非你先用 MBA 反混淆器处理它,否则基本上不可能看出原始运算做了什么。当然,这种混淆有点弱,因为 MBA 是最老套的技巧,所以有很多工具可以对付它,例如 CoBRA,它可以将它成功反混淆回原始表达式:
./cobra-cli --mba "((x^y) - (((x^y)&0xFF)&0xA) + 10) * ((x^y) - ((x^y)|0xA) + 10) + ((x^y) - (((x^y)&0xFF)|0xFFFFFFF5) - 11) * (~(x^y) - (~(x^y)|0xA) + 10)" --bitwidth 32
10 * (x ^ y)
这就是我编写 AAMBA 处理的原因。
![]() 处理前 |
![]() 处理后 |
将二元运算的操作数替换为 ADC(X, 255) - 255 - CF 和 SBB(X, 255) + 255 + CF。当然,它始终都等价于 X,但这会让表达式依赖于进位标志。除非反编译器能跟踪 CF 的状态(这有时是不可能的),否则它会被彻底搞糊涂,无法折叠这些表达式。它与之前的 MBA 处理配合得非常好,能进一步混淆算术运算。如下所示,反编译器创建了一些额外的变量,并使用了大量 __PAIR64__ 和 __CFADD__ 调用,所以要把这些代码粘贴到 CoBRA 等工具中会困难得多(但并非不可能),IDA 的 gooMBA 插件在简化这些代码方面也帮不上忙。另外,IDA 的反编译器确实会在一定程度上跟踪进位标志,但把它与控制流混淆结合后,如果不实际执行,跟踪就基本不可能了,因为你不知道之前的运算是什么,它可能把 CF 置为 1,也可能没有,所以后续的处理还会在此基础上进一步叠加。
![]() 处理前 |
![]() 处理后 |
收集函数内的所有基本块,并将它们变成一个巨大的状态机。它在函数开头创建一个跳转表,把所有块指针都放进去。然后,每个块末尾不再是普通的跳转,而是全部通过分发器路由,分发器使用间接跳转,在不实际执行的情况下几乎无法静态解析这些跳转。它还会把寄存器降级到栈上,所以如果你把一个块拦腰截断,前一个块的所有变量都会在栈上,这意味着每个表达式中都会有大量的变量。如果你把单个块的操作交给 CoBRA 处理,它也无法反混淆出多少东西,因为未知变量太多了。
创建大量包含无效汇编指令的虚假块。这会极大地干扰反汇编器,因为如果反汇编器遇到一个技术上无效、但永远不会被执行的字节,它仍然会试图解读它。所以,如果这个字节不完整,它就会用其后恰好出现的字节拼出一条指令,本质上是把这些字节消耗掉。这会造成反汇编失步,破坏之后的所有指令。对于 Windows 二进制文件,IDA 还能在一定程度上从中恢复,极少数情况下它能生成流程图,并尽可能进行反编译(尽管结果是损坏且不完整的);而对于 Linux 二进制文件,它会彻底破坏流程图视图并禁用反编译。此外,它还会插入 RDTSC 定时器检查,如果耗时过长(例如附加了调试器时),程序就会崩溃。
![]() Windows |
![]() Linux |
除此之外,如果该处理遇到任何以 0xFF 开头的指令,它就会在其前面插入一个 0xEB 字节。这会形成 JMP RIP+1,因此控制流保持不变(RIP 只是向前推进一个字节,进入原始指令内部),但反汇编器会再次失步。
以 0xFF 开头的指令大多是 INC/DEC 和间接 JMP/CALL。遗憾的是,大多数普通调用和跳转都是相对寻址的(0xE8/0xE9/0xEB),不受影响。但这项技术对分发器处理尤其有用,因为那里的一切都用间接跳转。不过它对调用就没那么有用了,受影响的只有间接调用,通常是虚调用、通过函数指针进行的调用,以及一些外部/库调用。
把函数中的每个栈局部变量都扔进一个大的共享栈缓冲区,并在运行时计算索引。这样一来,反编译器就无法对变量进行别名分析,同一个变量的多次访问会显示为访问不同的值。这与分发器配合得非常好,因为分发器会把寄存器降级到栈上,所以会有大量这样的栈槽。
![]() 处理前 |
![]() 处理后 |
通过异常来混淆控制流。它把所有调用替换为 int3 陷阱。当陷阱被触发时,控制流会转到异常处理器,由其将 RIP 调整为实际的调用目标。它还会在陷阱之后紧接着插入无效字节,让反汇编器进一步失步。
![]() 处理前 |
![]() 处理后 |
把所有的处理组合起来之后,如果没有一些额外的工具来做反混淆,静态分析会变得非常困难。即使你设法把所有的无效字节都 NOP 掉,并且能将代码反编译成某种伪代码,或者至少得到流程图视图,你仍然要面对通过异常和分发器进行的控制流混淆;就算你突破了这一层,还有堆积如山的冗余 MBA、虚假块、栈变量和字符串加密在掩盖实际操作。以下是之前那个简单的 xor Foo 函数所在的简单程序的主入口点,在三大主流反汇编器中的截图。如你所见,它们对这些代码基本无能为力。
|
我真的很想展示某种反混淆器试图破解这些代码的结果,但遗憾的是,我没能找到任何可用的、带符号执行、能看清实际控制流的反混淆器。我找到的那些反混淆器,要么非常局限于特定用例(比如专门反混淆 VMProtect),要么太老旧、无人维护且已经损坏(几乎我试过的每一个 IDA/BN 插件都是如此),要么就是需要我通过不熟悉的 API 做大量手动引导和配置的重型工具(Angr、Triton、IntelPin)。如果你知道任何能针对任意二进制文件提取出哪怕一点点信息的反混淆器,请告诉我。
当然,把所有这些乱七八糟的东西塞进二进制文件会极大地拖慢速度,在默认设置下平均慢 200 倍以上:

不过实际上并没有看起来那么糟糕,原因有两点。1. 这里几乎 95% 的性能开销是由 Nanomites 造成的,因为中断本身就很慢。异常必须离开用户态进入内核,再回到应用程序,这需要时间。去掉 Nanomites 后,速度只慢 7.5 倍:

因此,我强烈建议只手动标记你想用 Nanomites 混淆的函数和调用,而不是直接全都设置。混淆二进制中的每一个调用毫无意义,而且代价高昂。原因之二是,大多数时候你并不真正在乎想隐藏的东西的性能。这个混淆器支持有选择地启用和禁用。所以你可以为代码中性能关键的区段禁用混淆,在真正需要的地方启用它。没有人在意你的许可证检查是耗时 1 毫秒还是 0.001 毫秒,对人来说都一样察觉不到。
有关配置选项和完整指南,请参阅 wiki。
你还可以用 LEET_SKIP 宏标记 Leet.h 中与异常处理器相关的所有函数,这样异常处理器就不会被大部分处理混淆,从而把 Nanomites 的开销减半(默认设置下),但这也意味着会留下一个未混淆的异常处理器,我觉得这比轻微的性能损失更糟糕。
依赖项:
-DLLVM_USE_LINKER=mold。但使用 mold 会更快)git clone https://github.com/Zydak/LeetObfuscator.git --recursive
cd LeetObfuscator
mkdir build
cd build
cmake ../leet-llvm-project/llvm -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DLLVM_USE_LINKER=mold -DLLVM_USE_SPLIT_DWARF=ON -DLLVM_ENABLE_ASSERTIONS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DLLVM_ENABLE_PROJECTS=clang -DLLVM_TARGETS_TO_BUILD=X86 -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
ninja clang
修改后的编译器会位于 build/bin/ 中,直接用这个编译器编译你要混淆的源代码即可。
你也可以不用构建,releases 里有预编译好的二进制文件,下载解压即可。
目前不支持在 Windows 上构建这个项目,但支持用这个项目交叉编译到 Windows。所以如果你真的需要,可以获取 Linux 二进制文件,然后在 Linux 或 WSL 中为 Windows 交叉编译混淆后的应用。
关于具体用法的完整指南,请参阅 wiki。
示例用法:
把 Leet.h 复制到你的项目中,然后在某个 .c/.cpp 文件里包含它并定义 LEET_IMPLEMENTATION
不要在多个模块中定义它!
#define LEET_IMPLEMENTATION
#include "Leet.h"
然后只需用构建好的编译器编译源代码:
./build/bin/clang++ ./test.cpp -o test -fno-exceptions
![]() Binary Ninja Personal 5.2 |
![]() Ghidra 12.1.2 |