WEBKT

实战篇:基于 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 

第二步:符号执行探测路径

对于每一个真实块,我们需要知道在给定的状态变量下,它执行完后会走向哪个“下一个真实块”。

  1. 设置符号变量:将存放状态变量的寄存器或内存地址设为符号量。
  2. 约束寻找:从分发器开始执行,直到到达另一个真实块。
  3. 记录映射:记录 (当前状态值, 目标块地址)
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 指令需要被修改:

  1. 清理无效跳转:将跳向分发器的 jmp 指令改为 nop
  2. 重定向:根据提取的映射,将真实块末尾改为直接 jmpjcc 到下一个真实块。
  3. 使用 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. 注意事项与局限性

  1. 不透明谓词(Opaque Predicates):如果混淆中包含永远为真或为假的条件,angr 可能会在某些路径上卡死。需要结合 claripy 进行常量折叠优化。
  2. 状态爆炸:如果函数极度复杂,路径探索可能非常耗时。建议针对单个函数进行局部符号执行,而不是全量分析。
  3. 别名分析:当状态变量存储在堆栈中时,需要确保 angr 的内存模型(ForceScan)能够准确追踪其变化。

总结

利用 angr 处理 OLLVM 混淆的核心在于:把复杂的控制流跳转问题,转化为符号变量的可达性问题。 通过模拟分发器的逻辑,我们可以自动化地剥离掉外层的平坦化“外壳”,让真正的业务逻辑显露出来。对于复杂的 CTF 题目或加固软件,这套自动化流程能极大提升分析效率。

逆向先锋 angrOLLVM符号执行

评论点评