作者:xd0ol1(知道创宇404实验室)

0 引子

本文前两节将简要讨论 fuzzing 的基本理念以及 WinAFL 中所用到的插桩框架 DynamoRIO ,而后我们从源码和工具使用角度带你了解这个适用于 Windows 平台的 fuzzing 利器。

1 Fuzzing 101

就 fuzzing 而言,它是一种将无效、未知以及随机数据作为目标程序输入的自动化或半自动化软件测试技术,现而今大多被用在漏洞的挖掘上,其最基本的实现方案如下图所示,虽然看着不复杂,但在实际应用中却并非易事:

图0  基本的fuzzing实现方案

按输入用例获取方式的不同,一般可分为基于突变的 dumb fuzzing 、基于生成的 smart fuzzing 和基于进化算法的 fuzzing ,前两类相对比较成熟了,而第三类仍将是今后发展的主要方向。其中,基于进化算法的 fuzzing 会借助目标程序的反馈来不断完善测试用例,这就要求在设计时给出相关的评估策略,最常见的是以程序运行时的代码覆盖率作为衡量标准。

当然, fuzzer 的设计不应局限在相关理论的原型证明上,关键得经过实践证明才能算是真正有效的。

2 DynamoRIO 动态二进制插桩

我们再来看下后文涉及的插桩,DBI(Dynamic Binary Instrumentation)是一种通过注入探针代码实现二进制程序动态分析的技术,这些插桩代码会被当作正常的指令来执行。常见的此类框架包括 PIN、Valgrind、DynamoRIO 等,这里我们要关注的是 DynamoRIO

通过 DynamoRIO ,我们可以监控程序的运行代码,同时它还允许我们对运行的代码进行修改。准确来说, DynamoRIO 就相当于一个进程虚拟机,被监控程序的所有代码都被转移到其上的缓冲区空间中模拟执行,具体架构如下:

图1  DynamoRIO的架构设计

其中,基本块(basic block)是一个重要的概念。想象一下,将监控进程中的所有指令以控制转移类指令为边界进行分割,那么它们会被分割成许许多多的块,这些块以某一指令开始,但都是以控制转移类指令结束的,如下图:

图2  基本块(basic block)的概念

这些指令块就是 DynamoRIO 中定义的基本块概念,即运行的基本单元。 DynamoRIO 每次会模拟运行一个基本块中的指令,当这些指令运行完成后,将会通过上下文切换到另一基本块中运行,如此往复,直至被监控进程运行结束。

此外,该框架还为我们提供了丰富的函数编程接口,可以很方便的进行插件(client)开发,主要依赖于各种事件回调处理,同时做好指令过滤对提升性能也是很有帮助的。

3 WinAFL Fuzzer

接下去我们就来看下本文的重点,即 WinAFL 这个具体的 fuzzer ,本节内容分为3块,首先是概述部分,而后会对此工具的关键源码进行分析,最后我们将借助构造好的存在漏洞的程序进行一次实际 fuzzing 。

3.1 概述

对于 fuzzer 来说,AFL(American Fuzzy Lop)想必大家是不会陌生的,但由于其代码设计的原因使得它并不支持 Windows 平台,而 WinAFL 项目正是此 fuzzer 在 Windows 平台下的移植。 AFL 借助编译时插桩和遗传算法实现其功能,由于平台支持的关系,在 WinAFL 中该编译时插桩被替换成了 DynamoRIO 动态插桩,此外还基于 Windows API 对相关函数进行了重写。

在使用 WinAFL 进行 fuzzing 时需要指定目标程序及对应的输入测试用例文件,且必须存在这么一个用于插桩的目标函数,此函数的执行过程中包括了打开和关闭输入文件以及对该文件的解析,这样在插桩处理后能够保证目标程序循环的执行文件 fuzzing ,避免每次 fuzzing 操作都重新创建新的目标进程。同时,fuzzing 的输入文件会按照相应算法进行变换,且根据得到的目标模块覆盖率判断其是否被用于后续的 fuzzing 操作。

3.2 关键源码分析

我们这里分析的 WinAFL 版本为 1.08 ,可从 GitHub 上获取。其中 afl_docs 目录包含了关于设计原理、技术细节等相关说明文档,bin 目录则存放有已经编译好的相关程序,而 testcases 目录是各种测试用例文件,剩下的大部分是源码文件。总体来看,与源码相关的文件实际上不多,代码量在10k+左右,最关键的是 afl-fuzz.cwinafl.c 两个文件,这也是我们主要分析的。此外源码中还包括了一些辅助工具,例如显示跟踪位图信息的 afl-showmap.c 以及用于测试用例文件集合最小化的 winafl-cmin.py,而用于测试用例文件最小化的 afl-tmin 工具目前尚未被移植到该平台。当然,更多设计相关的说明还是具体参考 technical_details.txt 文件。

3.2.1 fuzzer模块
我们先看下 afl-fuzz.c ,此部分代码实现了 fuzzer 的功能,对于 fuzzing 中用到的输入测试文件,程序将使用结构体 queue_entry 链表进行维护,我们可在输出结果目录找到相应的 queue 文件夹,如下是添加测试用例的代码片段:

图3  添加新的测试文件

而输入文件的 fuzzing 则由 fuzz_one 函数来完成,此过程涵盖了多个阶段,包括位翻转、算术运算、整数插入这些确定性的 fuzzing 策略以及其它一些非确定性的 fuzzing 策略。且 fuzzing 中采用的突变方式和程序状态并不存在什么特殊关联,表面看该步骤完全是盲目的:

图4  测试文件的fuzzing

对上述的每个 fuzzing 策略,程序首先需要对测试用例做相应的修改,然后运行目标程序并处理得到的fuzzing结果:

图5  处理每个fuzzing策略

由于程序采用的是遗传算法的思想,所以会对每一 fuzzing 策略得到的执行结果进行评估,即根据目标程序的代码覆盖率来决定是否将当前的测试用例添加到 fuzzing 链表中:

图6  评估目标程序当前的执行路径

当然,在对测试文件进行 fuzzing 前可能还需进行必要的修正:

图7  修正测试用例文件

此外,在 fuzzing 过程中,相关结果的状态信息会不断进行更新,该界面展示是由 show_stats 函数实现的:

图8  实现fuzzing过程的界面展示

3.2.2 插桩模块
下面继续来看 winafl.c ,此文件对应编写的 DynamoRIO 插件代码,它有两个作用:

  1. 循环调用 fuzzing 的目标函数;
  2. 更新覆盖率相关的位图文件信息。

程序首先会进行初始化操作并注册各类事件回调函数,其中最重要的是基本块处理事件和模块加载事件:

图9  注册各类事件回调函数

在相应的模块加载事件回调函数中,如果当前模块为 fuzzing 的目标模块,那么会对其中相应的目标函数进行插桩处理:

图10  对目标函数进行插桩

即在目标函数执行前,通过 pre_fuzz_handler 调用记录下当前的寄存器环境,而在目标函数执行后,又会通过 post_fuzz_handler 调用进行寄存器环境的恢复,从而实现了待 fuzzing 目标函数的不断循环:

图11  恢复寄存器环境

此外另一关键问题是对位图文件的处理,关于位图文件的覆盖率计算有两种模式,即基本块(basic block)覆盖率模式和边界(edge)覆盖率模式。在 fuzzing 过程中会维护一个64KB大小的位图文件用于记录此覆盖率及其命中次数,在边界覆盖率模式下每个字节代表了特定的源地址和目标地址配对,这种模式更有助于形象化表述程序的执行流程,因为漏洞往往是由未知的或非正常的执行状态转换导致的,而非简单的基本块覆盖。对应的事件函数为 instrument_bb_coverageinstrument_edge_coverage ,也就是注册的基本块处理回调函数,位图文件的更新是通过插入的新增指令来实现的,对于边界覆盖率的情况其代码如下,相应基本块覆盖率的情形与之类似:

图12  插入更新边界覆盖率的指令

3.3 WinAFL 的使用

最后我们来进行一次实际的 fuzzing ,用到的目标程序是基于所给的 gdiplus.cpp 源码修改得到的,其中手动引入了一个 crash ,代码如下:

int (*func)(int x);  //定义func函数指针  
......
func = NULL;  
printf("%d", func(0));  //程序crash  

首先我们需要确定 fuzzing 的目标函数,即设置 -target_offset-target_method 对应的参数。在此例中 main 函数是符合条件的目标函数,若要使用 -target_offset ,则可简单通过 IDA 来查看此函数的偏移,此例中为 0x1090

图13  查看main函数的偏移

如果存在符号文件,那么可以直接设置 -target_method 的参数为main。对于 -coverage_module 的参数,我们可以执行如下命令来获取,注意 DynamoRIO 的目录需根据实际情况来设置。在得到的 log 文件中给出了目标程序执行过程中所加载的模块,同时,必须保证运行结果为“Everything appears to be running normally.”:

C:\temp\DynamoRIO\bin32\drrun.exe -c winafl.dll -debug -target_module test.exe -target_offset 0x1090 -fuzz_iterations 10 -nargs 2 -- test.exe in\input.bmp  

然后,我们就可以输入如下的命令进行 fuzzing 了,其中 “@@” 表示待 fuzzing 的测试用例文件在 in 目录下:

afl-fuzz.exe -i in -o out -D C:\temp\DynamoRIO\bin32 -t 20000 -- -coverage_module gdiplus.dll -coverage_module WindowsCodecs.dll -fuzz_iterations 5000 -target_module test.exe -target_method main -nargs 2 -- test.exe @@  

但上述命令参数中并没有出现 DynamoRIO 插件 winafl.dll ,事实上此命令执行后又创建了新的子进程,如下图:

图14  afl-fuzz进程树

我们可以得到 drrun.exe 执行的命令参数如下:

C:\temp\DynamoRIO\bin32\drrun.exe -pidfile childpid_95fa18fc9031bf0d.txt -no_follow_children -c winafl.dll -coverage_module gdiplus.dll -coverage_module WindowsCodecs.dll -fuzz_iterations 5000 -target_module test.exe -target_method main -nargs 2 -fuzzer_id 95fa18fc9031bf0d -- test.exe out\.cur_input  

如果没问题的话,那么我们会看到如下的 fuzzing 界面,至于 WinAFL 的编译以及其它参数设置可参考 README 文件:

图15  WinAFL执行时的界面

fuzzing 中各阶段的结果都将保存在 -o 选项设置的 out 目录中,其中 crash 或 hangs 目录保存着导致 bug 的测试用例文件,至于目标程序是否存在可利用的漏洞则需要进一步的确认:

图16  保存fuzzing结果的目录

4 结语

本文大体介绍了 WinAFL 这个 fuzzing 工具,但实际应用起来还是有很多方面需要考虑的。另外,笔者目前还是初学,错误之处还望各位斧正,欢迎一起交流:P

5 参考

[1] A fork of AFL for fuzzing Windows binaries
[2] Dynamic Instrumentation Tool Platform
[3] American fuzzy lop
[4] Real World Fuzzing
[5] Code Coverage
[6] Effective file format fuzzing