在现代商业二进制代码加固、移动端 Native SO 核心算法防护以及高级 APT 恶意载荷中代码混淆技术早已从简单的“去除符号表”和“花指令插入”全面演进为深度的“控制流混淆”。其中以开源混淆编译器OLLVMObfuscator-LLVM及其商业衍生品为代表的控制流平坦化Control Flow Flattening是逆向工程师在反汇编器中最不愿直视的“恶梦”之一。当一个原本逻辑清爽的 C 语言函数包含清晰的if-else、for循环与层次分明的嵌套逻辑经过控制流平坦化处理后其反编译出的控制流图CFG会退化成一个令人绝望的“巨型水母”结构原本的层级关系全部消失取而代之的是一个庞大的主分发器Dispatcher数十个甚至上百个被撕碎的基本块Basic Blocks像触须一样挂在分发器下方彼此之间仅通过一个在循环中不断演进的“状态变量State Variable”进行无休止的调度。面对这种混淆任何试图依靠手工单步跟踪或肉眼看 IDA F5 伪代码的分析方式都会迅速被淹没在无效的状态机流转中。破局的真正杀招在于利用现代符号执行Symbolic Execution框架如 Angr在抽象语法树与约束求解层面自动化剥离分发器重构出程序最初的真实控制流边。一、 控制流平坦化的内部解构与基本块分类在编写反混淆脚本之前必须在数学与图论层面对被平坦化函数的内部拓扑建立极其严密的分类模型。平坦化函数在本质上是一个受受控状态机驱动的死循环其内部的基本块Basic Blocks可以精准划分为四种类型----------------------------- | 函数入口块 (Prologue) | | - 初始化状态机变量: state0 | ----------------------------- | v ------------------------------------------------------------- | 主分发器 (Main Dispatcher) | | - 通常为一个庞大的 switch(state) 结构包含多层条件跳转 | ------------------------------------------------------------- | | | (state 0) (state 1) (state 2) v v v -------------------- -------------------- -------------------- | 真实业务块 A | | 真实业务块 B | | 真实业务块 C | | - 承载真实数学计算 | | - 承载真实数据操作 | | - 承载业务分支判断 | | - 修改: state 2 | | - 修改: state 0 | | - 修改: state ...| -------------------- -------------------- -------------------- \ | / -------------------------------------------- | v ----------------------------- | 预分发器 (Pre-Dispatcher) | | - 无条件无脑跳回主分发器 | -----------------------------入口块Prologue完成函数栈帧初始化并将状态机变量如寄存器ebx或局部变量var_4赋予初始值主分发器Main Dispatcher包含树状的二分查找条件跳转或跳转表根据状态变量的取值将执行流分发给具体的业务块真实业务块Relevant Blocks包含程序真正业务逻辑的基本块并在末尾给状态机变量赋一个新值预分发器Pre-Dispatcher各个真实业务块执行完毕后的汇合点它只有一条简单的无条件跳转指令jmp负责将执行流无脑交回给主分发器重新调度出口块Epilogue执行函数结语并ret返回。二、 基于 Angr 符号执行的反混淆核心算法思想控制流平坦化之所以能混淆视听是因为它用一个公共的分发器代理了所有业务块之间的直接跳转。因此去平坦化Deflattening的核心目标就是“绕过主分发器找出每一个真实业务块在执行结束后到底意图跳转到哪一个后续的真实业务块并直接在二进制文件中用一条绝对跳转指令jmp将二者物理缝合在一起”要计算这个“后续真实块”如果靠静态反汇编分析因为状态变量可能经过了复杂的算术运算如加减常数或位运算静态分析很难穷举而如果动态运行又必须提供真实的输入。这正是符号执行Symbolic Execution的绝对统治领域我们不需要提供任何具体的输入数据直接将寄存器和内存符号化让符号执行引擎从“某个真实业务块”的入口开始执行执行流自然会顺着业务块流经预分发器、进入主分发器并最终在约束求解器的驱动下停在“下一个真实业务块”的门口记录下这一对起点与终点一条真实的控制流边Edge便被成功捕获三、 实战基于 Python 与 Angr 的自动化去平坦化脚本以下展示工业级反混淆流程中基于angr框架的核心求解脚本骨架import angr import claripy import pyvex def analyze_and_deflatten(binary_path: str, func_addr: int): 使用 Angr 自动化去平坦化指定函数 print(f[*] 正在为二进制 [{binary_path}] 在地址 0x{func_addr:X} 构建符号执行工程...) project angr.Project(binary_path, load_options{auto_load_libs: False}) # 1. 恢复函数的控制流图 (CFG) cfg project.analyses.CFGEmulated(keep_stateTrue, starts[func_addr]) func cfg.kb.functions[func_addr] # 2. 启发式识别主分发器与预分发器 # 特征: 主分发器和预分发器是整个函数中入度 (In-degree) 和出度最高的基本块 sorted_blocks sorted(func.blocks, keylambda b: len(b.predecessors), reverseTrue) main_dispatcher sorted_blocks[0] pre_dispatcher sorted_blocks[1] print(f[] 识别出疑似主分发器: 0x{main_dispatcher.addr:X}, 预分发器: 0x{pre_dispatcher.addr:X}) # 3. 提取所有真实业务块 (Relevant Blocks) # 排除分发器、预分发器与无分支的纯中转块 relevant_blocks [] for block in func.blocks: if block.addr in [main_dispatcher.addr, pre_dispatcher.addr]: continue # 只要该基本块能最终流向预分发器说明它是产生状态演进的真实业务块 if pre_dispatcher.addr in [succ.addr for succ in block.successors]: relevant_blocks.append(block) print(f[] 成功筛选出 {len(relevant_blocks)} 个真实业务逻辑块准备开启符号推导演进...) # 4. 符号执行求解每一个真实块的下一跳目标 recovered_edges {} for from_block in relevant_blocks: # 从当前业务块的第一条指令初始化一个符号执行状态 (State) state project.factory.blank_state(addrfrom_block.addr) simgr project.factory.simulation_manager(state) # 让引擎步进执行探索直到离开当前块并抵达新的业务块 # 探索的目标是: 任何其他 relevant_blocks 的入口地址 target_addrs [b.addr for b in relevant_blocks if b.addr ! from_block.addr] simgr.explore( findtarget_addrs, avoid[main_dispatcher.addr] # 避免在分发器内部无限死循环 ) if simgr.found: for found_state in simgr.found: next_addr found_state.addr recovered_edges[from_block.addr] next_addr print(f[SUCCESS] 恢复控制流边: 0x{from_block.addr:X} ---- 0x{next_addr:X}) else: print(f[-] 业务块 0x{from_block.addr:X} 符号执行未捕获到显式后继可能属于条件双分支或函数出口) return recovered_edges四、 二进制重构Patching缝合控制流与重现 F5 清爽代码得到recovered_edges映射表后最后一步是物理修剪二进制文件计算相对跳转偏移在 x86-64 中一条无条件长跳转指令为jmp target机器码通常为E9 4字节相对偏移$$\text{Relative Offset} \text{Target Addr} - (\text{Current Patch Addr} 5)$$清除无用跳转与 NOP 填充将真实业务块原本末尾跳转到预分发器的指令如jmp pre_dispatcher直接覆写为计算出的jmp target回写文件并在 IDA Pro 中重新分析将 Patch 后的二进制重新载入 IDA Pro按一下空格键切换到图形视图Graph View原本层级错乱、盘根错节的巨型分发器瞬间被完全绕过反编译器Hex-Rays F5中立刻呈现出与原始源码近乎完全一致的清爽if-else、嵌套循环结构五、 结语二进制逆向工程的演进本质上是一场“混淆熵增”与“算法降熵”之间的数学对抗。控制流平坦化看似通过将逻辑打碎拉平制造了巨大的认知混乱但其确定性的状态机本质却恰恰成为了符号执行与约束求解算法最理想的猎物。面对现代高强度的代码保护唯有掌握从图论建模、符号推导到二进制字节级 Patch 的全栈工程能力方能在层层迷雾中拨云见日直抵代码最底层的核心灵魂。
阅读完成 · 觉得有帮助?