符号执行(symbolic executio)技术综述、论文阅读
概述
传统符号执行是一种静态分析技术,最初在1976年由King JC在ACM上提出。即通过使用抽象的符号代替具体值来模拟程序的执行,当遇到分支语句时,它会探索每一个分支, 将分支条件加入到相应的路径约束中,若约束可解,则说明该路径是可达的。符号执行的目的是在给定的时间内,尽可能的探索更多的路径。根据运行的目的来分,主要有两个:
从测试的角度来看,它可以模拟出各个路径的输入值,从而创建高覆盖率的测试套件。这里是静态的分析程序得到测试需要的输入,与动态执行程序的测试不同,动态执行程序的测试更多的是依赖完备的测试用例来提升测试的覆盖率,达到发现问题的目的。
从缺陷查找的角度来看,它为开发人员提供了触发的缺陷的具体输入,利用该输入,程序可用于缺陷的确认或调试。符号执行不仅限于查找诸如缓冲区溢出之类的问题,而且可以通过根据缺陷发现的条件,生成复杂的断言,来判断缺陷发生的可能性。
符号执行的优势是能够以尽可能少的测试用例集达到高测试覆盖率, 从而挖掘出复杂软件程序的深层错误。然而, 在实际的软件测试应用过程中, 不可避免地会遇到许多限制条件, 如路径爆炸问题、约束求解困难集内存建模等问题, 这会导致符号执行难以在现实的软件测试应用中达到理想的效果。
符号执行经过了传统符号执行(我理解就是Static Symbolic Execution)→动态符号执行(dynamic symbolic execution,也叫Concolic Execution)→选择性符号执行(Selective symbolic execution)的发展过程,动态符号执行包括混合测试 (Concolic Testing)和执行生成测试(Execution-Generated Testing(EGT))两种。
这里是《符号执行研究综述》中的观点,实际中可能选择性符号执行是比较新的一种符号执行方式,但不能说是一定优于动态符号执行的下一代技术。
static symbolic execution传统符号执行
传统符号执行的关键是使用符号值替代具体的值作为输入,并将程序变量的值表示为符号输入值的符号表达式。其结果是程序计算的输出值被表示为符号输入值的函数。
在遇到程序分支指令时, 程序的执行也相应地搜索每个分支, 分支条件被加入到符号执行保存的符号路径约束 PC, PC表示当前路径的约束条件。在收集了路径约束条件之后, 使用约束求解器来验证约束的可解性, 以确定该路径是否可达。若该路径约束可解, 则说明该路径是可达的;反之, 则说明该路径不可达, 结束对该路径的分析。
函数 testme() 有 3 条执行路径,组成右边的执行树。执行路径(execution path):一个true和false的序列seq ={p0, p1, …, pn}。其中,如果是一个条件语句,那么pi=ture则表示条件语句的取值为:true,否则取false。执行树(execution tree):一个程序的所有执行路径则可表示成一棵执行树。只需要针对路径给出输入,即可遍历这 3 条路径,例如:{x = 0, y = 1}、{x = 2, y = 1} 和 {x = 30, y = 15}。符号执行的目标就是去生成这样的输入集合,在给定的时间内遍历所有的路径。
符号执行维护了符号状态σ(symbolic state)和符号路径约束 PC(path constraint (or path condition)),其中 σ 表示变量到符号表达式的映射,PC 是符号表示的不含量词的一阶表达式。在符号执行的初始化阶段,σ 被初始化为空映射,而 PC 被初始化为 true,并随着符号执行的过程不断变化。在对程序的某一路径分支进行符号执行的终点,把 PC 输入约束求解器(constraint solver)以获得求解,生成实际的输入值。如果程序把生成的具体值作为输入执行,它将会和符号执行运行在同一路径,即此时PC的公式所表示的路径,并且以同一种方式结束。
例如上面的例子。
符号执行开始时,符号状态 σ 为空,符号路径约束 PC 为 true。
当我们遇到一个读语句,形式为var=sym input(),即接收程序输入,符号执行就会在符号状态 σ 中加入一个映射 var->s,这里s就是一个新的未约束的符号值。main()函数,16,17行会得到结果σ= {x ↦x0,y ↦ y0},其中x0,y0是两个初始不受约束的符号值;
当遇到一个赋值语句 v = e 时,符号执行就会在符号状态 σ 中加入一个 v 到 σ(e) 的映射,于是程序执行完第 6 行后得到 σ = {x->x0, y->y0, z->2y0}。
当每次遇到条件语句 if(e) S1 else S2 时,PC 会被更新为 PC∧σ(e),表示 then 分支,同时生成一个新的路径约束 PC’,初始化为 PC∧¬σ(e),表示 else 分支。
如果 PC 是可满足的,那么程序会走 then 分支(σ 和 PC),否则如果 PC’ 是可满足的,那么程序会走 else 分支(σ 和 PC’)。值得注意的是,符号执行不同于实际执行,其实两条路都会走,分别维护它们的状态。
如果 PC 和 PC’ 都不能满足,那么符号执行就在对应的路径终止。
第 7 行建立了符号执行实例,路径约束分别是 x0 = 2y0 和 x0 ≠ 2y0,在第 8 行又建立了两个实例,分别是 (x0 = 2y0)∧(x0 > y0 + 10) 和 (x0 = 2y0)∧(x0 ≤ y0 + 10)。
如果符号执行遇到了 exit 语句或者 error,当前实例会终止,并利用约束求解器对当前路径约束求出一个可满足的值,这个值就构成了测试输入:如果程序输入了这些实际的值,就会在同样的路径结束。例如Figure2中三个绿色块里的值。
如果符号执行的代码包含循环或递归,且它们的终止条件是符号化的,那么可能就会导致产生无数条路径。如Figure3。这段代码就有无数条执行路径,每条路径的可能性有两种:要么是任意数量的true加上一个false结尾,要么是无穷多数量的false。我们形式化地表示包含n个true条件和1个false条件的路径,其符号化约束如下。其中每个 Ni 都是一个新的符号值,执行结束的符号状态为 {N->Nn+1, sum->Σi∈[1,n]Ni}。在实践中我们需要通过一些方法限制这样的搜索,例如timeout,限制路径数量,循环迭代次数或探测深度。
经典的符号执行有一个关键的缺点,若符合执行路径的符号路径约束无法使用约束求解器进行有效的求解,则无法生成输入。例如Figure2对应 twice 函数替换成Figure4. 那么符号执行会得到路径约束 x0 ≠ (y0y0)mod50 和 x0 = (y0y0)mod50。更严格一点,如果我们不知道 twice 的源码,符号执行将得到路径约束 x0 ≠ twice(y0) 和 x0 = twice(y0)。在这两种情况下,符号执行都不能生成输入值。
Dynamic symbolic execution(Concolic Execution)
混合实际执行和符号执行,称为concolic execution,是真正意义上的动态符号执行。
经典的符号执行,过度的依赖了符号执行的约束求解能力,限制了传统符号执行的能力发挥。如果能加入具体值进行分析,将大大简化分析过程,降低分析的难度和提升效率;但分析过程中,仍不可避免的还是需要将各种条件表达式,进行符号化抽象后变成约束条件参与执行。将程序语句转换为符号约束的精度,对符号执行所达到的覆盖率以及约束求解的可伸缩性会产生重大影响。所以如何做好混合具体(Concrete)执行和符号(Symbolic)执行的能力的平衡,就成为现代符号执行的关键点。
Concolic Testing
最早将实际执行和符号执行结合起来的是2005年发表的DART,全称为“Directed Automated Random Testing”,以及2005年发表的CUTE,即“A concolic unit testing engine for C”。
Concolic执行维护一个实际状态和一个符号化状态:实际状态将所有变量映射到实际值,符号状态只映射那些有非实际值的变量。Concolic执行首先用一些给定的或者随机的输入来执行程序,收集执行过程中条件语句对输入的符号化约束,然后使用约束求解器去推理输入的变化,从而将下一次程序的执行导向另一条执行路径。简单地说来,就是在已有实际输入得到的路径上,对分支路径条件进行取反,就可以让执行走向另外一条路径。这个过程会不断地重复,加上系统化或启发式的路径选择算法,直到所有的路径都被探索,或者用户定义的覆盖目标达到,或者时间开销超过预计。
仍以figure2中例子为例。Concolic执行会先产生一些随机输入,例如{x=22, y=7},然后同时实际地和符号化地执行程序。这个实际执行会走到第7行的else分支,符号化执行会在实际执行路径生成路径约束x ≠ 2y0(结束)。然后 Concolic 执行会将该路径约束的连接词取反,求解 x0 = 2y0 得到测试输入 {x = 2, y = 1},这个新输入就会让执行走向一条不同的路径。concolic执行会在这个新的测试输入上再同时进行实际的和符号化的执行,执行会取与此前路径不同的分支,即第7行的then分支和第8行的else分支,这时产生的约束是 (x0 = 2y0)∧(x0 ≤ y0 + 10)(结束)。通过对将结合项 (x0 ≤ y0 + 10) 取反得到的约束 (x0 = 2y0)∧(x0 > y0 + 10) 进行求解,得到测试输入 {x = 30, y = 15},然后程序到达了 ERROR 语句。这样程序的所有 3 条路径就都探索完了,其使用的策略是深度优先搜索。
Execution-Generated Testing(EGT)
Cristian Cadar在2006年发表EXE,以及2008年发表EXE的改进版本KLEE,对上述concolic执行的方法做了进一步优化。其创新点主要是在实际状态和符号状态之间进行区分,称之为执行生成的测试(Execution-Generated Testing),简称EGT。
EGT 在每次执行前会动态地检查所涉及的值(比如语句、函数参数)是不是都是实际的值,如果是,则程序按照原样执行(即直接把实际值带入,然后像普通语句、函数一样执行),否则,如果至少有一个值是符号值,程序会通过更新当前路径的条件符号化地执行。例如上面的例子,把 17 行的 y = sym_input() 改成 y = 10,那么第 6 行就会用实参10去调用 twice 函数,就像普通程序那样执行。然后第 7 行将变成 if(20 == x),因为这里x是一个符号值,所以符号化执行。符号执行会通过添加约束 x = 20,走 then 分支,同时添加约束 x ≠ 20,走 else 分支。而在 then 分支上,第 8 行变成了 if(x > 20),不会到达 ERROR。
concolic执行的出现,让传统静态符号执行遇到的很多问题能够得到解决——那些符号执行不好处理的部分、求解器无法求解的部分,用实际值替换就好了。使用实际值,可以让因外部代码交互和约束求解超时造成的不精确大大降低,但付出的代价就是,会有丢失路径的缺陷,牺牲了路径探索的完全性。
selective symbolic execution选择性符号执行
受路径爆炸和约束求解问题的制约, 符号执行不适用于程序规模较大或逻辑复杂的情况, 并且对于与外部执行环境交互较多的程序尚无很好的解决方法。选择性符号执行有助于解决这类问题, 也是具体执行和符号执行混合的一种分析技术, 依据特定的分析, 决定符号执行和具体执行的切换使用。
在选择性符号执行中, 用户可以指定一个完整系统中的任意部分进行符号执行分析, 可以是应用程序、库文件、系统内核和设备驱动程序等。选择性符号执行在符号执行和具体执行间转换, 并透明地转换符号状态和具体状态。选择性符号执行极大地提高了符号执行在实际应用中对大型软件分析测试的可用性, 且不再需要对这些环境进行模拟建模。
选择性符号执行在指定区域内的符号化搜索, 就是完全的符号执行, 在该区域之外均使用具体执行完成。选择性符号执行的主要任务就是在分析前将大程序区分为具体执行区域和符号执行区域。
主要挑战和解决方法
符号执行存在路径爆炸(代码规模、复杂度)、约束求解(计算算法)、内存模型(工具设计)等挑战。
Path Explosion路径爆炸
由于在每一个条件分支都会产生两个不同约束,符号执行要探索的执行路径依分支数指数增长。在时间和资源有限的情况下,应该对最相关的路径进行探索。通过路径选择的方法缓解指数爆炸问题,主要有两种方法:启发式地优先探索最值得探索的路径,并使用合理的程序分析技术来降低路径探索的复杂性。
启发式搜索是一种路径搜索策略,大多数启发式的主要目标在于获得较高的语句和分支的覆盖率,不过也有可能用于其他优化目的。一种有限的方法是使用静态控制流图来知道探索,尽量选择与未覆盖指令最接近的路径。另一种启发式方法是随机探索,即在两边都可行的符号化分支处随机选择一边。另一个方法是符号执行与进化搜索相结合,其fitness function用来指导输入空间的搜索,其关键就在于fitness function的定义,例如利用从动态或静态分析中得到的实际状态信息或者符号信息来提升fitness function。
用程序分析和软件验证的思路去减少路径探索的复杂性。一个简单的方法是,通过静态融合减少需要探索的路径,具体说来就是使用select表达式直接传递给约束求解器,但实际上是将路径选择的复杂性传递给了求解器,对求解器提出了更高的要求。还有一种思路是重用,即通过缓存等方式存储函数摘要,可以将底层函数的计算结果重用到高级函数中,不需要重复计算,减小分析的复杂性。还有一种方法是剪枝冗余路径,RWset技术的关键思路就是,如果程序路径与此前探索过的路径在同样符号约束的情况下到达相同的程序点,那么这条路径就会从该点继续执行,所以可以被丢弃。
Constraint Solving约束求解
约束求解器优化。两种优化方法:不相关约束消除(Irrelevant constraint elimination),增量求解(Incremental solving.)。
通常一个程序分支只依赖于一小部分的程序变量,因此一种有效的优化是从当前路径条件中移除与识别当前分支不相关的约束。例如,当前路径的条件是:(x + y > 10)∧(z > 0)∧(y < 12)∧(z - x = 0),我们想通过求解 (x + y > 10)∧(z > 0)∧¬(y < 12)探索新的路径,其中 ¬(y < 12) 是取反的条件分支,那么我们就可以去掉对z的约束,因为对分支是不会有影响的。减小的约束集会给出x和y的新值,我们用此前执行的z值就可以生成新输入了。如果更形式化地说,算法会计算在取反条件所依赖的所有约束的传递闭包。
符号执行生成的约束集有一个重要的特征,就是它们被表示为程序源代码中的静态分支的固定集合。所以,多个路径可能会产生相似的约束集,所以可以使用相似的解决方案。通过重用以前相似请求得到的结果,可以提升约束求解的速度,这种方法被运用到了 CUTE 和 KLEE 中。在 KLEE 中,所有的请求结果都保存在缓存里,该缓存将约束集映射到实际的变量赋值。例如,在缓存中有这样一个映射:(x + y < 10)∧(x > 5) => {x = 6, y = 3}。利用这些映射,KLEE 可以迅速地解决一些相似的请求,例如请求 (x + y < 10)∧(x > 5)∧(y ≥ 0),KLEE 可以迅速检查得到 {x = 6, y = 3} 是可行的。
Memory Modeling内存建模
在访问内存的时候,内存地址用来引用一个内存单元,当这个地址的引用来自于用户输入时,内存地址就成为了一个表达式。当符号化执行时,我们必须决定什么时候将这个内存的引用进行实际化(看作实际值而非符号)。例如,一个memory model将int变量用某个实际值表示,可能会便于约束求解,但是会造成发现不了算术溢出。
别名问题,即地址别名导致两个内存运算引用同一个地址,比较好的方法是进行别名分析,事先推理两个引用是否指向相同的地址,但这个步骤要静态分析完成。KLEE使用了别名分析和让SMT考虑别名问题的混合方法。而DART和CUTE压根没解决这个问题
符号化跳转也是一个问题,主要是switch这样的语句,常用跳转表实现,跳转的目标是一个表达式而不是实际值。三种处理方法。1)使用concolic执行中的实际化策略,一旦跳转目标在实际执行中被执行,就可以将符号执行转向这个实际路径,但缺陷是实际化导致很难探索完全的状态空间,只能探索已知的跳转目标。2)使用SMT求解器。当我们到达符号跳转时,假设路径谓词为Π,跳转到e,我们可以让SMT求解器找到符合Π∧e的答案。但是这种方案效率低。3)使用静态分析,推理整个程序,定位可能的跳转目标。实际中,源代码的间接跳转分析主要是指针分析,而二进制的跳转分析,推理在跳转目标表达式中哪些值可能被引用。例如,函数指针表,通常实现为,可能的跳转目标表。
Handling Concurrency处理并发
大型程序通常是并发的,并发固有的非确定性,导致普通测试很困难。而动态符号执行适用于这类测试。
Symbolic execution for software testing: three decades later中举了一些例子.
历史发展和最新进展
符号执行最初提出是在70年代中期,静态符合执行的原理。到了2005年左右开始重新流行,引入了一些新的技术让符号执行更加实用。Concolic执行的提出让符号执行真正成为可实用的程序分析技术,并且大量用于软件测试、逆向工程等领域。
2005年作用涌现出很多工作,如DART、CUTE、EGT/EXE、CREST等等。真正值得关注和细读的,应该是2008年Cristian Cadar开发的KLEE。KLEE可以是源代码符号执行的经典作品,开源的,后来的许多优秀的符号执行工具都是建立在KLEE的基础上。
基于二进制的符号执行工具有2009年EPFL的George Candea团队开发的S2E最为著名,其开创了选择符号执行,S2E有开源的一个版本。2012年CMU的David Brumley团队提出的Mayhem则采用了混合offline和online的执行方式。2008年UC Berkeldy的Dawn Song团队提出的BitBlaze二进制分析平台中的Rudder模块使用了online的执行方式。
angr,是一个基于Python实现的二进制分析平台,完全开源且还在不断更新,其中也实现了多种不同的符号执行策略。
在优化技术上, 2014年David Brumley团队提出的路径融合方法, Veritesting,是比较重要的工作之一,angr中也实现了这种符号执行方式(在静态符号执行和动态符号执行间平衡,降低开销)。2015年Stanford的Dawson Engler(Cristian Cadar的老师)团队提出的Under-Constrained Symbolic Execution。
近年流行的符号执行与fuzzing技术相结合以提升挖掘漏洞效率,其实早在DART和2012年微软的SAGE工作中就已经有用到这种思想,但这两年真正火起来是2016年UCSB的Shellphish团队发表的Driller论文,符号辅助的fuzzing(symbolic-assisted fuzzing),也非常值得一看。
参考文献
[1] Schwartz E J, Avgerinos T, Brumley D. All You Ever Wanted to Know about Dynamic Taint Analysis and Forward Symbolic Execution (but Might Have Been Afraid to Ask) [C]// Security & Privacy. DBLP, 2010:317-331.
[2] Cadar C, Sen K. Symbolic execution for software testing: three decades later[M]. ACM, 2013.
[3] C. Cadar, D. Dunbar, and D. Engler. KLEE: Unassisted and Automatic Generation of High-Coverage Tests for Complex Systems Programs. In Proceedings of the 8th USENIX Symposium on Operating Systems Design and Implementation (OSDI’08), volume 8, pages 209–224, 2008.
[4] R. S. Boyer, B. Elspas, and K. N. Levitt. SELECT – a formal system for testing and debugging programs by symbolic execution. SIGPLAN Not., 10:234–245, 1975.
[5] P. Godefroid, N. Klarlund, and K. Sen. DART: Directed Automated Random Testing. In PLDI’05, June 2005.
[6] K. Sen, D. Marinov, and G. Agha. CUTE: A concolic unit testing engine for C. In ESEC/FSE’05, Sep 2005.
[7] C. Cadar, V. Ganesh, P. M. Pawlowski, D. L. Dill, and D. R. Engler. EXE: Automatically Generating Inputs of Death. In Proceedings of the 13th ACM Conference on Computer and Communications Security, pages 322–335, 2006.
[8] J. Burnim and K.Sen,“Heuristics for scalable dynamic test generation,” in Proc. 23rd IEEE/ACM Int. Conf. Autom. Software Engin., 2008, pp. 443–446.
[9] V. Chipounov, V. Georgescu, C. Zamfir, and G. Candea. Selective Symbolic Execution. In Proceedings of the 5th Workshop on Hot Topics in System Dependability, 2009.
[10] S. K. Cha, T. Avgerinos, A. Rebert, and D. Brumley. Unleashing Mayhem on Binary Code. In Proceedings of the IEEE Symposium on Security and Privacy, pages 380–394, 2012.
[11] Song D, Brumley D, Yin H, et al. BitBlaze: A New Approach to Computer Security via Binary Analysis[C]// Information Systems Security, International Conference, Iciss 2008, Hyderabad, India, December 16-20, 2008. Proceedings. DBLP, 2008:1-25.
[12] Yan S, Wang R, Salls C, et al. SOK: (State of) The Art of War: Offensive Techniques in Binary Analysis[C]// Security and Privacy. IEEE, 2016:138-157.
[13] T. Avgerinos, A. Rebert, S. K. Cha, and D. Brumley. Enhancing Symbolic Execution with Veritesting. pages 1083–1094, 2014.
[14] D. a. Ramos and D. Engler. Under-Constrained Symbolic Execution: Correctness Checking for Real Code. In Proceedings of the 24th USENIX Security Symposium, pages 49–64, 2015.
[15] P. Godefroid, M. Y. Levin, and D. Molnar. SAGE: Whitebox Fuzzing for Security Testing. ACM Queue, 10(1):20, 2012.
[16] N. Stephens, J. Grosen, C. Salls, A. Dutcher, R. Wang, J. Corbetta, Y. Shoshitaishvili, C. Kruegel, and G. Vigna. Driller: Augmenting Fuzzing Through Selective Symbolic Execution. In Proceedings of the Network and Distributed System Security Symposium, 2016.
[17] 叶志斌,严波. 符号执行研究综述[J]. 计算机科学, 2018, 45(6A): 28-35.
[18] Roberto Baldoni, Emilio Coppa, Daniele Cono D'Elia, Camil Demetrescu, Irene Finocchi: A Survey of Symbolic Execution Techniques. ACM Comput. Surv. 51(3): 50:1-50:39 (2018) 2017
参考博客:
https://zhuanlan.zhihu.com/p/26927127
https://www.bookstack.cn/read/CTF-All-In-One/doc-8.9_symbolic_execution.md
https://zhuanlan.zhihu.com/p/282771772
https://blog.csdn.net/A951860555/article/details/119102789