一、AFL简介
AFL(American Fuzzy Lop)号称是当前最高级的Fuzzing 测试工具之一 ,是由安全研究员Michał Zalewski(@lcamtuf)开发的一款 基于覆盖引导(Coverage-guided)的 模糊测试工具,它 通过记录输入样本的代码覆盖率,从而调整输入样本以提高覆盖率,增加发现漏洞的概率。
AFL 采用新型的 编译时插桩和 遗传算法自动发现新的测试用例,这些用例会触发目标 二进制文件中的新内部状态。这大大改善了模糊测试的代码覆盖范围。
AFL既可以对源码进行编译时插桩,也可以使用AFL的QEMU mode对二进制文件进行插桩,但是前者的效率相对来说要高很多。
AFL的 优点是可以轻松部署,配置相对简单,测试效率相对较高。 原生的AFL仅适配于C/C++程序的测试,不过目前已经衍生出很多分支,用于适配其他语言的模糊测试,如针对JAVA程序的Kelinci等 。
其 工作流程大致如下:
①从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage);
②选择一些输入文件,作为初始测试集加入输入队列(queue);
③将队列中的文件按一定的策略进行“突变”;
④如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;
⑤上述过程会一直循环进行,期间触发了crash的文件会被记录下来。
AFL变异的方式
AFL是采用遗传算法,基于变异生成的测试用例,变异的主要类型有下面这几种:
-
bitflip:按位翻转,1变为0,0变为1
-
arithmetic:整数加/减算术运算
-
interest:把一些特殊内容替换到原文件中。所谓的特殊内容是AFL预设的一些比较特殊的数,比如可能造成溢出的数。
-
dictionary:把自动生成或用户提供的token替换/插入到原文件中
-
havoc:“大破坏”,是前面几种变异的组合
-
splice:“连接”,此阶段会将两个文件拼接起来得到一个新的文件
二、AFL安装和使用
利用AFL进行模糊测试的大概 过程:①插桩编译;②设置种子测试用例;③开始fuzz 。模糊测试对于软件测试的帮助很大,其不受限于被测系统的内部实现细节和复杂程度。
2.1、安装
AFL 自带定制版本的 gcc 和 clang 编译器,建议选择 LLVM 的 clang 编译器,可以加快 fuzz 的速度。
下载AFL源码( GitHub - google/AFL: american fuzzy lop - a security-oriented fuzzer),解压后,编译安装:
AFL使用afl-gcc编译 :
make
sudo make install # 安装到系统目录
AFL使用afl-clang编译 :
cd llvm_mode
make
cd ..
sudo make install
查看路径可以看到afl安装的文件:
作用分别为
-
afl-gcc 和afl-g++ 分别对应的是gcc 和g++ 的封装
-
afl-clang 和afl-clang++ 分别对应clang 的c 和c++ 编译器封装À。
-
afl-fuzz 是AFL 的主体,用于对目标程序进行fuzz。
-
afl-analyze 可以对用例进行分析,通过分析给定的用例,看能否发现用例中有意义的字段。
-
afl-qemu-trace 用于qemu-mode ,默认不安装 ,需要手工执行qemu-mode 的编译脚本进行编译,后面会介绍。
-
afl-plot 生成测试任务的状态图
-
afl-tmin 和afl-cmin 对用例进行简化
-
afl-whatsup 用于查看fuzz 任务的状态
-
afl-gotcpu 用于查看当前CPU 状态
-
afl-showmap 用于对单个用例进行执行路径跟踪
afl-fuzz指令:
下面我们进行一个简单的AFL实验了解其使用流程。
2.2、准备被测程序
首先创建一个简单的test.c源文件,里面放入我们要测的c程序。
#include
#include
#include
#include
#include
int AFLTest(char *str)
{
int len = strlen(str);
if(str[0] == 'A' && len == 6)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为A并且长度为6,则异常退出
}
else if(str[0] == 'F' && len == 16)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为F并且长度为16,则异常退出
}
else if(str[0] == 'L' && len == 66)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为F并且长度为66,则异常退出
}
else
{
printf("it is good!\n");
}
return 0;
}
int main(int argc, char *argv[])
{
char buf[100]={0};
gets(buf);
//存在栈溢出漏洞
printf(buf);
//存在格式化字符串漏洞
AFLTest(buf);
return 0;
}
2.3、使用 afl-gcc 插桩编译
(编译插桩是指在代码编译期间修改或新增代码)
用 AFL 编译目标源码,其目的在于插桩,让编译得到的程序,反馈路径覆盖。AFL 自带定制版本的 gcc 和 clang 编译器,建议选择 LLVM 的 clang 编译器,可以加快 fuzz 的速度。
编译过程和普通gcc编译也是一样,除了使用的命令需要带上afl-前缀,因此
afl-gcc -g -o test test.c # 针对c文件, -g选项为使用gdb调试所需,如果不调试就不需加此选项。 在linux下编译链接程序,如果不加-o参数,生成的binary代码的名字都是默认的a.out
可以看到在编译过程中,编译器已经提示存在漏洞,不理会,用AFL去测试 。
编译后会有插桩符号,使用下面的命令可以验证这一点: readelf -s ./test | grep afl
注:
针对c++文件,则使用 afl-g++ -g -o test test.cpp
注意在编译项目时,通常有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++的使用方法类似。
2.4、准备种子语料库
2.4.1、搜集种子语料库
作为模糊测试,AFL需要提供初始的种子输入, 变异算法会根据此种子变异生成各种测试用例,喂给程序。但实际上,你完全可以提供任何无意义的输入作为种子,模糊测试也一般能达到效果,只不过效率会低一些而已,是否提供有意义种子?提供多少?无外乎在种子获取难度和测试的效率要求之间进行权衡而已。
这里,借用 Freebuf 提供的资料,给出一些开源的语料库
事实上很多程序也会自带一些案例,也可以作为测试用例。
在这里我们生成一好一坏两个种子语料库:
mkdir good-seeds bad-seeds
echo '1+1' > good-seeds/any-seed
echo 'this is a bad seed' > bad-seeds/any-seed
2.4.2、精简语料库
找到语料库之后,最好能够进行修剪, 合并重复用例,裁剪体积。 afl 推荐的每个用例体积小于 1KB,不然会影响 fuzz 的效率。
2.4.2.1、去重
afl-cmin 可以精简语料库,去掉可能重复的测试用例,可大大减少无用的 fuzz 用例
afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params]
更多的时候,我们是从文件中获取输入,因此,往往使用 @@ 替代 params(参数),即
afl-cmin -i input_dir -o output_dir -- /path/to/tested/program @@
2.4.2.2、裁剪体积
afl-tmin 可以缩短文件体积,因为 afl 要求测试用例的大小最好小于 1KB ,因此最好将精简后的用例进一步缩小体积。afl-tmin 有两种工作模式, instrumented mode 和 crash mode 。默认的工作方式是instrumented mode
afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@
由于 afl-cmin 一次性只能精简单个文件,如果用例特别多,需要手动花费很长时间,其实一条简单的 shell 脚本即可完成
for i in *; do afl-tmin -i $i -o tmin-$i -- ~/path/to/tested/program [params] @@; done;
2.5、开始测试
在执行afl-fuzz前,如果系统配置为将核心转储文件(core)通知发送到外部程序,将导致将崩溃信息发送到Fuzzer之间的延迟增大,进而可能将崩溃被误报为超时,所以我们得临时修改core_pattern文件: echo core >/proc/sys/kernel/core_pattern
对于可以直接从stdin读取输入的目标程序来说,执行命令, afl-fuzz -i good-seeds/ -o good-outputs -- ./test
对从文件读取输入的目标程序来说,要用“@@”: afl-fuzz -i ./in/ -o ./out/ -Q ./readelf -a @@ # 开始fuzz, @@表示从in 文件夹中找elf作为输入 ,实际上是在执行readelf -a 文件名
第二行显示的是二进制文件中元组命中计数的变化,如果对于所有尝试的输入,每个采用的分支都采用固定的次数。读数显示为1.00, 当设法触发每个分支的其他命中计数时,读数向8.00移动(8位映射命中每一位),但可能永远不会到达极限。
第一个数据指的是fuzzing过程中达到的路径深度,通常用户提供的测试用例视为1级,传统模糊测试从中获得的测试用例视为2级,通过它们作为后续模糊测试回合的输入得到的结果为3级,以此类推。
下一个数据pending指的是还有多少输入数据没有经过任何测试。pend fav时指fuzzer在这个队列中真正想到达的条目。接下来的是这个fuzzing部分找到的新路径数量和在进行并行化fuzzing时从其他fuzzer实例导入的路径数量 。
最后一个数据可以衡量观测到痕迹的一致性,如果程序对相同的输入始终表现相同,则为100%,若数值较低但仍然显示为紫色,测试过程不太可能受到负面影响,而显示红色说明出现了问题,afl可能难以区分调整输入文件的有意义的效果和幻象效果