优秀的模糊测试代码是如何炼成的?
发布时间:2019-10-03 15:38

原标题:优异的含糊测验代码是怎么炼成的?

所谓含糊测验,是指一种经过向方针体系供给非预期的输入并监督反常成果来发现软件缝隙的办法,它经过了近 20 年的打开,早已在程序员圈中成为一种干流缝隙开掘技能。根据此,开发者们该怎么编写杰出的含糊测验代码?

作者 | John Regehr

译者 | 弯月,责编 | 屠敏

以下为译文:

含糊测验(Fuzzing)是开掘缝隙和其他软件缺点的强力手法,但一般开发者都是运用这种办法来深化开掘已布置代码中的问题。实际上,咱们应该尽早完结含糊测验,并且开发人员应该花点时刻来编写更易于打开含糊测验的代码。

在这篇文章中,我想介绍一些办法,告知你怎么编写更便利含糊测验的代码。但这些办法并不全面,并且各个办法之间也或许有堆叠。纵观本文,我会运用“含糊器”来指代一切类型的随机测验用例生成器,无论是根据骤变的(afl、libFuzzer等)仍是生成的(jsfunfuzz、Csmith等)生成器。留意,本文提出的主张并非适用一切状况,但其间许多都是合理的软件工程主张。我会用粗体标明一些我以为特别重要的观念。

在测验预言上下功夫

测验预言(test oracle)决议了测验用例是否会触发bug。在默许状况下,afl等含糊器仅有能够运用的预言便是操作体系的页面维护机制。换句话说,它只能检测体系的溃散。可是咱们能做到远不止于此。

断言和由编译器刺进的sanitizer查看是另一种类型的预言。在含糊测验中,你应该尽或许多地运用这种类型的查看。除了这些简略的预言之外,还有许多其他的预言,例如:

  • 函数与反函数:解析与输出的闭环、紧缩与解紧缩的闭环、加密与解密的闭环及其他相似的代码是否会按预期正常作业?
  • 差异:两个不同的完成或同一个完成的两种不同的形式是否表现出相同的行为?
  • 变形:假如在确保语义的状况下修正测验用例,体系是否会表现出的行为,例如在表达式中再增加一层括号?
  • 资源:处理输入时,体系耗费的时刻、内存等是否合理?
  • 特定范畴:例如,一个因紧缩而导致画质受损的图画在视觉上是否与未紧缩的图画共同?

咱们值得花时刻和精力树立强壮的预言,因为它们往往能够发现应用程序级的逻辑过错,而一般咱们经过查找数组越界等办法只能捕获初级的过错。

几年前,我撰写过一篇有关于该论题的文章(https://blog.regehr.org/archives/856)。有一个Twitter用户主张:“在测验解析器的时分,你有必要查看它回来的方针,而不仅仅是查看解析这个动作的履行。”这是一个很好的主张。

干涉I/O与状况

无状况代码更便利打开含糊测验。但除此之外,你还需求API来操控状况和干涉I/O。例如,假如程序需求从操作体系中获取中心数、当时日期或剩下磁盘空间量等信息,那么你应该供给一种设置这些值的办法并写在文档中。我并不是说,咱们需求随时改动中心数量,而是咱们或许在单核形式下针对代码打开含糊测验,然后还需求在128核形式下再进行一次含糊测验。有些操控状况和I/O的办法十分重要,比方简化重置状况(为了支撑耐久形式的含糊测验),并避免会导致非确定性履行的躲藏输入等。在针对代码进行含糊测验时,咱们期望具有尽或许多确实定性。

避免或操控含糊测验的阻挠

含糊测验的阻挠便是那些无法含糊化的东西。典型的含糊测验阻挠是包括在输入中某处的校验和:根据骤变的含糊器随机修正输入会导致校验和不合法,然后下降代码掩盖率。这个问题根本上有两种处理方案。第一种,在含糊测验构建中封闭校验和验证;第二种,确保含糊器能够生成带有合法校验和的输出。根据生成的含糊器自带这种功用;假如运用根据骤变的含糊器,那么咱们需求编写一个小东西,在生成测验用例后,咱们需求赶在测验用例传递给含糊测验程序前,给测验用例加上有用的校验和。alf支撑这种办法。

打开全文

除了校验和之外,难以满意的输入验证特点也是一个严峻的问题。例如,假如你要针对强类型编程言语的编译器进行含糊测验,那么盲目地修正编译器的输入很难取得有用的编译器输入。我喜爱将有用性的束缚分为软束缚(无效输入除了浪费时刻外,并没有其他坏处)和硬束缚(体系在处理无效输入时或许会暴走,因而有必要避免无效输入,不然彻底无法进行含糊测验)。假如咱们经过针对C++编译器打开含糊测验来寻觅过错代码中的bug,就会面对硬有用性束缚,因为会导致未定义行为的编译器输入看上去很像是代码中有bug。关于这类问题,咱们没有简略的通用处理方案,只能经过一系列技巧来考虑有用性特点。有一个最简略的处理方案(但往往不是正确的处理方案),那便是自己编写一个含糊器。但问题在于,假如自己编写含糊器,就无法再运用现代掩盖驱动的含糊测验技能——这种技能十分了不得的。为了匹配掩盖率驱动的含糊测验结构,你有以下几种挑选:首要,编写一个能够满意有用性束缚的自定义变异器;其次,选用了解结构的含糊,意思是说从含糊器中获取修正后的数据,并将其转换为含糊测验程序所需求的内容。关于怎么让掩盖驱动的含糊器在有用性束缚下仍然杰出地运转,且不需求许多的手动作业,咱们还有许多的研讨作业需求打开。这其间触及许多细节,改日再深化介绍。一般来说,在含糊器中参加相似SAT的求解器并不能处理这个问题,其原因首要是,有的有用性束缚(比方校验和)关于求解器来说难度特别大;其次是因为有些有用性束缚(如C++程序中的未定义行为)是隐含的,咱们无法从体系中揣度,乃至从原理上也不或许。

一般来说,你无法经过为公共API供给输入的办法,来针对体系的大部分代码进行含糊测验,因为这些拜访会被体系中的其他代码阻挠。例如,假如你运用自定义的内存分配器完成或自定义的哈希表完成,那么应用程序等级的含糊测验或许无法针对分配器或哈希表进行有用的含糊测验。这些API应该直接露出给含糊测验。单元测验和含糊测验有激烈的联络:假如其间一个可行且可取,那么另一个也应该差不多。一般你应该一同统筹两者。

一般,Sanitizer和含糊器需求对构建进程进行调整,乃至是严重的改动。为了简化这一进程,请尽或许简化构建进程。确保能够轻松切换编译器以及修正编译器选项。尽量削减对特定东西(以及东西版别)的依靠。定时运用多个编译器构建和测验代码。构建体系的特别依靠都需求记录下来。

最终,有些含糊测验的阻挠有点傻并且很简略避免。假如你的代码存在走漏内存的问题,或许过深的调用栈会导致进程停止,那么运用耐久形式的含糊测验是一件苦楚的工作,所以请尽量避免这些问题。尽量不要处理SIGSEGV信号,假如无法避免的话,那么应当有办法在含糊器构建中禁用相应的信号处理程序。假如你的代码无法与ASan或UBSan兼容,那么这些十分有用的预言就很难善加运用了。特别是,假如你的代码运用自定义的内存分配器,那么你应该考虑在含糊测验构建中将其封闭,或许经过调整后与ASan一同运用,不然就有或许漏掉严重bug。

为掩盖驱动的含糊器排除障碍

因为掩盖驱动的含糊器的首要精力放在了测验未掩盖的分支上,因而或许会被特别的办法阻挠。例如,假如掩盖驱动的含糊器遇到了太多未掩盖的分支,它就会在这些分支上花费许多时刻,导致掩盖程序其他分支的或许性下降。例如,有一次我比较了一个程序在运用和不运用UBSan两种状况下的afl掩盖率,成果发现(在我设定的时刻约束下)sanitized程序的掩盖率要比没有sanitized的程序低得多。但另一方面,咱们也期望含糊器能找到sanitizer的过错。我的主张是有sanitized和没有sanitized的程序都进行含糊测验。我不知道应该怎么为这些含糊测验活动分配资源,也不知道是否有人在研讨这个问题。或许这个问题并不重要,因为含糊测验原本便是过度测验。

有时分,你的程序在履行前期调用的代码会包括许多分支且会被过度含糊的代码。例如,或许你需求在处理输入之前对输入进行解紧缩或解密。这很或许会影响根据掩盖的含糊器,导致含糊器花费许多时刻去含糊测验加密库或紧缩库。假如你不是期望这样,那么就应该供给一种办法,在含糊测验进程中禁用加密或紧缩。

程序中的解说器都或许会给根据掩盖的含糊器形成困难,因为相关的程序履行途径是在解说器数据中编码的,而关于含糊器而言,一般状况下数据是不透明的。假如你想让根据掩盖的含糊器发挥最大效果,那么就应该考虑避免编写解说器,或许至少大力简化解说器。处理嵌入式解说器有一个很明好的办法(我信任必定有人尝试过,不过我不知道)便是供给一个API,告知含糊器该怎么调查被解说言语的掩盖率。

支撑高吞吐量的含糊测验

含糊测验关于高吞吐量的体系最有用,特别是关于根据反应的含糊器而言,这种含糊器需求必定时刻来学习怎样才能测验难以掩盖的方针。有关吞吐量的一个简略技巧是:供给禁用慢速代码(例如具体日志)的办法。相似地,干涉I/O能够让咱们不需求凭借运转速度技巧,比方在内存盘上运转含糊器等办法。

“但我期望含糊我的代码变得更难,而不是更简略”

我不太附和这个观念。咱们不应该等待含糊代码来确保安全,而应该选用以下办法:

  • 尽早、尽或许彻底地施行含糊测验,在将代码发布到外部之前消除含糊测验能够发现的缺点。
  • 经过人见人爱的强类型体系的编程言语编写代码——这能够静态地消除因为过错的编程习气形成的问题,例如能够避免咱们将过错的东西放入哈希映射。
  • 积极地运用断言和sanitizer来动态查看类型体系无法静态强制履行的特点。
  • 反含糊技能确实存在,但我不以为它代表软件朝着更好的方向打开。

总结

随机测验十分强壮,咱们理应善加运用:假如你不针对你的代码打开含糊测验,那么他人也会。本文向软件开发人员介绍了一些施行含糊测验的好办法。当然还有许多其他方面本文未能提及,比方挑选一个优异的语料库以及编写一个杰出的含糊驱动程序。

原文:https://blog.regehr.org/archives/1687