模糊测试简介
模糊测试(Fuzzing),简而言之,就是为了触发新的或不可预见的代码执行路径或bug而在程序中插入异常的、非预期的、甚至是随机的输入。因为模糊测试涉及到为目标提供大量的测试样例,因此至少也会实现部分自动化。
模糊测试可以也应当用于测试每个需要接受某种形式输入的接口。实际上,模糊测试最起码就应该拿来用于测试每个从潜在恶意来源(比如互联网或用户提供的文件)获取输入的接口。
模糊测试是对其他测试技术的补充。由模糊测试揭露出的问题往往是开发人员不太可能构建的输入(例如,在处理一些边界情况,数据正确性和错误处理例程时的输入)触发的。在常规自动化测试过程中,模糊测试扩大了代码覆盖范围,提高了代码覆盖率测试程度。通过模糊测试使用的非预期输入通常会触发一些平时不会触发的执行流。
很多地方都需要进行模糊测试。它是你系统开发生命周期(SDLC)的一部分,在这部分里,你需要确保你完成了改善目标所要的系统性工作,或是你只是想解决一些bug也行。要如何费心于模糊测试取决于你的最终目标和相关资源,本文只是帮你如何从模糊测试中获取更多的回报。
开始之前
也许到现在你已经跃跃欲试了。很多组织和个体经常急于根据博客文章中的思路或会议上看到的酷炫演示来进行一次模糊测试,这虽不一定是坏事,但我们经常可以看到在模糊测试系统的背后有着大量的工作投入,这些模糊测试系统仅在作者分配去完成其他任务之前有在使用,稍加改动就会破坏兼容性。
更糟糕的是这些模糊测试系统长年消耗硬件资源,却经常没能得出什么结果来。就如同软件开发项目的其他任何部分一样,测试自动化和模糊测试与否,都需要一定的规划,维护和提交。
你想拿fuzz干什么?
这是一个简单的问题,但是答案却并不一定如你所想那样显而易见。如果你已经有了一个目标,那很不错。如果没有,那你就得去找一个接受输入的接口。接口可以是对外的,像是网络连接,可以是一些文件。当然也完全可以是对内的,像是一个你代码正在使用的实用程序库(utility)里的函数调用约定。模糊测试就是为你所选择的接口创建输入,并观察这些接口如何处理这些极端的输入。你可以通过威胁建模(Threat modelling)和回执数据流图来发现目标所拥有的潜在接口。
在每个接口背后可以有许多软件层,选择对哪个层进行模糊测试就显得至关重要,因为输入要到达那个层,就需要通过前面各层的所有检查。
举个例子,我们来看一个接收带签名二进制数据的HTTP服务器。我们有一个含JSON字符串的二进制数据,字符串里是我们应用程序要用到的值。在这个例子里,我们就有4个潜在的层需要进行模糊测试:
服务器接收的HTTP消息
二进制数据的签名校验
JSON字符串解析
我们处理实际值的代码
暂且假定我们的HTTP,签名和JSON库都是鲁棒的(我们并不想以这些库为目标)。要对我们自己的代码进行模糊测试,我们就需要生成这些实际值, 然后将这些实际值包装为JSON字符串,对二进制数据签名,创建一个HTTP消息并将其发送给目标。除非我们已经有了可以复用的自动化测试代码,否则单独构建这些测试样例需要相当长的时间。在堆栈中进行模糊测试也会不断带来开销,并且在更改某些层时也更容易被破坏。
在模糊测试中,测试样例的吞吐量也相当关键。你应当考虑下目标是否有一些可禁用或绕过的功能,以减少开销并扩大模糊测试覆盖范围。通常我们实现一个直接使用模糊值调用目标代码的小程序可以带来不少好处。
在上面这个例子里,写一个直接将值传递给我们处理代码的程序,就可以绕过发送网络消息,好几次哈希计算、加密检查、JSON转字符串以及解析这些步骤。
在一些优化更好的模糊测试环境里,诸如不必要的日志记录、CRC校验、文件I/O以及远程资源调用等功能都会在一个更适合模糊测试的模式("fuzz-friendly mode")下禁用。
我们可以用一些ifdef,创建虚拟(Mock)函数或其他仅用于构建模糊测试的配置来实现一个对模糊测试友好的模式("fuzz-friendly mode")。当然,当你在进行一些会改变目标行为的模糊测试优化时,你必须能确保这些修改不会创建或隐含任何的bug。
不过,在刚开始时,不要太担心想着要一个高效的每秒将数千个测试用例注入进优化的模糊测试环境中去的策略。开始模糊测试的一个非常有效的办法就是将随机(或位翻转)的数据发送到你找到的任何接口去。如果这能很快地找到问题,那么你就算是找到了你第一个目标接口了!
你想找寻的是什么?
很多时候当你进行模糊测试,目标可能会崩溃,这是很难避免的。然而,为了能充分利用你的劳动,你就还需要找到其他的错误情况。目标都有它自己的功能需求,需求里定义了程序应该干什么,你可以从跟这个点找到它不应该做的事情。
除此之外,所有程序都可能存在逻辑缺陷,可能导致内存泄露或CPU及内存消耗过多等问题。根据底层技术,目标也可能容易发生内存腐败,命令注入或其他应当注意的问题类别。
起初,所有可能的潜在问题类型及其影响都应该记录下来。现有的检测工具和技术可以适用于不同的问题类型,但有些检测工具和技术使用起来相当复杂,或是执行开销高昂。
影响评估有助于你判断使用工具或某技术是否值得。例如,图像压缩中颜色值的错误计算可能影响很小,但却难以检测。如果你只是想找到这些问题,那么一些能使用模糊的和无效的身份绕过验证的地方十分致命,也相当容易被检测到。
在研究不同的工具和技术时,还要考虑其他的自动化测试方法。例如,在很多情况下,你会发现你的单元测试(unit test)一次又一次触发了一些错误,但你可能因为没有用到单元测试而无法发觉。
如何进行模糊测试?
模糊测试是一项一人一机器就能执行的技术。中等规模的模糊测试可以作为持续集成(CI)系统的一部分来执行,针对不同的项目每天运行几次模糊测试。大规模的模糊测试可以通过使用数百上千台机器在云端并行自动地模糊测试。所有这些环境都有着最终系统必须满足的不同需求。因为最初基本不会考虑到与另一些部件的可用性,所以通常情况下不会使用大型fuzzer。
和所有的测试相同,测试规模越大,自动化就越重要。使用单个实例来fuzz你的程序非常简单。你可用不断地将模糊输入注入到目标程序中,直到触发bug,然后修复bug,如此不断重复即可。但当你同时处理成百上千个实例时,你就会知道为什么重复筛选等功能相当重要了。在一个在CI中针对不同构建版本并行运行模糊测试的大型组织中,你也可能会忽视自动问题报告,最小化测试用例和补丁验证这些需要注意的问题。
准备开始
在这里,你应该对模糊测试所需的三个部件建立一个粗略的需求规格说明:测试用例生成方案,测试用例注入方法和装置。现在你终于可以开始真正的工作了。
互联网里有许多开源或商业性质的模糊测试解答方案。有些仅仅实现了测试用例生成,有些则结合了测试用例生成和注入,还有一些则具备完整的含有装置和自动化的堆栈。一般而言,商业产品可用性更强,并且通常可以为大部分测试用例提供完整的解答方案。
特别是对于希望快速开始对多个产品进行模糊测试的组织而言,商业解决方案是真正的选择。而对于因为乐趣和利润,想解决bug的个人,商业解决方案通常会超出预算。
无论你是决定使用已有的解决方案或是自己实现一个,都总会遇上一些问题。
Fuzzer灵活性
特别当你的最终目标是能对多个不同目标使用同一个工具解决时,务必要确保你要使用的解决方案足够灵活,以涵盖所有的样例。如果整个系统必须进行重构,或者最坏可能需要构建另一个系统,那么对目标模糊测试会浪费大量的时间。
不同的工具也会揭示出不同的问题,以长远眼光来看,总会有新的工具再次揭露出新的问题。所以组件切换,特别是装置切换,是一个十分有价值的功能。
处理结果
如果你正在建立一个模糊测试系统,但你并不是实际发现问题解决问题的开发人员之一,那么请联系那些正准备处理你系统得出结果的人员。他们想从模糊测试的的bug报告中得到什么呀的信息呢? 如果开发人员每天早上在收件箱里一眼看到满满的像下面这样的bug报告,那他们是真的很少意识到自己想要的信息:
标题: 程序X出现了崩溃
描述: 附件里的数据使得程序X崩溃了
附件:fuzz-test-case-1337 (22MB)
默认情况下, 错误报告至少应该包含重现问题的全部信息。例如这样但不限于这样:配置信息,使用的操作系统,目标的版本或构建版本号,CPU和内存信息,以及适用的编译器选项和调试标志。
模糊测试中使用的设置对于开发人员来说应该要很容易重现才行,并且你应该为每次对目标模糊测试所做的优化进行解释。举个例子,开发人员可能并不想解决只有在CRC校验被关闭的情形下才能重现的问题,除非你能解释清楚,当启用CRC校验时如何构造输入也能重现bug。
自动化模糊测试还包括有:相似问题分类,测试用例最小化、回归范围查找、修复验证、甚至可以提供像容器、虚拟机、映像一样配好的测试环境。
跟踪进展
在长时间运行模糊测试之后,你可能没有发现任何新错误,这表明可能是如下两种情况之一:
你的模糊测试工作非常出色, 目标的鲁棒性正在提高.
或者你的模糊测试卡住了, 一次又一次地重复相同的代码路径.
正如模糊测试简介那一节所述,模糊测试需要不断的维护和提交,以保持长时间有效。你可以使用一些技术来帮助你确保你的模糊测试始终保持有效,并且还能对目标发生的一些变化进行测试。
如果你正在使用基于代码覆盖率的fuzzer,你也许已经覆盖到了该覆盖的范围。只要你的代码覆盖率在模糊测试过程中持续上升,就无需担心。但如果你的代码覆盖率不再上升了,那你可能就遇到了一些需要更加深入分析的问题。
你不能仅根据现已覆盖的代码行数获知多少信息。例如,目标可能有的代码行,没有特别配置是无法执行的。或是可能存在无法到达的代码,使得无法完全覆盖等等情况。使用工具来显示运行测试样例时,哪些部分的代码有执行,哪些没有执行是相当有帮助的。将这次的结果与之前fuzzer跑出的结果,或其他自动化测试的结果进行比对,并检查之前出现bug的位置,可以帮助确保你的模糊测试效果不会倒退,并且依旧涵盖着所有相关的代码路径。
如果还是少了一些未经过的代码路径,那么下一步就是分析如何让你的测试用例生成器生成能触发这些路径的测试数据了。特别是对于那些基于模型的fuzzer,你经常会发现说fuzzer并没有拿到模型实现所需要的信息或字段。变异测试的模糊器缺少代码覆盖率的原因通常都是因为初始样本文件覆盖率过低,或是对输入的验证过于严格所导致的。对于后一种情况,请考虑"fuzz-friendly mode"
没有代码覆盖反馈,事情就会变得有些棘手。如果你已经实现了"fuzz-friendly mode",那么你有一个简单的解决方案:那就是制造bug。在你fuzzer应该到达的位置添加适当的print函数,assert断言或aborts函数,并根据实际这些位置的到达情况进行跟踪。你也可以将类似的"bug"添加到之前存在bug的位置。
只要记住,你进行检查的时候不应影响到你的fuzzer,使得你的fuzzer过于注重你刚刚添加"bug"的那部分代码,并且在最后的投入使用之前将这些检查移除掉。你可以自动化测试可以使用旧的构建版本进行测试,那你还可以用那些有已知bug的版本来验证你的fuzzer能否找到这些已知bug,测试那些已发现的旧bug同样也是一个找到系统中有待提高之处的好办法。
本文由看雪翻译小组 Vancir 编译,来源github@attekett ,转载请注明来自看雪社区
更多资讯请关注看雪学院公众号(ikanxue)查看!