2、控制流混淆基础:基本块、控制流图(CFG)、常见混淆模式

好,咱们正式开始聊控制流混淆。说实话,很多人在逆向分析时一碰到混淆代码就头大,觉得无从下手。其实你只要把基础概念吃透了,再花哨的混淆也就是那几招。今天我就带你把这些基础打扎实。

2.1 基本块:代码的最小单元

先说说基本块。什么叫基本块?说白了,就是一段只能从入口进、只能从出口出的连续指令序列。中间没有跳转,没有分支,一条道走到黑。

我举个例子你就明白了:

0x100: mov eax, [ecx]
0x103: add eax, 1
0x106: mov [edx], eax
0x109: jmp 0x200   ; 这里结束基本块

从 0x100 到 0x106 这三条指令,就是典型的一个基本块。为什么?因为执行顺序是固定的,没有岔路。到了 0x109 的 jmp,基本块就结束了——因为控制流从这里开始分叉了。

我的经验:在 IDA Pro 里,基本块通常用不同颜色的矩形框表示。我刚开始学逆向时,总喜欢盯着汇编指令一行行看,效率极低。后来我养成了一个习惯:先看基本块,再看基本块之间的连线。这样整个函数的骨架一下子就清晰了。

2.2 控制流图(CFG):程序的交通地图

基本块是节点,那控制流图就是把这些节点用有向边连起来。每个节点是一个基本块,每条边代表一个可能的执行路径。

你想想看,一个正常的函数,它的 CFG 长什么样?通常是有规律的:一个入口,几个分支,几个循环,最后汇聚到一个出口。就像城市交通图,主干道清晰,岔路不多。

但混淆后的 CFG 就不一样了。我见过最夸张的一个样本,一个只有 50 行代码的函数,反编译出来的 CFG 有 200 多个基本块。密密麻麻的箭头,看得人头皮发麻。

核心要点:CFG 是逆向分析的导航图。你读懂了 CFG,就读懂了程序的执行逻辑。混淆的本质,就是把这个导航图搞得面目全非。

下面这张图展示了正常 CFG 和混淆后 CFG 的对比:

正常 CFG 入口基本块 分支A 分支B 循环体 出口基本块 混淆后 CFG

2.3 常见混淆模式之一:不透明谓词

不透明谓词,名字听着唬人,其实原理很简单。就是构造一个结果永远固定的条件判断,但让分析工具看不出来。

比如这段代码:

int x = 1, y = 2;
if (x * x + y * y == 5) {  // 永远为真
    // 真实逻辑
} else {
    // 虚假逻辑,永远不会执行
}

你看,1² + 2² = 5,这个条件永远成立。但静态分析工具很难在编译前就确定这一点。于是它不得不把两个分支都分析一遍,白白浪费时间和精力。

避坑指南:我曾经在一个恶意软件样本里遇到过这种情况——不透明谓词里藏了反调试代码。如果你用调试器单步跟踪,走到虚假分支时就会触发反调试检测。所以我的建议是:遇到不透明谓词,先别急着跟,用符号执行或者动态污点分析来搞定它。

不透明谓词常见的构造方式有几种:

类型 原理 例子
算术不透明 利用数学恒等式 (x+1)² - x² - 2x ≡ 1
指针不透明 利用内存别名分析困难 两个指针指向同一地址
并发不透明 利用线程调度不确定性 竞争条件判断

2.4 常见混淆模式之二:控制流平坦化

这个就更有意思了。控制流平坦化,说白了就是把一个正常的、有层次结构的 CFG,拍成一张扁平的大饼

怎么拍的呢?核心思路是引入一个状态变量和一个主分发器。原来的每个基本块都被改造成一个 case 分支,通过修改状态变量来跳转到下一个基本块。

看个例子就明白了。原始代码是这样的:

if (a > 0) {
    b = 1;
} else {
    b = 2;
}
c = b + 3;

经过控制流平坦化后,变成这样:

int state = 0;
while (1) {
    switch (state) {
        case 0:
            if (a > 0) state = 1;
            else state = 2;
            break;
        case 1:
            b = 1;
            state = 3;
            break;
        case 2:
            b = 2;
            state = 3;
            break;
        case 3:
            c = b + 3;
            state = -1;  // 结束
            break;
    }
    if (state == -1) break;
}

你发现没有?原来的 if-else 结构完全消失了。取而代之的是一个巨大的 switch 循环。所有基本块都在同一层级,看不出哪个是分支、哪个是循环。

我的感受:我第一次遇到控制流平坦化时,对着反编译出来的代码看了整整一下午,愣是没看出逻辑来。后来我才明白,对付这种混淆,关键是要恢复状态变量和基本块之间的跳转关系。符号执行在这方面特别管用。

2.5 两种混淆的对比

咱们把这两种混淆放在一起对比一下:

特性 不透明谓词 控制流平坦化
目标 增加虚假路径 消除结构层次
CFG 变化 增加分支数量 变成扁平大循环
分析难度 中等 较高
常见组合 常与平坦化配合 常与不透明谓词配合
实用技巧:在实际样本中,这两种混淆经常组合使用。比如在控制流平坦化的每个 case 分支里,再插入不透明谓词。这样既扁平化了结构,又增加了虚假路径。嗯,对付这种组合拳,我一般先用符号执行恢复平坦化,再用约束求解剔除不透明谓词。

好了,这一章的基础概念就讲到这里。基本块、CFG、不透明谓词、控制流平坦化——这四个概念是理解后续所有混淆技巧的基石。下一章我们会深入实战,看看如何用符号执行来破解这些混淆。


专注资料整理