实战篇:基于 angr 符号执行自动修复 OLLVM 控制流平坦化
6
0
0
0
在逆向工程中,OLLVM(Obfuscator-LLVM)的控制流平坦化(Control Flow Flattening)是令许多分析者头疼的手段。它通过引入一个“主分发器”和“状态变量”,将函数原本错落有致的逻辑块全部打散,并行地放置在同一层级。
本文将介绍如何利用符号执行框架 angr,通过自动化脚本识别混淆块,并尝试恢复原始的逻辑跳转,实现二进制文件的“去平坦化”。
1. 核心原理回顾:什么是平坦化?
被平坦化后的函数通常包含以下几个关键部分:
- 入口块(Entry):初始化状态变量。
- 分发器(Dispatcher):根据状态变量的值决定下一个执行哪个块。
- 真实块(Real Blocks):原始代码的逻辑片段,执行完后会更新状态变量并跳回分发器。
- 预分发器(Pre-Dispatcher):通常是真实块跳回分发器之前的过渡。
我们的目标是:找出所有真实块之间的逻辑指向关系,绕过分发器直接连接它们。
2. 基于 angr 的修复流程
利用 angr 的路径探索能力,我们可以模拟状态变量的变化,从而推导出逻辑流向。
第一步:定位分发器与真实块
首先,我们需要通过 CFG(控制流图)分析定位到主分发器的入口。在平坦化函数中,分发器通常是引用计数最高、且处于循环中心位置的节点。
import angr
import pyvex
project = angr.Project("obfuscated_bin", load_options={'auto_load_libs': False})
cfg = project.analyses.CFGFast()
# 假设已知目标函数地址
target_func = cfg.functions.get(0x401000)
# 寻找分发器:通常是入度最高的块
dispatcher_addr = 0x401050
第二步:符号执行探测路径
对于每一个真实块,我们需要知道在给定的状态变量下,它执行完后会走向哪个“下一个真实块”。
- 设置符号变量:将存放状态变量的寄存器或内存地址设为符号量。
- 约束寻找:从分发器开始执行,直到到达另一个真实块。
- 记录映射:记录
(当前状态值, 目标块地址)。
def get_relevant_blocks(func):
# 过滤掉分发器相关的辅助块,提取出带有业务逻辑的真实块
# 逻辑通常是:排除掉那些只进行状态变量比较和跳转的块
pass
relevant_blocks = [0x401080, 0x4010a0, ...] # 预先识别的真实块列表
第三步:状态机路径提取
这是最关键的一步。我们利用 SimulationManager 在每个真实块的末尾进行探测:
state = project.factory.blank_state(addr=block_addr)
simgr = project.factory.simulation_manager(state)
# 符号执行直到再次碰到其他真实块或退出
simgr.explore(find=lambda s: s.addr in relevant_blocks and s.addr != block_addr)
if simgr.found:
for found_state in simgr.found:
# 获取到达该块时,状态变量的值
# 以及该路径的跳转条件(针对分支混淆)
print(f"From {hex(block_addr)} to {hex(found_state.addr)}")
3. 处理条件跳转(Conditional Branching)
OLLVM 经常会在真实块末尾根据条件设置不同的状态变量值。这时,符号执行会产生两个分支。我们需要分别捕获这两条路径:
- 路径 A (True):状态变量 = X
- 路径 B (False):状态变量 = Y
通过 state.solver.eval 提取出具体的状态常量,我们就能重建 if...else 逻辑。
4. 自动化 Patch 重构二进制
得到所有逻辑关系后,原始的 jump 指令需要被修改:
- 清理无效跳转:将跳向分发器的
jmp指令改为nop。 - 重定向:根据提取的映射,将真实块末尾改为直接
jmp或jcc到下一个真实块。 - 使用 Keystone/Capstone:利用这些工具生成机器码并写入二进制文件。
# 伪代码:利用 keystone 修复跳转
from keystone import *
ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, _ = ks.asm(f"jmp {next_real_block_addr}")
# 将 encoding 写入文件的对应偏移处
5. 注意事项与局限性
- 不透明谓词(Opaque Predicates):如果混淆中包含永远为真或为假的条件,angr 可能会在某些路径上卡死。需要结合
claripy进行常量折叠优化。 - 状态爆炸:如果函数极度复杂,路径探索可能非常耗时。建议针对单个函数进行局部符号执行,而不是全量分析。
- 别名分析:当状态变量存储在堆栈中时,需要确保 angr 的内存模型(ForceScan)能够准确追踪其变化。
总结
利用 angr 处理 OLLVM 混淆的核心在于:把复杂的控制流跳转问题,转化为符号变量的可达性问题。 通过模拟分发器的逻辑,我们可以自动化地剥离掉外层的平坦化“外壳”,让真正的业务逻辑显露出来。对于复杂的 CTF 题目或加固软件,这套自动化流程能极大提升分析效率。