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;
}
}
}
手动还原步骤
- 识别 dispatcher 与状态变量:确认
state是唯一的分发变量,switch(state)是分发器,每个case对应一个原始基本块。 - 绘制状态转移关系:逐个 case 记录 state 的取值变化,形成状态转移表:
0 → (x>10 ? 1 : 2),1 → 5,2 → (x<0 ? 3 : 4),3 → 5,4 → 5,5 → return - 合并无分支链:状态 1、3、4 都是直接赋值后跳转到 5,说明它们是互斥分支的终点;状态 0 和 2 是条件判断点。
- 重构控制流:从状态 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 可能被编译器优化为跳转表,但手动跟踪原理相同。
常见问题
- 状态变量被混淆:实际 OLLVM 平坦化可能对 state 进行不透明谓词或编码变换,此时需先识别编码/解码逻辑,再恢复真实状态值。
- 多个 dispatcher:复杂函数可能拆分为多个 switch 层级,需分层还原,先处理最外层,再逐层内嵌。
- switch 默认分支:若存在 default 分支,通常对应原始逻辑中的异常路径或编译器填充的无效状态,可暂时忽略或标记为不可达。