近些年符号执行因为其具有生成高覆盖率的测试用例和在复杂程序中发现更深层次的错误的能力,而再次受到关注。本篇博客简单探讨符号执行的入门知识,力求用最精简的语言告诉初学者什么是符号执行。其大部分内容来自 英国帝国理工 的 Cristian Cadar 发表在 ACM 通讯上的一篇短文 Symbolic Execution for Software Testing: Three Decades Later,这篇论文用非常简单的案例和通俗的语言阐释了符号执行的基本原理和细节,是为数不多的科普性论文。
经典符号执行
这是一个论证符号执行的简单案例,用户输入 x、y,在 line9 存在一个错误分支。传统的模糊测试会编译 x、y 值,最终走到错误分支,触发 bug。而符号执行则不然,符号执行的关键是**使用符号值代替具体值, 并将程序变量的值表示为符号输入值的符号表达式 , 其结果是程序计算的输出值被表示为符号输入值的函数 **
The key idea behind symbolic execution is to use symbolic values, instead of concrete data values as input and to represent the values of program variables as symbolic expressions over the symbolic input values.
其实看到上面的句子还是一头雾水,没关心,举个简单例子就明白了,如下所示
int twice(int v) {
return 2 * v;
}
void testme(int x, int y) {
z = twice(y);
if (z == x) {
if (x > y + 10)
ERROR;
}
}
int main() {
x = sym_input();
y = sym_input();
testme(x, y);
return 0;
}
一个符号执行的路径就是一个 true 和 false 组成的序列,其中第 i 个 true(或false)表示在该路径的执行中遇到的第 i 个条件语句,并且走的是 then(或else) 这个分支。一个程序所有的执行路径可以用执行树(Execution Tree)来表示。 testme
函数可以转化为下面的执行树
函数有 3 条执行路径,{x = 0, y =1} 、{x = 2, y = 1} 和 {x = 30, y = 15} 即可遍历这 3 条路径。符号执行的目标就是生成这样的输入集合,在给定时间内遍历所有路径。
Symbolic execution maintains a symbolic state σ, which maps variables to symbolic expressions, and a symbolic path constraint PC, which is a quantifier-free first-order formula over symbolic expressions. At the beginning of a symbolic execution, σ is initialized to an empty map and PC is initialized to true. Both σ and PC are updated during the course of symbolic execution. At the end of a symbolic execution along an execution path of the program, PC is solved using a constraint solver to generate concrete input values. If the program is executed on these concrete input values, it will take exactly the same path as the symbolic execution and terminate in the same way.
符号执行维护符号状态 σ 和符号路径约束 PC
- σ 表示变量到符号表达式的映射
- PC 是符号表示的不含量词的一阶表达式
在符号执行开始时,σ 初始化为空映射,PC 初始化为真,在符号执行的过程中不断变化。在对程序的某一路径分支进行符号执行的终点,把 PC 输入约束求解器获得求解。如果用这些具体的输入值输入程序执行,它将会和符号执行运行在同一路径,并且以相同方式结束。
符号状态 σ
如上文所提的例子,符号状态 σ 开始为空,符号路径约束 PC 为 true。每次遇到 var = sym_input()
语句,符号执行就会向 σ 添加一个 var->s
的映射,s
是一个新的不受约束的符号值。程序 lines 14-15 执行结果是 σ = {x ->
x
0
x_0
x0, y ->
y
0
y_0
y0} 。每当遇到赋值语句 v = e
,符号执行会更新 σ,添加一个 v 到 σ(e) 的映射。程序 lines 6 执行结果 σ = {x ->
x
0
x_0
x0, y ->
y
0
y_0
y0, z -> 2
y
0
y_0
y0}。
符号路径约束 PC
每个条件语句 if (e) S1 else S2
,PC 更新为 PC∧σ(e),表示 then 分支;同时生成新的路径约束 PC‘ ,并且初始化为 PC∧¬σ(e),表示为 else 分支。如果 PC 满足,则程序走到 then 分支,如果 PC’ 满足,那么走到 else 分支。请注意,符号执行与实际执行的不同,符号执行两条路径都是可以走的,只需要分别维护它们的状态。如果 PC 和 PC‘ 都不能满足,符号执行就会在对应的位置终止。例如,程序 lines 7 执行建立符号执行实例,路径约束
2
y
0
=
x
0
2y_0 = x_0
2y0=x0 和
2
y
0
≠
x
0
2y_0 ≠ x_0
2y0=x0;程序 lines 8 继续建立两个实例,分别是
(
x
0
=
2
y
0
)
(
x
0
>
y
0
+
10
)
(x_0 = 2y_0) ^ (x_0 > y_0 + 10)
(x0=2y0)(x0>y0+10) 和
(
x
0
=
2
y
0
)
(
x
0
≤
y
0
+
10
)
(x0 = 2y0) ^ ( x_0 ≤ y_0 + 10 )
(x0=2y0)(x0≤y0+10)。
符号执行遇到 exit 或者 error(比如程序崩溃或者违反断言),当前实例将会终止。此时利用约束求解器对当前路径约束求解,得到测试输入;如果程序输入这些实际值,就会在同样路径结束。
传统符号执行的缺陷
如果代码包含递归或者循环,且终止条件符号化,可能会产生无数条路径(路径爆炸),例如
void testme_inf() {
int sum = 0;
int N = sym_input();
while(N > 0) {
sum = sum + N;
N = sym_input();
}
}
这段程序执行路径包含两种状态:无数的 trues 加上一个 false,或者是无数的 trues。第一种状态
(
⋀
i
∈
[
1
,
n
]
N
i
>
0
)
Λ
(
N
n
+
1
≤
0
)
({\bigwedge \atop i\in[1,n]} N_i > 0) \Lambda (N_{n+1} \leq 0)
(i∈[1,n]⋀Ni>0)Λ(Nn+1≤0)
其中每个 Ni 都是一个新的符号值,执行结束的符号状态为
N
→
N
n
+
1
,
s
u
m
→
∑
i
∈
[
1
,
n
]
N
i
{N \rightarrow N_{n+1}, sum \rightarrow \sum_{i\in[1,n]} N_i}
N→Nn+1,sum→∑i∈[1,n]Ni。在实践中我们需要通过一些方法限制这样的搜索,比如限制时间或者路径数量。
A key disadvantage of classical symbolic execution is that it cannot generate an input if the symbolic path constraint along an execution path contains formulas that can-not be (efficiently) solved by a constraint solver.
传统符号执行一个关键缺陷是,当符号路径约束包含了不能通过约束求解器求解的公式时,就不能得到输入值。现在假设我们的约束求解器不能解决非线性运算,如果把 twice
换成如下函数
int twice(int v) {
return (v * v) % 50;
}
这时符号路径约束
y
0
∗
y
0
m
o
d
50
=
x
0
y_0 *y_0 mod 50 = x_0
y0∗y0mod50=x0 和
y
0
∗
y
0
m
o
d
50
≠
x
0
y_0 * y_0 mod 50 ≠ x_0
y0∗y0mod50=x0无法求解。或者 twice
本身就不是一个具体的函数,如引用的外部函数,则路径约束
t
w
i
c
e
(
y
0
)
=
x
0
twice(y_0) = x_0
twice(y0)=x0 和
t
w
i
c
e
(
y
0
)
≠
x
0
twice(y_0) ≠ x_0
twice(y0)=x0 同样无法通过计算得到输入值。
现代符号执行
很快大家发现在分析过程中,如果能加入具体值进行分析,将大大简化分析过程,降低分析的难度和提升效率,无法避免的还是需要将各种条件表达式,进行符号化抽象后变成约束条件参与执行。将程序语句转换为符号约束的精度,对符号执行所达到的覆盖率以及约束求解的可伸缩性会产生重大影响。所以如何做好混合具体(Concrete)执行和符号(Symbolic)执行的能力的平衡,就成为现代符号执行的关键点。
One of the key elements of modern symbolic execution techniques is their ability to mix concrete and symbolic execution.
现代符号执行最显著的特点是将实际执行和符号执行结合起来。
Concolic Testing
混合符号执行由 K. Sen(库希克·森)于2005年提出,混合符号执行核心思想是在程序运行过程中,判断哪些代码需要符号化执行,哪些代码可以直接执行,代表工具有 EXE、CUTE 和 DART 等。
EGT
执行生成测试(Execution-Generated Testing)。在执行每个操作前,动态检查每个相关值是否是具体的,如果是具体的,就按照正常源码执行,如果至少有一个变量是符号化的,就符号化执行,并为当前路径更新符号路径约束 PC。
传统符号执行中,因为外部函数或者无法进行约束求解造成的问题通过使用实际值得到了解决。但同时因为在执行中使用了实际值,固定了某些执行路径,由此将造成路径完成性的缺失。
挑战和方案
路径爆炸 (Path Explosion)
符号执行面临最直接的问题就是路径爆炸,即在一段循环或者递归代码中,由于同一个路径约束,可能会造成程序无限循环下去,导致产生无数条路径分支。从宏观角度讲
- 限制符号执行时间
- 限制循环迭代次数
都可以限制路径数量,从具体方法来讲
- 启发式地优先探索当前最有效的路径
- static control-flow graph (CFG)
- interleave symbolic ex-ploration with random testing
- 使用可靠的程序分析技术降低路径探索的复杂性
- select expressions
- 通过缓存和重用底层函数的计算结果,减小分析的复杂性
约束求解 (Constraint Solving)
虽然近几年约束求解器的能力有明显提升,但依然是符号执行的关键瓶颈之一。因此,实现约束求解器的优化十分重要。
- 不相关约束消除
- 增量求解
通过重用以前相似请求得到的结果,可以提升约束求解的速度,这种方法被运用到了 CUTE 和 KLEE 中。
内存建模 (Memory Modeling)
将程序语句转换为符号约束的精度对符号执行实现的覆盖率以及约束解决的可伸缩性有很大的影响。例如,使用一个内存模型近似固定宽度的整数变量与实际数学整数可能更有效,但另一方面可能导致不精确的分析代码取决于角落算术溢出等情况下,这可能导致符号执行遗漏路径或探索不可行。
其实,内存建模可以引申为其他方面的挑战,主要是符号执行如何模拟具体的系统环境。
如何处理代码中的系统调用? 一些系统函数调用无法用数学表示,如 fopen 等系统调用会截断符号化赋值过程。现代符号执行引擎会对系统函数进行重新建模。比如在新模型中,fopen函数返回一个数组,fgets函数修改为从一个变量中获取内容(实际在对系统函数进行建模的时候,会要处理更复杂的情况)。
如何处理系统环境问题? 引擎使用模型模拟系统调用,其优点是,能够得到正确的符号执行结果;缺点是需要实现和维护许多可能用到的系统调用模型。
总结
从 1976 年符号执行的提出,到 1980 年之后的三十年里无人问津,再到 2005 年跨时代意义的混合符号执行出现,符号执行每个重大转折点无不伴随着技术上的重大突破,包括 2014 年 AFL 的横空出世,后续的符号执行与相应的漏洞挖掘技术如雨后春笋相继涌现。
与 Fuzzing 技术的融合
Title | Conference | Institution | Summary |
---|---|---|---|
Pangolin:Incremental Hybrid Fuzzing with Polyhedral Path Abstraction, 2020 | IEEE S&P | 香港科技大学 | 作者将约束求解后对信息重用起来,实现 Constrained Mutation 和 Guided Constraint Solving,从而提升混合 fuzz 效率。(混合fuzzing) |
Angora: Efficient Fuzzing by Principled Search, 2018 | IEEE S&P | 上海交通大学 | Angora 是一个基于突变的模糊器。Angora 的主要目标是通过在没有符号执行的情况下解决路径约束来增加分支覆盖率。(已开源) |
Learning to Fuzz from Symbolic Execution with Application to Smart Contracts, 2019 | ACM CCS | 苏黎世联邦理工学院 | Jingxuan He 等人提出了一种从符号执行中学习 fuzzer 的新方法,将其应用于智能合约中。 |
HFL: Hybrid Fuzzing on the Linux Kernel, 2020 | NDSS | 俄勒冈州立大学 | 新兴混合 fuzz 工具。据作者所属,HFL 代码覆盖率分别比 Moonshine 和 Syzkaller 高出15%和26%,并发现 20+ 个内核漏洞。(未开源) |
QSYM : A Practical Concolic Execution Engine Tailored for Hybrid Fuzzing, 2018 | USENIX Security | 佐治亚理工学院 | 作者设计了一种快速的,称为 QSYM 的 Conolic 执行引擎,支持混合 fuzzing。(已开源) |
One Engine to Fuzz 'em All: Generic Language Processor Testing with Semantic Validation, 2021 | IEEE S&P | 佐治亚理工学院 | 一个通用 fuzzing 框架(s3team/Polyglot ),目的是为了探索不同编程语言的处理器而生成高质量的模糊测试用例(已开源) |
二进制符号执行的经典工具是 2009 年谷歌 Candea 团队开发的 S2E,开创选择符号执行的先河;2012 年 David Brumley 团队提出 Mayhem,并于 2016 年的 CGC 中获得冠军。
相关工具
Tool | Target | URL | Can anybody use it/ Open source/ Downloadable |
---|---|---|---|
Verifast | C, Java | https://people.cs.kuleuven.be/~bart.jacobs/verifast | yes |
Triton | x86 and x86-64 | https://triton.quarkslab.com | yes |
SymJS | JavaScript | https://core.ac.uk/download/pdf/24067593.pdf | no |
SymDroid | Dalvik bytecode | http://www.cs.umd.edu/~jfoster/papers/symdroid.pdf | no |
Symbolic PathFinder (SPF) | Java Bytecode | https://github.com/SymbolicPathFinder | yes |
S2E | x86, x86-64, ARM / User and kernel-mode binaries | http://s2e.systems/ | yes |
Rubyx | Ruby | http://www.cs.umd.edu/~avik/papers/ssarorwa.pdf | no |
Rosette | Dialect of Racket | https://emina.github.io/rosette/ | yes |
pysymemu | x86-64 / Native | https://github.com/feliam/pysymemu/ | yes |
Pex | .NET Framework | http://research.microsoft.com/en-us/projects/pex/ | no |
Pathgrind[10] | Native 32-bit Valgrind-based | https://github.com/codelion/pathgrind | yes |
Oyente-NG | Ethereum Virtual Machine (EVM) / Native | http://www.comp.ita.br/labsca/waiaf/papers/RafaelShigemura_paper_16.pdf | no |
Otter | C | https://bitbucket.org/khooyp/otter/overview | yes |
Mythril | Ethereum Virtual Machine (EVM) / Native | https://github.com/ConsenSys/mythril | yes |
MPro | Ethereum Virtual Machine (EVM) / Native | https://sites.google.com/view/smartcontract-analysis/home | yes |
Mayhem | Binary | http://forallsecure.com | no |
Manticore | x86-64, ARMv7, Ethereum Virtual Machine (EVM) / Native | https://github.com/trailofbits/manticore/ | yes |
Kudzu | JavaScript | http://webblaze.cs.berkeley.edu/2010/kudzu/kudzu.pdf | no |
KLEE | LLVM | https://klee.github.io/ | yes |
Kite | LLVM | http://www.cs.ubc.ca/labs/isd/Projects/Kite/ | yes |
KeY | Java | http://www.key-project.org/ | yes |
JPF | Java | http://babelfish.arc.nasa.gov/trac/jpf | yes |
jCUTE | Java | https://github.com/osl/jcute | yes |
JBSE | Java | https://github.com/pietrobraione/jbse | yes |
JaVerT | JavaScript | https://www.doc.ic.ac.uk/~pg/publications/FragosoSantos2019JaVerT.pdf | yes |
janala2 | Java | https://github.com/ksen007/janala2 | yes |
Jalangi2 | JavaScript | https://github.com/Samsung/jalangi2 | yes |
FuzzBALL | VineIL / Native | http://bitblaze.cs.berkeley.edu/fuzzball.html | yes |
ExpoSE | JavaScript | https://github.com/ExpoSEJS/ExpoSE | yes |
crucible | LLVM, JVM, etc | https://github.com/GaloisInc/crucible | yes |
BINSEC | x86, ARM, RISC-V (32 bits) | http://binsec.github.io | yes |
BE-PUM | x86 | https://github.com/NMHai/BE-PUM | yes |
angr | libVEX based (supporting x86, x86-64, ARM, AARCH64, MIPS, MIPS64, PPC, PPC64, and Java) | http://angr.io/ | yes |
如果你想了解更多,可以参考如下文献
参考文献
- All You Ever Wanted to Know about Dynamic Taint Analysis and Forward Symbolic Execution (but Might Have Been Afraid to Ask)
- Symbolic execution for software testing: three decades later
- Playing with Dynamic symbolic execution
- 玩转动态符号执行
- K. Sen, D. Marinov, and G. Agha. CUTE: A concolic unittesting engine for C. InESEC/FSE’05, Sep 2005.
- Cadar, Cristian, and Koushik Sen. “Symbolic execution for software testing: three decades
later.” Communications of the ACM 56.2 (2013): 82-90. - https://en.wikipedia.org/wiki/Symbolic_execution (符号执行相关工具集)