代码改变世界

SYNFUZZ: Efficient Concolic Execution via Branch Condition Synthesis

2020-08-31 20:37  huhee  阅读(314)  评论(0编辑  收藏  举报

题目:SYNFUZZ: Efficient Concolic Execution via Branch Condition Synthesis

作者:Wookhyun Han∗, Md Lutfor Rahman†, Yuxuan Chen‡, Chengyu Song†, Byoungyoung Lee§, Insik Shin∗

单位:∗KAIST, †UC Riverside, ‡Purdue, §Seoul National University

出版:arXiv:1905.09532v1 [cs.CR] 23 May 2019

研究问题

可扩展的混合执行

研究背景

混合执行:

优点:可以系统探索程序路径,擅长探索有着复杂和强约束的分支谓词的路径

缺点:缺乏可扩展性。1-需要解释执行指令以获取程序变量的符号表达式,相对于本地执行速度慢很多;2-约束求解的限制。

基于随机变异的fuzz

虽然简单但是可以有效地发现bug。

Program Synthesis程序合成

程序合成器通常在程序空间中执行某种形式的搜索,以生成满足约束条件的程序。约束可以表示为输入输出示例、演示、自然语言、部分程序和数学逻辑。搜索可以是枚举式、演绎式或约束式。

本文使用oracle-guided, sketch-based and solver-assisted search 来合成分支条件。
在本文中,我们关注的是条件语句,所以将条件语句当做一个函数,组合得到它的内容,输入是包含的变量,输出是true和false

输入:一个输入字节的向量x;一系列的具体值;一个可能的操作集合;

输出:枚举n个函数f,每个函数是以上输入的排列组合。f包括l行,每行一个操作(指令),一个输出,一个或多个操作数,操作数可以是具体值、符号值或者前面操作的结果。

在构建好函数之后,将输入输出对以断言的形式加入到f中。

如果构造的这个函数可以进行约束求解,那就求解一个x'走反向分支。

将x'给到原来的程序,如果真的执行反向分支,说明是函数构造正确,否则把这个x'输入输出对加进函数f继续构造。

动机

对于覆盖率驱动的测试技术,基本目标是访问条件分支的true和false分支。为了实现这个目标,需要有翻转条件分支的能力。实现这个目标的两种方式:1-把分支语句作为黑盒,代表有fuzzing,模拟退火、二分查找以及梯度下降。2-生成分支,视为白盒,然后进行计算。代表是动态符号执行通过符号化解释生成分支条件,并求解得到输入。使用深度神经网络模拟分支条件,使用梯度下降翻转输出。

总结:白盒方法更加强大但是生成分支的过程太慢了,相对于黑盒方法效率较低。
为了加快生成条件分支的过程,本文有以下观察:
分支条件往往不算复杂:依赖的输入数目不太多;计算的复杂程度不高。
动态污点分析可以用来记录比简单的输入依赖更多的信息,比如涉及到的操作、执行树。这些信息帮助降低oracle-guided synthesizer的搜索空间。
挑战:
方案:为了进行可扩展的混合执行,SYNFuzz将解释指令替换为动态污点分析和程序合成(program synthesis)。为了翻转一个条件分支,SynFuzz首先使用操作(operation)敏感的的动态污点分析来记录局部表达式(partial expression),即概要。然后使用oracle-guided程序组合,基于输入输出对重建符号表达式。最后,类似于传统的混合执行,SynFuzz使用SMT求解器生成一个可以翻转这个分支的输入。通过这种方式,SYNFuzz可以实现在执行速度上接近fuzz同时具有混合测试的翻转分支的能力。

设计:

A. Overview

目标:对于每一个被输入影响的分支,都将生成一个输入,可以翻转一个分支指令的结果。
对于程序P,以字节向量x为输入,执行一个路径T=b1,b2,...,bn,则主要目标是构建新的输入x'可以将分支bi的结果取反。
步骤:
1)一个分支驱动的污点分析器taint analyzer.通过轻量级的数据流分析来提取:影响目标分支的输入字节;这些输入字节是如何影响分支的(概要)。
2)一个分支谓词合成器 synthesizer.通过污点分析的结果以及输入输出对来合成目标分支的符号化分支谓词。
3)一个分支反向器flipper,使用合成器的结果(包含目标分支及其之前的分支谓词),获取一个新的具体输入x',来翻转目标分支的取值。

B. Context-Sensitive Branch Tracking

上下文敏感的 即考虑了执行路径。同一分支在执行路径的不同位置也有不同的符号谓词。
将程序编译成两个版本,一个是做污点分析,收集输入对分支的影响;另一个是收集分支谓词输入输出对的。
两个挑战:
1如何将输入输出对与分支对应起来?即分支的表示
基于AFL的静态分析得到的分支ID和调用栈信息

2如何在两个binary上定义SID?( 同插桩导致指令的位置不同,不可以直接用AFL llvm_mode的通过数字顺序命名的方式)
通过源代码(llvm ir)中的定位来命名一个指令,然后再插桩,就不会被插桩的内容干扰了。

C. Branch-Driven Taint Analyzer

目标:收集执行历史的部分信息,为分支谓词synthesizer重建分支bi的符号约束提供指导信息(概要)。

SynFuzz与传统的动态污点分析的对比:

1.收集的信息不同

传统的动态污点分析:只关注source(输入)和sink(分支谓词)之间的依赖关系,不回答依赖如何形成的问题。
SYNFuzz收集了依赖的更多信息,比如操作和常见的常量。相对于传统动态污点分析更加重量级,但相对于传统符号执行更加轻量级,因为没有记录完整的依赖,一些常量没有记录。不需要解释执行,所有的执行都是在真机上,是本地执行的速度。

2.sink的设置不同

SynFuzz的sink是条件分支,更关注控制条件分支的谓词。为了获取这个谓词,需要对comparison相关的指令插装。
给定一个比较指令,SYNFuzz首先判断两个操作数是否有污染的。如果是,就打印出来它们的污染信息,以及关系操作符。
Byte-Level Taint Tracking.字节级别的污点追踪--label的组织形式
基于数据流嗅探器(DFSAN)的字节级动态污点分析,为每个内存字节和寄存器增加了存储taint label的tag。

三个机制:

  1. 何时引入污点:源代码;
  2. 何时检查label,在sink处;
  3. label如何传播,如何合并多个label。

问题:每个输入字节都需要有一个唯一的label,label随着输入字节数目的增加而增加;这些字节的可能组合呈阶乘级增加,所以在taint label中存储所有的信息是不可能的。

解决方案:data-flow sanitizer使用一个叫做union table的数据结构存储taint/dependence信息,该数据结构通过taint label来索引。union table中的label都组织为二叉树森林:所有的输入label作为叶节点,当遇到两个label需要合并的时候(比如add操作),就新建一个label,其左右孩子指向两个label。

Operation-Aware Taint

Analysis.
操作敏感的污点分析---label的内容包含操作符,AST like
为了降低合成空间,SYNFuzz已经记录了数据依赖之外的信息。对DFSAN得到的union table进行改造,组织成为了类似AST的二叉树的形式,每次进行合并的时候,都在产生一个新的入口,而被合并的两个label的入口就变成了两个孩子。在扩展的union table中,每个入口都是一个元组(op, operand1, operand2, size)。这种信息也是通过插桩得到的啊!!!!!真厉害了,那就是对每个指令都插桩了。

与混合执行不同的是,SYNFuzz中的具体值label都是一样的。这样做的影响:

1-减少taint label 的数目,避免空间耗尽

2-具体值可以被很快恢复出来。(不清楚何出此言,怎么得出的?)

各种优化

优化原因:记录大量的污染label将带来如下负面影响:
1-union table使用的内存空间将会很大;
2-符号公式规模很大且复杂

优化手段:

1-Optimization: Common Constants.

记录少量的常用常量以提升速度。
观察:非线性操作,比如移位,相对于线性操作需要更多的输入输出对来恢复正确的操作数。同时,很多的非线性操作频繁地使用一个具体操作数的小集合。记录少量的常量可以加快组合的速度。

2-Optimization: Load and Store.

针对load store指令,操作大于一个字节的数据需要一系列的指令。
所以SYNFuzz提供了一个特殊的指令uload,表示load一个字节序列。

在处理load指令的时候,首先按检测uload指令是否适合(对应字节的label是否连续),不适合就回到原来的处理方式。
在处理store指令的时候,如果label是一个uload的结果,将会直接提取出对应字节的label,否则对所有字节赋予相同的label而不拆开。

3-Optimization: Concrete Folding.

在处理二元操作时,如果一个操作数是具体的,而另一个操作数是相同操作的结果;然后我们将检查这两个具体值是否可以组合,如果可以,我们将使用相同的标签而不是创建一个新的。比如,对于语句x=a+b+c,如果b和c都是具体值,就把它们合并成单一的具体值。

4-Optimization: Avoiding Duplication.

每次要merge一个label的时候,先去看下union table中有没有,没有才加入,避免重复的label。
局限:当两个操作数都是具体值时,这种方式会导致错误的别名。因为具体值有着相同的taint label但是其具体取值可能不一样。

D. Branch Predicate Synthesizer 分支谓词合成器

1-构建分支谓词的符号化公式:

首先,针对两个操作数构建两个符号化函数,以相关输入字节为变量。

然后,通过变异相关的输入字节得到输入输出对集合。通过将变量绑定到具体的输入字节,把返回值绑定到比较操作的具体的操作数,就可以创建一系列关于丢失的具体值的约束。

最后,使用SMT求解丢失的具体值。
如果求解不出来,那就继续添加输入输出对。
如果求解得到遗失的具体值,就得到了一个候选的谓词公式。

2-通过SMT求解翻转符号化函数的具体输入。

将参数和具体值解绑,将可满足的赋值绑定到具体值。
分支条件的左右表达式都准备好后,使用SMT求解出一组相关输入字节的具体值,使得比较操作得到相反的值(反向分支)。

由于分支条件左右表达式对应的合成函数可能是错误的,所以生成的新输入可能在原始程序上执行不会走反向的分支。为了修复合成函数的的错误,将这个反例加入到输入输出对集合中,重新使用SMT来获取新的赋值(这里是遗失的具体值赋值吧?)。

这个过程将一直重复,直到找到一个正确的谓词表达式或者迭代次数超时。

Reconstructing Symbolic Predicate.

首先,对于比较操作的两边(前提是污染的),合成器创建一个符号函数:fi,l和fi,r.

然后,通过从叶节点到根节点解析序列化的偏序AST,填充变量和函数体。

叶节点将作为函数的参数。
对每个非叶节点,将构建一条语句用来填充进函数体中。(无论是否是污染的变量,都进行符号化:因为遗失的具体值不是污染的,但是需要求解来确定)

最后,偏序AST的根节点将作为函数的返回值。

问题:并不是每一个比较操作都代表一个分支吧,但是这样也行吧。a=s>t; b=a<a;if(a&&b)

Collecting Input-Output Pairs.

目的:求解符号公式中的具体值
变异函数fi,l的参数字节;使用突变后的输入执行目标程序;收集比较操作的操作数(即函数fi,l的返回值)
收集到的信息写入文件(目标分支ID,左操作数,右操作数,分支取向)

此步骤在不进行污点分析的程序上执行,是native fuzz的速度。
Solving Concrete Values.
将遗失的具体值作为符号值,将输入输出值作为具体值,得到断言约束:

对每一个输入输出对都构造这样的约束,然后synthesizer将请求SMT检查约束的可满足性。如果可满足,就得到了这些具体值的赋值。

Optimistic Solving.

问题:虽然在追踪分支的时候已经加入了上下文信息,但由于哈希碰撞问题,不能保证这种分支追踪的路径敏感是正确的。所以,收集得到的输入输出对有可能是不准确的,这就导致加入错误约束,无法求解出遗失的具体值。

方案:在加入约束之前,先检查该约束是否与已有约束是相容的。相容的加入,否则不加入。

E. Branch Flipper

得到目标分支谓词两边的合成之后,将构建一个翻转分支的新的输入。类似于混合执行。
构建分支谓词的符号公式,求解具体输入。
相对于混合执行,SYNFuzz的挑战:
synthesizer实现的正确性:Incorrect Synthesis Result.
Nested Conditions.因为约束里面不仅要加目标分支翻转的约束,还需要添加路径前面的分支取向不要改变的约束,这样就导致约束很长。

实现:

A. Taint Analyzer
基于LLVM的DataFlowSanitizer (DFSAN)
Taint Storage

评估:
不足与未来工作: