符号执行技术总结(A Brief Summary of Symbol Execution)- wcventure

Prologue

近期查阅了符号执行的经典文章(Symbolic Execution for Software Testing: Three Decades Later),整理部分资料如下。(之前还读过ICSE2018的Towards Optimal Concolic Testing

Recommended reading
[1] King J C. Symbolic execution and program testing[J]. Communications of the ACM, 1976, 19(7): 385-394.
https://yurichev.com/mirrors/king76symbolicexecution.pdf
[2] Cadar C, Sen K. Symbolic execution for software testing: three decades later[J]. Communications of the ACM, 2013, 56(2): 82-90.
https://people.eecs.berkeley.edu/~ksen/papers/cacm13.pdf
[3] Baldoni R, Coppa E, D’elia D C, et al. A survey of symbolic execution techniques[J]. ACM Computing Surveys (CSUR), 2018, 51(3): 50.
https://dl.acm.org/citation.cfm?id=3182657

Lecture
1.Symbolic Execution Lecture at Harvard.
https://www.seas.harvard.edu/courses/cs252/2011sp/slides/Lec13-SymExec.pdf
2.Symbolic Execution Lecture at Iowa State University.
http://web.cs.iastate.edu/~weile/cs641/9.SymbolicExecution.pdf
3.Symbolic Execution Lecture at University of Maryland.
https://www.cs.umd.edu/class/spring2013/cmsc631/lectures/symbolic-exec.pdf

Video
Symbolic Execution Lecture at MIT.
https://www.youtube.com/watch?v=mffhPgsl8Ws
Symbolic Execution Lecture (part of Software Security course on Coursera).
https://www.coursera.org/learn/software-security/lecture/agCNF/introducing-symbolic-execution

摘要

在过去的40年中,人们对软件测试的符号执行产生了极大的兴趣,因为它能够生成高覆盖率的测试套件并在复杂的软件应用程序中发现深层次的错误。符号执行已经在数十种开发工具中孵化,从而在许多突出的软件可靠性应用中取得重大的实际突破。

许多安全和软件测试应用程序需要检查程序的某些属性是否适用于任何可能的使用场景。例如,识别软件漏洞的工具可能需要排除任何后门的存在,以绕过程序的身份验证。一种方法是使用不同的,可能是随机的输入来测试程序。由于后门可能只针对非常具体的程序工作负载,因此自动探索可能的输入空间至关重要。符号执行通过同时系统地探索许多可能的执行路径,在不需要具体输入的情况下,为问题提供了一个优雅的解决方案。该技术不是采用完全指定的输入值,而是抽象地将它们表示为符号,使用约束解算器来构造可能导致属性冲突的实际实例。

本文概述了现代符号执行技术,讨论了它们在路径探索、约束解决和内存建模方面面临的主要挑战,并讨论了主要从作者自己的工作中得出的几种解决方案。

简介

符号执行作为一种生成高覆盖测试套件和在复杂软件应用中发现深层错误的有效技术,引起了人们的广泛关注。符号执行是一种流行的程序分析技术,在70年代中期引入,用于测试某个软件是否可以违反某些属性。本文概述了现代符号执行技术,并从路径探索、约束解决和内存建模方面讨论了它们的挑战。请注意,本文并不打算在这里提供对该领域现有工作的全面调查,而是选择使用简明的示例来说明一些关键挑战和建议的解决方案。有关符号执行技术的详细概述,我们将读者引见之前在该领域发布的调查(A survey of symbolic execution techniques)[3].

定义
符号执行 (Symbolic Execution)是一种程序分析技术,它可以通过分析程序来得到让特定代码区域执行的输入。顾名思义,使用符号执行分析一个程序时,该程序会使用符号值作为输入,而非一般执行程序时使用的具体值。在达到目标代码时,分析器可以得到相应的路径约束,然后通过约束求解器来得到可以触发目标代码的具体值。

目标
软件测试中的符号执行主要目标是: 在给定的探索尽可能多的、不同的程序路径(program path)。对于每一条程序路径,(1) 生成一个具体输入的集合(主要能力);(2) 检查是否存在各种错误,包括断言违规、未捕获异常、安全漏洞和内存损坏。

生成具体测试输入的能力是符号执行的主要优势之一:从测试生成的角度来看,它允许创建高覆盖率的测试套件,而从bug查找的角度来看,它为开发人员提供了触发bug的具体输入,该输入可用于确认和调试打开的错误。生成它的符号执行工具的数据。

此外,请注意,在查找给定程序路径上的错误时,符号执行比传统的动态执行技术(如由Valgrind或Purify等流行工具实现的技术)更强大,这取决于触发错误的具体输入的可用性。最后,与某些其他程序分析技术不同,符号执行不仅限于查找诸如缓冲区溢出之类的一般性错误,而且可以导致更高级别的程序属性,例如复杂的程序断言。

我们从一个简单的例子开始,它强调了本文其余部分所讨论的许多基本问题。
在这里插入图片描述
考虑上图的C语言代码,并假设我们的目标是确定哪些输入使函数foobar的第8行的断言失败。 由于每个4字节输入参数可以采用多达2 ^ 32个不同的整数值,因此在随机生成的输入上具体运行foobar的方法不太可能准确地获取断言失败的输入。 通过使用符号作为其输入而不是具体值来评估代码,符号执行克服了这种限制,并且可以推断输入类而不是单个输入值。更详细地,通过代码的静态分析不能确定的每个值,例如函数的实际参数或从流中读取数据的系统调用的结果,由符号αi表示。 在任何时候,符号执行引擎都维持一个状态(stmt,σ,π)。

  1. stmt是下一个要评估的语句。 目前,我们假设stmt可以是赋值,条件分支或跳转。
  2. σ是一个符号存储,它将程序变量与具体值或符号值αi上的表达式相关联。
  3. π表示路径约束,即,是表示由于在执行中为了达到stmt而采取的分支而对符号αi的一组假设的公式。 在分析开始时,π=真。
    在这里插入图片描述
    函数foobar的符号执行,可以有效地表示为树,如下图所示。
    在这里插入图片描述
    在这里插入图片描述

经典符号执行技术

符号执行的主要思想就是将输入(input)用符号来表征而不是具体值,同时将程序变量表征成符号表达式。因此,程序的输出就会被表征成一个程序输入的函数,即fun(input)。在软件测试中,符号执行被用于生成执行路径(execution path)的输入。在具体的执行过程中,程序在特定的输入上运行,并对单个控制流路径进行了探索。因此,在大多数情况下,具体的执行只能在对相关财产进行近似分析的情况下进行。相反,符号执行可以同时探索程序在不同输入下可能采用的多种路径。这为可靠的分析铺平了道路,可以对检查的属性产生强有力的保证。关键思想是允许程序采用符号而不是具体的输入值。

执行路径(execution path):一个true和false的序列seq={p0,p1,…,pn}。其中,如果是一个条件语句,那么pi=ture则表示这条条件语句取true,否则取false。
执行树(execution tree):一个程序的所有执行路径则可表征成一棵执行树。

例如,图1中的函数testme()有三个执行路径,形成了图2所示的执行树。

在这里插入图片描述
在这里插入图片描述

3条不同的执行路径构成了一棵执行树。三组输入{x=0,y=1},{x=2,y=1},{x=30,y=15}覆盖了所有的执行路径。

符号状态(symbolic state):符号执行维护一个符号状态e,它将变量映射到符号表达式。
符号路径约束(symbolic path constraint):符号路径约束PC,它是符号表达式上无量词的一阶公式。

符号执行后的结果如下图所示:e和PC都在符号执行过程中更新。 在沿着程序的执行路径的符号执行结束时,使用约束求解器来求解PC以生成具体的输入值。如果程序在这些具体的输入值上执行,它将采用与符号执行完全相同的路径并以相同的方式终止。例如,图1中代码的符号执行以空符号状态开始,符号路径约束为true。执行过程中的状态如图中红色部分所示。

在这里插入图片描述

如果符号执行实例命中退出语句或错误(例如,程序崩溃或违反断言),则终止符号执行的当前实例并且生成对当前符号路径约束的令人满意的分配,使用现成的约束求解器。 令人满意的赋值形成了测试输入:如果程序是在这些具体的输入值上执行的,它将采用与符号执行完全相同的路径并以相同的方式终止。

当代码中包含循环和递归时,如果终止条件是符号的话,那么符号执行会产生无限数量的执行路径。例如下图所示,

在这里插入图片描述

如果循环或递归的终止条件是符号的,则包含循环或递归的代码的符号执行可能导致无限数量的路径。 例如,图3中的代码具有无限数量的执行路径,这个时候,符号路径要么就是一串有限长度的ture后面跟着一个false(跳出循环),要么就是无限长的true。如图所示,
在这里插入图片描述
在实践中,需要对搜索施加限制,例如超时,或对路径数量,循环迭代或探索深度的限制。

符号执行的主要缺点:如果符号路径约束不可解(不能被solver解决)或者是不能被高效(时间效率)的解,则不能生成input。回到之前的例子,如果把函数twice改成(v*v)%50(非线性):假设采用的sovler不可解非线性约束,那么,符号执行将失败,即无法生成input。假设twice函数是第三方库函数,没有解释,那么约束求解器也无法对其进行求解。由于约束求解器无法解决这些约束中的任何一个,符号执行将无法为修改后的程序生成任何输入。接下来,我们将描述两种现代符号执行技术,它们可以缓解这个问题,并为修改后的程序生成至少一些输入。

在这里插入图片描述

现代符号执行技术

现代符号执行技术的关键要素之一是它们混合具体(Concrete)执行和符号(Symbolic)执行的能力。 我们在下面介绍两个这样的扩展,然后讨论它们提供的关键优势。
在这里插入图片描述

混合执行测试(Concolic testing)

当给定若干个具体的输入时,Concolic testing动态地执行符号执行。Concolic testing会同时维护两个状态:

精确状态(concrete state): maps all variables to their concrete values.
符号状态(symbolic state): only maps variables that have non-concrete values.

不同于传统的符号执行技术,由于Concolic testing需要维护程序执行时的整个精确状态,因此它需要一个精确的初始值。举例说明,还是之前这个例子:对于图1中的示例,concolic执行将生成一些随机输入,比如{ x = 22; y = 7} 并具体和象征性地执行程序。具体执行将在第7行采用“else”分支,符号执行将沿具体执行路径生成路径约束 x0 != 2y0。Concolic测试否定路径约束中的合取并解决x0 = 2y0以获得测试输入{x = 2; y = 1}; 执行采用与前一个不同的路径 - 第7行的“then”分支,现在在此执行中获取第8行的“else”分支。与前面的执行一样,concolic测试也沿着这个具体的执行执行符号执行,并生成路径约束。Concolic测试将生成一个新的测试输入,强制程序以及之前未执行的执行路径。在第三次执行程序之后,concoic测试报告已经探索了程序的所有执行路径并终止了测试输入生成。
在这里插入图片描述

执行生成测试(Execution-Generated Testing (EGT))

由EXE和KLEE工具实施和扩展的EGT方法的工作原理是区分程序的具体和符号状态。EGT在执行每个操作之前,检查每个相关的值是精确的还是已经符号化了的,然后动态地混合精确执行和符号执行。如果所有的相关值都是一个实际的值(即精确的,concrete),那么,直接执行原始程序(即,操作,operation)。否则(至少一个值是符号化的),这个操作将会被符号执行。举例说明,假设之前那个例子,第17行改成y=10。那么,在调用twice时,传入的参数是concrete的,返回的也是一个concrete value,因此z也是一个concrete value。第7、8行中的z和y+10也会被转换成concrete vaule。然后再进行符号执行。
在这里插入图片描述
Concoic测试和EGT是现代符号执行技术的两个实例,其主要优势在于它们混合具体和符号执行的能力。 为简单起见,在本文的其余部分,我们将这些技术统称为“动态符号执行”。

动态符号执行中的不精确性(imprecision) vs.完整性(completeness)

不精确性: 代码调用了第三方代码库(由于种种原因无法进行代码插装),假设传入的参数都是concrete value,那么就像EGT中的一样,可以全部当作concrete execution。即使传入的参数中包含符号,动态符号执行还是可以对符号设一个具体的值。Concolic和EGT有不同的解决方法,后面再介绍。除了调用外部代码,对于难以处理的操作数(如浮点型)或是一些复杂的函数(solver无法解决或需要耗费大量时间),使用concrete value可以帮助符号执行环节这种impression。

完备性: 动态符号执行通过concrete value可以简化约束,从而防止符号执行get stuck。但是这也会带来一个新问题,就是由于这种简化,它可能会导致一些不完整性,即有时候无法对所有的execution path都能生成input。但是,当遇到不支持的操作或外部调用时,这显然优于简单地中止执行。

动态符号执行使用具体值简化约束的能力有助于为符号执行卡住的执行路径生成测试输入,但这需要注意:由于简化,它可能会失去完整性,即它们可能无法生成测试 某些执行路径的输入。 例如,在我们的示例中,动态符号执行将无法为路径true,false生成输入。 但是,当遇到不支持的操作或外部调用时,这显然优于简单地中止执行。

主要挑战和解决方案

接下来我们将讨论符号执行中的关键挑战,以及为响应它们而开发的一些有趣的解决方案。

路径爆炸(Path Explosion)

描述:
首先,要知道,符号执行implicitly过滤两种路径:
不依赖于符号输入的路径;
对于当前的路径约束,不可解的路径。
但是,尽管符号执行已经做了这些过滤,路径爆炸依旧是符号执行的最大挑战。

解决方案:

  1. 利用启发式搜索搜索最佳路径
    目前,主要的启发式搜索主要focus在对语句和分支达到高覆盖率,同时他们也可被用于优化理想的准则。
    方法1:利用控制流图来guideexporation。
    方法2:interleave 符号执行和随机测试。
    方法3(more recently):符号执行结合演化搜索(evolutionary search)。其中,fitness function用于drive the exploration of the input space。
  2. 利用可靠的(sound)程序分析技术来减小路径爆炸的复杂度
    方法1:静态地合并路径,然后再feed solver。尽管这个方法在很多场合都有效,但是他把复杂度转移给了solver,从而导致了下一个challenge,即约束求解的复杂度。
    方法2: 在后续的计算中,记录并重用low-level function的分析结果。
    方法3 : 自动化剪枝

约束求解(Constraint Solving)

描述:
约束求解是符号执行的技术瓶颈。因此,对于solver的优化(提高solver的求解能力)成了解决这个技术瓶颈的手段。

解决方案:

  1. 去除不相关的约束
    一般来说,程序分支主要依赖于一小部分的程序变量。也就是说,程序分支依赖于一小部分来自于路径条件(path condition)的约束。因此,一种有效的方法就是去掉那些与当前分支的输出不相关的路径条件。例如,现有路径条件:(x+y>10)(z>0)(y<12)(z-x=0)。假设我们现在想生成满足(x+y>10)(z>0)^¬(y<12),其中我们想建立对¬(y<12)(与y有关)的feasibility。那么,(z>0)和(z-x=0)这两个约束都可以去掉,因为与y不相关。
  2. 递增求解
    核心思想就是缓存已经求解过的约束,例如(x+y<10)^(x>5)=>{x=6,y=3}。对于新的约束,首先判断这个新约束的搜索空间是缓存里约束的超集还是子集。如果是新的约束的搜索空间是缓存的约束的子集,那么,就把缓存中的约束去掉多余的条件后继续求解。如果是超集,那么直接把解代入去验证。

内存建模(Memory Modeling)

描述:
程序语句转化成符号约束的精度对符号执行的覆盖率有很大的影响。例如,内存建模是用一个具体的数值去近似一个固定位数的整数变量可能会很有效,但是另一方面,它也会导致imprecision,比如丢失一些符号执行的路径,或探索一些无用的解。另一个例子就是指针,一些工具如DART[3]只能处理精确的指针。

解决方案:
precision和scalability是一个trade-off。需要考虑的因素有:
a) 代码是high level的application code还是low-level的system code。
b) 不同的约束求解器的实际效果。

并发控制(Handling Concurrency)

描述:
大型真实世界的节目通常是并发的。 由于这些程序固有的非确定性(non-deteminism),测试是非常困难的。 尽管存在这些挑战,但动态符号执行已经有效地用于测试并发程序,包括具有复杂数据输入,分布式系统和GPGPU程序的应用程序。

符号执行工具

动态符号执行已经由学术界和研究实验室的几个工具实现。 这些工具支持多种语言,包括C / C ++,Java和x86指令集,实现几种不同的内存模型,针对不同类型的应用程序,并使用几种不同的约束求解器和理论。

Java

  • JPF-Symbc - Symbolic execution tool built on Java PathFinder. Supports multiple constraint solvers, lazy initialization, etc.
  • JDart - Dynamic symbolic execution tool built on Java PathFinder. Supports multiple constraint solvers using JConstraints.
  • CATG - Concolic execution tool that uses ASM for instrumentation. Uses CVC4.
  • LimeTB - Concolic execution tool that uses Soot for instrumentation. Supports Yices and Boolector. Concolic execution can be distributed.
  • Acteve - Concolic execution tool that uses Soot for instrumentation. Originally for Android analysis. Supports Z3.
  • jCUTE - Concolic execution tool that uses Soot for instrumentation. Supports lp_solve.
  • JFuzz - Concolic execution tool built on Java PathFinder.
  • JBSE - Symbolic execution tool that uses a custom JVM. Supports CVC3, CVC4, Sicstus, and Z3.
  • Key - Theorem Prover that uses specifications written in Java Modeling Language (JML).

LLVM

  • KLEE - Symbolic execution engine built on LLVM.
  • Cloud9 - Parallel symbolic execution engine built on KLEE.
  • Kite - Based on KLEE and LLVM.

.NET

  • PEX - Dynamic symbolic execution tool for .NET.

C

  • CREST.
  • Otter.
  • CIVL - A framework that includes the CIVL-C programming language, a model checker and a symbolic execution tool.

JavaScript

Python

  • PyExZ3 - Symbolic execution of Python functions. A rewrite of the NICE project’s symbolic execution tool.

Ruby

  • Rubyx - Symbolic execution tool for Ruby on Rails web apps.

Android

Binaries

  • Mayhem.
  • SAGE - Whitebox file fuzzing tool for X86 Windows applications.
  • DART.
  • BitBlaze.
  • PathGrind - Path-based dynamic analysis for 32-bit programs.
  • FuzzBALL - Symbolic execution tool built on the BitBlaze Vine component.
  • S2E - Symbolic execution platform supporting x86, x86-64, or ARM software stacks.
  • miasm - Reverse engineering framework. Includes symbolic execution.
  • pysymemu - Supports x86/x64 binaries.
  • BAP - Binary Analysis Platform provides a framework for writing program analysis tools.
  • angr - Python framework for analyzing binaries. Includes a symbolic execution tool.
  • Triton - Dynamic binary analysis platform that includes a dynamic symbolic execution tool.
  • manticore - Symbolic execution tool for binaries (x86, x86_64 and ARMV7) and Ethereum smart contract bytecode.

Misc

  • Symbooglix - Symbolic execution tool for Boogie programs.

博客:更好地理解符号执行

  1. 符号执行基础
    http://pwn4.fun/2017/03/20/符号执行基础/

  2. 符号执行:基础概念
    https://blog.csdn.net/wannafly1995/article/details/80842459

  3. 符号执行入门
    https://zhuanlan.zhihu.com/p/26927127

  4. 软件测试中的符号执行
    https://www.cnblogs.com/XBWer/p/9510884.html

  5. 符号执行——从入门到上高速
    https://www.colabug.com/4288118.html

结束语

2016年美国高等研究计划署(DAPRA)主办了自动网络攻防赛(CyberGrandChallenge),旨在建立实时自动化的网络防御系统,并且能够快速地应对大量新的攻击手法,以应对频发的网络攻击。符号执行在参赛队的自动攻防系统中起到了举足轻重的作用,被广泛应用在程序脆弱性的分析上,并涌现出了新的二进制符号执行分析框架,如圣塔芭芭拉大学Shellphish团队的angr和卡内基梅隆大学ForAllSecure团队的Mayhem。自动网络攻防赛的举办掀起了二进制符号执行的又一波热潮。

虽然近年来各种符号执行优化技术不断被提出,符号执行的可用性和实用性也极大地提高了,但在现实软件分析应用中还面临着严峻的挑战,除了路径空间启发式搜索和约束求解之外,并行处理问题、内存建模和执行环境仿真等问题都有待进一步研究与改进。

符号执行的另一发展方向是与Fuzzing技术相结合,以提高程序脆弱性检测的能力。微软研究机构在SAGE中就已经进行了这一尝试和探索,并成功将其应用在对Office和Windows等产品的错误检测上,也取得了显著的成果。2016年,Shellphish团队基于angr分析框架和AFL模糊测试器开发了Driller,用符号执行来增强模糊测试,为后续的研究提供了新思路。

发布了18 篇原创文章 · 获赞 23 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/wcventure/article/details/86773290