3. 控制流图 (CFG)
说实话,控制流图这东西,我刚开始做静态分析的时候觉得它挺简单的——不就是把代码画成图嘛。但真正深入进去才发现,这里面的门道比想象中多得多。今天我就带你把这个基础打扎实。
什么是控制流图?
控制流图,简称CFG,是代码执行路径的抽象表示。说白了,就是把代码里「先走哪条路、后走哪条路」的关系画出来。
我习惯这么理解:代码就像一座迷宫,CFG就是这座迷宫的地图。每个房间是一个基本块,走廊就是边。有了这张地图,你才能分析哪些路径可能出问题。
核心定义:CFG是一个有向图 G = (V, E),其中V是基本块集合,E是控制流边集合。
基本块:代码的最小分析单元
基本块是CFG的基石。它是一段顺序执行的代码,只有一个入口和一个出口。
判断基本块的关键点有三个:
- 入口唯一:只能从块的第一条指令进入
- 出口唯一:只能从块的最后一条指令离开
- 内部无分支:块内不能有跳转指令
我在项目中遇到过一个问题:有人把整个函数当做一个基本块来分析,结果漏掉了中间的条件分支。嗯,这其实是个很常见的误区。
如何划分基本块?
划分基本块,我总结了一个三步法:
- 找领导者:代码的第一条指令、跳转目标、跳转指令的下一条
- 划范围:从领导者开始,到下一个领导者之前结束
- 连边:根据跳转关系连接基本块
来看个例子:
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,说白了就是把代码翻译成图。我常用的方法有两种:
方法一:手动构建(适合小规模代码)
对于几百行的函数,手动构建反而更快。步骤很简单:
- 通读代码,标记所有分支点(if、while、for、switch)
- 按基本块划分规则切分代码
- 画出基本块之间的连接关系
- 检查是否有不可达的基本块
方法二:自动构建(适合大规模代码)
对于真实项目,我建议用工具自动生成。常用的工具有:
| 工具 | 适用语言 | 特点 |
|---|---|---|
| LLVM | C/C++ | 支持多级优化,生成IR级别的CFG |
| JDT | Java | Eclipse的AST分析,精度高 |
| Angr | 二进制 | 支持反编译,适合逆向分析 |
避坑指南:自动构建CFG时,要注意间接跳转(比如函数指针、虚函数调用)。我曾经因为没处理间接跳转,漏掉了一个关键路径,导致分析结果完全不对。
构建CFG的实战要点
在实际项目中,我总结了几条经验:
- 处理异常路径:try-catch、异常处理都会产生额外的边,别漏了
- 注意循环:循环会产生回边,这是CFG分析的重点
- 函数调用:要不要展开内联?这取决于你的分析粒度
- 死代码:不可达的基本块要标记出来,它们可能是bug的征兆
我记得有一次分析一个网络协议栈,发现了一个死代码块。按理说那行代码永远不会执行,但攻击者通过精心构造的报文,硬是让程序走进了那个块。结果呢?一个远程代码执行漏洞。
CFG在漏洞挖掘中的应用
CFG不是画出来好看的,它是漏洞分析的基础工具。我常用的几个场景:
- 路径可达性分析:检查某个危险函数是否可以被攻击者控制的数据触发
- 循环分析:找出可能导致无限循环或资源耗尽的路径
- 条件覆盖:检查所有分支是否都被测试覆盖到
- 数据流分析:结合CFG追踪变量的传播路径
记住:CFG是静态分析的骨架。没有它,你的分析就是散的。有了它,你才能系统地找出代码中的问题。