[size=12.573px]c
i=0;while (i < 4) { if (serial[i != prefix[i) return 0; i++;}return 1;
原始逻辑完全恢复。
3.4 处理更复杂的平坦化复杂情况包括:多个返回点、嵌套循环、异常处理。手动恢复需要耐心,但对于几百行以内的函数是可行的。超过1000行的平坦化函数则需要转向自动化工具。
第四章 自动化反混淆:符号执行工具Angr实战4.1 符号执行简介符号执行(Symbolic Execution)是一种程序分析技术。它不将变量绑定为具体值(如x=5),而是将其表示为符号(如x = α),然后模拟程序的执行路径。当程序遇到分支时(如if (x > 0)),符号执行引擎会同时探索两条路径,并记录每条路径的约束条件(如α > 0和α ≤ 0)。
在反混淆控制流平坦化时,符号执行的优势在于:它可以自动枚举所有可能的执行路径,并忽略那些代码中的虚假分支(如永假的不透明谓词)。
4.2 Angr的安装与配置Angr是一个开源的二进制分析框架,集成了符号执行、控制流恢复、反混淆等多种功能。
安装(Ubuntu 22.04):
[size=12.573px]bash
sudo apt-get install python3-dev libffi-dev build-essentialvirtualenv -p python3 angr_envsource angr_env/bin/activatepip install angr
Windows安装(WSL2推荐):在WSL2 Ubuntu中安装,然后通过Python脚本处理Windows PE文件。
4.3 使用Angr反混淆平坦化函数以下是一个完整的Angr脚本,用于恢复平坦化函数的控制流,并提取所有可能的执行路径中的指令序列。
[size=12.573px]python
import angrimport claripy# 加载二进制proj = angr.Project("obfuscated_target.exe", auto_load_libs=False)# 获取目标函数地址(假设通过IDA提前找到)func_addr = 0x004015A0 # check_license函数的地址# 创建初始状态:在函数入口处暂停state = proj.factory.blank_state(addr=func_addr)# 如果需要模拟输入参数(char *serial),可以创建符号变量serial_ptr = state.regs.ecx # 假设ecx传递第一个参数serial_buf = claripy.BVS("serial", 8*20) # 20字节符号字符串state.memory.store(serial_ptr, serial_buf)# 添加约束:字符串以null结尾state.add_constraints(serial_buf.get_byte(19) == 0)# 创建模拟管理器simgr = proj.factory.simulation_manager(state)# 探索所有执行路径,直到函数返回simgr.explore(find=lambda s: s.addr == func_addr + 0x100) # 需要找到函数结束地址# 更简单的方法:探索所有路径,记录基本块simgr.run()# 输出每个路径的基本块序列for path in simgr.deadended: print("Path found:") for addr in path.history.bbl_addrs: print(f" Basic block at 0x{addr:x}")
实际上,社区已经为Angr编写了专门反混淆控制流平坦化的脚本deflat.py(GitHub搜索“deflat angr”)。使用该脚本可以一键恢复线性代码:
[size=12.573px]bash
python deflat.py -f obfuscated_target.exe --addr 0x4015A0
该脚本会输出恢复后的汇编代码或生成一个新的二进制文件,其中所有平坦化的函数都被还原为正常形状。
第五章 字符串加密:隐藏敏感信息5.1 字符串加密的工作原理许多商业保护方案会将程序中的所有字面字符串(literal strings)加密存储。例如,原始代码中的"Registration failed"在二进制文件中不会直接出现,而是一段密文(如"\x8F\xA3\x12\x9C...")。
运行时,程序会先调用一个字符串解密函数,将密文动态解密到栈上或堆中,然后产生一个临时字符串供后续API调用(如MessageBox)使用。解密完成后,临时字符串可能被立即覆盖(防止被dump)。
伪代码示例:
[size=12.573px]c
// 加密字符串存储在.data段char encrypted_string[ = {0x8F, 0xA3, 0x12, 0x9C, 0x00};// 运行时解密char *decrypt(char *enc) { char *buf = alloca(16); for (int i = 0; enc[i; i++) buf[i = enc[i ^ 0xFF; return buf;}// 使用MessageBox(0, decrypt(encrypted_string), "Error", 0);
静态分析时,黑客在字符串窗口中看到的只有"\x8F\xA3\x12\x9C",无法直接定位到错误提示。
5.2 识别字符串解密函数字符串解密函数往往具有以下特征:
循环结构:遍历字符串的每个字节,执行一个固定算数操作(XOR、加减、移位)。
返回临时缓冲区:解密后的字符串通常存在于栈上(返回局部数组指针——虽然不安全,但常见)或堆中。
对称性:解密算法往往是对称的(同一个函数既用于加密也用于解密,或者解密与加密算法相同)。
多次调用:一个程序可能有几十个加密字符串,它们都由同一个解密函数处理。
在IDA中,搜索xor byte ptr [eax], 0x55、add al, bl等模式,可以定位解密函数。
5.3 静态解密字符串一旦找到解密函数,黑客可以提取所有被加密的字符串常量,然后用Python模拟解密,替换掉原始数据。
步骤1:收集加密字符串
在IDA中,找到对解密函数的每次调用。例如:
[size=12.573px]assembly
push offset encrypted_string_1call decrypt_stringpush eaxpush offset "Error"call MessageBoxA
记下每个encrypted_string_X的地址和数据。
步骤2:逆向解密算法
假设解密算法为:
[size=12.573px]c
char *decrypt(char *src) { static char buf[256; char *dst = buf; while (*src) { *dst++ = *src++ ^ 0xAA; } *dst = 0; return buf;}
即单字节XOR 0xAA。
步骤3:编写脚本解密
[size=12.573px]python
encrypted = [0xE5, 0xC7, 0xE2, 0xC9, 0xCE, 0xDB, 0xC5, 0x00decrypted = ''.join(chr(b ^ 0xAA) for b in encrypted)print(decrypted) # 输出 "License"
步骤4:将解密结果写回IDA数据库
在IDA中,可以用Edit→Patch program→Change byte,将加密字节替换为解密后的明文字符串(需要保持长度相同或调整)。也可以简单地在注释中记录明文,不修改原始文件。
5.4 动态追踪字符串解密如果解密算法非常复杂(多重循环、密钥动态生成),静态分析难以还原,黑客可以采用动态追踪法:
对于VMProtect这类强壳中的字符串加密,解密函数本身可能也被虚拟化,此时动态追踪成为唯一可行的方法。
第六章 虚假控制流与不透明谓词6.1 不透明谓词的定义不透明谓词(Opaque Predicate)是一个永远为真或永远为假的布尔表达式,但其真假值在静态分析时表面上看起来取决于一些变量,使得分析者无法立即判断分支走向。
经典示例:
[size=12.573px]c
int x = rand();if ((x * x) % 2 == 0) { // 实际上,任何整数的平方除以2的余数都等于该数平方的奇偶性,但这里难以判断 // 真实代码} else { // 永远不会执行的死代码}
更简单的可识别不透明谓词:if (1 + 1 == 3) { ... },但高级混淆器会使用一些数学恒等式(如x*(x+1) % 2 == 0永远为真)。
6.2 虚假控制流的识别在控制流平坦化的基础上,混淆器还会插入大量指向虚假块(永远不会被执行的代码块)的边。这些虚假块包含大量垃圾指令,用于污染反编译器的输出。
在IDA中识别虚假控制流:
检查某个case是否从不被任何状态转换指向(入度为0)。
检查某个case的结尾设置的状态变量,是否导致它跳转到一个永远不会到达正常return的死循环。
使用交叉引用分析,查看哪些基本块引用了不透明的全局变量。
6.3 移除虚假控制流的策略策略1:常量传播
如果一个分支取决于某个编译时已知的常量,可以在IDA中手动将其求值。例如:
[size=12.573px]c
int condition = 5 * 5 - 25; // 永远为0if (condition) { // 虚假分支}
可以将其改为if (0),然后用IDA的Patch功能删除死代码。
策略2:动态跟踪
运行程序,使用x64dbg的Trace功能记录程序实际执行过的指令地址。从未被执行过的基本块,就可以标记为虚假控制流,在分析时忽略。
策略3:符号执行
Angr等工具可以自动证明某些分支不可达。通过求解约束condition == true,如果无解,则分支为死代码。
第七章 花指令与反反汇编7.1 花指令的原理花指令(Junk Code)是指插入到正常指令序列中的无用指令,其目的不是执行,而是欺骗反汇编器,使其输出错误的汇编代码。当反汇编器错误地将数据解释为指令时,下一个实际指令的地址就可能被错位。
一个经典的x86花指令序列:
[size=12.573px]assembly
jmp label1db 0xE8 ; 0xE8是call的机器码,但这里只是一个字节label1:...
由于jmp直接跳过了0xE8,该字节永远不会被执行。但线性扫描反汇编器(如早期的objdump)从地址顺序解析时,会将0xE8解释为call指令的开头,从而导致后续所有指令地址错位。
7.2 绕过花指令的方法方法1:使用递归下降反汇编器
IDA Pro使用递归下降算法(从入口点开始,只分析通过控制流可达的地址),天然避免了线性扫描反汇编器的问题。因此,简单的花指令对IDA无效。
方法2:手动修复
对于更高级的花指令(如故意插入0xEB 0xFE死循环,然后通过异常恢复),黑客需要在x64dbg中执行到花指令之后,观察真实执行的指令,然后回到IDA中手动告诉反汇编器“这里开始是代码”。
在IDA中,选中被误认为数据的字节,按C键(Code)强制解释为指令。按D键(Data)恢复为数据。
方法3:NOP掉花指令
如果花指令永远不会执行,可以直接将其填充为0x90(NOP),简化分析。
第八章 实战案例(一):手动反混淆一个OLLVM平坦化函数8.1 目标程序目标:一个Linux命令行程序license_check,使用OLLVM最高混淆级别编译。该程序读取命令行参数作为序列号,判断是否有效,输出“Valid”或“Invalid”。
使用IDA加载后,main函数被平坦化,反编译输出有800行。
8.2 步骤1:定位状态变量在伪代码中搜索while ( 1 )和switch。发现状态变量是v10。
[size=12.573px]c
v10 = 0;while ( 1 ) { switch ( v10 ) { case 0: v9 = strlen(argv[1); v10 = 1; break; case 1: if ( v9 == 16 ) v10 = 2; else v10 = 8; break; // ... 更多case }}
8.3 步骤2:绘制状态转换图手工记录每个case的state转换(使用Python辅助打印)。梳理后发现:
case 0:初始化 → state=1
case 1:长度检查 → 长度为16则state=2,否则state=8
case 2:开始循环,i=0 → state=3
case 3:i<16则state=4,否则state=6
case 4:计算checksum → state=5
case 5:i++ → state=3
case 6:checksum == 0x12345678则state=7,否则state=8
case 7:输出"Valid",return
case 8:输出"Invalid",return
8.4 步骤3:重构代码根据状态图重构出原始逻辑:
[size=12.573px]c
int main(int argc, char **argv) { if (argc < 2) return 1; char *serial = argv[1; if (strlen(serial) != 16) { puts("Invalid"); return 0; } int sum = 0; for (int i = 0; i < 16; i++) { sum += serial[i; } if (sum == 0x12345678) { puts("Valid"); } else { puts("Invalid"); } return 0;}
成功恢复!整个过程耗时约2小时(包括手动记录)。如果有十几个这样的函数,手动恢复不可行,必须使用Angr自动化。
第九章 实战案例(二):使用Angr自动化反混淆9.1 编写Angr脚本恢复控制流对于上面的平坦化二进制,使用deflat.py脚本:
[size=12.573px]bash
python deflat.py -f license_check --addr 0x400620
该脚本执行以下步骤:
输出:license_check_deflat,直接运行该文件,功能与原版完全相同,但所有平坦化函数已被还原为线性控制流。
9.2 验证反混淆结果用IDA加载反混淆后的文件,main函数的反编译结果整洁、可读。原本需要数小时的分析工作,在3分钟内自动完成。
注意:deflat.py依赖Angr,对于大型二进制(超过10MB)可能运行缓慢或内存溢出。此时可以手动提取关键函数单独处理。
第十章 实战案例(三):字符串加密与动态解密追踪10.1 目标程序simple_string_encrypted.exe,一个简单的Windows程序,弹窗提示“Registration Failed”。但字符串窗口中没有该字符串。
10.2 定位解密函数在IDA中搜索xor循环。找到函数sub_4012A0:
[size=12.573px]c
char *__cdecl sub_4012A0(char *enc) { char *buf = (char *)malloc(100); int i = 0; do { buf[i = enc[i ^ 0x55; i++; } while (enc[i-1); return buf;}
10.3 提取所有加密字符串交叉引用查看哪些地方调用了sub_4012A0。发现3处调用,分别传入地址aVh、aYk、aFsn。鼠标悬停在这三个地址上,显示的数据分别是:
编写Python脚本解密:
[size=12.573px]python
enc_data = bytearray(b'VhZNZXl0ZXJz')dec = bytearray(b ^ 0x55 for b in enc_data)print(dec) # b'Registration'enc2 = bytearray(b'YXRpb24gZmFpbGVk')print(bytearray(b ^ 0x55 for b in enc2)) # b'ation failed'
实际拼接后是"Registration failed"。
10.4 整理解密结果将所有解密后的字符串整理成列表,在IDA中用注释标记,或直接修改二进制文件中的加密字节为明文(保持长度相同或调整空间)。
对于更复杂的情况(如每隔一个字节异或不同的值、使用RC4),只需将解密算法用Python重现即可。
第十一章 自修改代码(SMC)的分析方法11.1 自修改代码的原理自修改代码(Self-Modifying Code)是指程序在运行时动态改写自己的指令。这在早期DRM保护和病毒中常见。
简单示例:
[size=12.573px]c
// 代码段最初存储的是垃圾数据char encrypted_code[ = {0x31, 0xC0, 0x40, ...}; // 代表 xor eax,eax; inc eaxvoid decrypt_code() { for (int i = 0; i < len; i++) { ((char*)target_function)[i ^= 0x55; }}
在加密状态不执行,解密后正常执行。
11.2 静态分析的困境静态分析时,自修改代码呈现为乱码或不完整的指令。黑客无法通过静态分析得知执行时的真实指令。
11.3 动态dump法步骤1:在x64dbg中加载程序,在自修改代码执行完成后(例如在修改后的代码被调用之前)下断点。
步骤2:此时内存中的代码已经被解密,使用x64dbg的“Dump Region”将整个代码段保存。
步骤3:将保存的文件作为新的二进制进行分析,或者在IDA中加载原始文件,然后手动同步内存中的修改(Edit→Patch program→Apply patches from memory)。
11.4 对抗自修改代码的自动化工具Scylla插件支持从内存中Dump并修复自修改代码。对于加壳后使用SMC的程序,脱壳过程本质就是Dump解密后的代码。
第十二章 混合混淆与防御建议12.1 黑客的终极挑战:多层混淆现实中,商业保护工具会同时使用多种混淆技术:
面对多层混淆,单一方法往往失效。黑客需要:
先绕过反调试(确保可以动态分析)
在运行时dump出解密后的代码
使用符号执行反平坦化
提取所有解密后的字符串
重构出接近原始的逻辑
12.2 对防御者的建议如果你是一名开发者,希望使用混淆保护你的软件:
不要迷信混淆:混淆只能增加分析时间,不能保证安全。核心逻辑应当放在服务器端。
选择合适的强度:OLLVM默认配置已经能阻挡大部分业余黑客。VMProtect则更安全。
结合其他保护:混淆+加壳+反调试+完整性校验,形成纵深防御。
定期更新混淆模式:公开的反混淆工具往往针对特定混淆器版本。更换变种或自定义混淆可以延长被攻破的时间。
12.3 学习资源推荐
第十三章 总结本文以超过一万三千字的篇幅,全面深入地讲解了代码混淆与反混淆的攻防技术。从控制流平坦化到字符串加密,从虚假控制流到自修改代码,从手工反混淆到符号执行自动化,每一个环节都给出了具体的识别方法和应对策略。
掌握这些技术,黑客就能从混乱不堪的混淆代码中提取出程序的真实意图,让保护者的“迷雾”散去。值得注意的是,混淆与反混淆的对抗永远不会停止——新的混淆器不断涌现,新的反混淆工具也紧随其后。作为安全研究者,保持学习、动手实践,才是应对变化的不二法门。
后续本系列将继续深入网络验证破解、移动端逆向等专题。
关键词:代码混淆;反混淆;控制流平坦化;字符串加密;符号执行;Angr;黑客;破解软件;不透明谓词;花指令