控制流平坦化(CFF)
控制流平坦化(Control Flow Flattening,简称 CFF) 是一种高级的代码混淆技术。它的核心目的是将程序原本清晰、有层次的执行流程(如树状或网状结构)“压扁”,变成一个扁平的、难以阅读的结构。
1. 核心概念:从“树状”到“扁平”
在正常的程序中,代码的执行流通常是层级化的。
-
正常结构:像一棵树或一张有向图。函数 A 调用函数 B,B 中有一个
if-else分支,循环 C 在分支内部。反汇编工具(如 IDA Pro)生成的控制流图(CFG)会显示出清晰的块与块之间的跳转关系,分析者一眼就能看出“如果满足条件 X,就跳到代码块 Y”。 -
平坦化结构:像一个中心调度站。所有的代码块(Basic Blocks)都被打散,并排放在同一个层级上。它们不再直接互相跳转,而是执行完自己的任务后,把控制权交还给一个“中央调度器”(Dispatcher)。调度器根据当前的“状态变量”,决定下一个让谁执行。
2. 代码层面的实现原理
控制流平坦化通常由三个核心组件构成:
-
状态变量(State Variable):一个用于记录“当前程序执行到哪一步”的变量(在之前的题目中就是那个
char c)。 -
调度器(Dispatcher):通常是一个无限循环
while(true)包裹着一个巨大的switch语句。它负责检查状态变量,并跳转到对应的代码块。 -
原代码块(Original Basic Blocks):原本的业务逻辑被切碎成
case 0、case 1、case 2... 放在switch的不同分支里。
举例
假设我们需要通过这一段简单的 C 语言逻辑来理解:
正常代码(Normal Code):
void check_password(int input) {
printf("Checking..."); // 步骤 A
if (input == 1234) { // 步骤 B (判断)
printf("Correct!"); // 步骤 C
} else {
printf("Wrong!"); // 步骤 D
}
printf("Done."); // 步骤 E
}
逻辑流向非常直观:A -> B -> (C 或 D) -> E。
平坦化后的代码(Flattened Code):
void check_password_flattened(int input) {
int state = 1; // 1. 状态变量初始化
while (1) { // 2. 巨大的循环
switch (state) { // 3. 调度器 (Dispatcher)
case 1: // 对应 步骤 A
printf("Checking...");
state = 2; // 更新状态,指向下一步
break;
case 2: // 对应 步骤 B
if (input == 1234) {
state = 3; // 准备去 C
} else {
state = 4; // 准备去 D
}
break;
case 3: // 对应 步骤 C
printf("Correct!");
state = 5; // 准备去 E
break;
case 4: // 对应 步骤 D
printf("Wrong!");
state = 5; // 准备去 E
break;
case 5: // 对应 步骤 E
printf("Done.");
state = 0; // 0 表示结束
break;
case 0: // 结束块
return;
}
}
}
3. 目的
控制流平坦化极大地增加了逆向工程的成本:
-
破坏反编译器的逻辑恢复:工具(如 IDA 的 F5 功能或 JADX)依赖代码的特定模式来还原
if、while等结构。一旦结构被打碎成switch-case,反编译器就只能生成一堆goto或巨大的switch,导致生成的伪代码极难阅读。 -
隐藏执行顺序:在静态分析中,无法直接看到“块 A 之后执行块 B”。只能看到“块 A 把状态改成了 X”,然后必须去寻找“谁是 case X”。如果状态变量的计算过程再复杂一点(例如
state = (x * 3) ^ key),静态追踪几乎是不可能的。 -
视觉疲劳:逆向工程师通过肉眼观察图形化视图(Graph View)时,看到的不再是逻辑树,而是一团乱麻,很难通过形状判断程序的意图。
4. 反混淆思路
-
符号执行(Symbolic Execution):使用工具(如 Miasm, Triton, Angr)模拟代码执行。不关心具体数值,只关心状态变量的变化。通过模拟执行,可以算出“在块 A 结尾,状态变量一定会变成 X”,从而重建块 A 到块 X 的直接连线。
-
编译器优化:平坦化本质上是引入了冗余的控制流。一些高级的编译器优化算法(如死代码消除、常量传播)在一定程度上能识别出这种固定的跳转模式,并将其优化回原始结构。
-
模式匹配脚本:针对特定的混淆器(如 OLLVM),编写脚本(如 IDA Python)自动寻找分发器和状态变量,修改二进制代码中的跳转指令,手动“修补”回正常的层级结构。