自20世纪90年代以来,模糊测试一直是漏洞挖掘领域最广泛使用的技术之一。许多“白帽子”使用模糊测试技术在“黑帽子”之前发现漏洞,以进行安全防御。各大公司在实际的项目开发中,也经常使用模糊测试技术检测产品的安全性。同时,越来越多的与模糊测试相关的研究成果也出现在安全领域的顶级会议上。
然而很多fuzzer的描述文档很不完善,而且不同的fuzzer在命名方面碎片化严重。文章要统一fuzz技术中的术语并建立一个统一的fuzzing模型。
Fuzzing技术是一种基于黑盒(或灰盒)的测试技术,通过自动化生成并执行大量的随机测试用例来发现产品或协议的未知漏洞。
Fuzzing test:模糊测试是使用fuzzing测试被测程序是否存在违反安全策略。
Fuzzer:fuzzer是在被测程序上执行fuzz测试的程序。
Fuzz Campaign: 在特定安全策略下的一轮fuzz,目的是发现违反安全策略的bug。
Bug Oracle:检测程序执行是否违反安全策略的一种机制。
Fuzz Configuration:Fuzz算法的参数值。
顶会
算法 part 1 以一系列fuzz configuration C,和限定时间t作为输入,以发现的bug B作为输出。
算法 part 2 分为两个部分,第一个是preprocess的部分,在fuzz campaign的开头执行,第二个部分是五个函数的循环:
schedule,inputGen, inputEval,confUpdate,continue。
每次循环为一个fuzz迭代, 循环中的inputEval函数执行fuzz测试的过程叫做fuzz run。
根据用户输入的fuzz configuration来生成potentially-modified的fuzz configurations,其中的操作根据具体的fuzz算法来定,可能的操作包括给代码插桩,测量原始fuzz文件的执行时间等。
根据当前时间和deadline从输入的fuzz configurations选择一个进行fuzz迭代。
根据选择的fuzz configuration生成测试用例。
用tcs(用例)测试,通过bug oracle(嵌入在fuzzer中)检查本次执行是否违反了安全策略。输出bugs和每次fuzz run得到的信息execinfos。
根据现有的configuration和执行信息更新(优化)fuzz configurations。
根据现有fuzz configurations判断是否该运行新的迭代,主要用于白盒测试中路径已遍历完整情况。
3种fuzzer,提出异于传统的灰盒fuzz
2.4.1 Black-box Fuzzer
IO-driven or data-driven testing
2.4.2 White-box Fuzzer
2.4.3 Grey-box Fuzzer
不完全解析程序语义,而是执行轻量级的静态分析或收集执行的动态信息(如代码覆盖率)。灰盒牺牲了信息的完整性来换取速度和测试输入数量。
早期的灰盒fuzzer有EFS,算法根据每次fuzz收集的代码覆盖率信息来生成测试用例。Randoop也类似,不过它不是针对安全漏洞的fuzzer。
AFL和VUzzer是灰盒业界标杆。
预处理通常是检测被测程序,包括插桩,去除冗余配置(种子选择),修剪种子,生成驱动程序,还有准备生成输入的模型。
白盒和灰盒fuzz可以通过插桩收集fuzz执行时得到的或通过fuzz runtime内存得到的程序反馈。(收到程序反馈的量决定了这是什么颜色的fuzzer)
插桩分为动态和静态,静态在程序运行之前,由于它发生在运行时之前,因此通常比动态插桩占用更少的运行时开销。动态在程序运行时。其中动态插桩在本算法的inputEval部分。
插桩又分为源码插桩和二进制插桩。
动态插桩的好处在于容易对动态链接库进行插桩,工具例如DynInst , DynamoRIO , Pin , Valgrind, and QEMU.
一些fuzzer既可以动态插桩也可以静态插桩,AFL支持在有源代码时静态插桩,无源码利用QEMU工具课进行动态二进制插桩。动AFL还能记录外部库函数的覆盖率,fuzz外部库路径。
灰盒模糊器通常将执行反馈作为输入来演化测试用例。
路径覆盖:AFL(覆盖率),CollAFL(解决hash冲突)
节点覆盖:LibFuzzer,Syzkaller
对于需要启动一段时间后再接收输入文件的一些大程序来说,每次fuzz都要启动一遍效率很低,解决方案为将以就绪接收输入文件状态的程序打快照进内存,然后每次迭代直接从访问内存中快照的状态进行。此方法亦适用于有大型通讯流的网络应用。
一些fuzzers 执行in-memory fuzzing每次迭代时不需要重现受测程序的初始状态,这种技术叫in-memory API fuzzing。AFL的persistent mode就是可以实现fuzz loop却不用重启进程,在这种情况下,AFL忽略了在同一次执行中多次调用函数的潜在副作用。
缺点:bugs不好复现,多次重复执行一种函数带来的副作用,主要依赖于被测程序的入口函数,不是很好找。
Race condition(由于两个或者多个进程竞争使用不能被同时访问的资源,使得这些进程有可能因为时间上推进的先后原因而出现问题,这叫做竞争条件)的bugs比较难以触发,因为这种bugs触发条件为不确定的行为。但是,通过显式地控制线程的调度方式,插桩技术也可以用来触发不同的非确定性程序行为。现有的工作已经表明,即使随机调度线程可以有效地发现竞争条件错误。
怎样减小初始种子库大小的问题称为种
子选择问题。
找到最小的种子集,以使覆盖率(例如节点覆盖率)最大化,此过程称为计算最小集(minset)。
例如:两个种子s1,s2覆盖了PUT中的地址
{s1 → {10, 20} , s2 → {20, 30}},如果第三个种子为s3 → {10, 20, 30}且速度与s1和s2相同,则替换掉,可以省一半时间
较小的种子可能会消耗较少的内存并引发更高的吞吐量,所以一些fuzzers在fuzz之前减小种子的大小,这便是种子修剪。
它可以发生在preprocess或confUpdate过程中。
AFL的种子修剪使用代码覆盖率工具迭代地删除一部分种子,同时保证了修改后的种子有相同的覆盖率。
难以直接测试PUT时,安排一个driver来fuzz是说得通的。例如要fuzz一个库,我们写一个调用了库中函数的driver程序,再来fuzz driver程序。
在fuzz过程中每次迭代选用哪个fuzz configuration的工作叫做调度。
BFF和AFLFast,有着创新的调度算法。
调度的目标是分析当前可用的配置信息,并选择可能导致最有利结果的信息,例如,找到最大量的唯一错误,或者最大化由所产生的输入集获得的覆盖。从根本上说,每种调度算法都面临着相同的探索与利用冲突时间,可以将冲突时间花在收集每种配置的更准确信息以通知未来的决策(探索)上,也可以花在fuzz当前被认为会导致更有利结果(利用)的配置上。
在我们的模型fuzzer(算法1)中,功能调度基于
(i)当前的fuzz配置集C
(ii)当前时间终止
(iii)总时间预算tlimit来选择下一个配置。此配置随后用于下一次模糊迭代。请注意,调度安排只与决策有关。
在黑匣子设置中,FCS算法唯一可以使用的信息是FC的模糊结果、发现的崩溃和错误的数量以及在FC上花费的时间。
Householder和Foote是第一个研究如何在CERT-BFF黑箱变异模糊器中利用这些信息的人。他们假设应该首选观察到成功率较高的配置。
Woo等人转化为了WCCP/UW问题。 权重分配
在灰盒设置中,FCS算法可以选择使用关于每个配置的更丰富的信息集。
AFL基于覆盖率,进化算法
为了在进化算法环境下理解FCS,我们需要定义(i)什么使配置适合,(ii)如何选择配置,以及(iii)如何使用所选配置。
AFLFAST改进了种子队列调度和能量分配策略。
AFLGo通过修改其优先级属性来扩展AFLFast,以便针对特定的程序位置。
Hawkeye通过在种子调度和输入生成中利用静态分析,进一步改进了定向模糊。
FairFuzz通过为每一对种子和一个稀有分枝使用一个突变mask来指导运动,执行稀有分枝。
QTEP使用静态分析来推断二进制文件的哪个部分更“错误”,并对覆盖它们的配置进行优先级排序。
由于测试用例的内容直接控制是否触发bug,因此用于生成输入的技术自然是fuzzer中最有影响力的设计决策之一。传统上,模糊器可以分为基于生成的模糊器和基于变异的模糊器。基于生成的模糊器基于描述输入期望的给定模型生成测试用例。本文称之为基于模型的模糊器。另一方面,基于变异的模糊器通过变异给定的种子输入来生成测试用例。基于变异的模糊器通常被认为是无模型的,因为种子仅仅是示例输入,即使是大量的种子,它们也不能完全描述输入的期望输入空间。
基于模型的fuzzer基于一个给定的模型生成测试用例,该模型描述了被测程序可接受的输入或执行,比如精确描述输入格式的语法,或者不太精确的约束。
预定义模型-规定格式/片段重组
推断模型-PREPROCESS or CONFUPDATE时,推测inputs,生成合格式的输入
编码器模型
通过修改seed生成测试用例
白盒fuzzer也可以分为基于模型的fuzzer和无模型的fuzzer。并不是所有的白盒模糊器都是动态符号执行器。一些fuzzer利用白盒程序分析来查找关于被测程序接受的输入的信息。
动态符号执行(DSE,也叫做concolic execution)使用一个具体的值进行符号执行。这种符号执行可以避免由循环引起的路径爆炸,和在表达式太复杂或系统调用会生成值时给符号一个具体的值。
在这种方法中,我们每次只考虑一条路径,输入约束会随着程序路径的不断延伸而不断增长,当路径走到尽头时,将其中某一个约束条件取反,以到达新的代码路径。
一些fuzzer利用静态或动态程序分析技术来提高fuzzing的有效性。这些技术通常包括两个阶段的模糊化:(i)为获得关于PUT的有用信息而进行的代价高昂的程序分析,以及(ii)在前面分析的指导下生成测试用例。
绕过校验和验证
在生成输入之后,fuzzer将inputs发送到被测程序执行,并决定如何处理执行结果。这个过程叫做输入评估。有许多与输入评估相关的优化和设计决策会影响fuzzer的性能和有效性。
检测崩溃
内存安全错误可以分为两类:空间错误和时间错误。另一类内存安全保护是控制流完整性。
当编译器实现的未定义行为与程序员的期望不匹配时,常常会出现漏洞和错误。
测试输入验证漏洞(如XSS(跨站点脚本)和SQL注入漏洞)是一个具有挑战性的问题,因为它需要了解驱动web浏览器和数据库引擎的非常复杂的解析器的行为。
语义错误通常使用一种称为差异测试的技术来发现,该技术通过比较相似(但不完全相同)程序的行为发现错误。一些fuzzer使用了差异测试来识别相似程序之间的差异,这可能表明存在一个bug。
我们的模型考虑了各个模糊迭代的顺序执行。虽然这种方法的直接实现只需每次在模糊操作开始时启动一个新进程时加载被测程序,但可以显著减少重复加载过程。为此,现代的fuzzer提供了跳过这些重复加载过程的功能。例如,AFL提供了一个fork服务器,它允许每个新的fuzz迭代从已经初始化的进程派生。类似地,内存模糊是优化执行速度的另一种方法。
分类是分析和报告导致违反安全策略的测试用例的过程。分类可以分为三个步骤:消除重复数据、优先级划分和测试用例最小化。
重复数据消除是从触发与另一个测试用例相同错误的输出集中删除任何测试用例的过程。理想情况下,重复数据消除将返回一组测试用例,其中每个用例都会触发一个唯一的错误。
重复数据消除是大多数fuzzer的重要组成部分,原因有几个,通过在硬盘上存储重复的结果来避免浪费磁盘空间和其他资源。作为可用性方面的考虑,重复数据消除使用户能够很容易地大致了解存在多少不同的错误,并能够分析每个错误的示例。这对各种使用者很有用;例如,攻击者可能只希望查找可能导致可靠利用的“本地运行”漏洞。
目前有三种主要的重复数据消除方法在实践中使用:
优先级排序:可利用>可能可利用>未知>不可能可利用
覆盖所有violation的最小子集
种子库更新
维护一个MISET或一组测试覆盖率最大化的最小测试用例集