编译器模糊测试一年成果:发现上百个漏洞的技术实践

本文详细记录了一年多来对Solidity等编译器进行持续模糊测试的技术实践,包括测试策略、工具改进、漏洞发现统计以及在不同编译器上的应用成果,展示了独立模糊测试的重要价值。

编译器模糊测试一年记:技术实践与成果

2020年夏天,我们描述了对Solidity编译器solc进行模糊测试的工作。现在我们要重新审视这个项目,因为模糊测试活动往往会"饱和",随着时间的推移发现的新结果越来越少。Solidity模糊测试是否已经耗尽?对一个高风险项目进行模糊测试是否值得,特别是当它已经有自己活跃且有效的模糊测试工作时?

初始成果与持续价值

那次模糊测试活动的第一批漏洞于2020年2月使用afl变体提交。从那时起,我们已提交74份报告。67个被确认为漏洞,其中66个已被修复。7个是重复报告或不被认为是真正的漏洞。

鉴于其中21个漏洞是自去年12月以来提交的,可以说我们的模糊测试活动有力地证明了为什么独立模糊测试仍然重要,即使涉及基于OSSFuzz的测试,也能发现不同的漏洞。

持续模糊测试的价值

为什么可能无限期地对这样一个项目进行模糊测试是有用的?答案有三部分。

首先,更多的模糊测试覆盖了更多的代码执行路径,长时间的模糊测试运行尤其有帮助。使用我们知道的任何模糊测试器都很难接近路径覆盖饱和。即使运行afl 30天或更长时间,我们的测试仍然每1-2小时发现新路径,有时发现新边缘。我们报告的一些漏洞只有在模糊测试一个月后才被发现。

生产编译器极其复杂,因此深度模糊测试是有用的。生成输入需要时间,比如我们最近发现的solc Solidity级别的漏洞:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
pragma experimental SMTChecker;
contract A {
    function f() internal virtual {
        v();
    }
    function v() internal virtual {
    }
}

contract B is A {
    function f() internal virtual override {
        super.f();
    }
}

contract C is B {
    function v() internal override {
        if (0==1)
            f();
    }
}

这段代码应该编译无误,但没有。在漏洞修复之前,它导致SMT检查器崩溃,抛出致命的"Super contract not available"错误,原因是分支内虚拟调用中用于变量访问的合约上下文不正确。

由于其复杂性和可能的执行路径数量,编译器应该进行长时间的模糊测试。一个经验法则是,afl在任何非平凡目标上真正开始认真工作之前需要达到一百万次执行,而编译器可能需要更多。根据我们的经验,编译器运行速度从每秒不到一次执行到每秒40次执行不等。仅仅达到一百万次执行就可能需要几天时间!

独立测试方法

我们想要与OSSFuzz一起独立进行模糊测试的第二个原因是从不同角度接近目标。我们没有严格使用基于字典或语法的方法与传统的模糊测试器变异操作符,而是使用来自任何语言变异测试的想法,向afl添加"代码特定"的变异操作符,并主要(但不完全)依赖这些操作符,而不是afl更通用的变异,后者往往专注于二进制格式数据。做一些不同的事情可能是解决模糊测试器饱和的好方法。

最后,我们不断获取最新代码并开始对新版本的solc进行模糊测试。由于OSSFuzz持续集成不包括我们的技术,对其他模糊测试器困难但对我们的代码变异方法容易的漏洞有时会出现,我们的模糊测试器几乎会立即发现它们。

但我们不会获取每个新版本并重新开始,因为我们不想失去通过长期模糊测试活动获得的成果。我们也不会持续从上次停止的地方继续,因为afl可以生成的数万个测试语料库可能充满了无趣的路径,这可能使在新代码中查找漏洞更容易。我们有时会从现有运行中恢复,但只是偶尔。

扩展到其他编译器

我们对solc的成功启发我们对其他编译器进行模糊测试。首先,我们尝试对Vyper编译器进行模糊测试——这是一种旨在为编写以太坊区块链智能合约提供比Solidity更安全、类似Python的替代方案的语言。我们之前的Vyper模糊测试使用 essentially 基于语法的方法与TSTL(模板脚本测试语言)Python库通过python-afl发现了一些有趣的漏洞。我们在这次活动中发现了一些漏洞,但由于检测的Python测试速度慢和吞吐量差,选择不走极端。

相比之下,我的合作者、Sourcegraph的Rijnard van Tonder在模糊测试Diem项目的Move语言方面取得了更大的成功——这是以前称为Facebook 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语言一样。我们报告的一些漏洞比原本可能的情况更早地突出了开发人员的棘手角落案例。我们希望像这样的讨论将有助于产生更健壮的语言和编译器,减少为适应设计缺陷而进行的黑客行为,这些缺陷发现得太晚而难以更改。

我们计划继续模糊测试大多数这些编译器,因为solc努力表明模糊测试活动可以长期保持可行性,即使有其他针对同一编译器的模糊测试工作。

编译器很复杂,大多数也在快速变化。例如,Fe是一种全新的语言,还没有完全设计好,而Solidity以其对面向用户的语法和编译器内部的重大更改而闻名。

我们也在与领导Solidity内部模糊测试工作的Bhargava Shastry交谈,并应用他们在Yul优化级别的protobuf模糊测试中应用的一些语义检查。我们开始通过solc的strict-assembly选项直接模糊测试Yul,并且已经发现一个有趣的漏洞,该漏洞被迅速修复并引起了相当多的讨论!我们希望找到不仅仅是使solc崩溃的输入的能力将把这种模糊测试带到下一个水平。

更大的问题是模糊测试是否限于它能发现的漏洞,因为编译器无法检测许多错误代码错误。两个编译器的差异比较,或编译器在优化关闭时与其自身输出的比较,通常需要程序的一种更受限制的形式,这限制了您发现的漏洞,因为程序必须编译和执行以比较结果。

解决这个问题的一种方法是使编译器更频繁地崩溃。我们设想一个世界,编译器包括类似测试选项的东西,启用激进和昂贵的检查,这些检查在正常运行中不实用,例如寄存器分配的健全性检查。尽管这些检查对于正常运行可能太昂贵,但它们可以为一些模糊测试运行打开,因为编译的程序通常很小,而且,也许更重要的是,在极其关键代码(火星探测器代码、核反应堆控制代码——或高价值智能合约)的最终生产编译中打开,以确保没有错误代码漏洞潜入这些系统。

最后,我们想教育编译器开发人员和其他以源代码作为输入的工其开发人员,有效的模糊测试不必是高成本的工作,需要大量的开发人员时间。为编译器查找崩溃输入通常很容易,只需要一些空闲的CPU周期、一套体面的语言源代码示例和afl-compiler-fuzzer工具!

我们希望您喜欢了解我们的长期编译器模糊测试项目,我们很乐意在Twitter @trailofbits上听到您自己的模糊测试经验。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计