2. 手动还原简单平坦化控制流

任务目标

给定一个经过控制流平坦化处理的简单函数,通过静态阅读和手动跟踪状态变量(dispatcher)的取值变化,还原出原始的控制流结构(if-else / switch-case),并验证还原结果与原始逻辑一致。

前置条件

本案例使用纯文本形式的伪代码/LLVM IR 片段作为分析对象,不需要实际编译或运行环境。分析对象是一个将原始 if-else 结构平坦化后的函数,其核心特征是:所有原始基本块被放入一个大的 switch 分发循环中,通过状态变量 state 控制块间跳转。

待还原的平坦化函数

// 原始逻辑(已知):
// int original(int x) {
//     int r = 0;
//     if (x > 10)      r = 1;
//     else if (x < 0)  r = 2;
//     else             r = 3;
//     return r;
// }

// 平坦化后的等价形式(待分析):
int flattened(int x) {
    int r = 0;
    int state = 0;          // 状态变量(dispatcher)
    while (1) {
        switch (state) {
            case 0:         // 原始入口
                if (x > 10) state = 1;
                else        state = 2;
                break;
            case 1:         // 对应 r=1 的分支
                r = 1;
                state = 5;
                break;
            case 2:         // 对应 x<0 判断
                if (x < 0) state = 3;
                else        state = 4;
                break;
            case 3:         // 对应 r=2 的分支
                r = 2;
                state = 5;
                break;
            case 4:         // 对应 r=3 的分支
                r = 3;
                state = 5;
                break;
            case 5:         // 统一出口
                return r;
        }
    }
}

手动还原步骤

  1. 识别 dispatcher 与状态变量:确认 state 是唯一的分发变量,switch(state) 是分发器,每个 case 对应一个原始基本块。
  2. 绘制状态转移关系:逐个 case 记录 state 的取值变化,形成状态转移表:
    0 → (x>10 ? 1 : 2),1 → 5,2 → (x<0 ? 3 : 4),3 → 5,4 → 5,5 → return
  3. 合并无分支链:状态 1、3、4 都是直接赋值后跳转到 5,说明它们是互斥分支的终点;状态 0 和 2 是条件判断点。
  4. 重构控制流:从状态 0 开始,条件 x>10 为真进入状态 1(r=1),为假进入状态 2;状态 2 再判断 x<0,真→状态 3(r=2),假→状态 4(r=3)。因此原始结构为:
    if (x>10) r=1; else if (x<0) r=2; else r=3;

验证方法

将还原出的逻辑与原始函数对比,选取边界测试值进行人工推演:

  • x=11:平坦化路径 0→1→5,r=1;原始逻辑 x>10 成立,r=1 ✓
  • x=10:平坦化路径 0→2→4→5,r=3;原始逻辑 x>10 不成立且 x<0 不成立,r=3 ✓
  • x=-1:平坦化路径 0→2→3→5,r=2;原始逻辑 x<0 成立,r=2 ✓
  • x=0:平坦化路径 0→2→4→5,r=3;原始逻辑 r=3 ✓

所有路径均与原始 if-else 逻辑一致,还原正确。

关键要点

  • 平坦化将 块间跳转关系 编码为状态变量的赋值,还原的本质是重建状态转移图并识别条件分支点。
  • 状态 5 是公共出口(return),多个分支汇聚于此,对应原始函数中多个分支的汇合点。
  • 实际逆向中,状态变量可能是寄存器或栈变量,switch 可能被编译器优化为跳转表,但手动跟踪原理相同。
状态转移图(还原前) state=0 (入口) x > 10 ? 真 state=1 → r=1 假 state=2 x < 0 ? 真 state=3 → r=2 假 state=4 → r=3 state=5 → return r ■ 状态块 条件判断 → 真分支 → 假分支 - - 直接跳转

常见问题

  • 状态变量被混淆:实际 OLLVM 平坦化可能对 state 进行不透明谓词或编码变换,此时需先识别编码/解码逻辑,再恢复真实状态值。
  • 多个 dispatcher:复杂函数可能拆分为多个 switch 层级,需分层还原,先处理最外层,再逐层内嵌。
  • switch 默认分支:若存在 default 分支,通常对应原始逻辑中的异常路径或编译器填充的无效状态,可暂时忽略或标记为不可达。