Fuzzing是一种软件漏洞检测技术,通过提供非预期的、随机的或者错误的数据作为输入来检测软件漏洞,在软件出现崩溃时监视异常结果来找出其存在的潜在漏洞。
可以将模糊测试类比为如何闯进一幢房子。为了破门进入某人的家,假设采用纯白盒的方法,那么在实施破门之前应该能够得到对所有关于这个家的充分信息。这些信息可能要包括房屋设计图、各种锁的制造商列表、房屋建筑材料的详情,等等。尽管这种方法有独一无二的优点,但是也并非万无一失而没有短处。在这种方法下,执行破门的过程中,你要做的不是在实际执行时去检查房屋的设计而是要对房屋的设计执行静态分析。让我们打个比方,例如,事先研究表明起居室的侧面窗户代表了一个弱点,可以砸破这面窗户然后破门而入,如果是这样的话,那你肯定不希望到时候有一个拿着鸟枪的愤怒房主站在里面正在等着你。另一方面,如果采用一种纯的黑盒测试方法来完成破门的话,那么应该在黑夜的掩盖下逐步靠近这个房子,安静地尝试所有的门和窗户是否有漏洞,向房子内窥视以决定哪里可能是最好的突破口。最后,如果选择采用模糊测试来完成突破的话,便可以不必研究设计图更不用人工测试各种锁。要做的就是让找枪和破门而入的过程自动化–这就是强制性的漏洞发掘!
模糊测试方法的选择依赖不同的因素,可能有很大的变化。没有一种绝对正确的模糊测试方法。模糊测试方法的选择完全取决于目标应用程序、研究者的技能,以及需要测试的数据所采用的格式。然而,无论要对什么进行测试,也不论确定选择了哪种方法,模糊测试总要经历几个基本的阶段。
在没有考虑清楚目标应用程序的情况下,不可能对模糊测试工具或技术作出选择。如果是在安全审核的过程中对内部开发的应用程序进行模糊测试,目标应用程序的选择应该小心谨慎。相反,如果是对第三方应用程序进行安全漏洞的发掘研究,这种选择就有一定的灵活性。选择了目标应用程序之后,还必须选择应用程序中具体的目标文件或库。如果确要选择目标文件或库,应该选择那些被多个应用程序共享的库,因为这些库的用户群体较大,出现安全漏洞的风险也相应较高。
几乎所有可被人利用的漏洞都是因为应用程序接受了用户的输入并且在处理输入数据时没有首先清除非法数据或执行确认例程。枚举输入向量对模糊测试的成功至关重要。未能定位可能的输入源或预期的输入值对模糊测试将产生严重的限制。在查找输入向量同时应该运用水平思考。尽管有一些输入向量是很明显的,但是其它一些则难以捉摸。最后要说的是,任何从客户端发往目标应用程序的输入都应该被认为是输入向量。这些输入包括消息头、文件名、环境变量、注册键值等等。所有这些都应该被认为是输入向量,因此都应该是可能的模糊测试变量。
一旦识别出输入向量,模糊测试就必须被生成。如何使用预先确定的值、如何变异已有的数据或动态生成数据,这些决策将取决于目标应用程序及其数据格式。不管选择了哪种方法,这个过程中都应该引入自动化。
这一步与前一步并行进行,在这一步,模糊测试成为一个动词。执行过程可能包括发送数据包给目标应用程序、打开一个文件或发起一个目标进程。同样,这个过程中的自动化也是至关重要的。没有自动化,我们便无法执行真正的模糊测试。
在模糊测试过程中,一个至关紧要但却被经常忽视的步骤是对故障或异常的监视过程。举一个例子,如果我们没有办法准确指出是哪一个数据包引起崩溃的话,那么向目标Web服务器发送10000个模糊测试数据包并最终导致服务器崩溃便是一次无用的努力。监视可以采用多种形式并且应该不依赖目标应用程序和所选择的模糊测试类型。
一旦故障被识别,因审核的目标不同,还可能需要确定所发现的bug是否可被进一步利用。这典型地是一个人工过程,需要具备安全领域的专业知识。因此执行这一步的人可能不是最初执行模糊测试的人。
模糊测试阶段 |
---|
识别目标 |
识别输入 |
生成模糊测试数据 |
执行模糊测试数据 |
监视异常 |
确定可利用性 |
不管采用什么类型的模糊测试,所有上述阶段都应该被考虑到,只有确定可利用性这一步有可能例外。各个阶段的顺序和侧重点可依据研究者的目标而改变。尽管模糊测试非常强大,但绝不意味着它对任何被测软件都将发现百分之百的错误。
几种无法被模糊器发现的漏洞:
Sulley模糊测试是一种基于Python的fuzzing框架,主要应用于网络协议方面的测试,由Pedram AMINI 和 Aaron Portnoy设计。
Sulley利用了基于块的方法来生成单独的请求,这些请求稍后将组合在一起以构成一个会话。一开始,使用一个新的名字来初始化你的请求:
s_initialize("new request")
现在你就可以向请求中添加原始类型、块以及嵌套的块了。每个原始类型都可以被单独的显示和变异。显示一个原始类型将导致以原始数据格式来返回其内容。对原始类型进行变异将导致转换其内部内容。当可模糊化的值被用完时,每个可变异的原始类型将接收一个默认的恢复值。
s_static():
最简单的原始类型s_static()开始讨论,它将向请求中添加一个静态的、任意长度的未变异的值。为了便于使用,Sulley为它提供了许多不同的别名,例如s_dunno(),s_raw()以及s_unknown()都是s_static()的别名。
原始类型和块等原始都包含一个可选的名字关键字参数。指定一个名字就允许你在请求中通过request.names["name"]来直接的访问被命名的条目,而不用遍历块的结构以到达所需要的元素。
s_binary():
与前面的类型有些关联但并不等同的是s_binary()原始类型,它可以接收以多种格式来表示的二进制数据。
s_random():
由于大多数的Sulley的原始类型都是由模糊测试的启发式规则来驱动的,因此它只包含有限数量的变异。而其中的一个例外情形是s_random()原始类型,它可以被用来生成不同长度的随机数据。
这个原始类型包含两个强制使用的参数'min_length'和'max_length',它们分别指明了在每次循环迭代过程中所生成的随机数据的最大长度和最小长度。该原始类型同时也接受下面所列出的可选的关键字参数:
num_mutations(整数值,默认值是25):在恢复到默认值之前要进行的变异的数量。
fuzzable(布尔值,默认值是True):将对此原始类型的模糊测试设置为使能或者不使能。
name(字符串值,默认值是None):在所有Sulley对象中,指定一个名字就可以在整个请求中直接访问该原始类型。
num_mutations关键字参数指定了在该原始类型被认为用完之前,应当被重新显示多少次。为了用随机数据填充一个静态大小的字段,可以将'min_length'和'max_length'的值设置为相同值。
二进制协议的ASCII协议同样都包含有许多不同大小的整数值,例如HTTP中的内容长度字段。同大多数模糊测试框架一样,Sulley中的一部分负责表示以下这些类型:
八个字节:s_qword(),s_double()
每个整数类型至少接受一个单一的参数,即默认的整数值。另外,下面的可选的关键字参数也可以被指定:
endian(字符型,默认值为’<’):位字段的存放顺序。将低位字节先存放在低地址处的顺序指定为<,将高位字节先存放在低地址处的顺序指定为>。
随处都可以发现字符串。当进行模糊测试时,你必定会遇到许多的字符串构件,例如邮件地址、主机名、用户名、密码等等都是字符串的例子。Sulley提供了原始类型s_string()来表示这些字段。该原始类型含有一个单一的强制性的参数,这个参数为该类型指明了默认的有效值。也可以指定下面所列出的额外的关键字参数:
通过使用分隔符,字符串经常被解析为子字段。例如,空格字符在HTTP请求GET/index.html HTTP/1.0中就被用做一个分隔符。在这个请求当中,前面的斜线(/)和点(.)字符同样也是分隔符。当在Sulley中定义一个协议时,要确保使用s_delim()原始类型来表示分隔符。同其它的原始类型一样,第一个参数是强制使用的,并且被用来指定默认值。同样,与其它原始类型一样,s_delim()也接受可选的’fuzzable’和’name’关键字参数。针对分隔符的变异包括重复、替换和删除。
在掌握了原始类型的相关知识后,下面让我们来看一下如何将它们进行组织,并且在块的内部进行嵌套。通过使用s_block_start()来定义并打开一个新块,使用s_block_end()来关闭该块。每个块必须要给定一个名字,并将其作为s_block_start()的第一个参数。该例程同时也接受下面列出的可选的关键字参数:
分组允许你将一个块关联到一个群组原始类型,以指定该块应当为群组内部的每个值来循环遍历所有可能的变异。例如,群组原始类型对于表示带有相似参数结构的有效操作码或动词的一个列表是非常有用的。原始类型s_group()定义了一个群组,并且接受两个强制使用的参数。第一个参数指定了群组的名字,第二个参数指定了将要遍历的、可能的原始值的列表。作为一个简单的例子,考这个完整的Sulley请求,该请求被设计为对一个Web服务器进行模糊测试。
from sulley import *
s_initialize("HTTP BASIC")
s_group("verbs", values=["GET", "HEAD", "POST", "TRACE"])
if s_block_start("body", group="verbs"):
# break the remainder of the HTTP request into individual primitives.
s_delim(" ")
s_delim("/")
s_string("index.html")
s_delim(" ")
s_string("HTTP")
s_delim("/")
s_string("1")
s_delim(".")
s_string("1")
s_static("\r\n\r\n")
s_block_end("body")
在该脚本的开始,首先导入了Sulley的所有构件。接下来,初始化了一个新的请求,并且将其命名为HTTP BASIC。在后面可以通过引用这个名字来直接访问该请求。然后定义了一个群组,其名字为verbs,所包含的可能的字符串值为GET,HEAD,POST以及TRACE。接着启动了一个新块,其名字为body,并且通过可选的group关键字参数将其关联到前面所定义的群组原始类型。注意s_block_start()将一直返回True,这就允许你可以使用一个简单的if语句来识别出其所包含的原始类型。同时注意到s_block_end()的name参数也是可选的。这些框架的设计决定纯粹是出于美观的目的而做出的。接着,在body块的内部定义了一系列基本的分隔符和字符串原始类型,然后将该块关闭。当将这个被定义的请求加载到一个Sulley会话中时,模糊器将为该块生成并传递所有可能的值,每次为群组中定义的每个动词生成并传递值。
编码器是一个简单而又功能强大的块修饰符。可以指定一个函数并将其关联到一个块,以在通过网络返回和传输之前,来修改所显示的该块的内容。Sulley编码器包含一个单一的参数,即将要编码的数据,并且返回被编码的数据。现在可以将这个定义的编码器关联到包含可模糊化原始类型的一个块,并且允许模糊器的开发者继续工作就好像不存在这个小的障碍一样。
依赖允许你将一个条件应用于一个完整块的显示中。其实现过程如下,首先使用可选的dep关键字参数将一个块与它将要依赖的原始类型相连接。当Sulley准备显示依赖块时,它将会检查所连接的原始类型的值并相应的采取行为。可以使用dep_value关键字参数来指定一个依赖值。另外,可以使用dep_values关键字参数来指定依赖值的一个列表。最后,可以通过使用dep_compare关键字参数来修改实际的条件比较。例如,考虑这样一个情形,取决于一个整数的值而期望得到不同的数据。可以采用多种方法将块依赖串接起来,以构成功能更加强大(但也更为复杂)的组合。
为了有效的利用Sulley,你必须要熟悉的关于数据生成的一个重要方面就是块帮助函数。这些函数包括大小计算函数(sizers)、校验和函数(checksums)和重复函数(repeaters)等。
Sulley利用legos来表示用户定义的构件,如邮件地址、主机名以及在微软的RPC ,XDR,ASN.1以及其它标准中所使用的协议原始类型.
一旦你已经定义了许多的请求,那么就可以将它们在一个会话中连接起来。同其它的模糊测试框架相比,Sulley所具有的一个主要的优越性在于,它具备对协议进行深层次模糊测试的能力。这一功能是通过在一个图中将请求连接在一起来实现的。当开始进行模糊测试时,Sulley将从根节点开始遍历该图的结构,并且同时对每一个构件进行模糊测试。
当实例化一个会话时,可以指定如下所示的可选的关键字参数:
下一步就是定义目标,将它们同代理相连接,并且将目标添加到会话中。在下面的例子中,我们实例化了一个运行在VMWare虚拟机中的新目标,并且将其与三个代理相连接。
target = sessions.target("10.0.0.1", 5168)
target.netmon = pedrpc.client("10.0.0.1",26001)
target.procmon = pedrpc.client("10.0.0.1", 26002)
target.vmcontrol = pedrpc.client("127.0.0.1",26003)
target.procmon_options = \
{
"proc_name" : "SpntSvc.exe",
"stop_commands" : ['net stop "trend serverprotect"'],
"start_commands" : ['net start "trend serverprotect"'],
}
sess.add_target(target)
sess.fuzz()
实例化的目标被绑定在IP地址为10.0.0.1的主机上的TCP端口5168。在目标系统中运行着一个网络监视代理,该代理默认在端口26001进行监听。网络监视器将所有的socket通信记录到单独的PCAP文件中,这些文件通过测试用例号来进行标识。进程监视代理也运行在目标系统中,默认在端口26002处进行监听。该代理接受额外的参数以指明将要关联到的进程名,停止目标进程执行的命令以及启动目标进程执行的命令。最后,VMWare控制代理是运行在本地系统中的,默认在端口26003处进行监听。将目标添加到会话中,然后就可以开始进行模糊测试了。Sulley可以对多种目标进行模糊测试,对每个目标使用连接代理的唯一集合进行测试。这样,就可以通过将总的测试空间划分到不同的目标来节省测试时间。
下面让我们来详细的讨论一下每个单独代理的功能。
代理:网络监视器(network_monitor.py)
网络监视器代理负责监视网络通信,并且将它们作为日志记录到磁盘上的PCAP文件中。该代理被硬编码并被绑定到TCP端口26001,并且通过PedRPC定制的二进制协议接受来自于Sulley会话的连接。在将一个测试用例传递到目标之前,Sulley将关联到该代理,并且要求它开始记录网络通信。一旦测试用例被成功的传递,Sulley将再次关联到此代理,并且要求它将所记录的通信导出到磁盘上的一个PCAP文件中。用测试用例号来命名PCAP文件,以便于检索。该代理不需要被部署到与目标软件相同的系统中。然而,它必须可见的发送和接收网络通信。该代理接受命令行参数。
ERR> USAGE: network_monitor.py
<-d|--device DEVICE #> device to sniff on (see list below)
[-f|--filter PCAP FILTER] BPF filter string
[-P|--log_path PATH] log directory to store pcaps to
[-l|--log_level LEVEL] log level (default 1), increase for more verbosity
[--port PORT] TCP port to bind this agent to
Network Device List:
[0] {C2E1878A-A59A-4023-A4E5-77EE1ECCEFAE} 192.168.235.1
[1] {11E03B98-5931-490C-8617-EC82C36121B0}
[2] {E23A46F5-E2F9-46B7-B407-6868DF329C54} 192.168.75.1
[3] {554DA01B-6938-4FC5-A3C1-26CA49E8DF3D} 192.168.0.100
[4] {E0887D7D-3247-4896-ABAF-C0CDE3AC9C86}
代理:进程监视器(process_monitor.py)
进程监视器代理负责检测在模糊测试的目标进程中可能发生的错误。该代理被硬编码并被绑定到TCP端口26002,并且通过PedRPC定制的二进制协议接受来自于Sulley会话的连接。在成功的将每个单独的测试用例传递到目标之后,Sulley将关联到该代理以确定是否有一个错误被触发。如果有错误被触发,那么有关错误本质的高层信息将被传递回Sulley会话,以在Web服务器内部显示(稍后将对此进行更加详细的讨论)。所触发的错误也被记录到一个序列化的”崩溃二进制文件”中,以用于后续的分析。我们在后面将对该功能进行更加详细的研究。该代理接受命令行参数。
ERR> USAGE: process_monitor.py
<-c|--crash_bin FILENAME> filename to serialize crash bin class to
[-p|--proc_name NAME] process name to search for and attach to
[-i|--ignore_pid PID] ignore this PID when searching for the target process
[-l|--log_level LEVEL] log level (default 1), increase for more verbosity
[--port PORT] TCP port to bind this agent to
代理:VMWare控制(vmcontrol.py)
VMWare控制代理被硬编码并被绑定到TCP端口26003,并且通过PedRPC定制的二进制协议接受来自于Sulley会话的连接。该代理提供了一个API以同一个虚拟机映像进行通信,包括具备启动、停止、挂起或者重设映像的能力,同时也可以获取、删除和恢复快照。如果一个错误已经被发现或者目标不能被到达,Sulley可以连接到该代理并且将虚拟机恢复到一个已知的好的状态。测试序列处理工具将主要依赖该代理来实现其功能,即识别触发任何给定的复杂错误的准确的测试用例序列。该代理接受命令行参数。
Web监视接口
Sulley会话类含有一个内嵌的最小化Web服务器,该服务器被硬编码并被绑定到端口26000。一旦会话类的fuzz()方法被调用,那么该Web服务器线程将解锁,并且模糊器的执行进度包括中间结果都可以被显示出来。
可以通过点击适当的按钮来暂停和恢复模糊器的执行。每个所检测到错误的概要信息将被作为一个列表而显示,该列表的第一列是导致错误发生的测试用例的编号。点击测试用例编号将会加载错误发生时的详细崩溃信息。当然,该信息在崩溃二进制文件中也是可用的,并且可以通过编程来获取。一旦会话被执行完毕,就进入了事后验证阶段并开始分析所生成的结果。
一旦一个Sulley模糊测试会话过程执行完毕,下面就开始对结果进行评审并且进入到事后验证阶段。会话的内嵌Web服务器将会向你提供有关潜在的未发现问题的早期提示信息,但实际上这时你将会对结果进行分隔。在这个处理过程中,有一些可用的工具可以为你提供帮助。第一个是crash-bin_explorer.py工具,该工具接受命令行参数。
USAGE: crashbin_explorer.py
[-t|--test #] dump the crash synopsis for a specific test case number
[-g|--graph name] generate a graph of all crash paths, save to 'name'.udg
例如,我们可以使用该工具来查看检测到错误的每个位置,并且进一步列出在该地址触发一个错误的单个测试用例编号。
D:\Program Files (x86)\Sulley\sulley-master1>python crashbin_explorer.py D:\easyftpserver.crash
[2] easyftp.exe:0042d063 rep movsd from thread 6008 caused access violation
4559, 5682,
在上面所列出的错误点中,没有一个错误可能会作为一个明显的可利用问题而显得突出。我们可以通过使用-t命令行开关来指定一个测试用例编号,从而深入到一个单个错误的特定方面中。看一下编号为4559测试用例:
D:\Program Files (x86)\Sulley\sulley-master1>python crashbin_explorer.py D:\easyftpserver.crash -t 4559
easyftp.exe:0042d063 rep movsd from thread 6008 caused access violation
when attempting to read from 0x021af000
CONTEXT DUMP
EIP: 0042d063 rep movsd
EAX: 040b70af ( 67858607) -> N/A
EBX: 01f0fefa ( 32571130) -> N/A
ECX: 007c202c ( 8134700) -> N/A
EDX: 00000002 ( 2) -> N/A
EDI: 045ebe74 ( 73318004) -> (heap)
ESI: 021aeffd ( 35319805) -> N/A
EBP: 0297dc38 ( 43506744) -> X4E,@^q0lDCWGD,@^*[@0qt,G:~@hl0*lLjIhlkXh`h`h+|GLjI (stack)
ESP: 0297dc30 ( 43506736) -> X4E,@^q0lDCWGD,@^*[@0qt,G:~@hl0*lLjIhlkXh`h`h+ (stack)
+00: 0297dc94 ( 43506836) -> ,@^ (stack)
+04: 0297dcb8 ( 43506872) -> qt,G:~@hl0*lLjIhlkXh`h`h+|GLjIGdX\t\?[w| (stack)
+08: 0297dc58 ( 43506776) -> lDCWGD,@^*[@0qt,G:~@hl0*lLjIhlkXh`h`h+|GLjIG (stack)
+0c: 00453415 ( 4535317) -> N/A
+10: 045e402c ( 73285676) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%DTM %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% (heap)
+14: 021a71b5 ( 35287477) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%DTM %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% (heap)
disasm around:
0x0042d044 cmp edi,esi
0x0042d046 jna 0x42d050
0x0042d048 cmp edi,eax
0x0042d04a jc 0x42d1c8
0x0042d050 test edi,0x3
0x0042d056 jnz 0x42d06c
0x0042d058 shr ecx,0x2
0x0042d05b and edx,0x3
0x0042d05e cmp ecx,0x8
0x0042d061 jc 0x42d08c
0x0042d063 rep movsd
0x0042d065 jmp [edx*4+0x42d178]
0x0042d06c mov eax,edi
0x0042d06e mov edx,0x3
0x0042d073 sub ecx,0x4
0x0042d076 jc 0x42d084
0x0042d078 and eax,0x3
0x0042d07b add ecx,eax
0x0042d07d jmp [eax*4+0x42d090]
0x0042d084 jmp [ecx*4+0x42d188]
0x0042d08b nop
stack unwind:
easyftp.exe:00453415
easyftp.exe:0044bb6c
easyftp.exe:0044bafd
easyftp.exe:00405b17
SEH unwind:
0297dcbc -> easyftp.exe:00475743
0297de74 -> easyftp.exe:00472c08
0297de9c -> easyftp.exe:00473068
0297fefc -> easyftp.exe:00472be0
0297ff70 -> easyftp.exe:004730a1
0297ffcc -> easyftp.exe:00431ba0
0297ffe4 -> ntdll.dll:77e56750
ffffffff -> ntdll.dll:77e62ebc
以上崩溃信息根据PyDbg崩溃信息格式分析处理,发现可能存在的潜在漏洞。
我们可能想要执行的最后一个步骤就是删除所有的不包含错误相关信息的PCAP文件。可以编写pcap_cleaner.py构件来很好的完成这个操作。该构件将打开特定的崩溃二进制文件,在触发错误的测试用例编号列表中进行读取,并且从特定的目录中删除所有其它的PCAP文件。