什么是 "模糊测试"?
Fuzzing 是一种发现软件缺陷的方法,它通过向程序提供随机输入来寻找导致程序崩溃的测试场景(原理有点类似Monkey Test)。可以帮助你快速了解程序整体的健壮性,并帮助你发现和修复关键的缺陷。
它是一种黑盒测试技术,不需要访问源代码,但它仍然可以用来测试那些有源代码的软件。这是因为它能更快地发现缺陷,并降低大量代码评审成本。
模糊测试优缺点
Fuzzing在某些业务下虽然非常有用,但它毕竟不是银弹。以下是模糊技术的一些优点和缺点。
优点
·可以说不费“吹灰之力”就能得到结果--一旦fuzzer启动并运行,它就可以在没有交互的情况下停留数小时、数天或数月来寻找错误。
· 可以发现人工审计中遗漏的错误
· 能对目标软件的健壮性提供一个整体性概述
缺点
· 不会穷尽所有bug--模糊测试可能会遗漏那些不会触发整个程序崩溃的bug,而且对那些只在非常特殊情况下触发的bug也难以覆盖。
· 产生的崩溃测试用例可能难以分析,因为模糊测试的行为并不能告诉你关于软件内部运行方式的知识。
· 具有复杂输入的程序可能需要更多的工作来产生一个足够聪明的模糊测试器,以获得足够的代码覆盖率。
Smart/Dumb模糊测试
Fuzzers向软件系统提供随机输入。内容形式可能是某种网络协议、某种格式的文件或用户能直接输入的数据。其输入方式是完全随机的,并不知道预期的输入应该是什么样子,也可以是经过一些修改后看起来像是有效的输入。
产生完全随机输入的Fuzzer被称为 Dumb Fuzzer。少量的工作可以用很少的成本产生结果--这是模糊测试的一大优势。然而,有时一个程序只有在输入的特定场景才会执行某些处理。例如,一个程序的输入需要传入 "name"字段,而这个字段有一个与之相关的 "name length"。
如果这些字段没有以足够有效的形式出现让程序识别,它可能永远不会读取这个名字。如果这些字段以有效的形式存在,但长度值被设置为不正确的值,程序可能会读到包含名字的缓冲区之外,并引发崩溃。如果缺乏有效的输入,这是不可能发生的。在这些情况下,可以使用 Smart Fuzzer,Smart Fuzzer是基于输入特定的规则实现的,例如协议定义或文件格式的规则。它可以构建大部分有效的输入,并且只对该基本格式内的输入进行模糊处理。
Fuzzers的类型
广义上讲,Fuzzer可以根据它们创建程序输入的方式分为两类--基于变异与基于生成.
基于变异的fuzzer
基于突变的Fuzzer可以说是最容易创建的类型之一。这种技术适合Dumb Fuzzer,但也可用于智能的Fuzzer。通过突变,有效输入的样本被随机突变以产生畸形的输入。
dumb mutation Fuzzer可以简单地选择一个有效的输入样本,并随机地改变它的一部分。因为输入通常仍然与有效输入有足够的相似性,所以这意味着不需要进一步的智能化处理就可以实现良好的代码覆盖。
下面是基于突变的Fuzzer可以使用的两种技术。
流量回放
Fuzzer可以采取保存的样本输入,并在突变后重新播放。这对文件格式的模糊处理很有效,可以保存一些样本文件并进行模糊处理以提供给目标程序。
简单或无状态的网络协议也可以用重放来有效地进行模糊处理,因为Fuzzer不需要提出大量的合法请求来深入到协议中去。对于更复杂的协议,重放可能更困难。这是因为Fuzzer需要以动态方式响应程序,以允许处理继续深入协议。
代理
你可能听说过中间人(MITM)是渗透测试者和黑客使用的一种技术,但它也可以用于基于突变的网络协议模糊测试。通过MITM,你置身于客户端和服务器的中间,截获并可能修改它们之间传递的信息。通过这种方式,你就像两者之间的一个代理。
通过将你的Fuzzer设置为代理,它可以根据你对服务器或客户端的模糊处理来改变请求或响应。同样,Fuzzer可以随机地改变一些请求,也可以在你感兴趣的协议的特定层次上智能地锁定请求。基于代理的模糊测试可以让你利用现有的网络程序部署架构,快速插入模糊测试层,而不需要让你的fuzzer像客户端或服务器本身一样行动。
基于生成的fuzzer
基于生成的Fuzzer实际上是从零开始生成输入,而不是突变现有的输入。它们通常需要一定程度的智能来构建至少对程序有一定意义的输入,尽管生成完全随机的数据在技术上也是生成。
生成Fuzzer通常将协议或文件格式分成几块,它们可以按照有效的顺序建立起来,并随机地对其中一些块进行独立模糊。这可以创造出保留其整体结构的输入,但其中也包含不一致的数据。这些块的颗粒度和构建这些块的智能程度决定了Fuzzer的智能水平。虽然基于突变的模糊处理可以产生与生成模糊处理类似的效果(因为随着时间的推移,突变将被随机应用,而不会完全破坏输入的结构),但生成输入可以确保这种情况的发生。
生成fuzzers也可以更容易地深入到协议中,因为它可以构建有效的输入序列,对该通信的特定部分进行模糊处理。它还允许Fuzzer作为一个真正的客户/服务器,生成正确的、动态的响应,但这些响应不能盲目地重放。
进化型fuzzer
进化型模糊测试是一种先进的技术。它允许Fuzzer使用来自每个测试用例的反馈,以了解随着时间推移输入的格式。例如,通过测量每个测试用例的代码覆盖率,Fuzzer可以计算出测试用例的哪些属性可以锻炼给定的代码区域,并逐渐演化出一套覆盖大部分程序代码的测试用例。进化型模糊测试通常依赖于与遗传算法类似的其他技术,并且可能需要某种形式的二进制工具来正确操作。
模糊测试测什么?
即使是相对dumb的模糊测试,也要记住你的测试用例实际上有可能击中代码的哪一部分。举个简单的例子,如果你正在摸索一个使用TCP/IP的应用协议,而你的Fuzzers随机突变了一个原始数据包的捕获,你很可能会破坏TCP/IP数据包本身。因此,你的输入根本不可能被应用程序所处理。再者,如果你正在测试一个将文本的图像解析为真实文本的OCR程序,但你正在突变整个图像文件,你最终可能会更频繁地测试其图像解析代码而不是实际的OCR代码。如果你想专门针对该OCR处理,你可能希望保持图像文件的标题有效。
Fuzzer运行流程
为了有效地运行,Fuzzer需要执行以下重要任务:
·生成测试用例
· 记录测试用例或再现用例所需的任何信息
· 对目标程序接口提供测试case作为输入
· 检测崩溃
Fuzzer通常将其中的许多任务分成独立的模块。例如,有一个库可以突变数据或根据定义生成数据,另一个库可以向目标程序提供测试用例等等。
生成测试用例
测试用例的生成将取决于是否采用了基于突变或基于生成的模糊处理。无论采用哪种方法,都会有一些需要随机转换的东西,无论是特定类型的字段还是任意的数据块。
这些转换可以是完全随机的,但值得注意的是,边界和极端的情况往往是程序中错误的来源。因此,你可能希望偏向于这样的情况。
· 非常长超长字符串或Null
· 能支持的最大值和最小值整数
· 像-1、0、1和2这样的值
根据你要模糊处理的内容,可能会有一些特定的值或字符更容易触发bug。比如:
· Null
· 分号
· 格式化字符串值(%n,%s等等)
· 应用特定的关键词
可重复性
重现一个测试用例的最简单方法是记录检测到崩溃时使用的确切输入。在某些情况下,还有其他方法能实现重现性。一种方法是存储用于测试用例生成的随机部分的初始种子,并确保所有后续的随机行为遵循一个可以追溯到该种子的路径。通过用相同的种子重新运行Fuzzer,行为应该是可重复的。例如,你可以只记录测试用例编号和初始种子,然后用该种子快速重新执行生成,直到达到给定的测试用例。
当目标程序可能基于过去的输入积累了依赖性时,这种技术就很有用。以前的输入可能导致程序在其内存中初始化各种项目,而这些项目是触发错误所必须的。在这些情况下,简单地记录崩溃的测试用例并不足以重现该错误。
与目标程序对接
与目标程序连接以提供模糊输入通常是简单的。对于网络协议,它可能涉及在网络上发送测试用例,或响应客户的请求。对于文件格式,它可能意味着用一个指向测试用例的命令行参数来执行程序。然而,有时提供的输入的形式不容易以自动化的方式生成,或者编写程序脚本来执行每个测试用例的开销很大,证明是非常缓慢的。在这些情况下,创造性的思考可以发现用正确的数据来锻炼相关的代码片断的方法。
例如,这可以通过在内存中人为地设置程序来执行解析功能,而输入的参数完全在内存中。这可以消除程序在每个测试用例之前经过冗长的加载程序的需要。而且,通过让测试用例完全在内存中生成和提供,而不是通过硬盘驱动器,可以进一步提高速度。
崩溃检测
崩溃检测是模糊测试的关键。如果你不能准确地确定一个程序何时崩溃,你就不能确定一个测试用例是否触发了一个错误。
· 附加一个调试器
这可以为你提供最准确的结果,你可以编写调试器的脚本,以便在检测到崩溃时立即为你提供崩溃跟踪。然而,附加一个调试器会大大降低程序的速度,并会造成相当大的开销。在给定的时间内,你能产生的测试用例越少,你发现崩溃的机会就越少。
· 看看目标进程是否消失了
与其附加一个调试器,你可以简单地看看在执行测试用例后,目标的进程ID是否仍然存在于系统中。如果进程消失了,它可能已经崩溃了。如果你想了解更多关于崩溃的信息,你可以在以后用调试器重新运行测试用例。你甚至可以在每次崩溃时自动这样做,同时还可以避免在每个案例中都连接调试器而导致的速度下降。
超时
如果程序对你的测试用例有正常的响应,你可以设置一个超时,超时后你就认为程序已经崩溃。这也可以检测出导致程序无反应但不一定终止的错误。
无论你使用哪种方法,只要程序崩溃或变得无反应,就应该重新启动,以便让模糊测试继续进行。
模糊测试的质量
你可以做一些事情来衡量或提高你的模糊测试的质量。虽然这些都是需要记住的有用的东西,但如果你已经在一定的时间范围内得到了很多独特的崩溃,你可能不需要再为这些事情费心了。
速度
速度可能是模糊测试中最重要的因素之一。你每秒钟/每分钟能运行多少个测试用例?合理的数值当然取决于目标,但你能执行的测试用例越多,你就越有可能在给定的时间段内发现崩溃。模糊测试是随机的,所以每一个测试用例就像一张彩票,你要尽可能多地得到它们。
你可以做很多事情来提高测试用例的速度,比如提高生成或变异例程的效率,并行化测试用例,减少超时,或在不显示图形用户界面的 "无头 "模式下运行程序。如果你想的话,你可以简单地购买更快的套件。
对崩溃进行归类
找到崩溃只是过程的开始。一旦你找到一个崩溃的测试用例,你就需要分析它,找出错误所在,并根据你的动机,修复它或为它编写一个漏洞。如果你有成千上万个崩溃的测试用例,这可能是相当令人生畏的。通过对崩溃进行分类,你可以根据哪些崩溃是你最感兴趣的来确定它们的优先次序。这也可以帮助你识别一个测试用例何时触发了与另一个相同的错误,所以你只保留与独特崩溃有关的案例。
为了做到这一点,你需要一些关于崩溃的自动信息,以便你能做出决定。在目标机上运行测试用例并连接到调试器,可以提供崩溃跟踪,你可以对其进行分析,找到诸如异常类型、寄存器值、堆栈内容等值。
减少测试用例
由于模糊测试是随机改变输入的,一个崩溃的测试用例通常会有多个与触发该错误无关的改变。测试用例缩减是将测试用例缩减到触发bug所需的有效输入的最小改动集,因此你只需要在分析中关注输入的这一部分。
这种减少可以手动进行,但也可以由Fuzzer自动进行。当遇到一个崩溃的测试用例时,Fuzzer可以重新执行该测试用例几次。每一次,它都会逐渐减少对输入的改动,直到剩下最小的一组改动,同时仍然触发该错误。这可以简化你的分析,并有助于对崩溃的测试用例进行分类,因为你会准确知道输入的哪些部分受到影响。
代码覆盖率
这是一个衡量程序的代码有多少被Fuzzer执行的标准。其原理是,你得到的覆盖率越多,你实际测试的程序就越多。测量代码覆盖率可能很棘手,通常需要二进制仪器来跟踪代码的哪些部分正在被执行。你也可以用不同的方式测量代码覆盖率,比如按行、按基本块、按分支或按代码路径。
代码覆盖率对于模糊测试来说并不是一个完美的衡量标准,因为有可能在执行代码的同时并没有发现其中的漏洞。而且,经常有一些代码区域几乎不会被执行,例如安全错误检查,反正我们不太可能真正需要,也不太感兴趣。尽管如此,某种形式的代码覆盖率测量可以让我们了解到你的Fuzzer在程序中触发了什么。特别是当你的模糊测试是完全黑箱的时候,你可能还不太了解程序的内部运作。一些可能有助于代码覆盖率的工具和技术包括Pai Mei、Valgrind、DynamoRIO和DTrace。
模糊测试框架
目前市面上有许多框架可以让你创建Fuzzer,而不必从头开始造。下面列出了这些框架:
Radamsa
Radamsa被设计为易于使用和灵活。它试图对各种输入类型进行 "公正的工作",并包含一些不同的模糊算法进行突变。
Sulley
Sulley提供了一个全面的生成框架,允许结构化数据被表示为基于生成的模糊处理。它还包含帮助记录测试用例和检测崩溃的组件。
Peach
Peach框架可以对文件格式和网络协议进行智能模糊测试。它可以执行基于生成和突变的模糊测试,并包含帮助建立模型和监控目标的组件。
SPIKE
SPIKE是一个网络协议Fuzzer。它需要用户熟悉掌握C语言,并被设计为在Linux上运行。
Grinder
Grinder是一个网络浏览器Fuzzer,它还具有帮助管理大量崩溃的功能。
NodeFuzz
NodeFuzz是一个基于node.js的网络浏览器线束,它包括仪器模块,可以从客户端获得更多信息。
AFL
AFL是一个灰盒式的模糊测试工具,利用编译在目标代码中的仪器。AFL最初是为Linux中的C和C++程序编写的,后来被分叉以支持Windows、Java和.Net。
感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取