在过去的几个月里,我们一直在使用标准的Solc智能合约编译器,并且我们已经积累了近20个新的bug(现在大部分已经修复了)。其中一些是现有错误的副本,症状或触发器略有不同,但绝大多数是编译器中以前未报告的错误。
这是一场非常成功的起毛战役,据我们所知,这也是针对Solc发起的最成功的战役之一。这不是Solc第一次使用AFL进行毛化;通过AFL毛化Solc是一种由来已久的做法。自2019年1月以来,该编译器甚至已经在OSSFuzz上进行了测试。在大多数情况下,我们是如何设法找到这么多以前未发现的bug--以及值得相当快地修复的bug的呢?以下是我们活动的五个重要要素。
幸运的是,新奇的东西其实并不一定要保密,只要它是真正的新的,有点好吃就行了!基本上,我们在这个模糊的战役中使用了AFL,但不是任何现成的AFL。取而代之的是,我们使用了AFL的一个新变种,专门设计用来帮助开发人员不用做太多额外的工作就可以使用类似C语言的模糊语言工具。
与标准AFL相比的变化并不是特别大;这个Fuzzer只是添加了一些新的AFL破坏性突变,看起来就像一个天真的、基于文本的源代码突变测试工具(即,通用变异器)所使用的那些。新方法只需要不到500行代码就可以实现,其中大部分都非常简单和重复。
AFL的这种变体是与Sourcegraph的Rijnard van Tonder、CMU的Claire Le Goues和犹他大学的John Regehr共同研究项目的一部分。在我们的初步实验中,将该方法与普通的老式AFL进行了比较,结果对于Solc和Tmall C编译器TCC来说看起来都很好。作为科学,这种方法需要进一步的发展和验证;我们正在努力做到这一点。然而,在实践中,这种新方法几乎肯定帮助我们在Solc中发现了许多新的bug。
在实验比较中,我们发现了一些使用普通旧AFL报告的早期错误,并且我们用新方法很容易地发现了一些错误,我们最终也使用AFL在没有新方法的情况下复制了这些错误-但大多数错误没有在“正常”AFL中复制。下图显示了我们在GitHub上提交的问题数量,并强调了AFL更改的重要性:
2月下旬,在我们的AFL版本中添加了几个更智能的变异操作后,漏洞发现的数量立即出现了大幅跃升。这可能是巧合,但我们对此表示怀疑;我们手动检查了生成的文件,发现AFL模糊队列内容发生了质的变化。此外,AFL生成的实际可编译的文件的比例跃升了10%以上。
对一个从未进行过模糊化的系统进行模糊化肯定是有效的;该系统对模糊器产生的各种输入的“抵抗力”可能极低。但是,对以前进行过模糊化的系统进行模糊化也有好处。正如我们注意到的,我们并不是第一个用AFL迷惑Solc的人。以前的工作也不完全是自由职业者的临时工作;编译器团队参与了Fuzze Solc,并构建了我们可以使用的工具,使我们的工作变得更容易。
固态构建包括一个名为solfuzzer的可执行文件,该可执行文件将固态源文件作为输入,并使用各种选项(有优化和无优化等)编译它。寻找各种不变的违规和各种撞车事故。我们发现的几个bug不会在普通的Solc可执行文件中显示出来,除非您使用特定的命令行选项(尤其是优化),或者以某些其他非常不寻常的方式运行Solc;Solfuzzer发现了所有这些错误。我们还从其他人的经验中了解到,AFL模糊的良好起始语料库位于test/libsolidity/syntaxTests目录树中。这是其他人正在使用的,而且它肯定涵盖了很多“您可能在可靠的源文件中看到的内容”。
当然,即使有了这样的现有工作,你也需要知道自己在做什么,或者至少需要知道如何在谷歌上查找。没有任何东西会告诉你,简单地用AFL编译Solc实际上不会产生很好的模糊效果。首先,您需要注意,模糊会导致非常高的贴图密度,这会测量您“填充”AFL的覆盖散列的程度。然后,您需要了解《AFL用户手册》中给出的建议,或者搜索术语“AFL贴图密度”,然后看到您需要重新编译整个系统,并将AFL_INST_RATION设置为10,以使模糊器更容易识别新路径。根据美国劳工联合会的文件,这种情况只有在“你在使用毛茸茸的软件”时才会发生。因此,如果您习惯于模糊化编译器,您可能以前见过这种情况,但除此之外,您可能还没有遇到地图密度问题。
您可能注意到,提交的bug中的最后一次高峰出现在最后一次提交到AFL编译器模糊存储库之后很久。我们有没有做过还看不见的局部改变?不,我们只是换了我们用来做毛绒的语料库。特别是,我们超越了语法测试,并添加了我们可以在test/libsolity下找到的所有可靠性源文件。这样做完成的最重要的事情是允许我们找到SMT检查器bug,因为它引入了使用SMTChecker杂注的文件。如果没有使用该编译指示的语料库示例,AFL基本上没有机会探索SMT Checker行为。
我们发现的其他后期错误(当似乎不可能找到任何新的错误时)大多来自于构建一个“主”语料库,包括我们在那之前执行的每一次Fuzzer运行产生的每条有趣的路径,然后让Fuzzer探索它一个多月。
是的,我们说了超过一个月(在两个核心上)。我们运行了超过10亿次编译,以便找到一些更隐蔽的错误。这些错误在原始语料库的派生树中非常深。我们在Vyper编译器中发现的错误同样需要一些非常长的运行时间才能发现。当然,如果你的模糊努力不仅仅是玩弄一项新技术,你可能想要投入机器(因此也就是金钱)来解决这个问题。但根据一篇重要的新论文,如果这是你唯一的方法,你可能需要投入更多的机器来解决这个问题。
此外,对于基于反馈的模糊器,仅仅使用更多的机器可能不会产生一些需要很长时间才能找到的模糊错误;对于需要突变…突变的错误,并不总是有捷径可以找到。原始语料库路径的。激发一百万个“集群模糊”实例将产生很大的广度,但不一定达到深度,即使这些实例定期彼此共享它们的新路径。
在提交之前减少触发错误的源文件,或者尝试遵循您要向其报告错误的项目的实际问题提交指南,这并不是什么秘密。当然,即使这些指南中没有提到,执行快速搜索以避免提交重复项也是标准做法。我们做了那些事。它们并没有增加我们的bug数量,但它们确实加快了识别提交的问题是真正的bug并修复它们的过程。
有趣的是,通常不需要太多的削减。在大多数情况下,只需删除5-10行代码(不到文件的一半)即可生成“足够好”的输入。这部分是由于语料库,(我们认为)部分是因为我们的自定义突变倾向于保持输入较小,甚至超过了AFL沿着这些路线的内置启发式。
有些错误是非常简单的问题。例如,此约定用于导致编译器突然中断,并显示消息“编译期间出现未知异常:std::BAD_CAST”:
契约C{function f()public return(uint,uint){try this(){}catch error(String Memory){}。
通过将TypeError更改为datalTypeError,可以轻松修复该问题,从而防止编译器继续处于错误状态。提交修复只有一行代码(尽管有相当多的新测试行)。
另一方面,这个问题会引发错误奖励,并将其列入0.6.8编译器版本的重要错误修复列表中,它可能会为某些字符串文字生成不正确的代码。它还需要相当多的代码来处理所需的报价。
即使是我们的引发错误的固体文件的未缩减版本看起来也像是固体源代码。这可能是因为我们的突变,这是AFL非常喜欢的,倾向于“保持源代码的y-ness”。似乎正在发生的很多事情都是一些小更改的混合,这些小更改不会使文件变得太无意义,再加上语料库示例的组合(AFL拼接)并没有偏离正常的可靠代码太远。AFL本身倾向于将源代码减少为无法编译的垃圾,即使与有趣的代码合并,也无法通过最初的编译阶段。但是有了更集中的突变,剪接通常可以完成工作,例如在下面的输入中,它触发了一个仍然开放的bug(在我们写作时):
合约C{function o(Int256)public return(Int256){Assembly{c:=shl(1,a)}}int常量c=2 Szabo+1秒+3 Finney*3小时;}。
触发输入结合了程序集和常量,但是我们使用的语料库中没有包含两者的文件,看起来很像这样。最近的是:
合同C{bool常量c=this;函数f()public{Assembly{let t:=c}
合同C{函数f(Uint X)公共返回(Uint Y){Assembly{y:=shl(2,x)}}。
像这样组合合同并不是一件容易的事;语料库中甚至没有任何实例像暴露bug的合同中的特定shl表达式那样出现。试图修改程序集中的常量不太可能出现在合法代码中。我们可以想象,手动生产这种奇怪但重要的输入是极其重要的。在这种情况下,就像Fuzing经常发生的那样,如果您能想到一个合同,就像触发bug的合同一样,您或其他人很可能从一开始就编写了正确的代码。
在已经模糊化的高可见性软件中比在从未模糊化的软件中更难找到重要的错误。但是,由于您的方法具有一定的新颖性,基于以前的模糊活动(特别是针对Oracle、基础设施和语料库内容)的智能引导,再加上经验和专业知识,就有可能在复杂的软件系统中发现许多从未发现的bug,即使它们驻留在OSSFuzz上。归根结底,即使是我们最激进的模糊也只是触及了像现代产品编译器这样真正复杂的软件的皮毛-因此,除了暴力之外,还需要狡猾。
我们一直在开发工具来帮助您更快、更聪明地工作。您的下一个项目需要帮助吗?联系我们!