摘要:

模糊测试是一种高度自动化的测试技术,它使用无效数据(来自文件,网络协议,API调用和其他目标)作为应用程序输入来覆盖大量边界情况,以更好地确保不存在可利用的漏洞。模糊测试让开发人员或质量保证(QA)团队在使用功能测试等技术进行测试时测试大量边界情况成本过高。综合的负面测试案例( 那些验证产品不做它不应该做的事情,而不是它做了它应该做的事情(正测试案例)) 很难构建,因为可能的排列数量是天文数字。然而,模糊测试覆盖了大部分负面测试用例,而没有强迫测试人员针对给定的边界条件处理每个特定的测试用例。

介绍

例如,如果输入包含一个4字节有符号整数,应该在1到10之间,边界情况将包括0,-1,11,12,大负数和字节边界周围的情况(2^8,2^16,2^24,2^31)。单独编码这些情况很困难,考虑到整数是一种相对受限的输入形式(字符串具有更多的排列),这实际上是一组最好的测试用例。边界条件很重要,因为边界条件故障的重要子集是安全故障。因此,我们今天未测试的边界条件是我们明天必须发布的安全补丁。

fuzzer工具生成半有效数据(数据足够正确,以防止解析器立即解除它,但仍然无效导致问题),将其发送到目标应用程序进行处理,然后观察应用程序以查看它是否因为它失败消耗数据。如果是这样,该工具会保存提交的数据以供以后分析,并提交新的格式错误的数据。如果应用程序没有失败,该工具会选择是否删除格式错误的数据并继续执行下一步操作。手动执行这些步骤,我们只能执行几百次或几千次迭代。然而,通过自动化整个周期,模糊器工具可以执行数十万或数百万次这样的迭代,覆盖了大量有趣的排列,难以编写单独的测试用例。

在编写从不受信任的来源获取输入的安全应用程序时,开发人员必须针对各种边界条件测试输入解析器。模糊测试可以使这个过程变得更加容易,并为分配给测试的时间提供最佳结果,有助于发现数据解析中可能无法被忽视的问题。

方法论

模糊测试涉及编写生成“半有效数据”的工具,将其提交给应用程序,并确定应用程序是否失败。出于本文的目的,我将仅考虑模糊化的自动化方法。下图显示了模糊测试工具所经历的不同高级状态。

此处输入图片的描述

一个完整的模糊迭代,从一第一次迭代开始。模糊器首先通过用于测试的两种主要方法之一来获取“半有效数据”:生成或突变。然后,模糊器提交数据并跟踪错误输入是否导致应用程序崩溃(在这种情况下,它会保存数据以供以后分析)。如果不是,则模糊器自动进入下一次迭代。

信任边界是数据或执行从一个信任级别转移到另一个信任级别的任何位置,其中信任级别是资源的一组权限。例如,从操作系统中的用户模式转换到内核跨越信任边界,因为内核被信任对处理器做任何事情,而用户模式仅允许处理器操作的子集。类似地,网络和机器之间存在信任边界,因为网络上的任何人都可以修改网络数据,而机器上只有人可以修改机器数据。不同用户上下文之间的交叉呈现另一个信任边界。查看信任边界的另一种方法是将漏洞导致特权提升的位置。在决定应用程序中的模糊内容时,我们必须考虑信任边界;它们有助于确定要查看哪些输入的优先顺序以及执行此操作的顺序。

应用程序通常具有多个输入,但如果产品团队在威胁建模方面做得很好,则威胁模型将包含所有这些的详细列表。在大多数系统中,大多数输入来自文件,配置和注册表项,APls,用户界面,网络接口,数据库条目和命令行参数。因此,这些输入是模糊测试的主要目标,但系统接收输入的任何地方都是提交格式错误数据的候选者。

下一步是优先考虑哪些输入到模糊,这可能是一个棘手的过程。例如,我最近审查了一个带有网络接口的Windows服务,该接口从仅管理员身份验证的连接中获取数据。它还读取了一个注册表项,用户可以通过该注册表项指定哪些文件夹对搜索很重要。开发团队认为模糊网络接口很重要,因为组件是面向网络的。相反,我建议他们专注于注册表项,因为任何经过身份验证的用户都可以写入注册表项。此系统输入跨越信任边界,表示从任何用户到服务帐户的特权提升。网络接口只能由已经是管理员的用户攻击,因为它没有跨越信任边界;相反,软件中的漏洞意味着任何可以登录到计算机或远程访问注册表的人都可以写入允许他们升级到服务帐户的注册表设置。

一旦确定哪个条目指向模糊,就可以使用许多不同的技术,但模糊器首先需要一个格式错误的数据源。请注意,如果所有提交的数据都格式不正确,应用程序将在解析第一个无效数据块后丢弃输入,并且不会测试任何其他代码。因此,使用大多数有效但包括一些无效或“半有效数据”至关重要获取此数据有两种主要方式:数据生成和数据突变。

模糊器可以根据其外观规格生成数据。数据描述可以像“it’s an int.”一样简单。实际描述将取决于您的应用程序所使用的语言,但它不应该对您的特定应用程序不明确。 (我将忽略特殊的复杂情况,例如网络应用程序,可能需要根据整数是从网络还是本地机器写入或读取整数重新排序;在大多数情况下,整数是最简单的情况。)或者,描述可能与XML文档一样复杂,它描述了任意复杂的嵌套二进制数据结构的各个偏移量和数据结构。当然,这会使实现模糊器变得更加困难,因为我们首先必须了解格式的细节,然后创建XML模式以准确描述格式,最后编写模糊器来解析XML模式和XML文档以生成“半有效数据

获取格式错误数据的第二种方法是从一组已知的良好数据开始,并在特定位置进行变更。 HTML是复杂文件格式的一个很好的例子,很难创建一个可以覆盖整个规范的生成器。模糊器不是从HTML格式规范生成代码,而是可以使用有效的示例或模板文件,在几个关键区域中复制和修改它,并将其提交到目标应用程序 - Web服务器或浏览器。创建新的测试用例就像从现有文件的可用源(在本例中为Internet)中收集模板文件一样简单

当模糊器或运行它的人可以轻松获得良好的数据源,用于配置和注册表设置,用户界面,命令行界面和数据库界面时变异通常是更简单的方法在其他情况下,获取输入的良好副本可能比简单地从头开始生成(例如使用APls)更困难。格式也可能对格式错误的数据高度敏感。例如,网络协议通常是严格定义的,因此过多的变化,特别是在关键控制字段中,可以在代码路径的早期终止解析,可能在应用程序看到它之前在网络堆栈中。在这种情况下,突变最终会做更多的工作,因为解析好的数据以确定可以改变模糊的内容是很困难的。

智能模糊器 VS 非智能模糊器

在编写模糊器时,您经常会发现模式可以为成本带来最大的好处。基于模式的模糊测试的结果根据数据格式的复杂性以及诸如校验和或众多相对参考字段的结构和技术的使用而变化。尽管如此,基于模式的模糊器通常比智能模糊器花费更少,同时提供类似口径的结果。

基于模式的模糊器查找特定的数据模式,然后在找到它们时执行一些数据修改。例如,对应于二进制代码块中可打印字符范围(0×20-0×7F)中的ASCII数据的n个连续字节值的模式可以指示字符串。类似地,在ASCII范围内的值和零之间交替的字节值可能表示unicode数据。在识别出这种模式之后,模糊器可以采取一些有趣的操作,例如将额外的有效字符串数据插入块中以试图找到缓冲区溢出。正如我所提到的,从标识的字符串中删除尾随的空终止字符是另一种有用的模糊测试技术,因为解析器有时会期望这个字符并在它不存在时失败。字符串数据结构通常还会在字符串前面加上长度,这意味着修改字符串前面的值可能会暴露出有趣的错误。例如,缓冲区溢出可能会发生,因为解析器依赖于前置长度来分配缓冲区,而空终止符可以复制数据。

文本格式提供与字符串数据类似的目标。不同之处在于,基于文本格式的所有数据在某些时候都是字符串数据。对字符串数据进行标记并基于标记对其进行操作可以因此产生一些有趣的结果。例如,更改顺序,插入重复项以及修改标记可能会导致解析器失败。此外,解析器识别分隔符并使用它们将数据分成令牌。在这些标记的中间或它们之间插入分隔符也可以很好地进行模糊测试。对于基于文本的格式,使用空字符,空格,CRLF,CR,LF和编码序列可能会暴露解析器关于这些应该在何处的可能不准确的假设。

整数是二进制数据的一个很好的目标,它通常很大程度上依赖于它们来指定任意结构的大小和数量。简单地用特殊值替换整数值通常会导致解析器以奇怪和奇妙的方式崩溃。将所有Os(0×00,0×0000 0×00000000等)或全1(0×FF,0×FFFF,0×FFFFFFFF等)插入二进制块可以有效识别整数溢出整数用于在解析期间分配内存或索引缓冲区。在当前值中添加或减去一些小值也可以定位问题,因为二进制结构通常由某种类型标识,可能是具有特定小范围的枚举。例如,Microsoft SQL Server的表格数据流(TDS)网络协议使用偏移量为4的字节,该字节必须在0-5范围内,以表示TDS数据包类型。对这个数字的一个小的改变导致了一个不同的解析代码路径,它可能期望一个类似但不同的格式,而一个大的改变可能会通过范围检查抛出这种情况。翻转整数值的最高位使其为负(按0或0×80,0×8000等按位)有助于识别有符号/无符号不匹配错误。最后,通过交换数据流中的不同块来移动数据也可以发现错误。

FUZZ 中的一般问题

构建模糊器时,必须考虑几个常见问题。有些你可以通过仔细检查你模糊的格式来预测,但是在你开始开发模糊器后你会发现其他的。通过易于理解的格式,您可以确定是否必须处理以下任何验证问题,这可能会妨碍模糊器的有效性。

许多格式和协议执行各种类型的验证。例如,网络协议和文件格式通常使用散列和校验和来帮助验证数据包和文件内容的完整性。当然,这些机制为模糊测试提供了障碍,因为我们需要能够为测试目的更改内容。解决方案是在模糊器中提供额外的逻辑,以便在内容突变或生成后重新计算正确的哈希值。

加密的哈希和数字签名甚至更成问题,因为它们的设计目的不仅是验证数据内容自源发送以来没有改变,而且源还具有一些已知的身份。在这些情况下,模糊器还需要知道数字签名算法和用于签名的私钥,以便它可以假装它是源方。

对于数字签名或加密数据,仅当加密或签名数据跨越信任边界时,输入才代表重大威胁。例如,加密(并因此签名)的电子邮件对模糊很重要,因为即使发件人使用公钥进行身份验证,阅读电子邮件的用户也可能不会认为发件人可信。加密和签名数据跨越发送者和接收者之间的信任边界,其信任级别不一定相同。

另一方面,补丁是通常不跨越信任边界的数据的示例。如果软件应用程序在补丁上验证软件公司的数字签名,则考虑恶意输入可能并不重要,因为公司始终可以提供良好的数据。当然,如果威胁是软件公司的流氓员工可能产生恶意补丁,那么模糊测试将再次变得重要。这个题外话说明了威胁建模的价值:一旦你理解了需要防范的威胁,通过对输入进行模糊测试就可以更轻松地缓解它们。

通常,加密格式不适合模糊测试。如果模糊器执行除随机位翻转之外的任何操作,则其模式识别器或解析器将无法解析数据。这里的关键是确保模糊器在变异和重新加密之前具有解密数据的附加功能;在生成数据时,它需要能够在提交到应用程序之前进行加密。使用压缩的格式存在几乎相同的问题,解决方案是相同的:在模糊器中提供解压缩和压缩功能。

当然,这不是一个在开发模糊器时可能遇到的问题的综合列表。一旦有了工作工具,就可以通过代码覆盖率分析识别其他问题。通过在监视代码覆盖率的同时对应用程序运行模糊器,您可以快速确定是否在代码中的同一位置拒绝了所有周期。通过分析代码的特定部分,您可以确定如何让模糊器继续超过该点(至少在某些时候)

应用行为

将模糊数据提交到目标应用程序时实际发生的情况在很大程度上取决于您的模糊测试。如果您正在测试网络堆栈,则数据会通过网络传输到应用程序,但是一旦数据到达,就很难判断数据是否具有所需的效果。确定应用程序何时失败比证明成功何时更容易。

在模糊测试中,最好先寻找真正错误的东西。例如,Windows操作系统使用异常处理将故障情况通知给应用程序和操作系统的其他部分。调试器可以看到这些异常,因此将其构建到模糊器中可以确定应用程序何时崩溃

在模糊测试期间检查应用程序正确性的其他方法包括查找程序内存使用量或CPU利用率的峰值,这可能表明应用程序正在使用格式错误的输入来计算内存分配。如果程序对输入值执行算术运算,它还可以指示整数溢出条件。这可能会导致可利用的缓冲区溢出情况。类似地,CPU峰值通常意味着程序正在使用一个没有正确限制的密集算法 - 可能就像一个循环一样简单,在该循环中,用于确定要执行多少循环的变量来自恶意输入。至少,这表明拒绝服务,如果不是更危险的事情。

从这些简单的故障模型中,我们可以设想更复杂和完整的故障和成功模型,这些模型实际检查系统以确保在解析格式错误的数据后它正常工作。我建议在模糊器中使用可扩展模式来实现对解析格式错误数据的应用程序成功或失败的检查。这是真的,因为成功和失败的构成将在模糊器的生命周期中发生变化。在创建软件时,开发人员必须认真考虑如何覆盖所有边界条件,特别关注那些可能导致安全漏洞的情况。我在这里提出的想法应该提供一些关于如何建立模糊器以及从模糊测试过程中得到什么的理解。

个人总结

看了一下这篇文章主要讲述的是比较基础的 模糊测试的流程和方法,即通过生成和突变获取到半有效数据(数据足够正确,以防止解析器立即丢弃它,但仍然无效导致问题),然后输入系统进行测试,根据返回的结果是否出错判断是否需要保存测试结果或者进行下一次迭代。

还讲到了什么地方需要我们进行重点的关注和测试,比如涉及系统边界条件,边界条件的漏洞很有可能导致特权提升的问题,所以在很多情况下我们要有限检测这些位置

数据生成:根据外观生成数据
数据突变:在正确数据基础上进行修改和替换

讲到了模糊测试中遇到的困难,比如加密和签名校验等

最后是如何判断测试结果,比如观察返回数据,观察计算机 CPU 的占用率(其实就是观察不同输入对计算结果产生的差异)

原文链接

Violating assumptions with fuzzing