4、数据流分析基础:到达定值分析、活跃变量分析、可用表达式分析
好,咱们今天聊点硬核的。数据流分析,说白了就是静态分析的核心引擎。你想想看,编译器也好,漏洞扫描器也罢,它们凭什么能知道变量在哪儿被赋值、在哪儿被使用?凭的就是数据流分析。
我个人习惯把数据流分析比作「代码的血液循环系统」。数据就像血液,变量就是血管,而赋值和使用就是心脏的跳动。搞懂了这个,你才能看懂漏洞是怎么传播的。
4.1 到达定值分析(Reaching Definitions)
先问个问题:一段代码里,某个变量在某个位置的值,到底是由哪一行赋值的?这就是到达定值分析要回答的。
定义很简单:如果存在一条从定值语句 d 到程序点 p 的路径,且路径上 d 没有被杀死(即变量没有被重新赋值),那么定值 d 就到达了 p。
嗯,这里要注意「杀死」这个词。一个变量被重新赋值,之前的定值就被杀死了。
核心公式(数据流方程):
IN[B] = ∪ OUT[p] (p 是 B 的前驱基本块)
OUT[B] = GEN[B] ∪ (IN[B] - KILL[B])
其中 GEN[B] 是基本块 B 内新生成的定值,KILL[B] 是 B 内被覆盖的定值。
我在项目中遇到过这样一个坑:分析一个大型 C 项目时,发现某个全局变量在 500 行后突然变成了野指针。用到达定值一追,发现中间有个条件分支,某个路径上变量根本没被初始化。这种问题,人工看代码根本盯不住。
实战技巧:做漏洞挖掘时,重点关注「未到达的定值」。如果一个变量在某个路径上没有任何定值到达,那它就是个未初始化变量,典型的漏洞入口。
4.2 活跃变量分析(Live Variable Analysis)
这个分析问的是:在程序点 p,变量 x 的值是否会在未来被使用?如果会,x 就是活跃的;否则就是死的。
你可能会问:「知道这个有什么用?」用处大了去了。寄存器分配、死代码消除,都靠它。更重要的是,在漏洞挖掘里,活跃变量分析能帮你判断一个变量是否「值得跟踪」。
举个例子:
int x = 10; // 定值
x = 20; // 再次定值,第一次的定值被杀死了
printf("%d", x); // 使用 x,x 在这里是活跃的
// 之后 x 不再被使用,x 变成死的
数据流方程是反着来的,因为活跃性是从后往前分析:
OUT[B] = ∪ IN[s] (s 是 B 的后继基本块)
IN[B] = USE[B] ∪ (OUT[B] - DEF[B])
这里 USE[B] 是在 B 中被使用但之前未定义的变量,DEF[B] 是在 B 中被定义的变量。
我曾经在分析一个缓冲区溢出漏洞时,就是靠活跃变量分析锁定了关键变量。那个变量在函数末尾被释放了,但在释放之前还有一次写操作。活跃分析告诉我,这个变量在写操作时是活跃的,所以写操作是有效的,漏洞就藏在那次写操作里。
避坑指南:我曾经在分析循环时翻过车。循环体内的变量活跃性会反复变化,必须迭代到不动点。别偷懒,一定要做不动点迭代,否则结果全是错的。
4.3 可用表达式分析(Available Expressions)
这个分析稍微抽象一点。它问的是:在程序点 p,某个表达式 e 是否已经被计算过,且其操作数没有被重新赋值?
说白了,就是「能不能复用之前算过的结果」。编译器用它来做公共子表达式消除,漏洞挖掘里用它来检测重复计算导致的性能问题,或者更隐蔽的——某些表达式在特定路径上不可用,导致计算结果不一致。
数据流方程:
IN[B] = ∩ OUT[p] (p 是 B 的前驱基本块)
OUT[B] = GEN[B] ∪ (IN[B] - KILL[B])
注意这里的交运算。一个表达式要到达基本块 B,必须在所有前驱路径上都可用。这跟到达定值不一样,到达定值用的是并运算。
为什么用交?你想想看,如果有一条路径上表达式不可用,那在 B 的入口处就不能保证这个表达式已经被计算过。安全第一嘛。
三种分析的对比:
| 分析类型 | 方向 | 交汇运算 | 典型应用 |
|---|---|---|---|
| 到达定值 | 前向 | ∪(并) | 未初始化变量检测 |
| 活跃变量 | 后向 | ∪(并) | 死代码消除、寄存器分配 |
| 可用表达式 | 前向 | ∩(交) | 公共子表达式消除 |
我个人习惯在做漏洞挖掘时,把这三种分析组合起来用。到达定值告诉我数据从哪来,活跃变量告诉我数据往哪去,可用表达式告诉我哪些计算可以复用。三管齐下,代码的流动就全在掌控之中了。
4.4 三种分析的协同应用
光讲理论没意思,咱们来点实际的。假设你在审计一段代码,怀疑有 Use-After-Free 漏洞:
- 到达定值分析:找到 free() 调用后,指针变量还有哪些定值到达了后续的使用点。
- 活跃变量分析:判断 free() 之后,指针变量是否还在活跃状态。如果不活跃,那 Use-After-Free 的风险就低。
- 可用表达式分析:检查某些条件表达式是否在多个路径上可用,避免因路径不一致导致的漏报。
你看,三种分析不是孤立的。它们就像三把手术刀,从不同角度切开代码,让你看到数据流动的全貌。
我的经验:刚开始学数据流分析时,别急着上复杂的框架。拿一张纸,画个控制流图,手动跑一遍数据流方程。跑上三五个例子,你就能感受到数据是怎么在代码里「流」的了。这个手感,比看一百页理论都管用。
嗯,数据流分析的基础就这些。别觉得简单,这三个分析是所有高级分析(比如污点分析、指针分析)的基石。搞不懂它们,后面的内容你会越看越迷糊。所以,花点时间,把这三个分析吃透。
下一章,咱们会把这些分析用到实际的漏洞挖掘场景中。到时候你会发现,原来漏洞在数据流面前,几乎是透明的。