百趣云 百趣云的博客

OLLVM 控制流平坦化识别与还原思路

先看懂平坦化长什么样

正常函数的 CFG(控制流图)是树状的:入口 → 分支 → 汇合 → 返回,层次分明。被 OLLVM 控制流平坦化(Control Flow Flattening)处理过的函数,CFG 变成一个大循环套 switch

入口
  ↓
state = 初始值
  ↓
┌─────────────┐
│  while(1)   │ ←──┐
│  switch(state) { │ │
│    case 0: 真实块A; state = X; break; │
│    case 1: 真实块B; state = Y; break; │
│    ...分发器块...  │ │
│  }            │  │
└─────────────┘  │
     ↑___________┘

所有真实代码块被拍平到同一层级,执行顺序由 state 变量驱动。读这种代码就像看被洗牌的电影——每个镜头都在,但顺序全靠猜。

识别:三个特征一眼确认

在 IDA 里判断一个函数是否被平坦化:

  1. CFG 形态:按空格看图形视图,大量基本块汇聚到一两个"分发器"块,分发器又指回所有块——典型的"轮辐状"结构
  2. 状态变量:存在一个变量被反复赋值(mov w8, #0x1234 后立即用于比较),且赋值来源分散在各个真实块里
  3. 块数量异常:逻辑上很简单的函数(比如就做个加法)却有几十个基本块

手工还原:小函数的可行解

对关键小函数(比如签名函数),手工还原是可行的:

第一步:找到状态变量。在 IDA 里找被所有块引用、且参与 switch 比较的变量,重命名为 state

第二步:建立状态转移表。逐个基本块记录:case 值 → 真实逻辑 → 下一个 state 值。比如:

case 0x2A: 读取参数 → state = 0x7B
case 0x7B: 异或运算 → 条件跳转: 真→0xC1, 假→0x55
case 0x55: 返回结果

第三步:按转移表重排逻辑。把状态链翻译成正常控制流:顺序执行的部分连成直线,条件分叉还原成 if-else,回边还原成循环。

工具还原:大函数的唯一解

手工还原超过 20 个块的函数就是酷刑,必须上工具:

  1. angr 符号执行:用 angr 的 CFG 分析 + 符号执行,自动求解状态转移关系。社区有现成的 deflat 脚本(基于 angr),对 ARM64 的支持需要适配。
  2. D810(IDA 插件):基于微码(microcode)的优化插件,能自动识别并折叠平坦化模式,IDA 7.5+ 可用,ARM64 SO 效果实测不错。
  3. Unicorn 模拟执行:对关键路径做模拟执行,记录实际执行轨迹,按轨迹重排代码块。
# D810 使用:IDA 里 Edit -> Plugins -> D810,勾选 unflattener 规则
# 对目标函数按 F5 反编译,插件自动优化掉大部分分发逻辑

精髓:三个实战认知

  1. 平坦化只增加阅读成本,不增加执行成本。所以"不还原,直接用"经常是更聪明的选择——Frida 直接调用被平坦化的函数,或 unidbg 模拟执行,绕过阅读环节。还原是为了理解算法,理解不是目的时别硬刚。
  2. 真假块分辨有技巧。平坦化常配合虚假控制流(bogus control flow),插入永真/永假条件的废块。识别废块:块内计算结果从未被后续使用,或条件里出现 x*(x-1) % 2 == 0 这类恒真式(不透明谓词)。
  3. 指令替换(substitution)常和平坦化叠加a + b 被改写成 a - (-b) 再展开成一堆位运算。这种局部膨胀用 IDA 的 F5 反编译结果看比汇编清晰——伪 C 代码里模式很明显。

总结

  1. 平坦化特征:轮辐状 CFG + 状态变量 + 块数量异常
  2. 小函数手工建状态转移表,大函数上 D810/angr
  3. 能调用就别还原:Frida/unidbg 直调绕过阅读
  4. 警惕叠加的虚假控制流和指令替换

交流微信:run1255

By 百趣云 阅读量:6 On