背景
作为Doyensec研究的一部分,我花时间试图理解当前的模糊测试技术,这些技术可用于针对流行的JavaScript引擎(JSE),重点是V8。请注意,在开始这项研究之前,我没有任何模糊测试JSE的经验。
Dharma
我的实验从一个上下文无关文法(CFG)生成器Dharma开始。我很快意识到,生成执行有趣操作的有效JavaScript代码的文法规则过于复杂。类型混淆和JIT引擎错误是我的主要焦点,然而,大多数生成的代码在语法上是不正确的。每个语句都包装在try/catch块中以处理不正确的代码。经过几天的模糊测试,我只能发现内存不足(OOM)错误。如果您想了解更多关于V8 JIT和Dharma的信息,我推荐这项深思熟虑的研究。
Dharma允许您为各种目的指定三个部分。第一个称为variable,允许您定义稍后在value部分中使用的变量。最后一个variance通常用于指定扩展CFG树的起始符号。
链接在value部分内部实现,Dharma的一个不错的功能是,这里您只定义赋值规则或函数调用,变量在需要时自动创建。但是,如果我们将类型A的变量分配给类型B的变量,我们必须将类型A的所有规则包含在类型B对象中。
以下是此类规则的示例:
|
|
正如您可以想象的那样,在没有编写额外库的情况下,代码很快变得复杂和笨拙。
当针对流行软件时,带有覆盖率的模糊测试是强制性的,因为纯黑盒方法只能触及攻击表面。当二进制文件使用特定的Clang(编译器前端,LLVM基础设施的一部分)标志编译时,可以轻松获得覆盖率。部分输出可以在下面的图片中看到。在我的情况下,它仅对手动代码审查和文法调整有用,因为没有方便的方法如何在JavaScript源代码上实现变异器。
Fuzzilli
作为替代方法,我开始使用Fuzzilli,我认为这是一个令人难以置信且仍然被严重低估的模糊测试器,由Samuel Groß(又名Saelo)实现。Fuzzilli使用一种称为FuzzIL的中间表示(IR)语言,非常适合变异。此外,任何FuzzIL程序都可以始终转换(提升)为有效的JavaScript代码。
当时,支持的目标是V8、SpiderMonkey和JavaScriptCore。由于这些引擎持续进行广泛的模糊测试,我决定改为实现对不同JavaScript引擎的支持。我也对模糊测试器和引擎之间的通信协议感兴趣,因此我认为扩展这个模糊测试器是一个很好的练习。
我决定添加对JerryScript的支持。在过去几年中,Fuzzinator使用ANTLR v4测试用例生成器Grammarinator发现了许多此目标的安全问题。这些错误已被调查和修复,所以我想看看Fuzzilli是否能发现新的问题。
Fuzzilli基础
REPRL
关于Fuzzilli的最佳可用高级文档是Samuel的硕士论文,其中介绍了它,我强烈建议阅读它,因为本文总结了一些新颖的想法。
许多现代模糊测试器架构使用Forkserver。其背后的想法是运行程序直到初始化完成,但在处理任何输入之前。之后,从模糊测试器读取输入并传递给新fork的子进程。开销很低,因为初始化可能只发生一次,或者当需要重启时(例如,在连续内存泄漏的情况下)。
Fuzzilli使用REPRL方法,它节省了由fork()引起的开销,每个样本的测量执行速度可能快约7倍。JSE引擎被修改为从模糊测试器读取输入,并在执行样本后获取覆盖率。关键部分是重置状态,这通常(显然)不会完成,因为引擎使用已定义变量的上下文。与Forkserver相比,我们需要对引擎有初步的了解。了解引擎的字符串表示是如何内部实现的,以提供输入或添加附加命令,这是有用的。
覆盖率
LLVM提供了一种方便的方法来获取边缘覆盖率。向Clang提供-fsanitize-coverage=trace-pc-guard编译器标志,我们可以接收指向区域开始和结束的指针,这些区域由保护号初始化,如llvm文档中所述:
|
|
保护区域包含在JSE目标中。这意味着JavaScript引擎必须被修改以适应这些变化。每当执行分支时,调用__sanitizer_cov_trace_pc_guard回调。Fuzzilli使用POSIX共享内存对象(shmem)来避免将数据传递给父进程时的开销。Shmem表示一个位图,其中设置访问过的边缘,并且在每个JavaScript输入传递后,边缘保护被重新初始化。
生成
我们不会重复程序生成算法,因为它们在论文中有详细描述。令人惊讶的事实是,所有程序都源于这个简单的JavaScript,通过巧妙应用多个变异器:
|
|
与JerryScript的集成
要添加新目标,应该对Fuzzilli实现一些修改。从高层次来看,REPRL伪代码在这里描述。
正如我们已经提到的,JavaScript引擎必须被修改以符合Fuzzilli的协议。为了保持相同的代码标准和逻辑,我们建议向引擎添加自定义命令行参数。如果我们决定在没有它的情况下运行解释器,它将正常运行。否则,它使用硬编码的描述符编号让父进程知道解释器已准备好处理我们的输入。
Fuzzilli内部使用自定义命令,默认称为fuzzilli,解释器也应该实现。第一个参数代表操作符 - 它可以是FUZZILLI_CRASH或FUZZILLI_PRINT。前者用于检查我们是否可以拦截分段错误,而后者(可选)用于打印作为参数传递的输出。根据设计,当某些检查失败时,模糊测试器阻止执行,例如,操作FUZZILLI_CRASH未实现。
代码在不同目标之间非常相似,正如您在我们提交的JerryScript补丁中看到的那样。
对于基本设置,需要编写一个简短的配置文件,存储在Sources/FuzzilliCli/Profiles/中。在这里,我们可以指定引擎特定的附加内置函数、参数,或者感谢WilliamParks最近的贡献,还可以指定ECMAScriptVersion。
结果
通过将Fuzzilli与JerryScript集成,Doyensec在四周内通过GitHub报告了多个错误。所有这些问题都已修复。 所有问题也已添加到Fuzzilli错误展示中:
Fuzzilli设计上对具有JIT编译器的目标高效。它可以通过生成嵌套回调、原型或代理对象来滥用非线性执行流,其中可以修改不同对象的状态。Fuzzilli产生的样本专门生成以包含这些属性,这是发现类型混淆错误所必需的。
这种行为在问题#3836中可以轻易看到。在大多数情况下,由Fuzzilli生成的概念证明非常简单:
|
|
这可以在不改变语义的情况下重写为更简单的代码:
|
|
此问题的根本原因在修复中描述。 在JavaScript中,当创建像Float64Array这样的类型化数组时,可以通过buffer属性访问原始二进制数据缓冲区,由ArrayBuffer类型表示。然而,类型后来被更改为类型化数组视图Uint8Array。在初始化期间,引擎期望一个ArrayBuffer而不是类型化数组。当调用ecma_arraybuffer_get_buffer函数时,类型化数组指针被强制转换为ArrayBuffer。请注意,这是可能的,因为生产版本的断言被移除。这导致在第196行的类型混淆错误。
因此,目标缓冲区dst_buf_p包含不正确的指针,正如我们可以通过gdb从分类中看到的内存损坏:
|
|
一些问题,包括上述问题,可能可以从拒绝服务升级到代码执行。由于时间限制和附加价值 little,我们没有尝试实现一个有效的漏洞利用。
我想感谢Saelo将我的JerryScript补丁纳入Fuzzilli。也非常感谢Doyensec提供资助的25%研究时间,使这个项目成为可能。
附加参考
- FuzzIL: Coverage Guided Fuzzing for JavaScript Engines by Saelo
- Efficient Approach to Fuzzing Interpreters by Marcin Dominiak and Wojciech Rauner