TLDR,简介
模糊测试,或称为模糊测试,是一种自动化的软件安全性和稳定性测试方法。它通常通过提供专门设计的输入来识别意外行为甚至危险行为。如果您不熟悉模糊测试的基础知识,可以在 Firefox 模糊测试文档 和 模糊测试书籍 中找到更多信息。
在过去的三年里,Firefox 模糊测试团队一直在开发一个新的模糊测试器,以帮助识别 Firefox 中 WebAPI 实现的安全性漏洞。这个模糊测试器,我们称之为 Domino,利用 WebAPI 自身的 WebIDL 定义作为模糊测试语法。我们的方法已经识别出超过 850 个错误。其中 116 个错误被评定为安全问题。在这篇文章中,我想讨论一下 Domino 的一些关键特性,以及它们与我们之前的 WebAPI 模糊测试工作有何不同。
模糊测试基础
在我们开始讨论 Domino 是什么以及它是如何工作的之前,我们首先需要讨论一下如今可用的模糊测试技术类型。
模糊测试器类型
模糊测试器通常被分类为黑盒、灰盒或白盒。这些名称基于模糊测试器与目标应用程序之间通信的级别。最常见的两种类型是黑盒和灰盒模糊测试器。
黑盒模糊测试
黑盒模糊测试将数据提交到目标应用程序,基本上不了解该数据如何影响目标应用程序。由于这种限制,黑盒模糊测试器的有效性完全取决于生成的数据的适用性。
黑盒模糊测试通常用于大型非确定性应用程序或处理高度结构化数据的应用程序。
白盒模糊测试
白盒模糊测试允许模糊测试器与目标应用程序之间进行直接关联,以生成满足应用程序“要求”的数据。这通常涉及使用定理求解器来评估分支条件并生成数据以有意地执行所有分支。通过这样做,模糊测试器可以测试黑盒或灰盒模糊测试器可能永远无法测试到的难以触及的分支。
这种模糊测试类型的主要缺点是它在计算上非常昂贵。具有复杂分支的大型应用程序可能需要大量时间来求解。这极大地减少了测试的输入数量。除了学术练习之外,白盒模糊测试通常不适合现实世界中的应用程序。
灰盒模糊测试
灰盒模糊测试已成为最流行和最有效的模糊测试技术之一。这些模糊测试器实现了一种反馈机制,通常通过检测来实现,以告知关于未来生成哪些数据的决策。似乎覆盖更多代码的输入被用作以后测试的基础。降低覆盖率的输入被丢弃。
这种方法非常受欢迎,因为它能够快速有效地到达难以触及的代码路径。然而,并非所有目标都适合进行灰盒模糊测试。灰盒模糊测试通常最适合 较小、确定性的目标,这些目标可以快速处理大量输入(每秒几百个)。
我们经常使用这些类型的模糊测试器来测试 Firefox 中的各个组件,例如媒体解析器。如果您有兴趣了解如何利用这些模糊测试器来测试您的代码,请查看模糊测试接口文档这里。
不幸的是,我们在对 WebAPI 进行模糊测试时,可用的技术有一些限制。浏览器本质上是非确定性的,并且输入高度结构化。此外,启动浏览器、执行测试以及监控故障的过程很慢(每次测试几秒到几分钟)。鉴于这些限制,黑盒模糊测试是最合适的解决方案。
但是,由于这些 API 预期的输入高度结构化,我们需要确保我们的模糊测试器生成被认为有效的 data。
基于语法的模糊测试
基于语法的模糊测试是一种模糊测试技术,它使用形式语言语法来定义要生成的数据的结构。这些语法通常以纯文本形式表示,并使用符号和常量组合来表示数据。然后,模糊测试器可以解析语法并使用它来生成模糊的输出。
这里的示例演示了来自 Domato 和 Dharma 模糊测试器的两个简化的语法摘录。这些语法描述了创建 <a href="https://mdn.org.cn/en-US/docs/Web/API/HTMLCanvasElement" target="_blank" rel="noopener noreferrer">HTMLCanvasElement</a>
以及操作其属性和操作的过程。
传统语法的弊端
不幸的是,开发语法的努力程度与您尝试表示的数据的大小和复杂性成正比。这是基于语法的模糊测试最大的缺点。作为参考,Firefox 中的 WebAPI 公开了超过 730 个接口,约有 6300 个成员。请记住,这个数字不包括其他所需的数据结构,例如回调、枚举 或字典等等。创建语法来准确地描述这些 API 将是一项艰巨的任务;更不用说容易出错且难以维护了。
为了更有效地对这些 API 进行模糊测试,我们希望尽可能地避免手动开发语法。
WebIDL 作为模糊测试语法
typedef (BufferSource or Blob or USVString) BlobPart;
[Exposed=(Window,Worker)]
interface Blob {
[Throws]
constructor(optional sequence blobParts,
optional BlobPropertyBag options = {});
[GetterThrows]
readonly attribute unsigned long long size;
readonly attribute DOMString type;
[Throws]
Blob slice(optional [Clamp] long long start,
optional [Clamp] long long end,
optional DOMString contentType);
[NewObject, Throws] ReadableStream stream();
[NewObject] Promise text();
[NewObject] Promise arrayBuffer();
};
enum EndingType { "transparent", "native" };
dictionary BlobPropertyBag {
DOMString type = "";
EndingType endings = "transparent";
};
Blob WebIDL 定义的简化示例
WebIDL 是一种 接口描述语言(IDL),用于描述浏览器实现的 API。它列出了这些 API 公开的接口、成员和值,以及语法。
WebIDL 定义在浏览器模糊测试社区中是众所周知的,因为它们包含大量信息。在这方面已经进行了一些工作,从这些 IDL 中提取数据以用作模糊测试语法,即 Sensepost 的 WADI 模糊测试器。然而,在我们调查的每个示例中,我们发现这些定义中的信息被提取出来并使用模糊测试器的本机语法重新实现。这种方法仍然需要大量的 manual effort。此外,模糊测试语法的语法使得描述 WebAPI 特定行为变得很困难,在某些情况下甚至是不可能的。
基于这些问题,我们决定直接使用 WebIDL 定义,而不是将其转换为现有的模糊测试语法。这种方法为我们提供了许多好处。
标准化语法
首先也是最重要的是,WebIDL 规范定义了一种标准化语法,这些定义必须遵守该语法。这让我们可以利用现有的工具,例如 WebIDL2.js,来解析原始 WebIDL 定义并将其转换为 抽象语法树(AST)。然后,模糊测试器可以解释 AST 来生成测试用例。
简化的语法开发
其次,WebIDL 定义了我们打算定位的 API 的结构和行为。因此,我们大大减少了所需的规则开发量。相反,如果我们使用前面提到的语法之一来描述这些 API,我们将不得不为 API 定义的每个接口、成员和值创建单独的规则。
ECMAScript 扩展属性
与仅定义数据结构的传统语法不同,WebIDL 规范通过 ECMAScript 扩展属性提供了有关接口行为的附加信息。扩展属性可以描述各种行为,包括
- 可以使用特定接口的上下文。
- 返回的对象是新实例还是重复实例。
- 成员实例是否可以被替换。
这些类型的行为通常不会由传统语法表示。
自动检测 API 更改
最后,由于 WebIDL 文件与浏览器实现的接口相关联,我们可以确保对 WebIDL 的更新反映了对接口的更新。
将 IDL 转换为 JavaScript
为了利用 WebIDL 进行模糊测试,我们首先需要解析它。幸运的是,我们可以使用 WebIDL2.js 库将原始 IDL 文件转换为 抽象语法树(AST)。WebIDL2.js 生成的 AST 将数据描述为树上的节点序列。这些节点中的每一个都定义了 WebIDL 语法的某个构造。
有关 WebIDL2 AST 结构的更多信息,请点击此处。
获得 AST 后,我们只需要为这些结构定义转换。在 Domino 中,我们已经实现了一系列工具,用于遍历 AST 并将 AST 节点转换为 JavaScript。上图演示了其中一些转换。
大多数节点可以使用静态转换来表示。这意味着 AST 中的结构在 JavaScript 中始终具有相同的表示。例如,constructor 关键字将始终替换为 JavaScript 中的“new”运算符,并与接口名称结合使用。然而,在某些情况下,WebIDL 结构可能具有多种含义,需要动态生成。
泛型类型
WebIDL 规范列出了用于表示泛型值的多种类型。对于每种类型,Domino 都实现了函数,这些函数将返回与请求类型匹配的随机生成的值,或者返回之前记录的相同类型的对象。例如,当遍历 AST 时,octet、short 和 long 等数字类型的出现将返回这些数字范围内的值。
对象引用
在构造类型引用另一个 IDL 定义并用作参数的地方,这些值需要该 IDL 类型的对象实例。当识别出其中一个值时,Domino 将尝试创建对象的新的实例(通过其构造函数)。或者,它将尝试通过识别和访问返回该类型对象的另一个成员来实现这一点。
回调处理程序
WebIDL 规范还定义了许多类型,这些类型表示函数(即,promises、callbacks 和 event listeners)。对于每种类型,Domino 都将生成一个唯一的函数,该函数对提供的参数(如果存在)执行随机操作。
当然,以上步骤只占了将 IDLs 完整转换为 JavaScript 所需步骤的一小部分。Domino 的生成器实现了对整个 WebIDL 规范的支持。让我们看看使用 Blob WebIDL 作为模糊语法,我们的输出可能是什么样子。
零配置模糊测试
> const { Domino } = require('~/domino/dist/src/index.js')
> const { Random } = require('~/domino/dist/src/strategies/index.js')
> const domino = new Domino(blob, { strategy: Random, output: '~/test/' })
> domino.generateTestcase()
…
const o = []
o[2] = new ArrayBuffer(8484)
o[1] = new Float64Array(o[2])
o[0] = new Blob([o[1]])
o[0].text().then(function (arg0) {
o[0].text().then(function (arg1) {
o[3] = o[0].slice()
o[3].stream()
o[3].slice(65535, 1, ‘foobar’)
})
})
o[0].arrayBuffer().then(function (arg2) {
o[3].text().then(function (arg3) {
O[4] = arg3
o[0].slice()
})
})
正如我们在这里看到的,IDL 提供的信息足以生成有效的测试用例。这些用例测试了 Blob 相关代码的相当大一部分。反过来,这使我们能够快速为新的 API 开发基线模糊测试器,无需人工干预。
不幸的是,并不是所有东西都像我们希望的那样精确。例如,提供给 slice 操作的值。在查看了Blob 规范之后,我们发现 start 和 end 参数应该是相对于 Blob 大小的字节顺序位置。我们目前是随机生成这些数字。因此,我们不太可能能够返回 Blob 长度范围内的值。
此外,slice 操作的 contentType
参数和 BlobPropertyBag
字典上的 type 属性都定义为 <a href="https://mdn.org.cn/en-US/docs/Web/API/DOMString" target="_blank" rel="noopener noreferrer">DOMString</a>
。与我们的数值类似,我们随机生成字符串。但是,进一步查看规范表明,这些值用于表示 Blob 数据的媒体类型。现在,这些值似乎对 Blob 对象本身没有太大影响。然而,我们不能确定这些值不会对使用这些 Blob 的 API 产生影响。
为了解决这些问题,我们需要开发一种区分这些泛型类型的方法。
使用 GrIDL 进行规则修补
出于这种需要,我们开发了另一个名为 GrIDL 的工具。GrIDL 利用 WebIDL2.js 库将我们的 IDL 定义转换为 AST。它还对 AST 进行了一些优化,使其更适合用作模糊测试语法。
但是,GrIDL 最有趣的功能是:我们可以动态修补需要更精确值的 IDL 声明。使用基于规则的匹配系统,GrIDL 识别目标值并插入唯一标识符。这些标识符对应于 Domino 实现的匹配生成器。在遍历 AST 时,如果遇到其中一个标识符,Domino 会调用匹配生成器并发出返回的值。
上图演示了 GrIDL 标识符和 Domino 生成器之间的关联。在这里,我们定义了两个生成器。一个返回字节偏移量,另一个返回有效的 MIME 类型。
重要的是要注意,每个生成器也将获得对当前正在模糊测试的对象的实时表示的访问权限。这使我们能够生成受对象当前状态影响的值。
在上面的示例中,我们利用此对象为 slice 函数生成相对于其长度的字节偏移量。但是,考虑与 WebGLRenderingContextBase 接口关联的任何属性或操作。此接口可以由 WebGL 或 WebGL2 上下文实现。每个上下文所需的参数可能会有很大差异。通过引用正在模糊测试的当前对象,我们可以确定上下文类型并相应地返回值。
> domino.generateTestcase()
…
const o = []
o[1] = new Uint8Array(14471)
o[0] = new Blob([null, null, o[1]], {
'type': 'image/*',
'endings': 'transparent'
})
o[2] = o[0].slice((1642420336 % o[0].size), (3884321603 % o[0].size), 'application/xhtml+xml')
o[0].arrayBuffer().then(function (arg0) {
setTimeout(function () { o[0].text().then(function (arg1) { o[0].stream() }) }, 180)
o[2].arrayBuffer().then(function (arg2) {
o[0].slice((3412050218 % o[0].size), (646665894 % o[0].size), 'text/plain')
o[0].stream()
})
o[2].text().then(function (arg3) {
o[2].slice((2025414481 % o[2].size), (2615146387 % o[2].size), 'text/html')
o[3] = o[0].slice((753872984 % o[0].size), (883984089 % o[0].size), 'text/xml')
o[3].stream()
})
})
有了我们新创建的规则,我们现在能够生成更接近规范描述的值。
现实世界中的示例
本文中包含的示例已大大简化。通常很难看到如何将这种方法应用于更复杂的 API。因此,我想留给您一个 Domino 发现的更复杂漏洞之一的示例。
在bug 1558522 中,我们发现了一个影响IndexedDB API 的严重use-after-free 漏洞。从模糊测试的角度来看,这个漏洞非常有趣,因为它需要触发问题所需的复杂性。Domino 能够通过在全局上下文中创建一个文件,然后将该文件对象传递给建立 IndexedDB 数据库连接的工作器上下文来触发此漏洞。
上下文之间这种级别的协调通常很难用传统的语法来描述。但是,由于 WebIDL 提供了这些 API 的详细描述,Domino 可以轻松地识别出此类漏洞。
贡献
最后一点:Domino 继续在我们的代码中发现安全漏洞。不幸的是,这意味着我们目前还不能公开发布它。但是,我们计划在不久的将来发布一个更通用的版本。敬请关注。如果您想开始为 Firefox 的开发贡献代码,有很多开放的机会。如果您是 Mozilla 员工或 NDA 签署的代码贡献者,并且想参与 Domino 的工作,请随时在 Riot(Matrix)上的 Fuzzing 房间与团队联系!
4 条评论