编译器模糊测试一年回顾:从Solidity到多语言实践
2020年夏季,我们曾介绍过对Solidity编译器solc进行模糊测试的工作。现在我们需要重新审视这个项目,因为模糊测试活动往往会随时间推移而"饱和",发现的新结果越来越少。Solidity模糊测试是否已经耗尽潜力?对高风险项目进行模糊测试是否值得,特别是当项目自身已经拥有活跃且有效的模糊测试工作时?
成果与数据
该模糊测试活动的首批漏洞于2020年2月使用afl变体提交。截至目前,我们已提交74份报告,其中67个被确认为漏洞,66个已修复,7个是重复报告或不被认为是真正的漏洞。
自去年12月以来提交的21个漏洞充分证明,即使存在基于OSSFuzz的测试,独立模糊测试仍然重要且能够发现不同类型的漏洞。
持续模糊测试的价值
为什么需要可能无限期地对这类项目进行模糊测试?答案包含三个部分:
1. 更全面的代码路径覆盖
更多的模糊测试能够覆盖更多代码执行路径,长时间的模糊测试运行尤其有帮助。使用任何已知的模糊测试工具都很难达到路径覆盖饱和。即使运行afl超过30天,我们的测试仍然每1-2小时发现新路径,有时还会发现新的边界。我们报告的一些漏洞仅在模糊测试一个月后才被发现。
生产级编译器极其复杂,因此深度模糊测试很有价值。生成输入需要时间,比如我们最近发现的solc Solidity级别漏洞:
|
|
这段代码本应无错误编译,但在漏洞修复前会导致SMT检查器崩溃,抛出致命的"Super contract not available"错误,原因是分支内虚拟调用中变量访问使用了错误的合约上下文。
由于其复杂性和大量可能的执行路径,编译器需要进行长时间的模糊测试。经验法则是,afl在任何非平凡目标上真正开始有效工作需要达到一百万次执行,而编译器可能需要更多。根据我们的经验,编译器运行速度从每秒不到一次执行到每秒40次执行不等。仅达到一百万次执行就可能需要几天时间!
2. 不同的测试角度
我们希望在OSSFuzz之外进行独立模糊测试的第二个原因是从不同角度接近目标。我们没有严格使用基于字典或语法的方法与传统的模糊测试变异操作符,而是使用来自任意语言变异测试的思想,向afl添加"代码特定"的变异操作符,并主要(但不完全)依赖这些操作符,而不是afl更通用的变异(通常专注于二进制格式数据)。做不同的事情很可能是解决模糊测试饱和的好方法。
3. 持续跟进新版本
我们持续获取最新代码并在新版本的solc上开始模糊测试。由于OSSFuzz持续集成不包含我们的技术,对其他模糊测试工具困难但对我们的代码变异方法容易的漏洞有时会出现,我们的模糊测试工具几乎会立即发现它们。
但我们不会获取每个新版本并重新开始,因为我们不想失去通过长期模糊测试活动获得的成果。我们也不会持续从上次停止的地方继续,因为afl可以生成的数万个测试语料库可能充满了无趣的路径,这可能使在新代码中发现漏洞更容易。我们有时会从现有运行中恢复,但很少这样做。
扩展到其他语言和编译器
我们在solc上的成功激励我们对其他编译器进行模糊测试。
Vyper编译器
首先,我们尝试对Vyper编译器进行模糊测试——这是一种旨在为编写以太坊区块链智能合约提供比Solidity更安全、类似Python的替代方案的语言。我们之前的Vyper模糊测试使用基于语法的方法,通过python-afl使用TSTL(模板脚本测试语言)Python库发现了一些有趣的漏洞。我们在这个活动中发现了一些漏洞,但由于检测的Python测试速度慢和吞吐量差,选择不进行极端测试。
Move语言
相比之下,我在Sourcegraph的合作者Rijnard van Tonder在模糊测试Diem项目的Move语言(原名Facebook Libra的区块链语言)方面取得了更大成功。这里的编译器速度快,检测成本低。Rijnard迄今已报告编译器中的14个漏洞,所有这些都被确认和分配,其中11个已修复。考虑到模糊测试仅在两个月前开始,这是一个令人印象深刻的漏洞收获!
Fe语言
使用Rijnard关于使用afl.rs模糊测试Rust代码的笔记,我在Fe上尝试了我们的工具——这是一种由以太坊基金会支持的新智能合约语言。Fe在某种意义上是Vyper的继承者,但更多受到Rust的启发,编译器速度更快。我在其第一个alpha版本发布之日开始模糊测试Fe,并在九天后提交了第一个问题。
为了支持我的模糊测试活动,Fe团队更改了Yul后端(使用solc编译Yul)中的故障,以产生对afl可见的Rust恐慌,我们开始了竞赛。到目前为止,这项工作已产生31个问题,略高于Fe所有GitHub问题(包括功能请求)的18%。其中14个被确认为漏洞,10个已修复;其余漏洞仍在审查中。
Zig编译器
我们不仅模糊测试智能合约语言。Rijnard模糊测试了Zig编译器——一种旨在简化和透明的新系统编程语言,并发现了两个漏洞(已确认但未修复)。
模糊测试活动的未来
我们在afl编译器模糊测试活动中发现了88个已修复的漏洞,另外还有14个已确认但尚未修复的漏洞。
有趣的是,模糊测试工具没有使用字典或语法。除了测试用例中适度示例程序语料库所表达的内容外,它们对这些语言一无所知。那么我们如何能如此有效地模糊测试编译器呢?
模糊测试工具在正则表达式级别操作。它们甚至不使用上下文无关语言信息。大多数模糊测试使用快速的基于C字符串的启发式方法进行"类似代码"的更改,例如删除括号间的代码、更改算术或逻辑操作符、只是交换代码行,以及将if语句更改为while和删除函数参数。换句话说,它们应用变异测试工具会进行的那种更改。即使Vyper和Fe不是很像C,并且只表示Python的空白、逗号和括号使用,这种方法也很有效。
自定义字典和语言感知的变异规则可能更有效,但目标是在不需要太多资源的情况下为编译器项目提供有效的模糊测试。我们还希望看到好的模糊测试策略在项目开发早期阶段的影响,就像Fe语言一样。我们报告的一些漏洞比原本可能的情况更早地向开发人员突出了棘手的边缘情况。我们希望像这样的讨论将有助于产生更健壮的语言和编译器,减少为适应设计缺陷而进行的临时修改,这些缺陷发现得太晚而难以轻易更改。
技术挑战与未来方向
大规模的问题在于模糊测试是否因其无法检测许多错误代码错误而受到限制。两个编译器的差异比较,或编译器在关闭优化时与其自身输出的比较,通常需要程序的一种更受限的形式,这限制了您发现的漏洞,因为必须编译和执行程序以比较结果。
解决此问题的一种方法是使编译器更频繁地崩溃。我们设想一个世界,编译器包含类似测试选项的东西,启用激进且昂贵的检查,这些检查在正常运行中不实用,例如寄存器分配的健全性检查。尽管这些检查对于正常运行可能太昂贵,但它们可以在一些模糊测试运行中启用,因为编译的程序通常很小,而且,也许更重要的是,在极其关键代码(火星探测器代码、核反应堆控制代码——或高价值智能合约)的最终生产编译中启用,以确保没有错误代码漏洞潜入此类系统。
最后,我们想要教育编译器开发人员和其他以源代码为输入的工具的开发人员,有效的模糊测试不一定需要大量开发人员时间的高成本工作。使用一些空闲的CPU周期、一组良好的语言源代码示例和afl-compiler-fuzzer工具,为编译器找到崩溃输入通常很容易!
我们希望您喜欢了解我们的长期编译器模糊测试项目,我们很乐意在Twitter @trailofbits上听取您自己的模糊测试经验。