模糊测试-Solidity智能合约编译器

  • 时间:
  • 浏览:42
  • 来源:区块链技术网

在过去的几个月中,我们一直在对solc(标准的Solidity智能合约编译器)进行模糊测试,我们已经发现了近20个(现在大部分已经修复)新的bug。其中一些是症状或触发器略与的现有bug不同,但绝大多数是编译器中以前未报告的bug。

这是一次非常成功的模糊测试,据我们所知,这是有史以来最成功的针对solc的攻击之一。这不是第一次用AFL来解决问题。通过AFL对solc进行模糊化测试是一个长期的实践。自2019年1月以来,该编译器甚至已经在OSSFuzz上进行了测试。我们如何设法找到这么多以前未发现的错误-在大多数情况下,值得快速修复的错误?以下是我们挑选出来的的五个重要因素。

1、本质上,我们在这个模糊化测试中使用了AFL,但不并只是任何现成的AFL。取而代之的是,我们使用了AFL的一个新变体,它被明确地设计用来帮助开发人员在不做太多额外工作的情况下模糊C语言的语言工具。

标准AFL的变化不是特别大;该模糊器仅添加了一些新的AFL破坏性改变,基于文本的源代码突变测试工具(即universalmutator)所使用的突变。新方法只需要不到500行代码就可以实现,大多数代码都非常简单且重复。

AFL的这种变化是Sourcegraph的Rijnard van Tonder,CMU的Claire Le Goues和犹他大学的John Regehr共同研究项目的一部分。在我们的初步实验中,将该方法与普通的AFL进行了比较,结果对于solc和Tiny C Compiler,tcc来说看起来不错。作为一门科学,该方法需要进一步开发和验证。我们正在努力。但是实际上,这种新方法几乎可以肯定地帮助我们发现了solc中的许多新错误。

在实验比较中,我们发现了一些使用普通的旧AFL报告的早期bug,并且我们使用新方法很容易发现的这些bug,我们在没有使用新方法的情况下,使用AFL进行复制这些bug-但大多数bug尚未被复制 在“普通” AFL中。下图显示了我们在GitHub上提交的bug数量,并强调了AFL更改的重要性:

2月底,在我们为AFL版本添加了一些更智能的突变操作之后,Bug发现的大幅度增长就出现了。可能是巧合,但我们对此表示怀疑。于是我们手动检查了生成的文件,发现AFL模糊队列内容发生了质的变化。此外,AFL生成的实际可编译Solidity文件的比例跃升了10%以上。

2、站在前人的肩膀上模糊化一个从未被模糊化过的系统肯定是有效的;系统对模糊器产生的各种输入的“抵抗力”可能非常低。但是模糊化以前被模糊化过的系统也有好处。正如我们所指出的,我们并不是第一个用AFL模糊solc的人。编译器团队参与了solc的模糊化,并且构建了一些工具,我们可以使用这些工具来简化工作。

Solidity构建包括一个名为solfuzzer的可执行文件,该文件将Solidity源文件作为输入,并使用各种选项(带有和不带有优化等)对其进行编译,以查找各种不变的冲突和各种崩溃。我们发现的一些bug不会出现在普通的solc可执行文件中,除非您使用特定的命令行选项(特别是优化)或以某些其他非常不寻常的方式运行solc;solfuzzer发现了所有这些bug。我们还从其他人的经验中学到,AFL模糊测试的一个很好的开始语料库在test/libsolidity/syntaxTests目录树中。这就是其他人所使用的,并且肯定涵盖了很多“您可能在Solidity源文件中看到的内容”。

当然即使有这样的工作,你也需要知道你在做什么,或者至少知道如何在网络上查找。没有什么能告诉您,仅仅用AFL编译solc实际上不会产生良好的模糊化。首先需要注意的是,模糊化会导致非常高的映射密度,这将测量您“填充”AFL的覆盖散列的程度。然后您要么需要知道AFL用户指南中给出的建议,要么搜索术语“AFL map density”,并查看是否需要重新编译整个系统,将AFL_INST_RATIO设置为10,以使模糊器更容易识别新路径。根据AFL文档,只有在“您要测试非常冗长的软件时”才会发生这种情况。所以如果你习惯了模糊编译器,那么您可能以前曾经看过这种情况,但是否则您可能还没有遇到过映射密度问题。

3、您可能会注意到,提交的错误的最后一次高峰发生在对我们的AFL-compiler-fuzzer存储库的最后一次提交之后很长时间。我们是否进行了尚不可见的本地更改?不,我们只是更改了用于模糊测试的语料库。特别是,我们不仅查看语法测试,还添加了我们可以在test/libsolidity下找到的所有Solidity源文件。完成的最重要的事情是允许我们查找SMT检查器错误,因为它引入了使用SMTChecker编译指示的文件。如果没有使用该语料库的语料库示例,则AFL基本上没有机会探索SMT Checker行为。

我们发现的其他后期bug(当似乎不可能找到任何新错误时)主要来自建立“主”语料库,其中包括到那时为止我们执行的每次模糊测试所产生的每条有趣的路径,然后让模糊测试了一个多月。

4、是的,我们说了一个多月(两个核心)。我们运行了超过10亿次的编译,以便找到一些更隐晦的bug。这些错误在原始语料库的派生树中非常深。同样我们在Vyper编译器中发现的bug也需要很长时间才能发现。当然如果你的模糊化工作不仅仅是玩弄一种新技术,你可能会想在这个问题上投入机器(从而投入资金)。但是根据一篇重要的新论文,如果这是你唯一的方法,你可能需要投入更多的机器来解决这个问题。

而且对于基于反馈的模糊器,仅使用更多的机器可能不会产生一些需要较长时间才能发现的晦涩的bug;也不一定总是有捷径可以到达需要对原始语料库路径进行突变的bug。使用一百万个“ clusterfuzz”实例会产生很大的广度,但是即使这些实例定期共享彼此的新颖路径,也不一定能达到深度。

5、提交之前减少bug触发源文件或尝试遵循要向其报告bug的项目的实际问题提交准则没有什么秘密。当然即使这些指南中没有提到,执行快速搜索以避免提交重复也是标准操作。我们做了那些事,并没有为我们的错误增加多少,但是他们当然加快了将提交的问题识别为真正的bug并进行修复的过程。

有趣的是,通常不需要减少多少。在大多数情况下,仅仅删除5-10行代码(不到文件的一半)就产生了“足够好”的输入。这部分归因于语料库,(我们认为)部分归因于我们的自定义突变趋向于使输入保持较小,甚至超出了AFL沿这些方向的内置启发式方法。

我们发现了什么?

有些bug是非常简单的问题。例如此智能合约用于导致编译器崩溃,并显示消息“Unknown exception during compilation:std::bad_cast”:

contractC{functionf()publicreturns(uint,uint){trythis(){}catchError(stringmemory){}}}

通过将typeError更改为fatalTypeError,可以轻松解决此问题,这可以防止编译器继续处于错误状态。提交修复仅是一行代码(尽管有很多行新测试)。

另一方面,这个问题导致了bug奖励,并使其成为0.6.8编译器版本的重要bug修复列表,可能会为某些字符串文本生成错误代码。它还需要更多的代码来处理所需的代价。

即使是我们的bug触发Solidity文件的未精简版本看起来也像Solidity源代码。这可能是因为AFL大力支持的我们的变异倾向于“保留源代码的y-ness”。似乎正在发生的大部分情况是微小的变化的混合,这些微小的变化不会使文件变得过于荒谬,加上语料库示例的组合(AFL拼接)与普通的Solidity代码相距不远。AFL本身倾向于将源代码减少为不可编译的垃圾,即使将其与有趣的代码合并,也不会使其超出初始编译器阶段。但是有了更集中的突变,拼接通常就可以完成工作,因为在此输入中会触发仍然打开的错误(如我们所写):

contractC{functiono(int256)publicreturns(int256){assembly{c:=shl(1,a)}}intconstantc=2szabo+1seconds+3finney*3hours;}

触发输入结合了汇编和常量,但是我们使用的语料库中没有包含这两者的文件,它们看起来很像。最接近的是:

contractC{boolconstantc=this;functionf()public{assembly{lett:=c}}}

同时包含汇编和shl的最接近的文件是:

contractC{functionf(uintx)publicreturns(uinty){assembly{y:=shl(2,x)}}

像这样合并智能合约并非易事。没有一个实例与漏洞暴露合同中的特定shl表达式非常相似,甚至没有出现在语料库中的任何地方。试图修改汇编中的常量不太可能显示在合法代码中。而且我们认为手动产生如此奇怪但重要的输入是极其重要的。在这种情况下,就像模糊测试经常发生的那样,如果您完全可以想到一个智能合约,就像触发该bug的智能合约,那么您或其他人可能一开始就可以编写正确的代码。

结论

在已经模糊的高可见性软件中,比从不模糊的软件中发现重要bug更困难。但是由于您采用了一些新颖的方法,基于以前的模糊测试(特别是针对oracle,基础结构和语料库内容)的智能引导,再加上经验和专业知识,即使在复杂的软件系统中也可以找到许多从未发现的bug,即使 它们托管在OSSFuzz上。最后,即使是我们最激进的模糊处理也只能触及真正复杂的软件的表面,就像现代生产编译器那样,除了使用暴力之外,还需要一定的思考模式。

猜你喜欢