模糊测试技术入门,Part 1:理论概述
首先,到底什么是模糊测试?当我们对一个接收输入(任何类型的输入)的程序或函数进行模糊测试时,我们会尝试不同的输入组合,直至令程序崩溃或获得其他想要的结果(经常是内存泄漏)为止。如果一个程序没有正确地对其输入进行过滤的话,那么,当用户提供畸形或不正确的输入内容时,就会导致程序崩溃,或引发意想不到或未定义的行为。对于模糊测试,大家可能听说过的一个例子就是dirbusting技术——当我们尝试不同的URL,可以先从常见的开始,直到找到网站中的合法目录为止。为了完成这种测试,我们可以借助于像fuff这样的工具。
实际上,模糊测试技术特别适用于用低级编程语言或过时的编程语言编写的程序,这些编程语言包括Basic、Assembly、C/C++等,因为这类程序在编写过程中更容易出错,比如不恰当的类型转换、释放后使用漏洞、或者内存溢出漏洞(缓冲区溢出、栈溢出、整数溢出)。更重要的是,这些错误通常不一定会导致运行时崩溃或编译时错误,因此,它们更有可能被精明的攻击者所利用。通常情况下,当提到模糊测试一词时,指的就是这种类型的模糊测试。
模糊测试的类型
实际上,常见的模糊测试技术有三种,下面分别进行介绍。
白盒模糊测试
我们所说的白盒模糊测试,实际上指的是这样一种测试技术:Fuzzer会试图分析程序的内部结构,以跟踪和最大化代码覆盖率。代码覆盖率指的是我们的代码所达到的分支比例;每次运行if或while等条件语句时,代码都会选择一个分支,也就是说,当条件语句为真时,代码会选择一个分支;当条件为假时,代码会选择另一个分支。白盒模糊测试的工作原理是,如果某个分支中隐藏着编程错误,我们首先必须确保在第一时间到达了那个分支。要进行这种分析,必须在编译过程中对程序进行插桩处理。
插桩技术的工作原理是,每次运行一个分支时,都会调用一个特殊的函数,将该分支记录为已经运行,有时甚至会记录哪一行被运行,以便跟踪覆盖范围。此外,插桩技术还可以用来发现安全漏洞。例如,Address Sanitation(ASan)有助于找到通常不会导致崩溃的内存泄漏。由于完整的结构分析需要消耗大量的系统资源,所以白盒模糊测试的速度通常很慢,但在寻找编程错误和漏洞方面,效果还是非常好的,尤其是隐藏在程序深处的编程错误和安全漏洞。当然为了进行广泛的插桩分析,用户必须能够访问源代码,而源代码并不总是可用的,特别是对于攻击者来说,尤其如此。
黑盒模糊测试
对于这种模糊测试方法来说,我们并不关心程序的内部结构,而是把它当作一个黑盒子看待。不过,我们还是可以利用程序的输出来试图弄清楚程序里面发生的事情,从而制作出更巧妙、更有效的输入。由于避免了白盒模糊测试相关的开销,因此,黑盒模糊测试的速度要更快一些,但在挖掘编程错误和安全漏洞方面,其效果要差一些。
灰盒模糊测试
这种方法试图在上述方法各自的优点和缺点之间取得平衡,它会在编译时进行轻量级的插桩处理,而不是进行全面的分析处理,以便计算代码覆盖率和跟踪程序的状态。对于目前流行的大多数Fuzzer来说,如AFL、HongFuzz和LibFuzzer等,都是采用灰盒模糊测试技术。为了执行这样的模糊测试,通常需要访问源代码,但如果只有可执行文件可用的话,也有一些变通的方法,但这些方法会严重影响其性能。在本文中,我不会讲解这些技术。
模糊测试相关技术
和dirbusting技术一样,模糊测试技术并不需要借助于暴力攻击,也就是尝试随机输入。事实上,暴力攻击并不是一种很好的模糊测试技术,而且很少有人这样做。幸运的是,实际上还有一些更聪明的技术,可以在合理的时间和资源内带来更好的结果。接下来,我将讨论其中的两种。
基于突变的模糊测试技术
这种技术是基于灰盒模糊测试的。也就是说,我们首先从有效的、形式良好的输入开始,并对其应用各种类型的突变处理。同时,我们还可以利用插桩技术,来获得最成功的突变,揭示新的分支,并把它们作为下一代的种子。这个过程有点让人联想到我们在自然界看到的达尔文自然选择。实际上,这种方法还能带来另外一个好处,那就是被程序过滤掉的突变会被淘汰,这样的话,就会只留下有用的输入。
对于该技术,最著名的工具(也可以说是最著名的Fuzzer)是AFL,即American Fuzzy Lop。
AFL使用三种类型的基本突变,具体如下所示:
· 随机删除一个二进制位
· 随机插入一个二进制位。
· 随机翻转一个二进制位(把0变成1,把1变为0)。
AFL在任何时候都不会只使用一种突变处理,而是在多种突变处理之间分配资源,并优先选择“能量”最大的突变。每个突变的“能量”取决于它运行的代码中的具体分支:由于输入越多的分支越为罕见,因此会为其分配更多的能量。此外,由于每个分支都有一个唯一的哈希值,这样就可以跟踪其执行次数。值得一提的是,这种技术中的初始输入可以来自提供各种突变的字典。
基于语法的模糊测试技术
您可能知道,有许多程序是无法接收非结构化输入的,因为它们对如何形成输入有特定的规则。这些规则被称为输入的语法,因为它们类似于口语中的语法规则。为了对这类程序进行模糊测试,并使其涵盖大多数语法选项,Fuzzer必须了解相关的语法,否则,直接使用简单突变产生的输入的话,它们将被过滤和丢弃。这样的模糊Fuzzer早已面世,可以对JavaScript、WASM、URL等进行模糊测试,但截至本文写作时,大部分都是实验性的,而且速度很慢,据我所知,这些Fuzzer都是用Python编写的,对于原型设计和演示来说,Python语言是个不错的选择,但对于优化的生产级Fuzzer来说,该语言就有点力不从心了。因此,它们的应用并不广泛。
小结
在本文中,我们对模糊测试的理论进行了简单的介绍,在本系列的下一篇文章中,我们将为读者详细介绍如何利用AFL对一个简单的程序进行模糊测试。
模糊测试技术入门,Part 2:基于AFL的模糊测试技术
在本文中,我将将为读者详细介绍如何在可以访问源代码的情况下通过AFL进行模糊测试。为了便于演示,这里将以GitHub网站上一个名为ccalc的开源程序为例进行讲解。顾名思义,这是一个用C语言编写的简单计算器。之所以选择它,是因为我认为这个程序较旧,有较大概率可以通过模糊测试在其中发现导致崩溃的漏洞,正如您将看到的,事实证明这个猜测是正确的。
首先,我们需要确定一个目标。就这里来说,我决定直接对main()函数进行模糊测试,而不是寻找一些特定的函数进行模糊测试。其原因是,该程序在将输入内容交给实际进行计算的函数之前,会先通过一系列函数解析原始的输入内容。由于该程序在解析过程中会筛选掉不正确的输入,因此直接特定函数是不合理的。
最初的main()函数大致如下所示(我稍微删减了一下):
概括来说,它包含两个分支:要么通过argc、argv[]获得输入,要么通过标准输入(stdin)获得输入。我不知道为什么,但AFL的创建者明确建议不要通过argc、argv[]以及所有其他在线资源进行模糊测试。因此,我对这个函数做了一些修改:首先,我删除了处理来自argc、argv的输入内容的代码,然后尽可能地对其进行优化。毕竟,每次Fuzzer尝试另一个输入时,都会执行这个函数。因此,如果该函数效率低下的话,会拖慢整个测试过程。修改后的函数如下所示:
在这里,请注意我添加的循环语句。它指示AFL使用持久模式。在AFL的默认模式下,每当AFL运行程序时,它都会使用系统调用fork()来创建一个新的子进程。但是,由于这个系统调用的开销非常大,从而会严重拖慢整个模糊测试过程。当使用持久模式时,Fuzzer会重复使用同一个进程。这样做的唯一的要求是,在每次循环运行时都要擦除所有变量和缓冲区。
修改上面的函数后,将其保存到名为harness.c的文件中。在编译代码时,为了使用该文件而不是main.c编译程序,需要对automake文件进行相应的修改。修改前的automake文件如下所示:
将其改为:
现在,我们就可以对程序进行模糊测试了。为此,可以通过automake来生成make文件,然后执行下列命令:
这样程序就可以通过AFL进行插桩并编译了。
现在,唯一要做的就是创建AFL的初始输入文件夹,这里将其命名为“in”,将输出文件夹命名为“out”。在输入文件夹中,可以存放简单文本文件(没有格式),用于提供合法的输入,比如“20/2”或“5*4”。由于我们想同时运行多个AFL进程(每个进程在自己的CPU核上运行),所以,我们需要为每个进程创建一个单独的输入文件夹。
要进行模糊测试,只需运行下面的命令即可:
上面,我们创建了第一个进程,下面,我们再来创建下一个:
然后,如法炮制。
从下面的截图中可以看到,AFL提供了一个GUI,用于显示模糊测试过程的重要信息。例如,在map coverage部分出了覆盖率,在findings in depth部分,GUI也给出了崩溃次数和相关信息。
在发现足够数量(具体数量由您决定)的崩溃后,通常是在长时间没有发现新的独特崩溃后,我们就可以终止该进程了。对于运行的每一个AFL进程,都会在输出文件夹中生成一个单独的文件夹。
在这些文件夹里面,数据是这样排列的:
现在,我们对它们分别加以介绍:
· plot_data:以有助于生成图形的形式提供信息。
· fuzzer_stats:模糊测试过程中的统计数据。
· fuzz_bitmap:一个bitmap,其中每个字节对应于程序的一个分支。
· “AFL维护着一个“fuzz bitmap”,其中的每个字节代表着被测试程序中某一分支被测试的次数。AFL不会在特定分支和bitmap中的字节之间进行一对一的映射。相反,AFL的嵌入式插桩技术会将一个随机的两个字节的常量ID放入每个分支。每当执行到一个被检测的分支时,AFL就会对新分支的ID和到达新分支之前看到的最后一个分支ID进行XOR处理。这样就捕获到了当前分支和到达该分支的唯一路径(例如当同一个函数从代码中的多个位置被调用时)。然后,AFL会使用哈希函数对用XOR处理过的值进行相应的处理,以确定bitmap中的哪个条目代表该分支组合。每当一个特定的分支组合被测试后,bitmap中对应的字节中的值就会递增。"
· cmdline:提供给AFL的命令。实际上,就是该程序的名称。
· .cur_input:当前的输入。
· queue:到现在为止尝试过的所有输入。
· crashes与hangs:用于存放测试结果。这些文件夹中的每个文件都包含了导致崩溃或挂起的输入内容。在每个文件名中指定的是崩溃的内核信号(当然与挂起无关),AFL用来创建导致崩溃的输入的ID,AFL的运行时间,以及用来从其种子生成输入的突变方式。
本文翻译自:https://sayfer.io/blog/fuzzing-part-1-the-theory如若转载,请注明原文地址