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 漏洞:

  1. 到达定值分析:找到 free() 调用后,指针变量还有哪些定值到达了后续的使用点。
  2. 活跃变量分析:判断 free() 之后,指针变量是否还在活跃状态。如果不活跃,那 Use-After-Free 的风险就低。
  3. 可用表达式分析:检查某些条件表达式是否在多个路径上可用,避免因路径不一致导致的漏报。

你看,三种分析不是孤立的。它们就像三把手术刀,从不同角度切开代码,让你看到数据流动的全貌。

我的经验:刚开始学数据流分析时,别急着上复杂的框架。拿一张纸,画个控制流图,手动跑一遍数据流方程。跑上三五个例子,你就能感受到数据是怎么在代码里「流」的了。这个手感,比看一百页理论都管用。

嗯,数据流分析的基础就这些。别觉得简单,这三个分析是所有高级分析(比如污点分析、指针分析)的基石。搞不懂它们,后面的内容你会越看越迷糊。所以,花点时间,把这三个分析吃透。