AFL使用指南

0x00 前言

二进制分析方面主要利用技术包括:动态分析(Dynamic Analysis)、静态分析(Static Analysis)、符号化执行(Symbolic Execution)、Constraint Solving、资讯流追踪技术(Data Flow Tracking)以及自动化测试(Fuzz Testing)

AFL原理介绍参考:
《AFL漏洞挖掘技术漫谈(一):用AFL开始你的第一次Fuzzing》

本指南使用的环境是 kali linux 2019.1

0x01 AFL的基本使用

1. 使用afl-gcc

1.1 使用AFL插桩程序

目标程序

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进行插桩编译

1
afl-gcc -g -o ./zerotest/vuln ./zerotest/vuln.c

PS:
如果目标程序中有Makefile,那么分两种情况:

  1. 程序是用autoconf构建,那么此时只需要执行如下即可
1
./configure CC="afl-gcc" CXX="afl-g++"

此外,还可以执行如下语句设置LD_LIBRARY_PATH让程序加载经过AFL插桩的.so文件,进行静态构建而不是动态链接

1
./configure --disable-shared CC="afl-gcc" CXX="afl-g++"
  1. 程序不是用autoconf构建,那么直接修改Makefile文件中的编译器为afl-gcc/g++

为了后期更好的分析crash,在此处可以开启Address Sanitizer(ASAN)这个内存检测工具,此工具可以更好的检测出缓存区溢出、UAF 等内存漏洞,开启方法如下:

1
2
AFL_USE_ASAN=1 ./configure CC=afl-gcc CXX=afl-g++ LD=afl-gcc--disable-shared
AFL_USE_ASAN=1 make

不使用 AFL 编译插桩时,可使用以下方式开启 Address Sanitizer。

1
2
./configure CC=gcc CXX=g++ CFLAGS="-g -fsanitize=address"
make

1.2 开始fuzz

fuzz的语法一般情况是两种:

  1. 直接从stdin读取输入的目标程序
1
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program […params…]
  1. 从文件读取输入的目标程序,@@就是占位符,表示输入替换的位置
1
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

此处我采用第一种方式

1
afl-fuzz -m 300 -i ./zerotest/fuzz_in -o ./zerotest/fuzz_out ./zerotest/vuln -f

PS: 常见参数的含义如下

  • -f参数表示:testcase的内容会作为afl_test的stdin
  • -m参数表示分配的内存空间
  • -i 指定测试样本的路径
  • -o 指定输出结果的路径
  • /dev/null 使错误信息不输出到屏幕
  • -t:设置程序运行超时值,单位为 ms
  • -M:运行主(Master) Fuzzer
  • -S:运行从属(Slave) Fuzzer

1.3 fuzz的结果

60889809-4D22-4C14-AF3C-E98D2C5EB60D.png

从界面上主要注意以下几点:

  1. last new path 如果报错那么要及时修正命令行参数,不然继续fuzz也是徒劳(因为路径是不会改变的);
  2. cycles done 如果变绿就说明后面及时继续fuzz,出现crash的几率也很低了,可以选择在这个时候停止
  3. uniq crashes 代表的是crash的数量

1.4 crash分析

PS: xxd命令的作用就是将一个文件以十六进制的形式显示出来

1FF9D91F-F823-4DF7-B716-C68B58134D65.png

可以看到已经得到的几个crash文件,那么分析的话只需要将其作为之前vuln文件的输入,使用gdb调试分析就可以得到详细结果了,但是在这之前可以使用xxd看一下其中数据的内容做一个初步的判断。

分别看一下这几个crash的信息

  1. 可以看到应该是满足了开头是F且字符串长度为6的异常退出情况
    63005FA6-59BD-48BD-8B19-F5EF08C84D4E.png
  2. 看这个数据情况可能是栈溢出
    D74734D1-F13E-4256-B47C-260817BE102E.png
  3. 栈溢出
    B909DBCF-66FF-4C37-BF39-1ED413A6B0C3.png
  4. 符合首字符为A且栈溢出
    8213201E-7B35-45D7-9057-5493978CCABB.png
  5. 格式化字符串?可能
    C7F22A69-F1E6-4863-B9BD-08D88A82C789.png
  6. 符合首字符为A且字符串长度为66的异常退出情况
    41BAF060-7874-4D52-93CC-49FD86AF26D3.png

主要参考:
《初探Fuzz-AFL》

1.5 语料库蒸馏(Corpus Distillation)

一般来说在进行fuzz之前构建一份有效的语料库是十分有必要的,这将作为程序开始时的种子。

语料库的信息来源主要如下:

  • 使用项目自身提供的测试用例
  • 目标程序bug提交页面
  • 使用格式转换器,用从现有的文件格式生成一些不容易找到的文件格式:
  • afl源码的testcases目录下提供了一些测试用例
  • 其他开源的语料库

收集完后可以使用afl提供的工具来对语料库进行进一步的处理:

  1. afl-cmin: 移除执行相同代码的输入文件
    afl-cmin的核心思想是: 尝试找到与语料库全集具有相同覆盖范围的最小子集。
    它一般的两种执行模式是:
1
afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params]
1
afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] @@
  1. afl-tmin: 减小单个输入文件的大小
    它有两种工作模式: instrumented mode和crash mode。默认的工作方式是instrumented mode
1
2
# instrumented mode
afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@
1
2
# crash mode 将会剔除导致crash的文件
afl-tmin -x -i input_file -o output_file -- /path/to/tested/program [params] @@

由于只能针对单个目标进行使用,因此使用如下shell脚本进行批量处理

1
for i in *; do afl-tmin -i $i -o tmin-$i -- ~/path/to/tested/program [params] @@; done;

或者修改如下的Python脚本进行预处理

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import os
import sys
import shutil
def cmin():
command = ' -m 300 -t 5000 ./utilities/magick convert @@ /dev/null'
os.system('afl-cmin -i seeds/tmin -o seeds/cmin ' + command)
def tmin():
command = ' -m 300 -t 5000 ./utilities/magick convert @@ /dev/null'
seed_list = os.listdir('seeds/all_format')
for seed in seed_list:
in_file = os.path.join('seeds/all_format', seed)
out_file = os.path.join('seeds/tmin', seed)
if os.path.getsize(in_file) > 1024*1:
if os.path.getsize(in_file) < 1024*3 and not seed.endswith('.txt'):
os.system('afl-tmin -i ' + in_file + ' -o ' + out_file + command)
print('afl-tmin -i ' + in_file + ' -o ' + out_file + command)
else:
pass
elif os.path.getsize(in_file) > 0:
shutil.copyfile(in_file,out_file)
else:
pass
def convert(origin_seeds):
seed_list = os.listdir(origin_seeds)
for seed in seed_list:
seed_in = os.path.join(origin_seeds, seed)
file_name = (os.path.splitext(seed)[0])
coder_list = os.listdir('coders')
for cfile in coder_list:
if cfile.endswith('.c'):
extern = cfile[:cfile.find('.c')]
seed_out = 'seeds/all_format/' + file_name + '.' + extern
os.system('utilities/magick convert ' + seed_in + ' ' + seed_out)
if __name__ == '__main__':
if len(sys.argv) < 2:
print 'Usage: ' + sys.argv[0] + ' origin_seeds_dir'
else:
origin_seeds_dir = sys.argv[1]
try:
os.mkdir('seeds')
seeds_path = os.path.join(os.path.abspath('.'),'seeds')
os.mkdir(os.path.join(seeds_path,'all_format'))
os.mkdir(os.path.join(seeds_path,'cmin'))
os.mkdir(os.path.join(seeds_path,'tmin'))
except:
print 'make dir fail!'
convert(origin_seeds_dir)
tmin()
cmin()

预处理脚本来自:《使用 AFL 进行模糊测试》

2. LLVM Mode模式

2.1 启用llvm

LLVM Mode模式编译程序可以获得更快的Fuzzing速度,因此针对大型项目可以考虑启用。

下载必要的安装包

1
2
3
4
wget http://releases.llvm.org/8.0.0/llvm-8.0.0.src.tar.xz
wget http://releases.llvm.org/8.0.0/compiler-rt-8.0.0.src.tar.xz
wget http://releases.llvm.org/8.0.0/clang-tools-extra-8.0.0.src.tar.xz
wget http://releases.llvm.org/8.0.0/cfe-8.0.0.src.tar.xz

解压缩

1
2
3
4
5
xz -d ./*
tar xvf cfe-8.0.0.src.tar
tar xvf clang-tools-extra-8.0.0.src.tar
tar xvf llvm-8.0.0.src.tar
tar xvf compiler-rt-8.0.0.src.tar

源码合并

1
2
3
4
5
6
mv cfe-8.0.0.src clang
mv clang llvm-8.0.0.src/tools
mv clang-tools-extra-8.0.0.src extra
mv extra llvm-8.0.0.src/tools/clang
mv compiler-rt-8.0.0.src compiler-rt
mv compiler-rt llvm-8.0.0.src/projects

编译安装

1
2
3
4
5
mkdir build-8.0
cmake ../llvm-8.0.0.src/
cmake --build .
cmake --build . --target install
cmake -DCMAKE_INSTALL_PREFIX=/tmp/llvm -P cmake_install.cmake

上面的编译安装对硬件配置和硬盘的空间要求比较高,所以你可以直接使用源进行安装,比如:

1
apt install llvm clang

编译安装afl的llvm模块
(我的使用的是kali linux 2019.1进行编译的,clang版本过高会失败,使用clang++也会失败,所以最终发现下面方法可行)

1
2
3
4
cd afl/llvm_mode
export CXX=/usr/bin/g++
export CC=/usr/bin/clang-6.0
make

因为clang没有办法使用update-alternatives,因此我直接修改软连接

1
2
ln -s /usr/bin/clang-6.0 /usr/bin/clang
ln -sb /usr/bin/clang++-6.0 /usr/bin/clang++

之后就可以正常使用afl-clang-fast了
9B83B620-E1A2-420F-81CA-FC6C4777F4EB.png

其实以上均太费劲,还有更简单的方法,kali linux的源中包含了afl,所以可以直接apt进行安装,装好之后afl-clang-fast也就有了

1
apt install afl

2.2 使用LLVM Mode模式进行fuzz

编译插桩

1
root@kali-z ~/Desktop/fuzz/afl$ ./afl-clang-fast -g -o ./zerotest/vuln-fast ./zerotest/vuln.c

之后重复上面的方式进行fuzz即可,接下来展示一个使用此模式fuzz php内核代码的例子。

1. 下载目标代码
1
wget https://github.com/php/php-src/archive/php-7.2.11.tar.gz && tar xf php-7.2.11.tar.gz
2. 进行编译插桩
1
2
3
4
cd php-src-php-7.2.11
./buildconf --force
CC=afl-clang-fast CXX=afl-clang-fast++ ./configure
AFL_USE_ASAN=1 make

PS: 如果报错缺失libconv,则在Makefile中的EXTRA_LIBS =添加-liconv

3. 进行源代码的修改

未修改之前 sapi/cli/php_cli.c
BB750539-176B-4A52-A437-D370814C7CCE.png
修改之后 sapi/cli/php_cli.c
C21FA188-6FFE-440A-B0AD-BA0F8C60312C.png
修改完之后执行如下进行rebuild

1
AFL_USE_ASAN=1 make

PS: 之所以进行这样的修改,是因为我们使用php -r来eval php string,因此定位到sapi/cli/php_cli.c进行代码的修改离开提升后期fuzz的效率。

4. 构造一个输入点

我们想在fuzz的时候从stdin进行数据的输入,因此构造如下输入点

1
unserialize(file_get_contents(“php://stdin”));
5. 根据上述的构造点构造输入数据

此处账户要考虑构造不同类类型的输入数据,构造如下

1
2
3
4
5
6
7
mkdir serialized_data && cd serialized_data
../sapi/cli/php -r 'echo serialize("a");' > string
../sapi/cli/php -r 'echo serialize(1);' > number
../sapi/cli/php -r 'echo serialize([1,2]);' > array_of_num
../sapi/cli/php -r 'echo serialize(["1","2"]);' > array_of_str
../sapi/cli/php -r 'echo serialize([["1","2"],["3","4"],[1,2]]);' > array_of_array
echo 'O:6:"zeroyu":1:{s:4:"test";O:7:"npusec2":1:{s:5:"test2";s:10:"phpinfo();";}}' > class
6. 开始fuzz

为了从地址清理(ASAN)中获得有用的结果,有必要设置一个环境变量,以便PHP禁用其自定义内存分配器,从而使内存安全问题对ASAN可见。

1
USE_ZEND_ALLOC=0 screen -S zeroyu

使用screen可以随时进入查看fuzz的结果

1
screen -r zeroyu

使用如下命令开始fuzz

1
2
cd..
afl-fuzz -i serialized_data -o basic_fuzz -m none -- ./sapi/cli/php -r 'unserialize(file_get_contents("php://stdin"));'

11B4692E-E516-4743-9FAC-B39AB1C03ED0.png

7. 分析crash

用是使用如下bash脚本来寻找可能是bug的crash,因为有些是良性的crash,是由于ASAN无法分配足够的内存。这是因为ASAN需要额外的内存来跟踪所有分配,而精心编制的序列化对象可能会触发大内存分配。

1
for FILE in $(ls id*); do cat $FILE | ../../sapi/cli/php -r "unserialize(file_get_contents('php://stdin'));" 2>&1 | grep -E "SUMMARY|ERROR" | grep -v "LargeMmap" && echo $FILE; done

参考: 《Fuzzing PHP for Fun and Profit》

3. 黑盒测试

参考:《AFL漏洞挖掘技术漫谈(一):用AFL开始你的第一次Fuzzing》

4. 并行测试

4.1 单系统并行

查看系统核心数

1
cat /proc/cpuinfo| grep "cpu cores"| uniq

afl-fuzz并行Fuzzing,一般的做法是通过-M参数指定一个主Fuzzer(Master Fuzzer)、通过-S参数指定多个从Fuzzer(Slave Fuzzer)。

1
2
3
$ screen afl-fuzz -i testcases/ -o sync_dir/ -M fuzzer1 -- ./program
$ screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer2 -- ./program
$ screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer3 -- ./program

PS: -o指定的是一个同步目录,并行测试中,所有的Fuzzer将相互协作,在找到新的代码路径时,相互传递新的测试用例。所以不用担心重复的问题

两个辅助工具:

  • afl-whatsup工具可以查看每个fuzzer的运行状态和总体运行概况,加上-s选项只显示概况,其中的数据都是所有fuzzer的总和。
  • afl-gotcpu工具可以查看每个核心使用状态。

4.2 多系统并行

压缩每个fuzzer实例目录中queue下的文件,通过如下SSH脚本同步分发到其他机器上解压。

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
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/sh
# authorized_keys的方式进行ssh认证
# 所有要同步的主机
FUZZ_HOSTS='172.21.5.101 172.21.5.102'
# SSH user
FUZZ_USER=root
# 同步目录
SYNC_DIR='/root/syncdir'
# 同步间隔时间
SYNC_INTERVAL=$((30 * 60))
if [ "$AFL_ALLOW_TMP" = "" ]; then
if [ "$PWD" = "/tmp" -o "$PWD" = "/var/tmp" ]; then
echo "[-] Error: do not use shared /tmp or /var/tmp directories with this script." 1>&2
exit 1
fi
fi
rm -rf .sync_tmp 2>/dev/null
mkdir .sync_tmp || exit 1
while :; do
# 打包所有机器上的数据
for host in $FUZZ_HOSTS; do
echo "[*] Retrieving data from ${host}..."
ssh -o 'passwordauthentication no' ${FUZZ_USER}@${host} \
"cd '$SYNC_DIR' && tar -czf - SESSION*" >".sync_tmp/${host}.tgz"
done
# 分发数据
for dst_host in $FUZZ_HOSTS; do
echo "[*] Distributing data to ${dst_host}..."
for src_host in $FUZZ_HOSTS; do
test "$src_host" = "$dst_host" && continue
echo " Sending fuzzer data from ${src_host}..."
ssh -o 'passwordauthentication no' ${FUZZ_USER}@$dst_host \
"cd '$SYNC_DIR' && tar -xkzf - &>/dev/null" <".sync_tmp/${src_host}.tgz"
done
done
echo "[+] Done. Sleeping for $SYNC_INTERVAL seconds (Ctrl-C to quit)."
sleep $SYNC_INTERVAL
done

0x02 Fuzz结果分析和代码覆盖率

1. 工作状态

afl-fuzz永远不会停止,所以何时停止测试很多时候就是依靠afl-fuzz提供的状态来决定的。具体的几种方式如下所示:

  • 状态窗口的cycles done变为绿色;
  • afl-whatsup查看afl-fuzz状态;
  • afl-stat得到类似于afl-whatsup的输出结果;
  • 定制afl-whatsup->在所有代码外面加个循环就好;
  • afl-plot绘制各种状态指标的直观变化趋势;
  • pythia估算发现新crash和path概率。

2. fuzz结束判断

  • 状态窗口中”cycles done”字段颜色变为绿色该字段的颜色可以作为何时停止测试的参考;
  • 距上一次发现新路径(或者崩溃)已经过去很长时间了,至于具体多少时间还是需要自己把握;
  • 目标程序的代码几乎被测试用例完全覆盖,这种情况好像很少见;
  • pythia提供的各种数据中,path covera达到99或者correctness的值达到1e-08(含义: 从上次发现path/uniq crash到下一次发现之间大约需要1亿次执行)

3. 输出结果说明

queue:存放所有具有独特执行路径的测试用例。

crashes:导致目标接收致命signal而崩溃的独特测试用例。

crashes/README.txt:保存了目标执行这些crash文件的命令行参数。

hangs:导致目标超时的独特测试用例。

fuzzer_stats:afl-fuzz的运行状态。

plot_data:用于afl-plot绘图。

4. 对crash结果的简单分析和分类

  1. crash exploration mode
    afl-fuzz的一种运行模式,也称为peruvian rabbit mode,用于确定bug的可利用性,其输入的是crash的信息,之后使用-C启用这种模式,afl会自动探索并创造与之相关的crash来帮助你进行分析,比如判断能够控制某块内存地址的长度。
1
afl-fuzz -m none -C -i ./fuzz_out/crashes -o ./peruvian-were-rabbit_out -- ./vuln -f
  1. triage_crashes.sh
    AFL源码的experimental目录中有一个名为triage_crashes.sh的脚本,可以帮助我们触发收集到的crashes。

直接使用脚本跟参数的话,我们可以看到相关crash情况的寄存器等信息,但是如果只是大致分类的话,可以使用如下命令

1
/root/Desktop/fuzz/afl/experimental/crash_triage/triage_crashes.sh ./fuzz_out ./vuln 2>&1 | grep SIGNAL

效果如下,11代表了SIGSEGV信号,有可能是因为缓冲区溢出导致进程引用了无效的内存
0C1CB900-EA6C-4814-A67E-1FBE5994AB87.png

  1. crashwalk
    优点:可以显示更为详细的信息
    项目地址: https://github.com/bnagy/crashwalk
1
2
3
4
# 手动模式
~/go/bin/cwtriage -root ./fuzz_out/crashes -match id -- ./vuln -f
# afl自动化模式
~/go/bin/cwtriage -root ./fuzz_out/crashes -afl

6DD9C8EA-E494-4633-9F84-3F6B7959CD74.png

  1. afl-collect

项目地址: https://github.com/rc0r/afl-utils
afl-collect基于exploitable来检查crashes的可利用性。它可以自动删除无效的crash样本、删除重复样本以及自动化样本分类。

1
afl-collect -j 8 -d crashes.db -e gdb_script ./fuzz_out ./fuzz_in -- ./vuln --target-opts

效果如下

D9712AE7-55FB-4A82-B974-0D418D4C85D7.png

5. 代码覆盖率

原理部分参考:
《AFL漏洞挖掘技术漫谈(二):Fuzz结果分析和代码覆盖率》

afl-cov的使用说明如下:
首先使用gcov重新编译源码

1
gcc -fprofile-arcs -ftest-coverage vuln.c -o vuln_cov

如果遇到需要make进行编译的文件,执行如下:

1
2
3
4
$ make clean
$ ./configure --prefix=/root/tiff-4.0.10/build-cov CC="gcc" CXX="g++" CFLAGS="-fprofile-arcs -ftest-coverage" --disable-shared
$ make
$ make install

之后使用afl-cov来计算覆盖率

1
afl-cov -d ./fuzz_out --live --enable-branch-coverage -c . -e "cat AFL_FILE | ./vuln_cov AFL_FILE"

同时进行对插桩过的vuln的fuzz

1
afl-fuzz -i ./fuzz_in -o ./fuzz_out ./vuln -f

最终效果如下
60FD6E10-65F2-4CC1-B270-218F679D59B0.png

生成的报告会保存在/path/to/afl-fuzz-output/cov/web/lcov-web-final路径下。