Galaxy Lab

focus on information security

American Fuzzy Lop使用

AFL(American Fuzzy Lop)是由Google安全工程师Michał Zalewski开发的一款开源fuzzing测试工具,可以高效地对二进制程序进行fuzzing,挖掘可能存在的内存安全漏洞,如栈溢出、堆溢出、UAF、double free等。由于需要在相关代码处插桩,因此AFL主要用于对开源软件进行测试。当然配合QEMU等工具,也可对闭源二进制代码进行fuzzing,但执行效率会受到影响。

AFL主要由3部分组成:

  1. 编译器wrapper,该部分用于编译目标代码,如afl-gcc, afl-clang等
  2. fuzzer: afl-fuzz,即为用于fuzzing的主要工具
  3. 其他工具:如afl-cmin, afl-tmin等

这里我们重点关注编译器wrapper和fuzzer

AFL的安装

我们搭建的环境是64位Debian 9。首先,确保依赖的llvm, clang已正常安装:

apt install llvm clang

所安装的clang版本为3.8.1。

其实在源里已经有AFL了,其版本为2.32。我们选择的是官网下载的最新版本2.52b: http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz。历次更新说明可见http://lcamtuf.coredump.cx/afl/ChangeLog.txt

下载并解压后,进入AFL源码根目录,执行make即可。如果一切正常,会有以下提示:
[+] All right, the instrumentation seems to be working!
[+] LLVM users: see llvm_mode/README.llvm for a faster alternative to afl-gcc.
[+] All done! Be sure to review README - it's pretty short and useful.

这里,我们看到他介绍了另一种更快的模式:llvm-mode。具体会在后面介绍,这里我们只需要进入目录llvm_mode,再执行make即可,此时会编译得到afl-clang-fast和afl-clang-fast++并保存在上层目录。

当全部编译完成后,我们可以在AFL源码根目录下看到编译好的工具,如afl-gcc, afl-clang-fast, afl-fuzzer等。随后,通过make install或将其放置PATH路径中,完成安装。

AFL的基本使用

我们选取的测试目标是libtiff,这是一个开源的TIFF格式解析库。下载其源码并解压后,我们就可以通过AFL对其进行编译了。

为了指定afl-gcc为编译所使用的编译器,我们通过环境变量CC来完成;而如果目标是C++代码,则通过环境变量CXX指定。于是,在tiff源码根目录下,执行以下命令:

CC=afl-gcc ./configure --disable-shared

这里,我们运行./configure完成配置得到Makefile。注意到我们还添加了–disable-shared选项,这是用来告诉编译器,我们想编译得到静态库而非动态链接库。当然不是一定要编译成为静态库,但是这样做在最后调用库函数时,不需要再去考虑解析的问题了。所以对于库的fuzzing,一般都会添加上–disable-shared选项。

如果一切顺利,那么接下来我们执行make命令,就可以开始编译了。等待片刻后编译完成,我们便得到了插桩后用于fuzzing的libtiff.a,以及libtiff自带的在tools目录下的一些小工具,我们选取其中的tiffdump作为的测试目标。

接下来,就是需要准备测试环境了。我们需要创建2个文件夹,一个用于保存输入,一个用于保存输出。我们将这两个文件夹命名为in和out,并且需要在文件夹in中放置一个测试文件。由于tiffdump接收一个.tiff文件作为输入,所以这里我们需要提供的就是一个或者一些.tiff文件,作为fuzzing的初始文件。随后在AFL的fuzzing过程中,会将这些文件作为随机化的初始种子进行变化。

我们可以在网上找一些.tiff文件,但是为了执行的速度,一般我们选取的文件不要过大,建议小于1KB。幸运的是,对于一些常见的文件格式,AFL已经自带了相应的初始测试文件,其按照类别保存在AFL源码的testcases目录下。我们在其中找到not_kitty.tiff并复制到之前的目录in中即可。

最后,就是进行fuzzing了。我们运行以下命令:

afl-fuzz -i in -o out <path to tiffdump> @@

用于fuzzing的afl-fuzz,主要接收-i和-o这2个参数,分别用于指定我们之前创建的输入和输出文件夹。紧接其后的,便是要fuzz的目标二进制程序和命令行参数了。这里的@@是有特殊含义,其测试输入文件的名称(路径)。由于tiffdump的基本用法就是直接将.tiff文件名作为参数提供给他,所以这里我们的格式就是简单的tiffdump @@。如果想要使用tiffdump的其他参数,可以通过

tiffdump [args] @@

来指定。

如果没有出错,那么我们就会遇到这样的界面:

《American Fuzzy Lop使用》

这就是AFL的主用户界面了。

AFL界面信息简介

在AFL的用户界面中,实时展示了许多fuzzing的状态。我们主要关心以下信息。

  1. 运行时间信息

《American Fuzzy Lop使用》

这里展示了当前fuzzer的运行时间、最近一次发现新执行路径的时间、最近一次崩溃的时间、最近一次超时的时间。值得注意的是第2项,最近一次发现新路径的时间。如果由于目标二进制文件或者命令行参数出错,那么其执行路径应该是一直不变的,所以如果从fuzzing开始一直没有发现新的执行路径,那么就要考虑是否有二进制或者命令行参数错误的问题了。对于此状况,AFL也会智能地进行提醒。

  1. 总体状态

《American Fuzzy Lop使用》

这里包括运行的总周期数、总路径数、崩溃次数、超时次数。其中,总周期数可以用来作为何时停止fuzzing的参考。随着不断地fuzzing,周期数会不断增大,其颜色也会由洋红色,逐步变为黄色、蓝色、绿色。一般来说,当其变为绿色时,代表可执行的内容已经很少了,继续fuzzing下去也不会有什么新的发现了。此时,我们便可以通过Ctrl-C,中止当前的fuzzing。

  1. 执行状态

《American Fuzzy Lop使用》

这里包括正在测试的fuzzing策略、进度、目标的执行总次数、目标的执行速度。执行速度可以直观地反映当前跑的快不快,如果速度过慢,比如低于500次每秒,那么测试时间就会变得非常漫长。如果发生了这种情况,那么我们需要进一步调整优化我们的fuzzing。

以上是一些主要的运行状态指标,完整解释可参考http://lcamtuf.coredump.cx/afl/status_screen.txt

AFL运行结果

之前在运行afl-fuzz时指定的out目录,便是AFL用于保存执行状态和结果的目录。待执行的队列保存在目录queue中,造成目标超时的输入文件在目录hangs中,造成目标异常退出的输入文件在目录crashes中。我们最关心的当然是crashes,如果某个输入文件造成了目标二进制的异常退出(SIGABRT, SIGSEGV),那么该输入文件就会保存下来,用于之后的调试。这些文件的文件名中,包含了目标崩溃的原因(例如sig:06代表SIGABRT),这一信息可作为进一步筛选的依据。

AFL进阶使用

除了上面介绍的基本操作,AFL还支持以下进阶操作。

编译优化

之前提到的llvm-mode,可以在编译过程中对某些操作进一步分解,从而增大命中特殊路径的概率。此外,其还可以进行进一步优化。相比afl-gcc和afl-clang,使用llvm-mode的afl-clang-fast可以提高执行速度和效率,因此建议使用。

持久模式

正常情况下,对于每一个生成的测试文件,都会fork()出一个新的目标进程进行处理,而大量fork()无疑会带来一定开销。为此,llvm-mode支持持久模式。在持久模式下,每次fork()得到的进程,会对一批而非单个测试文件进行处理,从而减少了开销,提高了执行速度。这也是为什么建议使用llvm-mode的另一个原因。

持久模式的使用,是通过宏__AFL_LOOP(NUM)完成。例如,以下代码会处理2000个输入文件后再退出。

《American Fuzzy Lop使用》

不过使用持久模式,需要注意在循环中完成环境的重置、资源的释放,以避免初始状态错误或者资源耗尽的问题。

并行化

如果主机有多个CPU,那么可以考虑使用并行化,从而进一步加快执行速度。AFL最多支持个fuzzer同时运行,一般会启动一个主fuzzer和一批从fuzzer。主fuzzer和从fuzzer的启动命令基本一致,只需要通过-M 和-S 选项来指定是否为主/从fuzzer及命令。

ASAN

Address Sanitizer(ASAN)是clang和gcc支持的功能,用于运行时检查内存访问。开启之后,会在目标代码的关键位置,如mallc(), free(),栈上buffer分配等处添加检查代码,一旦发生内存访问错误,如堆栈溢出、UAF、double free等,就可以SIGABRT中止程序。

由于有些内存访问错误并不一定会造成程序崩溃,如越界读,因此在没有开启ASAN的情况下,许多内存漏洞是无法被AFL发现的。所以,编译目标二进制代码时,开启ASAN,也是推荐的做法。对于使用afl-xxx编译来说,只需要设定环境变量AFL_USE_ASAN=1即可。

不过,由于开启ASAN后fuzzing会消耗更多内存,所以这也是需要考虑的因素之一。对于32位程序,基本上800MB即可;但64位程序大概需要20TB!所以,如果要使用ASAN,建议添加CFLAGS=-m32指定编译目标为32位;否则,很有可能因为64位消耗内存过多,程序崩溃。

如果使用了ASAN,还需要注意为afl-fuzz通过选项-m 指定可使用的内存上限。一般对于启用了ASAN的32位程序,-m 1024即可。

初始文件处理

一般来说,提供给afl-fuzz的初始测试文件越少,文件越小,那么测试的速度越快。为此,AFL提供了afl-cmin和afl-tmin两个工具。afl-cmin用于将输入文件去重,即只保留会触发不同路径的文件。afl-tmin则是作用于单个文件,其通过不断删除字节直至改变所有字节都会触发路径变化,将一个测试文件最小化。

指定文件词典

默认情况下,AFL会根据初始测试文件进行不断的随机化。而往往我们的输入文件必须满足一定规则,如文件magic number等。为了避免生成无意义的测试文件,AFL支持指定生成词典。在AFL源码的dictionaries目录下就已经包含了一些常见文件的生成词典,使用时只需为afl-fuzz添加-x 选项即可。具体词典的原理和编写规则可参考AFL的README文件。

使用ramdisk

由于在AFL测试过程中会持续大量产生测试文件,因此这大量的文件IO也会对性能和硬盘寿命产生影响。为此,我们可以通过Linux系统的tmpfs虚拟文件格式,将文件系统映射到内存中,并将afl-fuzz的out目录创建在这块ramdisk中,从而提高IO速度并减少对硬盘的开销,一举两得。

不过需要注意的是,在系统重启后tmpfs中的文件会丢失,所以在重启系统前,切记将其中out目录的内容都复制到磁盘上,以避免数据丢失。

以上就是AFL的基本信息,具体的使用帮助可以参考AFL官网上的相关介绍页面。