AFL使用指南
0x00 前言
二进制分析方面主要利用技术包括:动态分析(Dynamic Analysis)、静态分析(Static Analysis)、符号化执行(Symbolic Execution)、Constraint Solving、资讯流追踪技术(Data Flow Tracking)以及自动化测试(Fuzz Testing)
0x01 AFL的基本使用
1. 使用afl-gcc
1.1 使用AFL插桩程序
目标程序
#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进行插桩编译
afl-gcc -g -o ./zerotest/vuln ./zerotest/vuln.c
PS:
如果目标程序中有Makefile,那么分两种情况:
- 程序是用autoconf构建,那么此时只需要执行如下即可
./configure CC="afl-gcc" CXX="afl-g++"
此外,还可以执行如下语句设置LD_LIBRARY_PATH让程序加载经过AFL插桩的.so文件,进行静态构建而不是动态链接
./configure --disable-shared CC="afl-gcc" CXX="afl-g++"
- 程序不是用autoconf构建,那么直接修改Makefile文件中的编译器为
afl-gcc/g++
。
为了后期更好的分析crash,在此处可以开启Address Sanitizer(ASAN)这个内存检测工具,此工具可以更好的检测出缓存区溢出、UAF 等内存漏洞,开启方法如下:
AFL_USE_ASAN=1 ./configure CC=afl-gcc CXX=afl-g++ LD=afl-gcc--disable-shared
AFL_USE_ASAN=1 make
不使用 AFL 编译插桩时,可使用以下方式开启 Address Sanitizer。
./configure CC=gcc CXX=g++ CFLAGS="-g -fsanitize=address"
make
1.2 开始fuzz
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 -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的结果
从界面上主要注意以下几点:
- last new path 如果报错那么要及时修正命令行参数,不然继续fuzz也是徒劳(因为路径是不会改变的);
- cycles done 如果变绿就说明后面及时继续fuzz,出现crash的几率也很低了,可以选择在这个时候停止
- uniq crashes 代表的是crash的数量
1.4 crash分析
PS: xxd命令的作用就是将一个文件以十六进制的形式显示出来
可以看到已经得到的几个crash文件,那么分析的话只需要将其作为之前vuln文件的输入,使用gdb调试分析就可以得到详细结果了,但是在这之前可以使用xxd看一下其中数据的内容做一个初步的判断。
分别看一下这几个crash的信息
- 可以看到应该是满足了开头是F且字符串长度为6的异常退出情况
- 看这个数据情况可能是栈溢出
- 栈溢出
- 符合首字符为A且栈溢出
- 格式化字符串?可能
- 符合首字符为A且字符串长度为66的异常退出情况
主要参考:
《初探Fuzz-AFL》
1.5 语料库蒸馏(Corpus Distillation)
一般来说在进行fuzz之前构建一份有效的语料库是十分有必要的,这将作为程序开始时的种子。
语料库的信息来源主要如下:
- 使用项目自身提供的测试用例
- 目标程序bug提交页面
- 使用格式转换器,用从现有的文件格式生成一些不容易找到的文件格式:
- afl源码的testcases目录下提供了一些测试用例
- 其他开源的语料库
收集完后可以使用afl提供的工具来对语料库进行进一步的处理:
- afl-cmin: 移除执行相同代码的输入文件
afl-cmin的核心思想是: 尝试找到与语料库全集具有相同覆盖范围的最小子集。
它一般的两种执行模式是:
afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params]
afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] @@
- afl-tmin: 减小单个输入文件的大小
它有两种工作模式: instrumented mode和crash mode。默认的工作方式是instrumented mode
# instrumented mode
afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@
# crash mode 将会剔除导致crash的文件
afl-tmin -x -i input_file -o output_file -- /path/to/tested/program [params] @@
由于只能针对单个目标进行使用,因此使用如下shell脚本进行批量处理
for i in *; do afl-tmin -i $i -o tmin-$i -- ~/path/to/tested/program [params] @@; done;
或者修改如下的Python脚本进行预处理
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速度,因此针对大型项目可以考虑启用。
下载必要的安装包
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
解压缩
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
源码合并
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
编译安装
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
上面的编译安装对硬件配置和硬盘的空间要求比较高,所以你可以直接使用源进行安装,比如:
apt install llvm clang
编译安装afl的llvm模块
(我的使用的是kali linux 2019.1进行编译的,clang版本过高会失败,使用clang++也会失败,所以最终发现下面方法可行)
cd afl/llvm_mode
export CXX=/usr/bin/g++
export CC=/usr/bin/clang-6.0
make
因为clang没有办法使用update-alternatives
,因此我直接修改软连接
ln -s /usr/bin/clang-6.0 /usr/bin/clang
ln -sb /usr/bin/clang++-6.0 /usr/bin/clang++
之后就可以正常使用afl-clang-fast了
其实以上均太费劲,还有更简单的方法,kali linux的源中包含了afl,所以可以直接apt进行安装,装好之后afl-clang-fast也就有了
apt install afl
2.2 使用LLVM Mode模式进行fuzz
编译插桩
root@kali-z ~/Desktop/fuzz/afl$ ./afl-clang-fast -g -o ./zerotest/vuln-fast ./zerotest/vuln.c
之后重复上面的方式进行fuzz即可,接下来展示一个使用此模式fuzz php内核代码的例子。
1. 下载目标代码
wget https://github.com/php/php-src/archive/php-7.2.11.tar.gz && tar xf php-7.2.11.tar.gz
2. 进行编译插桩
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
修改之后 sapi/cli/php_cli.c
修改完之后执行如下进行rebuild
AFL_USE_ASAN=1 make
PS: 之所以进行这样的修改,是因为我们使用php -r
来eval php string,因此定位到sapi/cli/php_cli.c
进行代码的修改离开提升后期fuzz的效率。
4. 构造一个输入点
我们想在fuzz的时候从stdin进行数据的输入,因此构造如下输入点
unserialize(file_get_contents(“php://stdin”));
5. 根据上述的构造点构造输入数据
此处账户要考虑构造不同类类型的输入数据,构造如下
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可见。
USE_ZEND_ALLOC=0 screen -S zeroyu
使用screen可以随时进入查看fuzz的结果
screen -r zeroyu
使用如下命令开始fuzz
cd..
afl-fuzz -i serialized_data -o basic_fuzz -m none -- ./sapi/cli/php -r 'unserialize(file_get_contents("php://stdin"));'
7. 分析crash
用是使用如下bash脚本来寻找可能是bug的crash,因为有些是良性的crash,是由于ASAN无法分配足够的内存。这是因为ASAN需要额外的内存来跟踪所有分配,而精心编制的序列化对象可能会触发大内存分配。
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 单系统并行
查看系统核心数
cat /proc/cpuinfo| grep "cpu cores"| uniq
afl-fuzz并行Fuzzing,一般的做法是通过-M参数指定一个主Fuzzer(Master Fuzzer)、通过-S参数指定多个从Fuzzer(Slave Fuzzer)。
$ 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脚本同步分发到其他机器上解压。
#!/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结果的简单分析和分类
- crash exploration mode
afl-fuzz的一种运行模式,也称为peruvian rabbit mode,用于确定bug的可利用性,其输入的是crash的信息,之后使用-C
启用这种模式,afl会自动探索并创造与之相关的crash来帮助你进行分析,比如判断能够控制某块内存地址的长度。
afl-fuzz -m none -C -i ./fuzz_out/crashes -o ./peruvian-were-rabbit_out -- ./vuln -f
- triage_crashes.sh
AFL源码的experimental目录中有一个名为triage_crashes.sh的脚本,可以帮助我们触发收集到的crashes。
直接使用脚本跟参数的话,我们可以看到相关crash情况的寄存器等信息,但是如果只是大致分类的话,可以使用如下命令
/root/Desktop/fuzz/afl/experimental/crash_triage/triage_crashes.sh ./fuzz_out ./vuln 2>&1 | grep SIGNAL
效果如下,11代表了SIGSEGV信号,有可能是因为缓冲区溢出导致进程引用了无效的内存
- crashwalk
优点:可以显示更为详细的信息
项目地址: https://github.com/bnagy/crashwalk
# 手动模式
~/go/bin/cwtriage -root ./fuzz_out/crashes -match id -- ./vuln -f
# afl自动化模式
~/go/bin/cwtriage -root ./fuzz_out/crashes -afl
- afl-collect
项目地址: https://github.com/rc0r/afl-utils
afl-collect基于exploitable来检查crashes的可利用性。它可以自动删除无效的crash样本、删除重复样本以及自动化样本分类。
afl-collect -j 8 -d crashes.db -e gdb_script ./fuzz_out ./fuzz_in -- ./vuln --target-opts
效果如下
5. 代码覆盖率
原理部分参考:
《AFL漏洞挖掘技术漫谈(二):Fuzz结果分析和代码覆盖率》
afl-cov的使用说明如下:
首先使用gcov重新编译源码
gcc -fprofile-arcs -ftest-coverage vuln.c -o vuln_cov
如果遇到需要make进行编译的文件,执行如下:
$ 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来计算覆盖率
afl-cov -d ./fuzz_out --live --enable-branch-coverage -c . -e "cat AFL_FILE | ./vuln_cov AFL_FILE"
同时进行对插桩过的vuln
的fuzz
afl-fuzz -i ./fuzz_in -o ./fuzz_out ./vuln -f
最终效果如下
生成的报告会保存在/path/to/afl-fuzz-output/cov/web/lcov-web-final
路径下。