符号执行导论:从具体到抽象的思维跃迁
说实话,我第一次接触符号执行时,脑子里就一个想法——这玩意儿不就是把程序当数学题做吗?后来真正啃下来才发现,这个想法既对也不对。今天咱们就来聊聊,符号执行到底是什么,它和咱们平时调试用的具体执行有啥区别,以及逆向工程里到底能用它干点啥。
什么是符号执行?
先别急着看定义。咱们从一个最直观的场景说起。
你写了个简单的C函数:
int check_password(int input) {
if (input == 0xdeadbeef) {
return 1; // 验证通过
} else {
return 0; // 验证失败
}
}
用具体执行来测试,你得给input传一个实际的值,比如0x12345678,然后看返回值是0还是1。但符号执行不一样——它把input变成一个符号变量,不赋值,只记录约束条件。
什么意思呢?
符号执行引擎会这样走:
- 遇到
if (input == 0xdeadbeef),它不判断真假,而是分叉成两条路径 - 路径1:假设
input == 0xdeadbeef成立,记录约束input == 0xdeadbeef - 路径2:假设
input != 0xdeadbeef成立,记录约束input != 0xdeadbeef
然后,它用SMT求解器(比如Z3)去解这些约束。路径1的约束一解,直接得到input = 0xdeadbeef。你看,不需要暴力穷举,一次求解就找到了触发验证通过的输入。
核心思想:符号执行把程序输入抽象成符号,用约束求解代替实际运行,从而覆盖所有可能的执行路径。
符号执行 vs 具体执行
我刚开始带团队时,有个新人问我:「老师,符号执行不就是自动化测试吗?」我说你想想看,具体执行和符号执行,本质上是两种完全不同的思维方式。
| 维度 | 具体执行 | 符号执行 |
|---|---|---|
| 输入 | 具体数值(如0x1234) | 符号变量(如α) |
| 变量值 | 确定的值 | 符号表达式(如α + 1) |
| 分支处理 | 只走一条路径 | 分叉成多条路径 |
| 路径覆盖 | 一次只覆盖一条 | 理论上覆盖所有路径 |
| 输出 | 具体结果 | 约束条件 + 求解结果 |
| 典型工具 | GDB, OllyDbg | Angr, KLEE, Triton |
举个例子你就明白了。假设有个程序:
int foo(int x, int y) {
int z = x + y;
if (z > 100) {
return z * 2;
} else {
return z / 2;
}
}
具体执行:你给x=30, y=20,算出z=50,走else分支,返回25。一次只看到一条路。
符号执行:x和y都是符号变量。走到if时,分叉成两条路径:
- 路径1:约束
x + y > 100,返回(x + y) * 2 - 路径2:约束
x + y <= 100,返回(x + y) / 2
然后求解器可以告诉你:要触发路径1,给x=60, y=50就行。要触发路径2,给x=10, y=20就行。
我的经验:具体执行像「摸着石头过河」,一次只能走一条路。符号执行像「上帝视角」,一次看到所有可能的路。但代价是——路径爆炸问题。我在逆向一个大型固件时,路径数量直接飙到百万级,求解器直接卡死。嗯,这个坑后面会专门讲。
符号执行在逆向工程中的应用场景
你可能会问:我平时用IDA、GDB逆向就够了,为啥要学符号执行?
我个人的看法是:符号执行解决的是逆向工程中那些「手工搞不定」的问题。具体来说,有这几个场景特别值得关注。
1. 自动化生成测试用例
逆向一个协议解析函数时,你看到一堆if-else嵌套,手动构造输入能累死。符号执行可以自动生成覆盖所有分支的输入。
我记得有一次逆向一个网络协议栈,里面有个状态机,光状态转换就有40多种。手工构造报文?一周都搞不定。用符号执行,半天就生成了所有状态的触发报文。
2. 漏洞挖掘与路径探索
符号执行特别擅长找那些「隐藏很深」的路径。比如:
void process_input(char *data, int len) {
if (len > 100) return;
if (data[0] == 'A' && data[1] == 'B' && data[2] == 'C') {
// 这里有个缓冲区溢出漏洞
memcpy(dst, data, len * 2); // 整数溢出!
}
}
手工逆向时,你可能会忽略len * 2这个整数溢出。但符号执行会同时追踪len的符号值和len * 2的约束,自动发现当len = 0x80000001时,len * 2会溢出成一个小值,从而绕过长度检查。
注意:符号执行不是万能的。我曾经在一个有循环的程序上跑符号执行,循环次数是符号变量,结果路径直接爆炸。后来加了循环边界约束才搞定。所以,实际使用时一定要结合具体执行做混合执行(concolic execution)。
3. 反混淆与代码还原
现在的恶意软件越来越喜欢用控制流平坦化(Control Flow Flattening)来混淆。符号执行可以自动还原原始的控制流。
原理很简单:符号执行会记录每条路径的约束条件,然后通过约束求解,把被混淆的跳转关系还原成原始的if-else结构。我处理过一个用Ollvm混淆的样本,IDA里看全是switch-case,用符号执行跑一遍,直接还原出清晰的流程图。
4. 协议逆向与状态机推断
符号执行可以自动推断协议的状态机。你只需要把网络数据包的关键字段设成符号变量,然后观察程序在不同输入下的行为路径,就能自动构建出状态转换图。
我之前逆向一个物联网设备的私有协议,就是用符号执行自动推断出握手、认证、数据传输三个状态,以及每个状态下的合法报文格式。手工做的话,至少得两周。
知识体系总览
下面这张图,是我自己总结的符号执行知识体系。你可以把它当作整个课程的地图。
这张图把符号执行拆成了三个维度:核心概念、关键技术、应用场景。你注意看,所有技术最终都落到「符号变量 + 路径约束 + SMT求解」这个三角上。说白了,符号执行就是这三样东西的组合拳。
写在前面的话
符号执行不是银弹。它有自己的适用范围和局限性。但如果你掌握了它,逆向工程的效率会提升一个量级。
我个人建议:刚开始学的时候,别急着上大型工具。先理解符号变量的思想,用手动推导的方式走几个小例子。等思维模式转过来了,再用Angr、KLEE这些工具去实战。
嗯,这一章就到这里。记住一句话:符号执行不是魔法,它只是把「猜输入」这件事,变成了「解方程」。