文献翻译之一。

摘要

旨在更加有效和高效地检测出各类软件的漏洞,越来越多的用于 Fuzzing 的工具(Fuzzers)在论文中被提出。由于各类 Fuzzer 的研究中采用的基准、性能指标和实验评估的不一致,Fuzzing 过程中可用的内部信息锅过少,阻碍了期望得到的 Fuzzing 结果,使得 Fuzzers 之间的比较仍然面临巨大的挑战。在本论文中,我们设计和开发了 UNIFUZZ,一个开源的并且以性能指标为驱动的 Fuzzers 评估平台,可以对每个 Fuzzer 进行综合定量的评估比较。特别地,UNIFUZZ 至今已经包含了 35 个可用的 Fuzzer,20 个真实世界中的程序样本以及 6 类性能指标。我们首先系统地学习了现有 Fuzzer 的可用性,查找并修复了一系列瑕疵,并将它们整合进了 UNIFUZZ。基于以上调研,我们从 6 个不同的角度提出了一系列实用的性能指标来用于检测 Fuzzer。通过 UNIFUZZ,我们深入引导了多种流行的 Fuzzer 的评估测试,包括 AFL、AFLFast、Angora、Honggfuzz、MOPT、QSYM、T-Fuzz、VUzzer64 等。我们发现,在经过所有目标程序的检测后,它们无一能够胜过除自己外的其他 Fuzzer。如果只使用单一的性能指标来评估一个 Fuzzer,可能就会得出单一的结论,也就说明通过综合性能来进行评估是十分重要的。再者,我们确认和研究了先前忽略的一些可能影响 Fuzzer 性能的因素,包括二进制插桩和程序崩溃样本分析工具等。我们的经验得出的结果显示,这些因素也对 Fuzzer 的评估极其关键。我们希望我们的发现可以为可靠评估 Fuzzing 性能的研究抛砖引玉,使得我们能够发现更多所期望的 Fuzzing 结果以促进未来针对 Fuzzer 的设计。

1、简介

Fuzzing 是一种软件测试的技术,其通过对目标程序执行大量的非正常或随机的测试用例,来达到检测恶意程序漏洞的目的。近年来,越来越多的 Fuzzing 相关的产品被融入到工业界和学术界。在工业界中,主流的软件提供商,如 Google 和 Microsoft 利用 Fuzzing 技术来帮助对其产品的漏洞检测。与此同时,GitHub 中托管了超过 2000 多个与 Fuzzing 相关的代码仓库。在学术界,据 DBLP 记录,自 2010 年后,已有超过 200 多篇与 Fuzzing 相关的论文被发表。

尽管 Fuzzing 技术发展迅速,仍然有一些大家共有的问题尚待解决。(1)这些 Fuzzers 在实际使用过程中怎么执行?(2)怎么以一个足够公平且全方面的性能指标来比较不同的 Fuzzer?(3)哪一种 Fuzzing 的技术可能是未来的主流,应该被我们广泛使用?但是,先前的一些工作都没能成功地解决以上这些问题,原因如下。第一点,很多现有的工作并没有实施合适的和足够的实验来给出一个令人信服的结果。比如说,常常能看到一些次数不够的重复实验,导致实际得出的结果是随机且不可靠的。另外,在和其他 Fuzzing 方法比较时,很多 Fuzzing 的工作都直接采用先前已经发表过的数据,而并没有重新对实验进行测试。这一点使得不同 Fuzzer 测试的实验环境(CPU、内存大小等)是不同的。第二点,现有 Fuzzer 的测试通常因为缺乏统一的标准而具有偏见的可能。目标测试程序的选择在不同的 Fuzzing 相关的论文中相差非常大。因此,很有可能是测试的 Fuzzer 在特定的测试程序上表现出了较好的性能而已。第三点,现有的一些性能指标对于测试 Fuzzer 来说既不合适也不够综合。如果只是利用程序崩溃的次数来代表 Fuzzer 查找漏洞的能力是不合适的,在程序崩溃次数和漏洞数量之间还是存在非常大的差异的。另外,大部分现有的 Fuzzing 相关工作并不会评估 Fuzzer 所消耗的计算资源。因此,现在亟需一个为当前的 Fuzzer 用于提供综合且实用的测试的统一平台。

对 Fuzzer 进行综合且实用的评估需要克服多个重要的挑战。第一点,虽然大部分 Fuzzer 已经开源了,但是他们在实践中的实用性是非常有限的。在最近的一些研究中有所阐述,大部分实验的复现存在问题,阻碍了不同 Fuzzer 间的比较。因此,有必要测试并加强 Fuzzer 的可用性。第二点,Fuzzer 的评估应该在一些实用的标准程序下进行。现有实用的检测程序是不够好的。一个可靠的 Fuzzer 评估需要具备实用性的标准。第三点,评估必须基于一个综合的性能指标集合来实现。然而,现有的指标数量不够且太过粗糙,导致产生并不完整的评估结果。因此,增加性能指标来达成一个更综合的评估是非常重要的。

为了说明上述的挑战,我们设计和开发了 UNIFUZZ,一个开源的,完整的,实用的,以性能指标为驱动的 Fuzzer 评估平台。总体来说,我们主要做了以下贡献。

  • 1)一个开源的,实用的,以性能指标为驱动的平台。我们设计和开发了 UNIFUZZ,第一个可以综合并定量地评估 Fuzzer 性能的开源平台。至今,已经有 35 个流行的 Fuzzer,20 个真实世界的程序样本以及 6 类性能指标。对于每一个 Fuzzer,我们测试了它们的可用性,并提供了一个 Dockerfile 来方便其安装和部署。另外,我们找到并修复(部分)超过 15 个代码缺陷,并已经汇报给相应的开发者。对于 20 个真实世界的程序样本,UNIFUZZ 提供了所有必要的相关信息,如软件安装,命令行参数等,来确保程序的可用性。再者,我们在 UNIFUZZ 中提供了相关的工具来处理对程序崩溃样本的分析,包括将程序崩溃分类成漏洞,和相关的 CVE 进行关联,以及分析漏洞的严重程度等。特别地,我们开发了一个 CVE 相关的关键词数据库,其中包括了所有 UNIFUZZ 测试程序样本相关的 CVE,极大地减少了在匹配 CVE 时人力上的消耗。我们也提供了一个性能指标的集合,分为 6 类:不同漏洞的数量,漏洞的质量,查找漏洞的速度,查找漏洞过程中的稳定性,覆盖率,额外开销。根据这些指标来综合地评判一个 Fuzzer 的性能。
  • 2)针对 Fuzzer 进行广泛的评估测试。通过应用 UNIFUZZ,我们应用了大量的实验来比较 8 个非常流行的基于覆盖率的 Fuzzer。实现结果显示,在所有测试目标程序中,没有任何一个 Fuzzer 能够完全胜过其他所有的 Fuzzer,与相应的已发表论文的结果相悖。这个观察结果说明先前的一些 Fuzzing 相关论文中的确存在主观性和某些偏见。再者,实验结果反映。使用单一的指标来衡量 Fuzzer 的性能智能得出一个单方面的结果,说明了使用综合指标的来评估 Fuzzer 的重要性。
  • 3)对未来 Fuzzing 技术相关的新发现和洞见。从评估结果中看,我们收获了对未来 Fuzzing 技术研究具有重要意义的一些发现。比如,我们发现先前尚未统计的一些因素可以对 Fuzzer 的性能产生重大的影响,如插桩和程序崩溃样本分析工具等。该结果说明,即使是非常小的改变,也能对 Fuzzer 的评估产生巨大的影响。因此,应该用更加严谨与精确的方式进行 Fuzzing 的实验,才能得出更加令人信服的结论。

2、开发 UNIFUZZ 的动机

为了评估现有 Fuzzer 的性能,并为新一代的 Fuzzer 开发提供契机,亟需对现有的不同 Fuzzer 进行一个深度的比较研究。不幸的是,以下有对一些 Fuzzer 进行综合比较的研究,为 UNIFUZZ 的设计提供了想法。

现有 Fuzzer 可用性存在问题。现有的 Fuzzer 在实践中存在一些问题。第一点,一些 Fuzzer 使用起来可能会很困难或很复杂。比如,Zhu 等人的论文中提到。他们团队不能很好地运行 Driller,T-Fuzz,VUzzer 等 Fuzzer。第二点,我们发现很多 Fuzzer 存在大量的代码缺陷(如,对程序崩溃的错误判断,Fuzzing 过程中存在异常行为等),可能会对其性能产生消极的影响。因此,有必要对现有的 Fuzzer 是否可用进行检测,并利用开源社区的影响来加强对 Fuzzer 在实践中的可用性。由于空间的限制,我们在 UNIFUZZ 开源平台上更详细地记录了对多个流行的 Fuzzer 框架缺陷的分析。

缺乏一些具有实用性的真实世界程序。目标测试程序是评估 Fuzzer 的基础,必须对此好好设计才能对 Fuzzer 进行足够公平的评估。因此,好的测试程序应该有以下的特性。第一点,它们必须和真实世界的程序有相似的特征,这些特征包括编码风格,程序大小以及漏洞等等。只有这样,一个 Fuzzer 在这些程序上所反映的性能才具有象征性。第二点,为了更综合地评估 Fuzzer,测试程序应该展现出在功能,程序大小,漏洞种类等方面的多样性。第三点,从更实用地评估 Fuzzer 挖掘漏洞能力的角度来讲,每个程序应该至少包括一个在一定时间内可被发现的漏洞,也就是一个实用的程序必须包括的两个重要属性。(1)测试程序至少应该包括一个漏洞。否则,无法区分出 Fuzzer 在挖掘漏洞方面的能力。(2)挖掘漏洞的难度必须是在可控范围内的。否则,测试过程可能产生过大的代价。例如,针对一个 Fuzzer 在一个测试程序上需要耗时一个月的 Fuzzing 实验,30 次的重复实验需要 21600 的 CPU 小时,更不用说在多个测试程序和种子序列上做一个令人信服且足够综合的评估了。第四点,测试程序的使用应该足够简单。为此,开发者应该为测试程序提供足够的信息,如安装方法,命令行参数,输入类型等。再者,如果测试程序的开发者能提供方法/工具来自动分析相关的崩溃样本就更好了。

现有的 Fuzzing 测试程序可以被分为两类:自行编写的程序以及现实生活中在使用的程序。自行编写的程序中,典型的例子包括 LAVAM 和 DARPA CGC。真实生活中的程序,典型例子包括 exiv2,mp3gain 等,都是一些 Linux 下的开源程序,并有很多已经公开的漏洞。但是,现有的测程序仅包括这两类是远远不够的。

现有的真实程序无法满足以上要求的原因有下。第一点,我们仍然缺少标准的和足够的真实程序,且现有的 Fuzzer 通常只会在一个自选的程序上进行测试,可能会带来一定的片面性。第二点,由于对漏洞的触发没有一个明确的显示,真实的程序并没有自行编写的简单程序那样方便地来验证漏洞。例如,现有的 Fuzzer 通常会对崩溃类型进行分类并应用不同的工具来过滤漏洞,如 AddressSanitizer(ASan)和 GDB 等。但是,由于不同工具之间自身的一些限制和不一致,这些不同的程序崩溃分类方法也可能会产生一些片面的结果。再者,许多论文都说它们手动地对 CVE 进行验证,或是根本没有提到对 CVE 进行验证。不过,手动验证的过程非常消耗时间且令人厌倦,还有可能存在一些片面和错误的结果。以上所有的问题急需开发一套多样且实用的测试程序,以及一系列自动化的程序崩溃样本分析工具。

缺少合适且综合的性能指标。大部分先前的工作通常使用三种事实的标准来评估 Fuzzer:不同的程序崩溃次数,不同漏洞数量以及覆盖率。但是,这些指标单独来说通常不能完整地计算出一个 Fuzzer 的性能表现。例如,单独地依赖于程序崩溃的次数可能会导致得出错误的结论,因为越多的崩溃次数并不等同于漏洞数。此外,除了漏洞的数量,漏洞的质量也是一个非常重要的指标,需要在评估时进行考虑。例如,当两个 Fuzzer 在同样的时间内找到了同样数量的漏洞,而其中一个 Fuzzer 找到了更难发现且更具威胁的漏洞,却单纯地认为两个 Fuzzer 的性能相同是不合适的。最后,资源使用率也是一个非常重要的指标。如果 Fuzzer A 找到了 Fuzzer B 两倍数量的漏洞,而 Fuzzer A 比 Fuzzer B 消耗了更多的时间和计算机资源,那并不能认为 Fuzzer A 的性能更好。因此,我们需要对这些性能指标进行完善,使得它们可以为 Fuzzer 提供更加综合可靠的性能评估。

3、UNIFUZZ 的设计

为了说明第 2 节提到的挑战,我们设计和开发了 UNIFUZZ,一个用于 Fuzzer 评估的开源平台。图 1 展示了 UNIFUZZ 的概览,其主要包括了三个部分:可用的 Fuzzer,实用的测试程序以及各类性能指标。

3.1、可用的 Fuzzer

UNIFUZZ 至今已经结合了 35 个可用的 Fuzzer,包括 AFL,AFLFast,AFLGo,AFLPIN,AFLSmart,Angora,CodeAlchemist,Driller,Domato,Dharma,Eclipser,FairFuzz,Fuzzilli,Grammarinator,Honggfuzz,Jsfuzz,Jsfunfuzz,LearnAFL,MoonLight,MOPT,NAUTILUS,NEUZZ,NEZHA,Orthrus,Peach,PTfuzz,QSYM,QuickFuzz,radamsa,slowfuzz,Superion,T-Fuzz,VUzzer,VUzzer64 以及 zzuf。合并的 Fuzzer 种类多样,包括基于语法的,基于变异的,定向的以及基于覆盖率的 Fuzzer。表 1 展示了可用的一些 Fuzzer 是如何和 UNIFUZZ 进行结合的。为了测试这些 Fuzzer 的可用性,我们手动编译并测试了每一个 Fuzzer。在这个过程中,我们找到了这些 Fuzzer 在设计上和部署上的很多缺陷。截至最新,我们已经在这些 Fuzzer 中找到了超过 15 个重要的缺陷,并汇报给了对应的开发者。在我们的帮助下,一部分缺陷已经快速地解决并发布了修改。对于这些细节更加具体的描述被记录在 UNIFUZZ 的开源平台上。表 1 中的每一个 Fuzzer,我们都设计了一个 Dockerfile 来对其进行安装并在 Docker 容器中方便地使用。我们选择在 Docker 容器中进行 Fuzzing 实验的原因如下。第一点,和物理机器上的实验相比较,Docker 对于资源的分配和隔离有更加方便帮助,可以为 Fuzzer 评估提供足够公平的环境。第二点,和虚拟机相比,Docker 更加轻量化并且消耗更少的计算机资源。因此,在消耗有限资源的情况下,用户使用 Docker 同时可以执行更多的 Fuzzing 实验。除了测试这些 Fuzzer 的可行性并最终使其可用,我们在 8 个较为突出的基于覆盖率的 Fuzzer 进行了测试,具体细节将在第四节进行展开。

3.2、实用的测试程序

根据第二节的内容,实用的测试程序必须包括以下属性:(1)和真实世界的程序相似,包括代码风格,程序大小,漏洞等。(2)综合性,在函数,大小,漏洞种类等方面有一定的区分。(3)实用性,意味着在合理的一定时间内,必定能在该程序中找到至少一个漏洞。(4)方便使用,意味着用户能够很简单地上手这个测试程序,并能获取得到评估的结果。根据上述的准则,我们构建了一个实用的测试程序集,包含了表 2 中所示的 20 个现实生活的程序。特别地,UNIFUZZ 对每个程序提供了详细全面的信息,包括版本,大小,安装信息,输入类型,命令行参数等,以便确保其可用性。对于每个程序来说,UNIFUZZ 提供了它的源代码和对应的 Dockerfile 来进行安装和使用。再者,UNIFUZZ 提供了实用的工具来方便分析目标程序的崩溃样本。该分析包括但不限于:(1)对崩溃样本进行重复删除以及分类;(2)将崩溃样本和相应地 CVE 进行匹配以及(3)分析崩溃所触发漏洞的严重性。值得注意的是,我们并没有对测试程序进行修改。作为结果,真实程序的原始特性被保留。相反,我们专注于如何去选择程序,以及开发方便分析实验结果的一些工具。接下来,我们将会描述对程序选择以及分析崩溃方法的一些细节。

程序的选择。为了选择更合适的测试程序,我们调研了一些 Fuzzing 相关的信息安全和软件工程的顶会论文,来进一步寻找它们在测试 Fuzzer 过程中应用的测试程序以及对应的版本。基于上述的过程,我们最终选择了如表 2 所示的 20 个现实生活中的程序。被选择的程序包括了六种功能:图像处理,音频处理,视频处理,文字处理,二进制分析,网络数据包处理软件。另外,它们也包括了不同种类的各种漏洞,包括:heap buffer overflow,stack overflow,segmentation fault,excessive memory allocation,global buffer overflow,stack buffer overflow,memory leak,free error,float point exception,alloc-dealloc mismatch,memcpy parameter overlap,use-after-free 等。因此,这些程序能够为 Fuzzer 提供全面的评估。

将崩溃分类成对应的漏洞。总的来说,总共有两种对崩溃分类成漏洞的方式:一个是基于分析漏洞的根本原因,另一个是基于分析漏洞的输出结果。对于第一个方式,常见的一种措施是每个漏洞的起因将程序打上补丁。如果 a 和 b 两个崩溃样本都触发了目标程序的漏洞,但是修复的程序没有被触发,那么可以说明 a 和 b 对应的是同一个漏洞。虽然这种方式似乎能够为测试程序提供较为准确可靠的信息,但对根源的分析是非常困难的,并且在实践中存在非常多的挑战。为了能够提供更加全面的信息,我们需要了解目标程序所有版本的补丁。每一个补丁版本必须只能修复一个单一的漏洞。否则,可能会造成巨大的误报/漏报。

第二种方式通常在漏洞被触发后对输出信息进行分析时才使用。和第一种方式相比较,第二种方式在部署过程中更加实用,能够提供相对来说更公平的评估结果。比如说,一个常用的方法是,应用 ASan 这样的工具,在触发漏洞时生成相应地堆栈调用信息。然后使用栈哈希方法来解压 N 个栈帧,并删除重复的漏洞。当选择了不同的 N 时,这种方法也有可能产生误报/漏报。然而,怎么选择 N 的值来提供最小的误报/漏报率是一个非常困难的研究问题,这个问题还没有完全被解决,并且也不在本文的讨论范围内。在先前一些论文的指导下,我们选择 N 的值为 3。另外,需要不同的工具选择不同的方法来探测漏洞,因为单个工具可能会疏忽某种漏洞的分析。因此,为了获得更加精确的探测漏洞结果,我们优先考虑通过 ASan 得到的结果,然后使用 GDB 等工具的结果作为辅助分析的材料。

和 CVE 进行匹配。Common Vulnerabilities and Exposures(CVE)提供了现有漏洞的相关信息。大部分现有的 Fuzzing 工作都通过 CVE 的相关信息来评估 Fuzzer 的漏洞挖掘能力。因此,弄清楚有多少已知/新漏洞(CVE 漏洞)被 Fuzzer 发现是最重要的一点。但是,将崩溃样本和 CVE 进行匹配是一个非常消耗时间和烦人的,原因如下。第一点,每个 CVE 的描述都是从自然语言的角度展开的,没有一个规范的固定结构。因此,直接从描述中解压出一些关键信息非常困难(如,漏洞函数名称等)。第二点,虽然每个 CVE 的引用可能提供了一些附加信息,如触发 CVE 的 PoC(Proof-of-the-Concept)文件以及通过崩溃分析工具得到的输出报告,但是这样的信息通常是不完整或者又缺少的,导致对 CVE 的匹配会更加困难。再者,这些引用链接是没有系统性的结构的。因此,需要一些人力来弄清楚每一个引用链接代表了什么意思(如,PoC 文件的下载链接或者是漏洞报告等)。第三点,不同的 CVE 描述中中可能包含的是不同的工具生成的输出报告,直接对不同的输出报告进行匹配也是非常困难的。

为了减少在匹配 CVE 时所消耗的人力,我们构建了一个 CVE 关键字数据库,其中包括了 UNIFUZZ 中目标测试程序相关的 CVE 信息。这个数据库可以方便地将崩溃样本与 CVE 之间进行一个匹配。和官方的 CVE 网站相比,在 CVE 关键字数据库中的信息的信息结构更加友好。在 CVE 关键字数据库中,每个目标测试程序对应一个 CVE 表。表中的每一项代表了一个 CVE 的信息。每一项的主键是 CVE 的 ID,其他的属性都是该 CVE 的一些重要信息,包括漏洞的种类,有漏洞的函数,有漏洞的文件,函数调用栈,生成函数调用栈的工具等。为了使用 CVE 关键字数据库,我们应用了一种方法来方便地生成初始匹配结果。基于 CVE 关键字数据库,匹配 CVE 的过程如下。(1)通过相关的工具对程序进行编译(如 ASan),并执行程序产生崩溃来产生相应地输出报告。(2)从输出报告中解压出一些必要的信息,包括函数调用栈,漏洞类型,有漏洞的函数,有漏洞的文件名等。(3)将得到的信息和该程序在 CVE 关键字数据库中对应的 CVE 表进行匹配,通过关键字的匹配数量来汇报匹配得到的 CVE。(4)手动检查初始的匹配结果来确认最终的结果。注意官方的 CVE 网站存在一定的缺陷和错误,如信息不完整,CVE 存在重复的信息等。从另一个角度来说,有可能会找到 0-day 漏洞。因此,在这种情况下,有必要执行最后一步来确认匹配结果更加精确和准确。

对标准答案的讨论。总的来说,由于漏洞的天性,不管从真实的程序中还是人为编写的程序中,获取到完全标准的漏洞检测结果是非常困难的。对于人为编写的程序来说,其他的部分是否包含漏洞是未知的(排除本意要写入漏洞的情况),使得其很难获得真实的漏洞结果。而真实的程序是相似的,除了已经知道的一些漏洞,其是否还包含有新的漏洞也是未知的。即便如此,我们尽可能为测试程序提供了足够准确的信息,标准如下。第一点,为了缓解不完全性的问题,我们收集了尽可能多的崩溃样本来探测可能存在的漏洞。第二点,我们使用了多个工具来探测漏洞。除了 8 个基于覆盖率的 Fuzzer 外,我们将定向 Fuzz 的 AFLGo 和三个静态分析工具(Flawfinder,RATS,Clang Static Analyzer)结合,以查找目标测试程序中的更多漏洞。由于空间上的限制,探测结果的细节都公布在了 UNIFUZZ 的开源平台上。第三点,我们通过多种工具对漏洞进行分析(如 ASan 和 GDB)来减少由于单一工具的限制而产生的影响。

3.3、性能指标

为了说明缺乏综合实用的性能指标的这个问题,我们系统地学习了现有 Fuzzing 相关论文的一些性能指标,总共可以被分为 6 个种类:漏洞的数量,漏洞的质量,挖掘漏洞的速度,挖掘漏洞的稳定性,覆盖率以及资源是否过度用量。每个种类代表了 Fuzzerr 的一个性能,每个属性可以通过很多明确的指标来进行评估,且具有扩展性。举个例子,当评估漏洞的数量时,我们可以应用很多实用的数学公式,如 p 值和 A score 等。接下来,我们对每一类提供了相关的硬性标准,作为在评估实践中的建议。

漏洞的数量。因为在 Fuzzing 的过程中存在随机的可能,一个具备鲁棒性的 Fuzzing 实验必须能够重复多次,以提供一个足够可靠的结果。因此,漏洞的数量指标基于统计学的一些方法。我们主要关注与两个问题:(1)一个 Fuzzing 实验应该被重复多少次?(2)什么样的统计指标可以提供一个相对可靠的结果?对于这些问题有非常多不同的观点。对于问题(1),Klees 等人建议对每个 Fuzz 测试进行 30 次的重复。对于问题(2)Klees 等人说明常用的统计指标,如平均数,中位数和方差可能会产生错误的结果。除此之外,他们极力推荐使用使用统计测试来计算 p 的值,来决定两个 Fuzzer 间的性能是否有一定的区别。特别地,他们建议使用曼-惠特尼 U 检验来作为检测方法。原因是曼-惠特尼 U 检验不需要任何参数,可以让得到结果的分布不需要任何假设(以 Fuzzing 结果的分布为例,所有重复的次数里,漏洞的数量是未知的),反之一些其他的方法可能会更加严格。举个例子,t 检验假设两个实验结果必须遵守正常的分布且有相同的方差。但是,对于统计检验来说有很多不同的视角。举例说,Nuzzo 指出 p 的值并没有很多科学家所说的这么可靠,Wasserstein 等人建议我们不应该在 p 小于 0.05 且有明显区分或者 p 大于 0.05 且没有明显区分时得出一个结论。

基于上述的讨论以及我们的经验,我们对于上面两个问题的建议如下。对于统计指标来说,没有任何一个单个指标是完全完美的,最好是能够将一些统计指标的集合作为一体,如平均值,中位数,p 值等等。除了衡量 Fuzzer A 是否比 Fuzzer B 表现得更好,衡量出 Fuzzer A 相比 Fuzzer B 表现好了多少也很重要。建议使用 Vargha-Delaney 的 A score 来展示 Fuzzer A 比 Fuzzer B 表现得更好的概率。至于重复实验的次数,和选择的统计指标有关。举个例子,当使用曼-惠特尼 U 检验时,重复的次数必须大于 20 次。另外,在未来对这两个问题进行更深入的研究也是非常有必要的。

漏洞的质量。为了评估 Fuzzer 的性能,我们定义了漏洞的质量这一指标。漏洞的质量不应只是反应漏洞的严重性,同样包括 Fuzzer 挖掘稀有漏洞的效率。一个可以找到更多高质量漏洞的 Fuzzer 才是更好的 Fuzzer。特别地,我们从两个方面来衡量一个漏洞的质量:(1)一个漏洞是否具有更高级别的严重性以及(2)一个漏洞是否更难以被发现。对于方面(1),我们应用了分析工具来衡量一个漏洞的严重性。比如说,Exploitable 是 GDB 的一个扩展插件,其可以根据严重性对 Linux 应用漏洞进行分类。再者,我们可以将漏洞和其对应的 CVE 进行映射,通过 Common Vulnerability Score System(CVSS)的评分来评估 CVE 的严重性。对于方面(2),一个漏洞是否难以被发现通常有以下的特点:它只能被一小部分 Fuzzer 找到,或者它只能跟很小一部分的崩溃样本进行匹配。

挖掘漏洞的速度。快速高效地挖掘漏洞是非常重要的,尤其是时间片有所限制时。我们可以通过以下两种方法来衡量挖掘漏洞的速度。第一点,对于一个程序的所有漏洞,我们可以画出在自定义的一段时间内挖掘漏洞数量的递增曲线。如果曲线的斜率很大,说明挖掘漏洞的速度足够快,是与其相关的一个指标。第二点,对于特定的一个漏洞,我们可以记录 time-to-exposure(TTE)指标来衡量漏洞第一次被发现的时间,也是与其相关的一个指标。

挖掘漏洞的稳定性。稳定性是另一个重要的指标。一个高稳定性的 Fuzzer 相对来说更加可靠和实用。我们可以通过以下的方法来量化一个 Fuzzer 的稳定性。第一点,我们可以计算在所有重复实验中找到漏洞数量的 relative standard deviation(RSD)。小的 RSD 值说明其具备更好的稳定性。第二点,对有特定的漏洞,我们可以计算 Fuzzer 可以成功挖掘到它的时间。更高的成功率代表一个 Fuzzer 具备更好的稳定性。

覆盖率。覆盖率的指标可用于衡量一个 Fuzzer 探索路径的能力,对于判断一个 Fuzzer 的能力来说非常重要。因为有漏洞的代码通常只是程序完整代码中的一部分,如果只考虑漏洞的数量可能不能更好地和 Fuzzer 探索路径的能力区分开来。覆盖率指标可以通过不同的细粒度等级来衡量,如函数,基本块,代码行覆盖等等。

资源过度占用率。资源过度占用的指标旨在测量一个 Fuzzer 在执行 Fuzzing 时会消耗多少的计算机资源,这点也是非常重要的。例如,我们可能因为一个 Fuzzer 找到了很多漏洞而认为它的性能很好,但如果它相比其他的 Fuzzer 消耗了很多计算机资源,这个结论显然是有误的。资源过度占用率可以通过以下的硬性指标来进行衡量:CPU 使用,内存消耗,磁盘读写等。

4、当前 Fuzzer 的评估方法

通过 UNIFUZZ,我们可以在现有的 Fuzzer 上进行大量的实验,并在 6 种性能指标上综合地对不同 Fuzzer 进行比较。我们根据 Evaluating fuzz testing 一文中的向导,进行了重复 30 次 24 小时的 Fuzz 测试。

4.1、实验设置

Fuzzers。在我们的测试中,我们从 UNIFUZZ 中选择了 8 个现有基于覆盖率的 Fuzzer。包括 AFL,AFLFast,Angora,Honggfuzz,MOPT,QSYM,T-Fuzz 和 VUzzer64。选择这些 Fuzzer 的原因如下。第一点,它们都是当前应用非常广的 Fuzzer。AFL 和 Honggfuzz 被广泛地应用在工业界。其他的 6 个 Fuzzer 都是近年来被发表在信息安全顶会上的,带哦表者学术界最前沿的 Fuzzing 技术。第二点,虽然也有一些 CollAFL 这样的前沿的 Fuzzer,但是它们不是开源的,导致这些 Fuzzer 很难用来评判。第三点,选择的这 8 个 Fuzzer 相比其他的更具有一定的可伸缩性,可以用来测试大部分的程序。比较之下,其他的像是 QuickFuzz 的 Fuzzer 只能生成一定数量的测试样例。因此,它们只能在一部分的测试程序上进行测试。第四点,使用不同种类的 Fuzz 来进行比较显然是不合适的,这里我们只选择了基于覆盖率的 Fuzzer 来进行比较测试。对于其他组合进 UNIFUZZ 的 Fuzzer,我们主要关注与它们的可用性以及使其可用。另外,要使测试更公平以及更具比较的意义,我们对几个 Fuzzer 做了一些必要的修改。对于 Angora,我们将输入大小的限制从 15KB 修改为了 1MB,使其能和其他 Fuzzer 更公平地比较。对于 VUzzer64,我们根据其开源仓库修改了几个较为严重的代码缺陷(#10,#11,#12 和#14)。另外,我们将变量 GENNUM 的值从 1000 修改为 1000000,其决定了迭代的次数,使 VUzzer64 执行更长的时间。对于 T-Fuzz,我们修改了它的命名缺陷。所有上面的修改都为了使得测试比较更加公平。对于其他的 Fuzzer,我们将其保持为原本的设计。

程序。我们应用了 UNIFUZZ 提供的 20 个真实世界的程序(表 2)来测试上述选择的 Fuzzer。另外,我们也使用了 LAVA-M 来探索人造程序和真实世界程序的差异。每个测试程序都通过每个 Fuzzer 的指示要求进行编译。当验证完找到的漏洞后,每个 Fuzzer 对应的程序会再通过 ASan 和 GDB 进行编译。

初始化种子。遵循 Fuzzing 过程中常见的一些实践,对于每个相同的测试程序,我们采用相同的初始化种子。对于 UNIFUZZ 的测试程序,我们按照以下过程对初始化种子进行选择。第一,我们通过网上查到的相似文件格式来收集一些种子。第二,我们将不能满足 Fuzzer 的要求的种子排除(如,AFL 要求种子的大小必须小于 1MB)。然后,对于每一个程序,我们在剩下的种子中随机取 100 个种子。对于 LAVA-M 的程序,我们使用 LAVA-M 提供的种子。

环境。我们在 5 台相同配置的服务器上执行了所有实验:20 核的 Intel Xeon E5-2650 v4 CPU,频率是 2.20GHz,64 位 Ubuntu 16.04 LTS 系统。对于每一个 Fuzzer,我们分配了一个 CPU 核,2GB 内存以及 1GB 的交换分区。如果一个 Fuzzer 不能在 2GB 下成功运行,我们将内存上限调整为 8GB,并将改变应用到所有相同的 Fuzzing 实验上。我们将每个 Fuzzing 实验都在一个独立隔离的 Docker 容器中执行。

接下来我们会展示基于 6 类性能指标的主要测试结果,而更加详细的测试结果和数据集都上传到了 UNIFUZZ 的开源平台上。需要注意的是,T-Fuzz 在 ffmpeg 上实验的时候花费了过多的内存,而 VUzzer64 由于不支持从 stdin 的输入所以不能测试 sqlite3。因此我们没有包含上述的这几种情形的结果。

4.2、漏洞的数量

这一小节的主要内容是分析哪个 Fuzzer 可以找到更多不同的漏洞。正如在 3.2 节中所描述的,我们使用 ASan 生成的输出报告来获取调用栈的前三个函数,以此为一组来删除重复的漏洞。拥有不同函数组和种类的的这些漏洞可以被认为是不同的。而对于 ASan 不能检测到的漏洞,我们进一步使用其他工具生成的输出报告来作为一个补充,如 GDB 等。如 float point exception 可以被 GDB 检测到,而 ASan 不可以。

不同漏洞的数量。我们将每个 Fuzzer 在 UNIFUZZ 提供的测试程序上找到的不同漏洞,以及 LAVA-M 的 30 次重复实验分别可视化地展示在图 a 和图 b 中。从这些图片中,我们有以下的观察结果。(1)没有一个 Fuzzer 在所有程序上展现的性能能完全胜过其他 Fuzzer。(2)对于 20 个真实程序,QSYM 在 5 个程序上表现最好(gdk,jhead,lame,mujs,tcpdump)。Angora 在 3 个程序上表现最好(exiv2,wav2swf,nm)。Honggfuzz 在 3 个程序上表现最好(ffmpeg,flvmeta,cflow)。MOPT 在 3 个程序上表现最好(imginfo,lame,pdftotext)。AFL 在 tiffsplit 上表现最好。AFLFast,T-Fuzz 和 VUzzer64 没有在任何程序上表现出最好的性能。(3)对于 LAVA-M,Angora 在所选的 Fuzzer 中表现最好,与此同时,只有 QSYM 在 md5sum 上展现的性能和 Angora 相似。

统计结果。这里我们将 Fuzzer 在 30 次重复实验中找到的不同漏洞数量进行了展示。由于空间上的限制,我们只展示了两个统计结果:p value 和 A score。我们将其他如平均数和中位数等的统计结果公布在 UNIFUZZ 的开源平台上。p 值主要用于测量在两个目标对象之间是否有足够大的差异(对应于本实验中的两个不同 Fuzzer)。而 A score 主要用于衡量影响大小(如,一个 Fuzzer 在所有重复实验后性能强于其他 Fuzzer 的可能性)。这里,我们使用 AFL 作为性能基准的 Fuzzer。特别地,我们应用了曼-惠特尼 U 检验来计算 p 值,并且我们认为 p 小于 0.05 即表示存在一个很大的区别。对于 A score,我们认为 A 大于等于 0.71 即表示存在很大的影响。表 3 展示了在 30 次重复实验中不同漏洞的 p 值和 A score。根据表 3,我们得出以下的观察结果。(1)剩余的 7 个 Fuzzer 在所有真实程序上的表现无一能全部超越 AFL。然而,有一些 Fuzzer(Angora,QSYM 和 VUzzer64)在 LAVA-M 数据集上的所有程序的表现比 AFL 更好。(2)基于 p 值的结果,MOPT 比 AFL 在 17 个真实程序上表现更好,是 7 个 Fuzzer 中表现最好的。QSYM,Angora 和 Honggfuzz 分别在 13,11 和 11 个真实程序上表现得比 AFL 更好。但是,AFLFast 只在 4 个真实程序上表现得比 AFL 更好。更有甚者,只有 T-Fuzz 和 VUzzer64 没有在任何真实程序上表现得比 AFL 更好。(3)考虑到 A score 这一指标 Fuzzer,因为一些 Fuzzer 在 p 值比较中展现出类似的性能。这些具有较大影响力(A 大于等于 0.71)的 Fuzzer 在所有的程序上相较于 AFL 表现更都好(p 值小于 0.05),反之亦然。例如,AFLFast 在 mujs 上比 AFL 表现更好(p=0.04),但是影响力并不大(A=0.55)。

4.3、漏洞的质量

正如 3.3 节中所介绍的,我们将漏洞质量通过它们的严重性和稀有性来定义。

4.3.1、漏洞的严重性

漏洞的严重性可以通过 CVE CVSS 的评分以及 Exploitable 的结果来进行衡量。

CVE CVSS。CVSS 根据每个 CVE 的严重性提供了一系列的评分。当分数大于等于 7.0 时,说明该 CVE 是一个高危漏洞。我们应用了在 3.2 节中的 CVE 关键字数据库和相应地匹配方法,来获取初始 CVE 匹配结果。然后,我们手动检查这些初始化结果来获取最终的匹配结果。接下来,我们将每个 CVE 和对应的 CVSS 评分进行结合。在匹配 CVE 的过程中,我们找到了 6 个新的 CVE:CVE-2019-17450,CVE-2019-17451,CVE-2019-17594,CVE-2019-17595,CVE-2019-18359 以及 CVE-2019-19035。表 4 展示了 Fuzzer 找到的高危漏洞,我们在 UNIFUZZ 开源平台上提供了这些 CVE 更加详细的内容,包括 CVSS 评分以及漏洞种类。正如表 4 中所示,Fuzzer 对不同类型的程序进行漏洞挖掘有一定的偏向性。例如,QSYM 在 tcpdump 上找到了 57 个高危漏洞,而 Honggfuzz 一个都没有找到。但是,对于 ffmpeg,Honggfuzz 可以找到 1 个高危漏洞,而其他剩余的 Fuzzer(包括 QSYM)一个都找不到。再者,有趣的是,AFL 和 AFLFast 在不同程序上挖掘到的高危漏洞数量非常相似。

Exploitable 的结果。Exploitable 是 GDB 的一个插件,其使用了启发式算法来评估一个崩溃样本的可利用性,可以被归为 4 个种类:EXPLOITABLE,PROBABLY_EXPLOITABLE,PROBABLY_NOT_EXPLOITABLE 以及 UNKNOWN。特别地,我们通过 Exploitable 提供的哈希值对每类的漏洞数量进行了重复删除。表 5 代表了被分类为 EXPLOITABLE 的不同漏洞。正如表 5 中所示,MOPT 比其他的 Fuzzer 在 9 个程序上挖掘 EXPLOITABLE 类漏洞的能力更强。Angora,Honggfuzz 和 QSYM 分别在 3,5 和 3 个程序展现出最好的性能。然而,VUzzer64 表现得并不好,只能在 2 个程序上挖掘到 EXPLOITABLE 的漏洞。

4.3.2、漏洞的稀有性

直观的来说,一个漏洞只能被一小部分 Fuzzer 发现的话,可以说明它是很难被发现的(可能在更深的路径里或者有更多复杂的路径约束)。这里,我们将只能被一个 Fuzzer 找到的漏洞成为稀有的漏洞。相应地,一个 Fuzzer 如果可以找到更多的稀有漏洞,说明它是足够强大的。表 6 展示了被测试的 Fuzzer 找到的稀有漏洞数量。对于所有的真实程序来说,QSYM 表现出了非常好的性能,能够挖掘到 262 个稀有漏洞。MOPT 的性能排第二,挖掘到了 90 个稀有漏洞。而 Angora 总共找到了 56 个稀有漏洞。然而,AFLFast 只在 2 个程序上找到了稀有漏洞。AFL,T-Fuzz 和 VUzzer64 都只在 1 个程序上找到了稀有漏洞。值得注意的是,Fuzzer 在挖掘稀有漏洞时,也对程序类型有一定的偏向性。例如,QSYM 在 tcpdump 上找到了 204 个稀有漏洞,而 Angora 在上面找到了 1 个稀有漏洞,其他的 Fuzzer 没有找到任何稀有漏洞。对于 nm 来说,Angora 发现了 25 个稀有漏洞,而剩余的 Fuzzer,包括 QSYM 没有找到任何漏洞。

4.4、挖掘漏洞的速度

图 3 展示了在 30 次重复实验中,Fuzzer 挖掘漏洞的数量,在这里我们也能看出 Fuzzer 的漏洞挖掘速度。第一,一个直观的感受就是没有一个 Fuzzer 能够完全胜过剩余的其他 Fuzzer。第二,在 Fuzzer 性能之间的比较上可能会在超过一定时间后产生相反的变化。比如,MOPT 在 sqlite3 上的 Fuzzing 在开始一段时间内比 QSYM 挖掘到更少的漏洞,但是在 10 个小时后找到了更多的漏洞。第三,虽然一些 Fuzzer 找到了相同数量的不同漏洞,它们挖掘漏洞的速度是不同的。比如,Angora,MOPT 和 QSYM 在 mp42aac 上花 24 小时找到了相同数量的漏洞(分别为 2.5,2.6 和 2.4 个漏洞数),而 MOPT 查找漏洞的速度远快于 Angora 和 QSYM。这个情况也同样说明了速度这一指标的重要性,所以如果单看漏洞数量可能会忽略 Fuzzer 在速度上的区别。

4.5、挖掘漏洞的稳定性

图 4 展示了在 30 次重复实验中,不同漏洞的 relative standard deviation(RSD),较低的值说明 Fuzzer 具备更好的稳定性。如图 4 中所描述的,第一,所有的 Fuzzer 都不会在找漏洞的过程中始终保持稳定,说明 Fuzzing 具有一定的随机性,以及重复实验的必要性。第二,在 7 个 Fuzzer 中,Angora 和 T-Fuzz 具有更低的 RSD 值,而 AFL 和 Honggfuzz 的 RSD 值更高。第三,Fuzzer 的稳定性在不同的程序上有不同的表现。比如,与 mp42aac 相比,AFL 在其他几个程序上有更好的稳定性,如 tiffsplit,mp3gain。需要注意的是,挖掘漏洞的稳定性是漏洞数量的一个辅助指标,也就是说,找到更多的漏洞远比稳定地找更少的漏洞重要。

4.6、覆盖率

现有 Fuzzer 采用不同的方法和细粒度来跟踪对程序的路径覆盖率。例如,AFL 使用插桩编译和位图的方式来跟踪覆盖率。Honggfuzz 采用 SanitizerVoverage 插桩的方法来跟踪基本块的覆盖率。为了更加公平地对这些 Fuzzer 的路径跟踪能力进行比较,有必要设计一套统一的方案(在相同的细粒度大小下使用相同的插桩方法)来跟踪不同 Fuzzer 的覆盖率。一个直观的方法是保存 Fuzzer 执行过的所有测试用例,然后使用相同的插桩程序计算它们的覆盖率。然而,这种方法是不现实的,因为实际测试用例的数量非常多。为了在准确率和效率之间做到尽量平衡,我们开发了一种高效的方法通过只考虑能够提升覆盖率测试用例。特别地,我们在 Fuzzing 过程中保存了所有能提高覆盖率的测试用例,然后根据保存的测试用例应用 afl-cov 来计算每个程序的代码行覆盖率。图 6 展示了行覆盖率的结果,我们可以观察到,没有一个 Fuzzer 的覆盖率能完全高于其他所有的 Fuzzer。通过对图 2(a)和图 6 的比较,我们观察到更高的覆盖率并不是意味着更多的漏洞。比如,MOPT 在 tcpdump 上在所有 Fuzzer 中具有最高的覆盖率,然而 QSYM 找到的最多的漏洞。为了进一步探索漏洞数量和行覆盖率的关系,我们计算了它们之间的 Spearman correlation coefficient rs 值,该值是两个变量之间无参数的一个校正措施,且 rs 在-1 到+1 之间。如果 rs 为正,说明两个变量是正相关的,反之亦然。图 5 展示了漏洞数量和刚覆盖率之间的 rs 值,我们观察到大部分值都小于 0.60,说明漏洞数量和行覆盖率之间的关系并不是很紧密。

4.7、资源过度利用率

表 7 展示了每个 Fuzzer 的平均和最大内存消耗,据此我们做出了一下总结。(1)从整体来看,AFL,AFLFast 和 MOPT 在 Fuzzing 过程中比其他的 Fuzzer 消耗了更少的内存,平均内存消耗分别为 24.6MB,22.2MB 和 51.8MB。然而,T-Fuzz 在 Fuzzing 过程中消耗了 1082MB,几乎是 AFLFast 的 50 倍,是所有 Fuzzer 中消耗最多的。一个可能的原因是 T-Fuzz 使用了 Angr 来获取程序执行流图(CFG),可能会消耗较大的内存。(2)当 Fuzzing 相同的程序时,不同 Fuzzer 的内存消耗差距很大。例如,当 Fuzzing exiv2 时,AFL 使用了不超过 25MB 的内存,与之相比,T-Fuzz 使用了大概 4GB 内存。(3)对于一些 Fuzzer 来说,在不同程序上的内存消耗同样有很大的差异。比如,Angora 在测试 pdftotext 时消耗了 7GB 内存,而其他程序则最多不会超过 2GB。

5、深入分析

在这里我们应用了一些测试来调查先前忽略的一些可能影响 Fuzzer 性能的因素,包括插桩和崩溃样本分析工具。

5.1、插桩方法

不同 Fuzzer 可能采用了不同形式的插桩,导致编译成的二进制文件有不同的特性。比如,AFL 和 Angora 通过编写一个包装过的编译器(如 afl-clang,angora-clang),而 VUzzer 应用了 Intel PIN 来进行二进制插桩。因此,一个会自然产生的问题是:不同的插桩技术是否会影响 Fuzzing 的测试?我们基于以下的一些观察提出了该问题。对于 infotocap 这样的程序,特定的崩溃样本只能 AFL 插桩过的程序崩溃,而不是 Angora 插桩的程序。在分析 infotocap 的这些漏洞时,我们发现它们和编译的方法相关。在这种情形下,Angora 这块的问题是因为插桩技术的问题,而不是发现漏洞的能力。但是,如果我们忽视不同的插桩技术,可能就会因为上述场景而得出 AFL 比 Angora 更好的结论。

这里,我们提供了一个样本来展示编译手段是如何影响漏洞的。图 7 展示了一个 C/C++代码片段,在第 4 行存在一个堆溢出的漏洞。但是,特定的编译优化可能会跳过错误代码行(第 4 行的 x[i]=0),并将整个赋值片段认为是一个常量 0 来输出。我们通过不同的编译器(gcc 和 clang)并采用不同的优化等级(O0-O3)对其进行编译。然后,我们发现在使用 clang 时,O1-O3 都无法触发堆溢出。而 gcc 的 O0-O3 以及 clang 的 O0 都仍然存在堆溢出。由于让所有的 Fuzzer 使用相同的插桩技术较为困难,不同编译(插桩)方法造成的差异很难避免。因此,一个潜在的更好解决方案是在分析崩溃样本时采用交叉验证。如,使用不同的编译程序来重新执行崩溃样本,以检查它们是否可以使部分程序崩溃。

5.2、崩溃样本分析工具

不同的工具被提出来分析程序崩溃样本可以触发那些漏洞,如 ASan 和 GDB。在我们的测试过程中,有意思的是,我们发现,使用不同的工具来对崩溃样本进行分析可能会产生不同的结果,比如可能会找到不同数量的漏洞。为了进一步检测这一结果,我们使用 ASan 和 GDB 来分析在第 4 节所做的实验。如果漏洞可以通过执行 ASan(或 GDB)编译的二进制程序和崩溃样本来被发现,我们认为相应的崩溃样本是被 ASan(或 GDB)验证的。为了展示不同工具在分析崩溃样本的影响,我们在表 8 中列出了可被各个工具验证的崩溃样本数量。在收集的 329857 个样本中,只有 61.1% 的可以同时被 ASan 和 GDB 验证。14.5% 的只能被 GDB 验证,而 12.2% 只能被 ASan 验证。此外,还有 12.2% 的崩溃样本不能被两样工具验证。让人感到意外的是,ASan 这样一个广泛应用的工具,只能验证 73.3%(12.2%+61.1%)的样本。

使用一样分析工具可能会限制找到的漏洞数量,导致不能在 Fuzzer 测试时得出一个综合的结论。比如,在第 4 节中我们的 Fuzzing 实验里,我们找到了一些崩溃样本可以在 ffmpeg 上触发 float point exception。但是我们无法根据该样本,再通过执行 ASan 编译的二进制程序来发现该漏洞,但是 GDB 却可以发现它。图 8 展示了 Fuzzer 使用 ASan 和 GDB 在 ffmpeg 上发现的漏洞数量。正如图 8 中所示,在使用不同的分析工具时,得到的测试结果是不同的。当使用 ASan 时,只有 Honggfuzz 可以发现漏洞。而使用 GDB 时,AFL 和 AFLFast 也可以发现漏洞。因此,最好可以结合更多的工具来分析崩溃样本,而不是依靠单一的工具而导致漏报了一些漏洞。在我们的测试中(第 4 节),我们使用 ASan 作为探测漏洞的主要工具,同时也将 GDB 作为一个辅助工具。

6、讨论

我们在本文中讨论了当前 Fuzzing 研究领域的如下问题。

6.1、Fuzzer 的可用性

一个具有较好可用性的 Fuzzer 可以促进其在实践中的应用。然而,基于我们的测试我们发现 Fuzzer 可用性的问题,尤其是学术领域的一些 Fuzzer,严重性远超我们的预期。这些可用性的问题包括:在安装部署时存在(严重的)问题,不能进行复现等。更糟糕的是,一些存在这些问题的 Fuzzer 近年来被发表在一些顶会上。在本文中,我们测试了 35 个 Fuzzer 的可用性,使其能够在 UNIFUZZ 平台上可用,并且在其中 8 个 Fuzzer 进行了更广泛的实验。我们希望这个工作可以促进未来 Fuzzer 可用性和性能上的研究。

如何能可在 Fuzzer 的可用性上进行综合的实验是一个非常有趣又极其重要的一项研究。然而,Fuzzer 的可用性是一个相对主观的话题,其容易受到很多因素的影响。第一,Fuzzer 的可用性极度依赖于使用者的相关知识。一个 Fuzzing 方面的专家可以在不使用任何文档的情况下上手一个 Fuzzer,而一个初学者可能在有限的帮助文档下也很难上手一个 Fuzzer。第二,有很多可能会影响 Fuzzer 可用性的因素,包括文档的排版风格,相关依赖库和工具的问题,部署问题,Fuzzing 过程中的鲁棒性等。对于未来在该问题上的研究,我们提供了以下可行的方法作为建议。(1)检查 Fuzzer 文档的正确性和完整性(比如在整体文档和部署之间是否存在不一致性)。(2)测试一个 Fuzzer 能否成功安装并通过作者提供的一系列测试。(3)测试 Fuzzer 在 Fuzzing 过程中的鲁棒性,并观察是否存在异常的行为(如 Fuzzer 自身是否会在 Fuzzing 过程中产生崩溃)。(4)测试一个 Fuzzer 是否能够复现在相关论文中的一些实验结果。

6.2、Fuzzing 实验

执行正确的 Fuzzing 实验是一个合理测试的基础。Klees 等人提出了 Fuzzing 测试过程中的一系列准则,如多重实验,使用不同的种子集等。另外,这里我们讨论了在 Fuzzing 实验中需要被考虑的一些实用问题。第一,监控 Fuzzing 实验的状态,如 CPU 使用率等,来检查实验是否在正常执行。通常,如果 CPU 使用率很低(如,少于 80%),可能说明 Fuzzing 状态是不正常的。比如,当有大量的磁盘读写操作,CPU 需要等待这些操作结束后才能开始 Fuzzing。第二,减少一些不必要的磁盘读写操作也非常重要,尤其是在一台服务器上同时执行多个 Fuzzing 实验时,磁盘读写非常容易成为瓶颈。比如,当目标程序可能在 Fuzzing 过程中会输出非常多的新文件,会导致产生非常多的磁盘写操作。在这种情况下,更推荐使用 RAM 或者不保存测试程序的输出文件。

6.3、测试 Fuzzer 的标准

当前 Fuzzing 的标准仍然不是很好。考虑到实践中可用性的问题,我们构建了一个具备实用性且包含 20 个当前版本的真实程序标准,其具有以下优点。第一,UNIFUZZ 标准程序在函数功能,大小,漏洞等方面具有全面性,其可以为 Fuzzer 提供一个足够全面的测试,且能更好地反映出一个 Fuzzer 在真实程序上的性能。第二,UNIFUZZ 标准可以被用来提供更佳客观和公正的测试。正如第 4 节中所示,没有一个 Fuzzer 能够在所有程序上完全胜过其余的 Fuzzer,从某种程度上来说说明了在一些 Fuzzing 相关的论文中存在一定的片面性和主观性。第三,不同于传统设计的一些人工注入漏洞的测试标准,我们的方法不仅不会改变真实程序的源代码,保证其原始功能,同样专注于提供一些方便的离线结果分析方法。特别地,对于每个程序,我们提供了一些崩溃样本分析方法,包括崩溃样本分类,CVE 匹配,漏洞严重性分析等。因此,UNIFUZZ 标准和人为构建的标准一样简单易用。另外,据我们所了解,我们是第一个构建了 CVE 关键字数据库的团队,并在 CVE 漏洞匹配中减少了大量人力。

值得注意的是 Fuzzing 标准需要在 Fuzzer 发展的同时进行更新和改进,且在标准的设计方面仍然有许多工作需要展开。这正是为什么我们将 UNIFUZZ 设计为一个开源可扩展的平台。UNIFUZZ 标准仍然存在很多限制,且可以从很多方面进一步改进和扩展。第一点,在本文中,我们主要选择了在顶会中 Fuzzing 相关的论文所采用的程序。在未来,仍然有许多其他的资源,如一些漏洞相关的网站可以被用来选择程序。第二点,当前 UNIFUZZ 的标准主要关注与通用的程序级别上的 Fuzzer。如果能将更多其他类型 Fuzzer,如编译器 Fuzzer 和内核 Fuzzer,这些标准都能结合到 UNIFUZZ 中会更好。

6.4、性能指标

现有的性能指标较为粗糙和不全面。为了解决这一问题,UNIFUZZ 提供了 6 类指标,来为 Fuzzer 提供更加全面的测试。这里我们讨论了这些指标的局限性在相关有趣的研究问题,可能可以被认为是未来的一些研究工作。第一个是指标的种类。在本文中,我们将指标分为了 6 类。需要对此进行更多的研究以提供更加合理的分类。第二,需要在每一类指标中具体的内容进行更多的研究。例如,我们使用 CVSS 的评分和 Exploitable 工具来衡量漏洞的严重性。但是,每个单个指标有其自身的限制。虽然 CVSS 评分考虑了多方面的因素(如攻击复杂性和需要的权限),单个的分数可能并不能准确地分别反映每个因素的影响。Exploitable 通过一系列规则来决定漏洞的严重性,其准确程序可能被规则的合理性所影响。因此,对测试漏洞严重性中具体指标的选择需要更好的标准/方法被提出后再进一步更新。另外,它需要在理论方面更加深入的研究。例如,当对漏洞数量进行统计测试时,我们可以只使用无参数的统计方法,如曼-惠特尼 U 检验,其不会对目标产生任何假设。在重复实验中学习漏洞数量的分布,并提供更多合适的指标来评估是一件有趣的事。第三,学习每个指标的优先级是非常重要的。就我们的观点来说,漏洞的数量和质量远比挖掘漏洞的稳定性重要,而稳定地挖掘更少的或普通的漏洞相比偶尔找到高危漏洞更没有意义。第四点,需要设计一套结合了不同指标得出结论性分数的评分方法,来更好地评估一个 Fuzzer 的性能。

7、总结

在本文中,我们提出和实现了 UNIFUZZ,一个开源的,整体的,实用的,以指标为驱动的 Fuzzer 评估平台,其可以更全面,公平地评估一个 Fuzzer。UNIFUZZ 结合了 35 个 Fuzzer,20 个真实标准程序以及 6 类性能指标。我们测试了 35 个 Fuzzer 的可用性并发现了一些代码问题。通过应用 UNIFUZZ,我们系统地比较了当前的一些 Fuzzer。基于实验结果,我们得出了以下观察结果。第一点,没有一个 Fuzzer 总能完全胜过其他的 Fuzzer,说明在现有的一些 Fuzzing 论文中存在一定的主观性和片面性。第二点,Fuzzer 在人为构造程序上的性能可能和真实程序上的性能并不一致,说明使用实用标准程序的重要性。第三点,Fuzzer 的性能随不同的性能指标而变化,说明 Fuzzer 需要更多综合的性能指标来提供更加可信的测试。另外,我们确认了一些新的可能影响 Fuzzer 测试的因素,如插桩方法和崩溃样本分析工具。我们已经将 UNIFUZZ 开源,以促进未来 Fuzzing 的研究。