使用模糊测试攻破Solidity编译器
在过去的几个月里,我们一直在对solc(标准的Solidity智能合约编译器)进行模糊测试,并成功发现了近20个新漏洞(目前大部分已修复)。其中一些是现有漏洞的重复,症状或触发条件略有不同,但绝大多数都是编译器之前未报告的漏洞。
这是一次非常成功的模糊测试活动,据我们所知,这是针对solc最成功的一次。这并不是第一次用AFL对solc进行模糊测试;通过AFL对solc进行模糊测试是一项长期实践。该编译器甚至从2019年1月就开始在OSSFuzz上进行测试。那么,我们是如何发现这么多以前未发现的漏洞——并且在大多数情况下值得快速修复的漏洞呢?以下是我们活动的五个重要要素。
1. 拥有秘密武器
幸运的是,新奇之处并不需要真正保密,只要是真正新颖且有点“美味”的就行!本质上,我们在这个模糊测试活动中使用了AFL,但不是任何现成的AFL。相反,我们使用了一种新的AFL变体,专门设计用于帮助开发者对类C语言工具进行模糊测试,而无需太多额外努力。
与标准AFL的变化并不特别大;这个模糊测试器只是添加了许多新的AFL havoc变异,看起来类似于朴素的、基于文本的源代码变异测试工具(即universalmutator)所使用的变异。新方法需要不到500行代码来实现,其中大部分非常简单且重复。
这种AFL变体是与Sourcegraph的Rijnard van Tonder、CMU的Claire Le Goues和犹他大学的John Regehr合作研究项目的一部分。在我们将该方法与普通旧AFL进行比较的初步实验中,结果对solc和Tiny C编译器(tcc)看起来不错。作为科学方法,该方法需要进一步开发和验证;我们正在努力。然而,在实践中,这种新方法几乎肯定帮助我们发现了solc中的许多新漏洞。
我们在实验比较中使用普通旧AFL发现了一些早期报告的漏洞,并且一些我们用新方法轻松发现的漏洞最终也在没有新方法的AFL中复制了出来——但大多数漏洞在“正常”AFL中尚未复制。下图显示了我们提交到GitHub的问题数量,并强调了AFL更改的重要性:
二月底漏洞发现的大幅跃升发生在我们向我们的AFL版本添加了一些更智能的变异操作之后。这可能是巧合,但我们怀疑不是;我们手动检查了生成的文件,并看到AFL模糊测试队列内容发生了质的变化。此外,AFL生成的文件中实际可编译的Solidity比例跃升了10%以上。
2. 基于他人的工作
对从未进行过模糊测试的系统进行模糊测试当然有效;系统对模糊测试器生成的输入类型的“抵抗力”可能极低。然而,对之前进行过模糊测试的系统进行模糊测试也可能有优势。正如我们提到的,我们并不是第一个用AFL对solc进行模糊测试的人。之前的工作也并非完全自由随意的;编译器团队参与了solc的模糊测试,并构建了我们可以使用的工具来简化我们的工作。
Solidity构建包括一个名为solfuzzer的可执行文件,它接受一个Solidity源文件作为输入,并使用各种选项(有优化和无优化等)进行编译,寻找各种不变式违反和崩溃类型。我们发现的几个漏洞在正常的solc可执行文件中不会表现出来,除非您使用特定的命令行选项(尤其是优化)或以其他相当不寻常的方式运行solc;solfuzzer找到了所有这些漏洞。我们还从他人的经验中了解到,AFL模糊测试的一个良好起始语料库位于test/libsolidity/syntaxTests目录树中。这是其他人正在使用的,它肯定覆盖了“在Solidity源文件中可能看到的内容”的很大一部分。
当然,即使有这些现有工作,您也需要知道自己在做什么,或者至少知道如何在Google上查找。没有什么会告诉您,简单地用AFL编译solc实际上不会产生良好的模糊测试结果。首先,您需要注意到模糊测试会导致非常高的映射密度,这衡量了您“填充”AFL覆盖哈希的程度。然后,您要么需要知道AFL用户指南中给出的建议,要么搜索术语“afl map density”并看到您需要使用AFL_INST_RATIO设置为10重新编译整个系统,以使模糊测试器更容易识别新路径。根据AFL文档,这只有在“您正在模糊测试极其复杂的软件”时才会发生。因此,如果您习惯于模糊测试编译器,您可能以前见过这种情况,否则您可能没有遇到过映射密度问题。
3. 玩弄语料库
您可能会注意到,提交漏洞的最后一个高峰发生在我们对AFL-compiler-fuzzer存储库的最后一次提交之后很久。我们是否进行了尚未可见的本地更改?不,我们只是更改了用于模糊测试的语料库。特别是,我们超越了语法测试,并添加了我们在test/libsolidity下能找到的所有Solidity源文件。这实现的最重要的事情是让我们能够找到SMT检查器漏洞,因为它引入了使用SMTChecker编译指示的文件。如果没有使用该编译指示的语料库示例,AFL基本上没有机会探索SMT检查器行为。
我们发现的其他后期开花漏洞(当似乎不可能找到任何新漏洞时)主要来自构建一个“主”语料库,包括我们到那时为止执行的每次模糊测试运行产生的每个有趣路径,然后让模糊测试器探索它一个多月。
4. 保持耐心
是的,我们说的是一个多月(在两个核心上)。我们运行了超过十亿次编译,以便击中一些我们发现的更晦涩的漏洞。这些漏洞在原始语料库的派生树中非常深。我们在Vyper编译器中发现的漏洞同样需要一些非常长的运行才能发现。当然,如果您的模糊测试工作不仅仅涉及玩弄新技术,您可能希望向问题投入机器(从而资金)。但根据一篇重要的新论文,如果这是您唯一的方法,您可能需要以指数方式投入更多机器。
此外,对于基于反馈的模糊测试器,仅仅使用更多机器可能不会产生一些需要长时间才能发现的晦涩漏洞;对于需要原始语料库路径的变异之变异之变异……的漏洞,并不总是有捷径。启动一百万个“clusterfuzz”实例会产生很大的广度,但不一定能达到深度,即使这些实例定期彼此共享它们的新颖路径。
5. 做明显必要的事情
在提交之前减少触发漏洞的源文件,或尝试遵循您报告漏洞项目的实际问题提交指南,并没有什么秘密。当然,即使指南中没有提到,执行快速搜索以避免提交重复项也是标准做法。我们做了这些事情。它们没有为我们的漏洞计数增加多少,但它们肯定加快了识别提交的问题为真实漏洞并修复它们的过程。
有趣的是,通常不需要太多减少。在大多数情况下,只需删除5-10行代码(不到文件的一半)就产生了“足够好”的输入。这部分是由于语料库,并且(我们认为)部分是由于我们的自定义变异倾向于保持输入小,甚至超越了AFL的内置启发式方法。
我们发现了什么?
一些漏洞是非常简单的问题。例如,这个合约曾经导致编译器爆炸,并显示消息“编译期间未知异常:std::bad_cast”:
|
|
通过将typeError更改为fatalTypeError,问题很容易修复,这可以防止编译器在不良状态下继续。修复该问题的提交只有一行代码(尽管有相当多的新测试行)。
另一方面,这个问题促使了漏洞赏金奖励,并进入了0.6.8编译器发布的重要漏洞修复列表,可能为某些字符串字面量产生不正确的代码。它还需要 substantially more code 来处理所需的引用。
即使我们未减少的触发漏洞的Solidity文件版本看起来也像Solidity源代码。这可能是因为我们的变异(受到AFL的 heavily favored)倾向于“保留源代码性”。大部分似乎发生的是小变化的混合,这些变化不会使文件太荒谬,加上语料库示例的组合(AFL拼接),这些示例没有偏离正常的Solidity代码太远。AFL本身倾向于将源代码减少为不可编译的垃圾,即使与有趣的代码合并,也无法通过初始编译器阶段。但通过更集中的变异,拼接通常可以完成工作,如在这个触发仍然开放(在我们撰写时)的漏洞的输入中:
|
|
触发输入结合了汇编和常量,但我们使用的语料库中没有文件同时包含两者并且看起来很像这个。最接近的是:
|
|
同时,最接近的包含汇编和shl的文件是:
|
|
像这样组合合约并非易事;在语料库中甚至没有出现任何类似于暴露漏洞合约中特定shl表达式的实例。尝试在汇编中修改常量不太可能出现在合法代码中。我们想象手动产生如此奇怪但重要的输入是非常不平凡的。在这种情况下,正如模糊测试中经常发生的那样,如果您能想到一个类似于触发漏洞的合约,您或其他人可能一开始就写出了正确的代码。
结论
在已经进行过模糊测试的高可见性软件中寻找重要漏洞比在从未进行过模糊测试的软件中更难。然而,通过方法中的一些新颖性、基于先前模糊测试活动的智能引导(特别是对于预言机、基础设施和语料库内容),加上经验和专业知识,可以在复杂软件系统中发现许多从未发现的漏洞,即使它们托管在OSSFuzz上。最终,即使我们最激进的模糊测试也只是触及了真正复杂软件(如现代生产编译器)的表面——因此,除了暴力之外,还需要技巧。
我们一直在开发工具来帮助您更快更聪明地工作。需要为您的下一个项目提供帮助吗?联系我们!