23R3F's Blog

初探Fuzz-AFL

字数统计: 2.3k阅读时长: 8 min
2019/03/04 Share

最近想学习一波fuzz,希望能往实际挖洞方向进行学习,光打CTF怕是会找不到工作,嘤嘤嘤

Fuzz-AFL介绍

Fuzzing是指通过构造测试输入,对软件进行大量测试来发现软件中的漏洞的一种模糊测试方法。在CTF中,fuzzing可能不常用,但在现实的漏洞挖掘中,fuzzing因其简单高效的优势,成为非常主流的漏洞挖掘方法。

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

工作原理:

通过对源码进行重新编译时进行插桩(简称编译时插桩)的方式自动产生测试用例来探索二进制程序内部新的执行路径。AFL也支持直接对没有源码的二进制程序进行测试,但需要QEMU的支持。

安装

直接去官网下载压缩包,解压后在目录中打开终端输入:

  • make
  • sudo make install

输入以上命令后基本就能安装成功了,在终端输入afl-后tab,就能出现以下这些命令了

1551667659270

说明安装成功

使用AFL插桩程序(有源码)

这里就以一个简单的c语言的程序作为例子来试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h> 
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

int vuln(char *str)
{
int len = strlen(str);
if(str[0] == 'A' && len == 66)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为A并且长度为66,则异常退出
}
else if(str[0] == 'F' && len == 6)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为F并且长度为6,则异常退出
}
else
{
printf("it is good!\n");
}
return 0;
}

int main(int argc, char *argv[])
{
char buf[100]={0};
gets(buf);//存在栈溢出漏洞
printf(buf);//存在格式化字符串漏洞
vuln(buf);

return 0;
}

总的流程概述:

首先是用afl-gcc编译源代码,然后以文件(最好小于1K)为输入,然后启动afl-fuzz程序,将testcase(输入的测试文件)作为程序的输入执行程序,afl会在这个testcase的基础上进行自动变异输入,使得程序产生crash,产生了crash就会被记录起来

插桩编译

首先把上面的afl_test.c进行编译:

afl-gcc -g -o afl_test afl_test.c

同样的,如果是编译一个c++的源码,那就需要用afl-g++

接着建立两个文件夹:fuzz_in和fuzz_out,用来存放程序的输入和fuzz的输出结果

在fuzz_in中还需要创建一个testcase文件,在这个程序的例子里,只需要随便输入一点东西就行了,那么testcase中就写aaa就可以了

注意

在编译项目时,通常有Makefile,这时就需要在Makefile中添加内容

gcc/g++重新编译目标程序的方法是:
CC=/path/to/afl/afl-gcc ./configure
make clean all
对于一个C++程序,要设置:
CXX=/path/to/afl/afl-g++.

afl-clang和afl-clang++的使用方法类似。

开始fuzz

对那些可以直接从stdin读取输入的目标程序来说,语法如下:
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program […params…]
对从文件读取输入的目标程序来说,要用“@@”,语法如下:
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

输入命令:afl-fuzz -i fuzz_in -o fuzz_out ./afl_test

表示,从fuzz_in中读取输入,输出放入fuzz_out中,afl_test是我们要进行fuzz的程序,-f参数表示:testcase的内容会作为afl_test的stdin

接下来一般都会报错:

1551668549863

需要根据提示设置一波core_pattern

  • sudo su
  • echo core >/proc/sys/kernel/core_pattern

再次执行afl-fuzz -i fuzz_in -o fuzz_out ./afl_test

AFL界面

进入fuzz后,就会出现这样的界面1551669897129

下面对界面进行一波介绍:

process timing

这里展示了当前fuzzer的运行时间、最近一次发现新执行路径的时间、最近一次崩溃的时间、最近一次超时的时间。

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

overall results

这里包括运行的总周期数、总路径数、崩溃次数、超时次数。

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

stage progress

这里包括正在测试的fuzzing策略、进度、目标的执行总次数、目标的执行速度

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

以上是简单的介绍,如果要看完整的可以查看官方的文档

分析crash

通过上图,我们发现跑了五分钟以后就发现了6个crash

这时,可以去我们先前建立的fuzz_out目录看看产生了啥东西:

1551671028453

crashes文件夹里面是我们产生crash的样例,hangs里面是产生超时的样例,queue里面是每个不同执行路径的测试用例

我们来看看crash的6个样例:

第一个样例,发现符合栈溢出漏洞的crash情况

1551671569818

第二个样例,发现符合首字符为F且字符串长度为6的异常退出情况

1551671630129

第三个样例,发现符合格式化字符串的%n任意地址写的漏洞情况

1551671681451

第四个样例,发现符合栈溢出漏洞的crash情况

1551671755851

第五个样例,发现符合栈溢出漏洞的crash情况

1551671795372

第六个样例,发现符合首字符为A且字符串长度为66的异常退出情况

1551671837521

至此,我们自己编写的简单程序的所有漏洞都被检测出来了,这个过程只用了5分钟,afl可以说是很强大的

无源码AFL测试

上面是对进简单的有源码的程序进行测试的,但是实际情况下不可能每次都有源码进行插桩编译测试,afl使用了qemu模式进行测试,只要在之前的命令的基础上加上-Q的参数即可

但是要先进行安装,在afl的根目录打开终端执行以下命令

  • cd qemu_mode

  • ./build_qemu_support.sh

  • cd ..
  • make install

简单无源码fuzz

接着还是用上面的简单c代码进行测试,但是这次采用gcc进行编译,不再用afl-gcc

  • gcc -g -o afl_test2 afl_test.c

得到afl_test2程序后,就可以进行fuzz了

同样的要在开始fuzz前创建fuzz_in 、fuzz_out两个文件夹

执行命令:

  • afl-fuzz -i fuzz_in -o fuzz_out -Q ./afl_test2

结果我们可以看到;

1551683025174

同样的程序,在qemu 模式下比在源码编译插桩的模式下慢了很多,同样五分钟的时间,只爆了4个crash,执行速度也只有600+/s,和之前的4700+/s形成鲜明对比

fuzz readelf

这里再用一个例子来fuzz一下,测试readelf

由于readelf的输入其实就elf文件,因此需要在in目录中放一个输入elf

按照流程创建文件夹和测试用的elf

  • mkdir in out
  • cd in
  • cp afl-2.52b/testcases/others/elf/small_exec.elf . #afl目录中自带一些常用文件的testcase
  • cd ..
  • sudo cp /usr/bin/readelf . #把readelf复制到当前目录中来
  • afl-fuzz -i in -o out -Q ./readelf -a @@ #开始fuzz,@@表示从in文件夹中找elf作为输入,实际上就是在执行:readelf -a 文件名

1551703157911

可以看到,这里我跑了51分钟,硬是第一轮都没跑完,佛了,听说跑fuzz多了,硬盘会损坏,吓得我赶紧买了一个阿里云的服务器来跑:

1551703247983

嗯。。可以看到。。还是没有跑出个啥。。。readelf牛逼!

参考资料

http://galaxylab.org/afl%E4%BD%BF%E7%94%A8101/

https://www.freebuf.com/articles/system/191536.html

https://blog.csdn.net/abcdyzhang/article/details/53727221

https://stfpeak.github.io/2017/06/11/Finding-bugs-using-AFL/#afl%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B

https://blog.csdn.net/qq_36779888/article/details/79998920

https://www.cnblogs.com/WangAoBo/p/8280352.html#_label4

CATALOG
  1. 1. Fuzz-AFL介绍
  2. 2. 安装
  3. 3. 使用AFL插桩程序(有源码)
    1. 3.1. 插桩编译
    2. 3.2. 开始fuzz
    3. 3.3. AFL界面
    4. 3.4. 分析crash
  4. 4. 无源码AFL测试
    1. 4.1. 简单无源码fuzz
    2. 4.2. fuzz readelf
  5. 5. 参考资料