简介
Fuzzing是指通过构造测试输入,对软件进行大量测试来发现软件中的漏洞的一种模糊测试方法。当前大多数远程代码执行和特权提升等比较严重的漏洞都是使用Fuzzing技术挖掘的,Fuzzing技术被证明是当前鉴别软件安全问题方面最强大测试技术。
然而Fuzzing技术仍然存在着覆盖率低的缺陷,而许多的代码漏洞需要更大的路径覆盖率才能触发,而不是通过纯粹的随机尝试。
AFL (American Fuzzy Lop)是一款采取遗传算法生成用例的Fuzzing工具,可以有效地解决这些问题。AFL有两种Fuzzing方法:
- 针对开源软件:AFL软件在编译的同时进行插桩,以方便fuzz
- 针对闭源软件:配合QEMU直接对闭源的二进制代码进行fuzz
另外,AFL有基于gcc和llvm的两种实现方式,本文只讨论基于llvm的AFL对开源软件即有源码的程序的插桩和fuzz过程。
使用
流程概述
首先用afl-clang-fast编译源代码进行插桩,然后启动afl-fuzz程序,将testcase(输入的测试文件)作为程序的输入执行程序,AFL会在这个testcase的基础上进行自动变异输入,使得程序产生crash,产生的crash会被记录起来用于进一步分析。
插桩编译
首先使用以下命令编译源文件,编译过程中会进行插桩:
afl-clang-fast -g -o afl_test afl_test.c
如果是编译c++的源码,那就需要用afl-clang-fast++
。
接着建立两个目录:fuzz_in和fuzz_out,用来存放程序的输入文件和fuzz的输出文件。在fuzz_in中创建一个testcase文件,用于写入被测试程序的输入。
在编译项目时,通常有Makefile,这时就需要在Makefile中添加内容:
CC=/path/to/afl/afl-clang-fast ./configure
make
使用afl-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界面
process timing:
这里展示了当前fuzz的运行时间、最近一次发现新执行路径的时间、最近一次崩溃的时间、最近一次超时的时间。
值得注意的是第2项,最近一次发现新路径的时间。如果从fuzzing开始一直没有发现新的执行路径,那么就要考虑是否有二进制或者命令行参数错误的问题了,对于此状况,AFL也会智能地进行提醒。
overall results:
这里包括运行的总周期数、总路径数、崩溃次数、超时次数。
其中,总周期数可以用来作为何时停止fuzzing的参考。随着不断地fuzzing,周期数会不断增大,其颜色也会由洋红色,逐步变为黄色、蓝色、绿色。一般来说,当其变为绿色时,代表可执行的内容已经很少了,继续fuzzing下去也不会有什么新的发现了。此时,我们便可以通过Ctrl-C,中止当前的fuzzing。
stage progress:
这里包括正在测试的fuzzing策略、进度、目标的执行总次数、目标的执行速度。
执行速度可以直观地反映当前跑的快不快,如果速度过慢,比如低于500次每秒,那么测试时间就会变得非常漫长。如果发生了这种情况,那么我们需要进一步调整优化我们的fuzzing。
分析
AFL-FUZZ框架在/AFL_PATH/llvm_mode下包含了基于LLVM的实现源码,包括alf-clang-fast.c/afl-llvm-pass.so.cc/afl-llvm-rt.o.c三个文件:
afl-clang-fast.c
其主要功能是在/AFL_PATH目录下编译出afl-clang-fast
和afl-clang-fast++
的ELF可执行文件。
main函数中包括了以下3个主要实现步骤:
find_obj(argv[0]);
edit_params(argc, argv);
execvp(cc_params[0], (char**)cc_params);
find_obj
是为了确定afl-llvm-rt.o和afl-llvm-pass.so是否存在,如果不存在将会报错。
edit_params
的功能是组装参数,过程中会根据宏USE_TRACE_PC来判断是普通模式还是trace-pc-guard模式,普通模式通过使用afl-llvm-pass.so来插桩。
clang -Xclang -load -Xclang /afl_path/afl-llvm-pass.so -Qunused-arguments ......
clang++ -Xclang -load -Xclang /afl_path/afl-llvm-pass.so -Qunused-arguments ......
trace-pc-guard模式使用原生LLVM回调函数来插桩。
clang -fsanitize-coverage=trace-pc-guard -mllvm -sanitizer-coverage-block-threshold=0 -Qunused-arguments ......
clang++ -fsanitize-coverage=trace-pc-guard -mllvm -sanitizer-coverage-block-threshold=0 -Qunused-arguments ......
下来会根据使用者具体的编译配置对编译命令的参数进行进一步改动(主要包括判断32位/64位环境,添加afl-llvm-rt.o库),把需要调用的clang或者clang++放到cc_params[0]
中,进一步传递给execvp
后开始执行程序。
afl-llvm-pass.so.cc
注册一个pass,下来就可以在命令行中通过-load来引用该pass。
static void registerAFLPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {
PM.add(new AFLCoverage());
}
static RegisterStandardPasses RegisterAFLPass(
PassManagerBuilder::EP_OptimizerLast, registerAFLPass);
static RegisterStandardPasses RegisterAFLPass0(
PassManagerBuilder::EP_EnabledOnOptLevel0, registerAFLPass);
该pass遍历所有代码块进行分块随机插桩。同时还会根据环境变量AFL_INST_RETIO来调整插桩的比例inst_ratio,范围为1-100。
for (auto &F : M)
for (auto &BB : F) {
BasicBlock::iterator IP = BB.getFirstInsertionPt();
IRBuilder<> IRB(&(*IP));
if (AFL_R(100) >= inst_ratio) continue;
……
}
afl-llvm-rt.o.c
该代码定义了persistent mode和trace-pc-guard mode的具体调用。
persistent mode
该模式将已经完成fuzz一个testcase的子进程进行复用,从而节省计算机开销。但是需要注意的是每次fuzz过程都会改变一些进程或线程的状态变量,因此,在复用这个fuzz子进程的时候需要将这些变量恢复成初始状态,否则会导致下一次fuzz过程的不准确。从下面代码中可以看到,状态初始化的工作只对第一个循环做,之后的初始化工作都交给父进程。
if (first_pass) {
/* ...... */
if (is_persistent) {
memset(__afl_area_ptr, 0, MAP_SIZE);
__afl_area_ptr[0] = 1;
__afl_prev_loc = 0;
}
cycle_cnt = max_cnt;
first_pass = 0;
return 1;
}
trace-pc-guard mode
该模式是依靠了LLVM本身的特性来实现,AFL中仅实现了使用LLVM-trace-pc-guard功能的两个回调函数。
__sanitizer_cov_trace_pc_guard(uint32_t* guard)
:
每个代码块的尾部都会插入这个函数的代码,并且每个代码块都有独立的guard变量可以操作。此处通过guard变量的值来代表各个代码块的ID,从而使用如下代码来标记目标代码块被执行到,以此作为新路径的判断依据
__afl_area_ptr[*guard]++;__sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop)
:
对guard做了一些初始化的工作,给每个代码块的guard都分配一个随机的ID。如果用户通过AFL_INST_RATIO环境变量来设置了插桩覆盖比例,则根据这个比例的值来对部分代码块的ID标记为0,代表不用插桩。主要逻辑代码如下:
if (R(100) < inst_ratio) *start = R(MAP_SIZE - 1) + 1;
else *start = 0;
同时该函数也会对如下constructor进行调用:
__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void) {
is_persistent = !!getenv(PERSIST_ENV_VAR);
if (getenv(DEFER_ENV_VAR)) return;
__afl_manual_init();
}
该constructor完成了共享内存,forkserver等功能。其过程与普通模式下afl-fuzz.c中实现的功能一致。