Full-speed Fuzzing: Reducing Fuzzing Overhead through Coverage-guided Tracing
文章来自SP2019(SP的文章内容是真的饱满)
作者来自弗吉尼亚理工大学
一、论文阅读
Abstract
引入了覆盖引导追踪(Coverage-guided tracing)的概念。只有一小部分增加覆盖的测试用例需要追踪;随着时间的增加,产生新覆盖的测试用例越来越少。
1. Introduction
AFL追踪测试用例的花销太大。产生新覆盖的测试用例占比不足1/10000。
通过插桩目标二进制文件实现增加覆盖时自动报告,作者称插桩后的目标文件为:interest oracles
。
产生新的覆盖,则移除相应部分的插桩代码,以反映覆盖范围。这样做会使追踪测试用例的时间变为2倍。
贡献:
-
引入了覆盖引导追踪的概念,只追踪增加覆盖的测试用例。
-
根据八个程序,量化了增加覆盖的测试用例的频率。
-
表明AFL和Driller大部分时间用在追踪测试用例上
-
基于Dyninst实现了UnTracer并与AFL-QEMU,AFL-Clang,AFL-Dyninst进行比较
-
基于QSYM实现了QSYM-UnTracer,效果蛮好
2. Background
没啥好说的,一些简单介绍和相关工作
2.1 An overview of fuzzing
基于语法生成、基于变异的模糊测试,目前工作主要集中在后者。黑白灰盒测试
2.2 Coverage-guided fuzzing
覆盖引导模糊测试流程。三种覆盖粒度,目前还没有研究路径覆盖的工作。
2.3 Coverage tracing performance
有无源码插桩,动静态插桩
2.4 Focus of this paper
只追踪增加覆盖的测试用例。实现了用于覆盖引导模糊测试的覆盖引导追踪框架UnTracer
3. Impact of discarded test cases
调查AFL和Driller中不增加覆盖的测试用例对性能的影响
每小时中执行用例+覆盖追踪的时间占比
AFL-Clang:91.8%
AFL-QEMU:97.3%
Driller-AFL:95.9%
增加覆盖的测试用例占比
AFL-Clang:0.0062%
AFL-QEMU:0.0257%
Driller-AFL:0.00653%
4. Coverage-guided tracing
4.1 Overview
覆盖引导追踪在测试用例生成和代码覆盖追踪之间增加了一个步骤:interest oracle
interest oracle是目标二进制的一个修改版本,在每个未覆盖的基本块头部插入断点。
触发断点则为增加覆盖,追踪并记录后移除对应的断点,这样以来,只有执行新的基本块会触发断点,即增加覆盖。
- Determine Interesting:用interest oracle 执行测试用例,根据是否触发断点判断是否为有趣的,即覆盖增加的测试用例
- Full Tracing:追踪覆盖增加的测试用例的完整执行路径。因为上一步触发断点后就退出了。
- Unmodify Oracle:对每个新覆盖的基本块,移除interest oracle中相应的断点。
- 返回第一步重复执行
4.2 The interest oracle
interest oracle 要求识别每个基本块的地址,angr和Dyninst都可以实现。
需要注意的问题:断点的中断信号不能和用于fuzz的信号冲突;插入的断点指令大小不能超过基本块的大小。
4.3 Tracing
interest oracle是在基本块级别插桩的,所以代码追踪也是在基本块级别操作(可以是基本块级别的追踪,也可以是边级别的追踪)。
4.4 Unmodifying
根据target binary 将插入的桩代码覆盖
4.5 Theoretical performance impact
随着时间推移,越来越多的基本块中的桩代码被移除,oracle和target binary也就越来越接近。到后期,绝大部分测试用例都执行和target binary一样的代码,也就没有额外的追踪开销。
### 5. Implementation:UnTracer
5.1 UnTracer overview
插桩两个版本的目标文件 interest oracle,tracer,一个识别增加覆盖的测试用例,一个追踪完整覆盖路径。二者都采用fork server执行。
- AFL初始化
- 插桩oracle和tracer
- 静态分析识别所有基本块
- 在oracle每个基本块开头插入断点
- 分别启动fork server
- 进入循环
- 生成测试用例
- 在oracle中执行,如果触发断点
- 标记为增加覆盖的测试用例
- 在tracer中收集完整执行路径
- 暂停fork server
- 移除oracle中对应基本块的桩代码
- 重启fork server
- AFL处理新的种子
- 下一次迭代
流程图如下
5.2 Fork server instrumentation
oracle执行所有的测试用例,tracer执行覆盖增加的测试用例,两者都需要执行许多测试用例,所以都采用了fork server模式加快速度,
首先在 .text 段插入forkserver函数,然后在main函数的第一个基本块链接函数的回调。
在tracer中用Dyninst的二进制重写功能插桩,包括插入forkserver。
在oracle中,用AFL的汇编时插桩将forkserver插入到目标程序中。因为oracle要执行所有的测试用例,二Dyninst还是有一些性能影响。
5.3 Interest oracle binary
利用Dyninst静态分析得到基本块列表,遍历该列表插入桩代码,为了防止在fork server 初始化之前触发中断,忽略掉main函数中fork server回调之前的函数_start
, _libc_start_main
, _init
, frame_dummy
使用SIGTRAP作为中断信号,因为经常用于细粒度执行控制以及分析,二进制0xCC表示为一个字节,可以覆盖所有大小的基本块。
5.4 Tracer Binary
在每个基本块插入用于追踪覆盖的回调,但执行完一个基本块后,回调会将基本块地址存入文件中
有一个问题是循环代码导致执行重复的基本块,进而重复记录相同的基本块,oracle也会重复处理这些基本块。文章的处理方式如下:
在fork server 中初始化一个全局的哈希表,之后每个fork出的子进程都会继承这个空哈希表,一旦执行完一个基本块,回调会查找这个基本块是否已经存在于哈希表中,如果没有,则更新哈希表和覆盖日志。
(这里虽说是全局的,但是其实每次执行开始时都是空哈希表。fork是写时复制的,对子进程中的哈希表更新并不会对fork server中的哈希表产生影响,所以这里消除重复处理的基本块也只是针对一次执行中的,并没有消除所有执行之间的重复。举个例子:第一次执行路径A->B->C->D,第二次执行路径A->B->C->B->C->D,第一次和第二次执行中的A、B、C三个基本块的重复处理没有消除,第二次执行中的B和C两个基本块会消除重复处理。论文读起来是这样的,还需要看看源码印证一下!)
5.5 Unmodifying the Oracle
设置了一个类似tracer中的哈希表,消除同一个基本块中的重复移除操作
6. Tracing-only evaluation
6.1 Evaluation overview
在8个程序上与AFL-Clang、AFL-QEMU,AFL-Dyninst对比。
6.2 Experiment infrasttucture
每个程序有五个输入数据集,输入数据来自AFL-QEMU运行生成
6.3 Benchmarks
baseline:afl-as的修改版本(去掉了追踪部分)作为基准开销
6.4 Timeouts
500ms
6.5 Untracer versus Covera-agnostic tracing
与AFL-QEMU和AFL-Dyninst进行黑盒比较,
与AFLClang进行白盒比较
6.6 Dissecting UnTracer’s overhead
启用fork server 和追踪(80%)是开销占比最大的两个部分
6.7 Overhead versus rate of Coverage-increasing test cases
在开始阶段,UntTracer相较于普通Fuzzer是慢一些的,因为开始时有很多覆盖增加的测试用例,因为跟踪读写以及文件输入输出操作。
作者的想法是设置一个阈值(增加覆盖的测试用例占比),大于阈值用普通Fuzzer,小于阈值用UnTracer
### 7. Hybrid fuzzing evaluation
QSYM-Tracer与QSYM比较
### 8.Discuss
8.1 UnTracer and Intel processor trace
硬件辅助覆盖跟踪
8.2 Incorporating edge coverage tracking
UnTracer使用块覆盖,作者讲到适用性(可能因为实现起来更容易吧)。
UnTracer目前不支持纯黑盒,需要两个版本的目标二进制文件:tracer(白盒插桩)和oracle(黑盒插桩)