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