第2章:符号执行基础回顾:符号变量、路径约束、求解器交互,理解路径爆炸的根源
好,咱们正式开始。在深入angr的各种骚操作之前,我觉得有必要先把符号执行的老底儿翻出来晒晒。你可能会想:“这些基础我早会了,跳过吧。” 别急,我当年也是这么想的,结果第一次用angr分析一个真实固件时,路径爆炸直接把我干懵了。后来我才发现,很多问题其实就出在最基础的理解上。
说白了,符号执行就是让程序“带着问号”去跑。普通执行是给程序一个具体的输入,比如a=5,然后看结果。符号执行呢?它给程序一个符号变量,比如a = sym_a,这个sym_a可以代表任何可能的整数。程序跑完之后,我们得到的不再是一个具体的结果,而是一个关于sym_a的表达式。
2.1 符号变量:程序里的“未知数”
符号变量,你可以把它想象成初中数学里的x。只不过在程序里,它代表了一个内存位置或者一个寄存器的值。我个人习惯把符号变量看作是“带标签的占位符”。
核心概念:符号变量不是具体值,而是一个抽象表示。它携带了类型信息(比如位宽),但值是不确定的。
在angr中,创建符号变量很简单:
import angr
import claripy
# 创建一个32位的符号变量,名字叫"input"
sym_var = claripy.BVS("input", 32)
# 也可以创建位向量(BitVector)
# BVS = BitVector Symbolic
嗯,这里要注意:claripy是angr背后的符号求解引擎。你直接跟它打交道的机会不多,但理解它的存在很重要。我在项目中遇到过一个问题:符号变量的位宽搞错了,导致求解器一直返回UNSAT(无解)。排查了半天,才发现是32位和64位的混用。这种坑,踩过一次就记住了。
2.2 路径约束:程序走过的路
程序执行时,会遇到各种分支判断,比如if (x > 10)。在符号执行中,每遇到一个分支,执行引擎就会分裂成两个状态:一个走True分支,一个走False分支。每个状态都带着一个“路径约束”(Path Constraint),记录了它走过的所有分支条件。
举个例子:
// 假设 x 是符号变量
if (x > 10) {
// 路径约束: x > 10
y = x + 5;
} else {
// 路径约束: x <= 10
y = x * 2;
}
你看,同一个程序,因为x的不同取值,会走出两条完全不同的路径。每条路径的约束条件,就是它一路走来所有分支条件的“与”(AND)组合。
我的经验:路径约束是符号执行的核心。你想想看,求解器就是靠这些约束来反推出具体的输入值的。如果约束写错了或者漏了,结果肯定不对。
2.3 求解器交互:把约束变成答案
有了路径约束,我们怎么知道走这条路径需要什么输入?这就需要求解器(Solver)出马了。求解器的作用很简单:给定一组约束,它告诉你是否存在一组具体的值,能让所有约束同时成立。
在angr中,求解器的交互是通过solver对象完成的:
# 假设 state 是某个执行状态
# 获取当前状态的求解器
solver = state.solver
# 检查当前路径是否可行
if solver.satisfiable():
# 获取符号变量 input 的一个具体解
concrete_value = solver.eval(sym_var)
print(f"可行的输入值: {concrete_value}")
else:
print("这条路径不可达")
我曾经用angr分析一个CTF题目,程序逻辑很简单,但求解器一直返回UNSAT。我检查了所有约束,发现是符号变量的位宽和实际使用的不一致。这种问题,说白了就是类型不匹配。所以,我建议你在创建符号变量时,一定要确认好位宽。
2.4 路径爆炸:符号执行的阿喀琉斯之踵
好了,现在我们来聊聊最头疼的问题——路径爆炸。为什么符号执行会路径爆炸?原因其实很简单:
- 分支数量指数增长:每遇到一个
if,路径数量就翻倍。如果有N个分支,理论上最多有2^N条路径。 - 循环是罪魁祸首:循环中的每次迭代都可能产生分支。一个循环100次的
if,就能产生2^100条路径。这个数字,比宇宙中的原子还多。 - 函数调用链:函数调用也会引入分支,尤其是那些包含复杂逻辑的函数。
避坑指南:我曾经分析一个网络协议栈的固件,代码只有几千行,但符号执行跑了三天三夜都没结束。后来发现是一个while循环里嵌套了多个if-else,路径数量直接爆炸。从那以后,我养成了一个习惯:在跑符号执行之前,先静态分析一下代码结构,看看有没有明显的循环和分支热点。
为什么会这样?你想想看,符号执行引擎需要探索每一条可能的路径。如果程序有100个分支,那就是2^100条路径。即使每条路径只花1纳秒,也需要比宇宙年龄还长的时间才能跑完。这显然不现实。
下面这张图展示了路径爆炸的核心逻辑:
这张图很直观地展示了路径爆炸的威力。3层分支就有8条路径,10层分支就是1024条,20层分支就是100万条。真实程序动辄成百上千个分支,路径数量可想而知。
2.5 理解根源:为什么路径爆炸如此棘手?
路径爆炸的本质,是符号执行需要“穷举”所有可能的执行路径。而程序中的分支结构,天然就是一棵指数增长的树。
我总结了几点核心原因:
| 原因 | 说明 | 影响程度 |
|---|---|---|
| 分支数量 | 每个if/else都产生两条路径 | 高 |
| 循环结构 | 循环体中的分支会重复执行 | 极高 |
| 函数调用 | 被调用函数内部的分支也会被展开 | 中 |
| 符号变量范围 | 符号变量的取值范围越大,路径越多 | 中 |
我的建议:面对路径爆炸,不要想着“硬刚”。符号执行不是万能的。你需要学会识别哪些路径是重要的,哪些可以合并或忽略。后面的章节,我会详细介绍各种缓解技术,比如路径剪枝、状态合并、符号摘要等。
好了,这一章的内容就到这里。我们回顾了符号执行的三个核心概念:符号变量、路径约束和求解器交互。也理解了路径爆炸的根源——指数增长的路径数量。这些基础打牢了,后面讲解决方案时你才能听得明白。
公众号:蓝海资料掘金营,微信deep3321