4、符号执行基础:符号变量与具体值、路径约束、路径爆炸问题

好,咱们今天聊聊符号执行的核心概念。说实话,很多人一上来就被「符号执行」这四个字吓住了,觉得是什么高深莫测的黑魔法。其实没那么玄乎,说白了就是让程序在「模糊」状态下跑一遍,看看它能走哪些路。

我个人习惯把符号执行比作「侦探破案」。具体执行就像警察跟着嫌疑人走一条确定的路线,而符号执行呢,就像把所有可能的逃跑路线都画出来,然后逐一分析。嗯,这个比喻可能不太严谨,但意思到了。

4.1 符号变量 vs 具体值

先搞清楚最基础的东西。具体值,就是我们在代码里直接写死的数字,比如 int x = 5。程序执行时,x 就是 5,没得商量。

符号变量就不一样了。它不是一个具体的数,而是一个「占位符」,代表所有可能的取值。在 angr 里,我们用 claripy.BVS() 来创建它。

核心区别一句话:具体值是「死」的,符号变量是「活」的。

来看个例子:

import angr
import claripy

# 创建一个 32 位的符号变量,名字叫 "input"
sym_var = claripy.BVS("input", 32)

# 创建一个具体值
concrete_val = 0x12345678

# 符号变量可以参与运算,但结果还是符号的
result = sym_var + 10
# result 现在代表 "input + 10" 这个表达式

我在项目中遇到过一个问题:有人把符号变量当具体值用,直接打印出来看。结果发现输出是一串看不懂的表达式。嗯,这很正常,因为符号变量本身不「求值」,它只记录约束关系。

小技巧:如果你想知道符号变量在某个约束下具体等于多少,需要用求解器去解。angr 里用 solver.eval() 来获取具体值。

4.2 路径约束:程序的分岔路口

程序执行时,遇到 if-else、switch 这些分支语句,就会产生不同的路径。符号执行的核心工作之一,就是记录每条路径上的「约束条件」。

举个例子,假设我们有这么一段代码:

int check(int input) {
    if (input > 10) {
        return 1;
    } else {
        return 0;
    }
}

如果用具体值执行,比如 input=5,那程序只会走 else 分支。但符号执行不一样,它会把 input 当作符号变量,然后同时探索两条路:

  • 路径 A:约束条件为 input > 10,返回 1
  • 路径 B:约束条件为 input <= 10,返回 0

每条路径都带着自己的「约束集」。这个约束集,就是路径约束(Path Constraints)。

路径约束的本质:就是一组逻辑公式,描述了「要走到这条路径,输入必须满足什么条件」。

在 angr 里,我们可以通过 state.solver.constraints 来查看当前状态的约束集。我曾经调试一个反调试样本时,就是靠打印约束集才发现某个分支被刻意隐藏了——嗯,那又是另一个故事了。

4.3 路径爆炸:符号执行的阿喀琉斯之踵

好,现在问题来了。你想想看,如果程序里有 10 个 if-else,理论上就有 2^10 = 1024 条路径。如果有 100 个呢?2^100,这个数字比宇宙中的原子数还多。

这就是路径爆炸问题。说白了,就是路径数量随着分支数量指数级增长,最终导致内存爆炸、时间爆炸,什么都炸。

分支数量 理论路径数 实际可处理性
10 1024 轻松
20 约 100 万 吃力
30 约 10 亿 几乎不可能
50+ 天文数字 必须优化

为什么会这样?因为符号执行是「穷举」的。它不像人脑会偷懒,它会老老实实地把每条路都走一遍。遇到循环更是灾难——一个循环跑 100 次,每次里面有个 if,那就是 2^100 条路径。

避坑指南:我曾经接手过一个样本,里面有个循环 1000 次的解密逻辑,每次循环都有 3 个分支。直接跑符号执行,结果 10 分钟后内存爆了。后来我改用「循环展开 + 路径剪枝」才搞定。所以,遇到循环一定要小心。

4.4 应对路径爆炸的常见策略

路径爆炸听起来吓人,但也不是无解。我总结了几种常用的应对手段:

  • 路径剪枝:如果某条路径的约束已经不可能满足(比如要求 x > 5 且 x < 3),直接丢弃。angr 会自动做这件事。
  • 符号执行 + 具体执行混合:对于不重要的分支,直接用具体值执行,减少路径数量。angr 的 SimProcedure 就是干这个的。
  • 状态合并:把多个相似的状态合并成一个,用「或」约束来表示。但合并后约束会变复杂,求解器压力反而可能增大。
  • 限制探索深度:设置最大路径长度或最大状态数,超过就放弃。虽然可能漏掉一些路径,但至少能跑完。

我个人习惯:先用 angr.Projectexplore() 方法跑一遍,看看大概有多少路径。如果路径数超过 1000,我就会考虑加一些限制条件,比如 findavoid 参数,只关注目标路径。

4.5 本章知识体系总览

下面这张图,是我自己画的,把本章的核心逻辑串起来了。你一看就明白:

符号执行基础:核心知识体系 符号变量 占位符,代表所有可能值 路径约束 每条路径的逻辑条件 路径爆炸 指数级增长问题 BVS() 创建 表达式运算 if-else 分支 约束集记录 循环展开 路径剪枝 核心目标:用符号变量探索所有路径,用约束求解找到答案 路径爆炸是最大挑战,需要结合剪枝、合并、混合执行等策略应对

这张图把三个核心概念串起来了。符号变量是基础,路径约束是桥梁,路径爆炸是挑战。理解了这三者的关系,符号执行你就入门了。

总结一下:符号执行不是万能药,但对付反调试和反逆向,它确实是一把利器。关键在于理解它的边界——知道什么时候该用,什么时候不该用。路径爆炸是绕不开的坎,但掌握了剪枝和混合执行,大部分实战场景都能应付。

好,这一章就到这儿。记住我说的:符号变量是活的,路径约束是钥匙,路径爆炸是敌人。下次遇到反调试,先想想能不能用符号执行来绕——说不定有惊喜。


公众号:蓝海资料掘金营,微信deep3321