控制流图(CFG):从代码到路径的抽象艺术

说实话,控制流图这东西,我刚开始做静态分析的时候觉得它挺简单的——不就是把代码画成图嘛。但真正深入进去才发现,这里面的门道比想象中多得多。今天我就带你把这个基础打扎实。

什么是控制流图?

控制流图,简称CFG,是代码执行路径的抽象表示。说白了,就是把代码里「先走哪条路、后走哪条路」的关系画出来。

我习惯这么理解:代码就像一座迷宫,CFG就是这座迷宫的地图。每个房间是一个基本块,走廊就是边。有了这张地图,你才能分析哪些路径可能出问题。

核心定义:CFG是一个有向图 G = (V, E),其中V是基本块集合,E是控制流边集合。

基本块:代码的最小分析单元

基本块是CFG的基石。它是一段顺序执行的代码,只有一个入口和一个出口。

判断基本块的关键点有三个:

  • 入口唯一:只能从块的第一条指令进入
  • 出口唯一:只能从块的最后一条指令离开
  • 内部无分支:块内不能有跳转指令

我在项目中遇到过一个问题:有人把整个函数当做一个基本块来分析,结果漏掉了中间的条件分支。嗯,这其实是个很常见的误区。

如何划分基本块?

划分基本块,我总结了一个三步法:

  1. 找领导者:代码的第一条指令、跳转目标、跳转指令的下一条
  2. 划范围:从领导者开始,到下一个领导者之前结束
  3. 连边:根据跳转关系连接基本块

来看个例子:

int example(int x, int y) {
    int z = 0;           // 基本块B1开始
    if (x > 0) {         // B1结束
        z = x + y;       // 基本块B2开始
    } else {             // B2结束
        z = x - y;       // 基本块B3开始
    }                    // B3结束
    return z;            // 基本块B4开始
}                        // B4结束

这段代码被划分为4个基本块:B1(初始化)、B2(if分支)、B3(else分支)、B4(返回)。

我的小技巧:写代码时养成习惯,把每个基本块控制在10条指令以内。这样生成的CFG更清晰,分析起来也更容易。

边:控制流的纽带

边就是基本块之间的控制流关系。它分为两种:

  • 无条件边:顺序执行,比如B1到B2
  • 条件边:分支跳转,比如B1到B2或B3

你想想看,边其实就代表了「程序可能怎么走」。在漏洞挖掘中,我们特别关注那些「不太可能走」的边——因为那里往往藏着bug。

我曾经分析过一个缓冲区溢出漏洞,就是因为一条异常处理路径上的边被忽略了。攻击者偏偏就走那条路,结果就出事了。

CFG的构建方法

构建CFG,说白了就是把代码翻译成图。我常用的方法有两种:

方法一:手动构建(适合小规模代码)

对于几百行的函数,手动构建反而更快。步骤很简单:

  1. 通读代码,标记所有分支点(if、while、for、switch)
  2. 按基本块划分规则切分代码
  3. 画出基本块之间的连接关系
  4. 检查是否有不可达的基本块

方法二:自动构建(适合大规模代码)

对于真实项目,我建议用工具自动生成。常用的工具有:

工具 适用语言 特点
LLVM C/C++ 支持多级优化,生成IR级别的CFG
JDT Java Eclipse的AST分析,精度高
Angr 二进制 支持反编译,适合逆向分析

避坑指南:自动构建CFG时,要注意间接跳转(比如函数指针、虚函数调用)。我曾经因为没处理间接跳转,漏掉了一个关键路径,导致分析结果完全不对。

构建CFG的实战要点

在实际项目中,我总结了几条经验:

  • 处理异常路径:try-catch、异常处理都会产生额外的边,别漏了
  • 注意循环:循环会产生回边,这是CFG分析的重点
  • 函数调用:要不要展开内联?这取决于你的分析粒度
  • 死代码:不可达的基本块要标记出来,它们可能是bug的征兆

我记得有一次分析一个网络协议栈,发现了一个死代码块。按理说那行代码永远不会执行,但攻击者通过精心构造的报文,硬是让程序走进了那个块。结果呢?一个远程代码执行漏洞。

CFG在漏洞挖掘中的应用

CFG不是画出来好看的,它是漏洞分析的基础工具。我常用的几个场景:

  1. 路径可达性分析:检查某个危险函数是否可以被攻击者控制的数据触发
  2. 循环分析:找出可能导致无限循环或资源耗尽的路径
  3. 条件覆盖:检查所有分支是否都被测试覆盖到
  4. 数据流分析:结合CFG追踪变量的传播路径

记住:CFG是静态分析的骨架。没有它,你的分析就是散的。有了它,你才能系统地找出代码中的问题。

好了,关于CFG的基础知识就讲到这里。下一章我们会深入讨论如何用CFG做路径敏感分析,那才是真正有意思的部分。