模糊测试

本节重点介绍一种名为“模糊测试”或“模糊测试”的软件测试技术及其在 Mozilla 代码库中的应用。总体目标是向开发人员普及模糊测试的功能和实用性,并让他们能够编写自己的模糊测试目标。请注意,并非 Mozilla 使用的所有模糊测试工具都是开源的。某些工具仅供内部使用,因为它们很容易发现严重的安全性漏洞。

什么是模糊测试?

模糊测试是一种随机使用程序或其部分程序以发现错误的技术。随机使用可以有多种形式,一些常见形式包括

  • 随机输入数据(例如文件格式、网络数据、源代码等)

  • 随机 API 使用

  • 随机 UI 交互

前两种方法是该领域中使用最广泛的实用方法。当然,这些方法并非完全独立,可以进行组合。模糊测试是发现质量问题(其中一些也是安全问题)的好方法。

随机输入数据

这可能是最明显的模糊测试方法:您拥有处理数据的代码,并向其提供随机或变异数据,希望它能够发现您实现中的错误。例如媒体格式,如 JPEG 或 H.264,但基本上任何涉及处理“数据块”的内容都可以成为有价值的目标。使用这种方法发现了各种库和程序中无数的安全漏洞(AFLFuzz 的 bug-o-rama 提供了一个很好的印象)。

此任务的常用工具例如 libFuzzerAFLFuzz,以及具有自定义逻辑的专用工具,例如 LangFuzzAvalanche

随机 API 使用

随机测试 API 对暴露了明确定义的接口(另请参阅 明确定义的行为和安全性)的软件部分特别有用。如果此接口还暴露给不受信任的方/内容,那么这是一个强烈的信号,表明此处值得进行随机 API 测试,出于安全原因也是如此。API 可以是任何内容,从 C++ 层代码到浏览器中提供的 API。

此处一个很好的模糊测试目标示例是 DOM(文档对象模型)和各种其他浏览器 API。浏览器公开了各种不同的 API,用于处理文档、媒体、通信、存储等,并且复杂性不断增加。每个 API 都可能存在可以通过模糊测试发现的错误。在 Mozilla,我们目前为此目的使用 domino(内部工具)。

随机 UI 交互

测试程序,特别是用户界面的第三种方法是通过以随机方式直接与 UI 交互,通常与程序必须执行的其他操作相结合。例如,想象一个自动浏览器在网络上浏览并随机执行诸如滚动、缩放和点击链接等操作。这种方法的优点在于,您可能会发现许多最终用户也遇到的问题。但是,这种方法通常存在可重复性差的问题(另请参阅 可重复性),因此经常用途有限。

使用此技术的模糊测试工具的一个示例是 Android Monkey。然而,在 Mozilla,我们目前很少使用这种方法。

为什么模糊测试对您有帮助

了解模糊测试对您作为开发人员以及软件质量的总体价值对于证明这种测试方法可能需要的支持至关重要。当您的组件第一次进行模糊测试时,您将遇到两个常见问题

看起来不像是真实错误或不重要的错误报告:模糊测试会在您组件的各个角落发现各种错误,即使是模糊的错误。这会自动导致大量错误,这些错误要么看起来不像是错误(另请参阅下面的 明确定义的行为和安全性 部分),要么看起来不像是重要的错误。

修复这些错误对于模糊测试仍然很重要,因为在模糊测试中忽略它们会浪费资源(性能、人力资源),甚至可能阻止模糊测试发现其他错误。例如,某些模糊测试工具(如 libFuzzer)在进程内运行,并且必须在每次崩溃时重新启动,这会涉及到代价高昂的模糊测试样本重新读取。

此外,由于我们的一些代码发展迅速,因此边缘情况可能会在几个月内变成热点代码路径。

新的复现步骤:模糊测试工具很可能使用与普通最终用户不同的方法来运行您的组件。一种常见技术是修改程序的现有部分或编写全新的代码以生成模糊测试“目标”。此目标专门设计用于与使用的模糊测试工具配合使用。复现报告的错误可能需要您学习这些新的复现步骤,包括构建/获取该目标并拥有正确的环境。

在某些情况下,这两个问题都可能看起来像是在浪费时间,但是,认识到这两个步骤是对持续流入的宝贵错误报告进行的一次性投资在这里至关重要。帮助您的安全工程师克服这些问题将确保代码中的未来回归可以在更早的阶段以更易于操作的形式被检测到。特别是如果您已经在处理代码中的回归问题,模糊测试有可能使您作为开发人员的工作变得更容易。

Mozilla 最好的例子之一是 JavaScript 引擎。JS 团队在启动模糊测试和支持我们的工作方面投入了大量精力。以下是 JavaScript 引擎的高级平台工程师 Jan de Mooij 对此的看法

“引擎中的错误会导致神秘的浏览器崩溃和难以追踪的错误。幸运的是,我们不必经常处理这些耗时的浏览器问题:通常模糊测试会在错误进入版本之前很久就找到可靠的 shell 测试。模糊测试对我们来说非常宝贵,我无法想象没有它就进行此项目的工作。”

Firefox/Gecko 中的模糊测试级别

例如,将模糊测试应用于 Firefox 会发生在不同的“级别”,类似于我们拥有的不同类型的自动化测试

完整浏览器模糊测试

最明显的方法是测试完整的浏览器,并且某些功能(如 DOM 和其他 API)需要这样做。这里的优势在于我们拥有浏览器的所有功能,并且测试非常接近我们实际发布的内容。但是,这里的缺点是浏览器测试是所有测试方法中最慢的。此外,它包含最多的不确定性(导致例如间歇性测试用例)。Mozilla 的浏览器模糊测试主要使用 Grizzly 框架元错误),并且最成功的模糊测试工具之一是 Domino 工具(元错误)。

总而言之,如果您的功能确实需要完整浏览器模糊测试,那么它是正确的技术。如果您的代码可以通过这种方式运行,请考虑使用其他方法(见下文)。

模糊测试接口

模糊测试接口

模糊测试接口是 mozilla-central 中的粘合代码,以便开发人员和安全研究人员能够更容易地使用 libFuzzer 或 afl-fuzz 测试 C/C++ 代码。

此接口提供了一种基于 gtest(C++ 单元测试)级别的组件模糊测试方法,适用于任何也可以使用 gtest 进行测试/运行的内容。这种方法是迄今为止最快的,但通常仅限于测试可以在此级别实例化的隔离组件。利用这种方法需要您编写一个模糊测试目标,类似于编写 gtest。此目标将自动可用于 libFuzzer 和 AFLFuzz。我们提供了一个 综合手册,其中描述了如何编写和利用您自己的目标。

此处一个简单的示例是 SDP 解析器目标,它测试我们代码库中的 SipccSdpParser。

基于 Shell 的模糊测试

我们的一些模糊测试(例如 JS 引擎测试)发生在单独的 shell 程序中。对于 JS,这是 JS shell,也用于大多数 JS 测试和开发。理论上,xpcshell 也可以用于测试,但到目前为止,还没有使用案例(大多数可以通过 xpcshell 访问的内容也可以在 gtest 级别进行测试)。

识别正确的模糊测试级别是实现代码持续模糊测试的第一步。

模糊测试的代码/流程要求

在本节中,我们将讨论如何编写代码才能获得最佳的模糊测试结果。

缺陷预言

只有当您能够知道何时发现问题时,模糊测试才有效。如果被测试的单元对模糊测试是安全的(见明确定义的行为和安全性),那么崩溃通常是问题。但是,您可能希望发现更多问题,例如正确性问题、不一定导致崩溃的损坏等。为此,您需要一个预言来告诉您某些内容出了问题。

最简单的缺陷预言是断言(例如:MOZ_ASSERT)。断言是一种非常强大的工具,因为它们可用于确定程序是否正在正确执行,即使错误不会导致任何类型的崩溃。它们可以对被认为是正确的方面进行任意复杂的编码,这些信息可能否则只存在于开发人员的脑海中。

像 Sanitizer(AddressSanitizer 又名 ASan、ThreadSanitizer 又名 TSan、MemorySanitizer 又名 MSan 和 UndefinedBehaviorSanitizer - UBSan)这样的外部工具也可以作为有时可能非常严重问题的预言,而这些问题不一定导致崩溃。确保这些工具可以用于您的代码非常有用。

使用 Sanitizer 发现的错误示例有 错误 1419608错误 1580288错误 922603,但自从我们开始使用 Sanitizer 以来,我们已经使用这些工具发现了 1000 多个错误。

另一个缺陷预言可以是参考实现。比较两个程序或同一程序的两种模式之间的程序行为(通常是输出),这两个程序或两种模式应该产生相同的输出,可以发现复杂的正确性问题。这种方法通常称为差异测试。

一个经常使用此方法来发现问题的例子是 Mozilla JavaScript 引擎:在启用和禁用 JIT 编译的情况下运行随机程序可以发现 JIT 实现中的大量问题。此类错误的一个示例是 错误 1404636

组件解耦

能够隔离地测试组件可以成为模糊测试的优势(对于性能和可重复性而言)。不同组件之间的明确边界以及解释合同的文档通常有助于实现此目标。有时,模拟目标组件正在交互的某个组件可能很有用,如果组件紧密耦合且其合同不清楚,则这将更加困难。当然,这并不意味着人们应该只隔离地测试组件。有时,测试它们之间的交互甚至更可取,并且根本不会影响性能。

避免外部 I/O

网络或文件交互等外部 I/O 会影响性能,并可能引入额外的非确定性。提供直接从内存处理数据的接口通常更有帮助。

明确的行为和安全性

此要求主要与缺陷预言机结束的地方相关,并且是当今模糊测试中最重要的问题之一。如果程序行为的一部分未指定,那么如果模糊测试将该行为视为缺陷,则可能会导致不良后果。例如,如果代码存在不被视为 bug 的崩溃,则您的代码可能不适合进行模糊测试。您的组件应该是模糊测试安全的,这意味着由模糊测试器触发的任何缺陷预言机(例如断言或崩溃)都被视为 bug。这个重要的方面经常被忽视。请注意,任何误报都会导致性能下降以及模糊测试团队需要进行额外的手动工作。例如,Mozilla JS 开发人员在“–fuzzing-safe”开关中实现了此概念,该开关禁用有害函数。有时,无法避免崩溃以处理某些错误条件。在这种情况下,务必以模糊测试器可以识别和区分它们与不希望发生的崩溃的方式标记这些崩溃。但是,请记住,总体而言,崩溃可能会干扰模糊测试过程。性能是模糊测试的一个重要方面,频繁的崩溃会严重降低性能。

可重复性

能够重现使用模糊测试发现的问题对于以下几个原因至关重要:首先,作为开发人员,您可能需要一个可以重现问题的测试,以便更好地调试它。我们从大多数开发人员那里得到的反馈是,没有可重复测试的跟踪可以帮助找到问题,但它会使整个过程变得非常复杂。一些这些不可重复的 bug 从未得到修复。其次,拥有可重复的测试还可以通过允许自动二分查找来找到负责的开发人员,从而帮助简化分类过程。最后但并非最不重要的是,该测试可以添加到测试套件中,用于自动验证修复,甚至可以作为更多模糊测试的基础。

因此,如果发现不可重复的问题,向程序添加提高可重复性的功能是一个好主意。下一节将显示一些示例。

虽然许多可重复性问题是您正在处理的项目的特定问题,但许多程序都有一个共同的此类问题来源:线程。虽然有些 bug 仅仅因为并发而发生,但其他一些 bug 在没有线程的情况下可以完美地重现,但在启用线程的情况下却间歇出现且难以重现。如果该 bug 确实是由于数据竞争引起的,那么 ThreadSanitizer 等工具将会有所帮助,我们目前正在努力使 ThreadSanitizer 能够在 Firefox 上使用。对于不是由线程引起的 bug,有时禁用线程或限制所涉及的工作线程数量是有意义的。

支持代码

在前面的章节中已经提到了模糊测试支持实现的一些可能性:用于改进可重复性和安全性的其他缺陷预言机和功能。实际上,许多专门为模糊测试添加的功能都属于其中一个类别。但是,还有更多空间:通常,有一些方法可以使模糊测试器更容易地执行代码的复杂且难以触及的部分。例如,如果某个特定优化功能仅在非常特定的条件下(不是优化要求)才会打开,那么添加一个强制打开该功能的功能是有意义的。然后,模糊测试器可以更频繁地命中优化代码,从而增加发现问题的可能性。Firefox 和 SpiderMonkey 中的一些示例

  • 浏览器中的FuzzingFunctions接口允许模糊测试工具执行 GC/CC,调整与垃圾收集相关的各种设置或启用辅助功能模式等功能。能够在特定时间强制执行垃圾收集过去帮助识别了许多问题。

  • JS shell 的 –ion-eager 和 –baseline-eager 标志强制在各个阶段进行 JIT 编译,而不是使用内置启发式方法仅对热函数启用它。

  • JS shell 的 –no-threads 标志禁用所有线程(如果可能)。这使得一些原本间歇出现且难以查找的 bug 可以确定性地重现。但是,某些仅在启用线程时才会出现的 bug 无法使用此选项启用时找到。

另一个必须为模糊测试关闭的重要功能是校验和。许多文件格式使用校验和在处理文件之前验证文件。如果校验和功能仍然启用,则模糊测试器可能永远无法生成有效文件。对于加密签名通常也是如此。能够在模糊测试开关的一部分中关闭这些功能的验证非常有帮助。

可以在FlacDemuxer中找到此类校验和的示例。

测试样本

一些模糊测试策略利用现有数据进行变异以生成新的随机数据。实际上,如果原始样本质量良好,则基于变异的策略通常优于其他策略,因为原始样本承载了模糊测试器无需了解或实现的大量语义。但是,这里的成功与否实际上取决于样本的质量。如果原始样本没有涵盖实现的某些部分,那么模糊测试器也需要做更多工作才能到达那里。

模糊测试阻碍因素

模糊测试阻碍因素是指阻止模糊测试器发挥最大效用的问题。根据模糊测试器及其范围,某个区域(或组件)中的模糊测试阻碍因素可能会阻碍其他区域的性能,在某些情况下甚至会完全阻止模糊测试器。一些示例包括

  • 频繁崩溃 - 这些可能会阻塞代码路径,并由于需要重新启动模糊测试目标并处理结果(无论是否被忽略或报告)而浪费计算资源。这可能还包括在许多情况下大多是良性的断言,但很容易被模糊测试器触发。

  • 频繁挂起/超时 - 这包括任何减慢或阻止模糊测试器或目标执行的问题。

  • 难以归类 - 这包括堆栈溢出等崩溃或任何在不一致位置崩溃的问题。这还包括损坏日志/调试器输出或提供损坏/无效崩溃报告的问题。

  • 构建中断 - 这非常简单,没有最新的构建,模糊测试器将无法运行或验证修复。

  • 缺少检测 - 在某些情况下,ASan 等工具用作缺陷预言机,并且模糊测试工具需要它们才能实现正确的自动化。在其他情况下,不完整的检测可能会产生虚假的稳定感,或者使调查问题变得更加耗时。虽然这并非一定阻止模糊测试器,但应优先考虑。

由于这些类型的崩溃会损害整体模糊测试进度,因此及时解决它们非常重要。即使 bug 本身看起来微不足道且对于产品而言优先级较低,它仍然会对模糊测试产生毁灭性的影响,从而阻止发现其他关键问题。

Bugzilla 中的问题通过在“白板”字段中添加“[fuzzblocker]”来标记为模糊测试阻碍因素。可以在Bugzilla上找到标记为模糊测试阻碍因素的未解决问题列表。

文档

模糊测试团队需要了解您的软件、测试和设计的工作原理非常重要。即使是显而易见的任务,例如测试程序应该如何调用、哪些选项是安全的等等,对于进行测试的人来说也可能很难弄清楚,就像您现在正在阅读本手册以了解模糊测试中哪些内容很重要一样。

联系我们

可以通过fuzzing@mozilla.comMatrix 上联系模糊测试团队,他们很乐意帮助您解答您可能遇到的任何关于模糊测试的问题。我们可以帮助您找到适合您的功能的模糊测试方法,协作实施并提供相应的运行基础设施和处理结果。