本来想一周内完成,看来要延期了,尽量希望能在这几天内完成吧,31号晚上还有qax的一面😭😭,水平果然还是太次了。
AFL,全称“American Fuzzy Lop”,是由安全研究员Michal Zalewski开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具,它通过记录输入样本的代码覆盖率(代码执行路径的覆盖情况),以此进行反馈,对输入样本进行调整以提高覆盖率,从而提升发现漏洞的可能性。AFL可以针对有源码和无源码的程序进行模糊测试,其设计思想和实现方案在模糊测试领域具有十分重要的意义。
源码模块图像一览,图源来自文章:AFL二三事 - FreeBuf,侵删
afl-as.h, afl-as.c, afl-gcc.c
:普通插桩模式,针对源码插桩,编译器可以使用gcc, clang;llvm_mode
:llvm 插桩模式,针对源码插桩,编译器使用clang;qemu_mode
:qemu 插桩模式,针对二进制文件插桩。afl-fuzz.c
:fuzzer 实现的核心代码,AFL 的主体。afl-analyze
:对测试用例进行分析,通过分析给定的用例,确定是否可以发现用例中有意义的字段;afl-plot
:生成测试任务的状态图;afl-tmin
:对测试用例进行最小化;afl-cmin
:对语料库进行精简操作;afl-showmap
:对单个测试用例进行执行路径跟踪;afl-whatsup
:各并行例程fuzzing结果统计;afl-gotcpu
:查看当前CPU状态。alloc-inl.h
:定义带检测功能的内存分配和释放操作;config.h
:定义配置信息;debug.h
:与提示信息相关的宏定义;hash.h
:哈希函数的实现定义;types.h
:部分类型及宏的定义。afl-gcc.c本质上是对GCC 和 clang的封装,正如其在注释中写到的wrapper for GCC and clang。最常见的使用方式,便是在使用**./configure**时,直接将afl-gcc或着afl-clang的路径传给CC;同理对于C++代码,只需用afl-g++或者afl-clang++路径传给CXX即可。
几个静态全局变量:
static u8* as_path; /* Path to the AFL 'as' wrapper */
static u8** cc_params; /* Parameters passed to the real CC */
static u32 cc_par_cnt = 1; /* Param count, including argv0 */
static u8 be_quiet, /* Quiet mode */
clang_mode; /* Invoked as afl-clang*? */
main函数的核心主体如下
int main(int argc, char** argv) {
......
find_as(argv[0]);
edit_params(argc, argv);
execvp(cc_params[0], (char**)cc_params);
......
return 0;
}
find_as
: try to find our “fake” GNU assembler in AFL_PATH or at the location derived from argv[0]. If that fails, abort.edit_params
: Copy argv to cc_params, making the necessary edits.execvp
: 执行命令在edit_params
中,关键逻辑如下
...
if (!strcmp(name, "afl-g++")) {
u8* alt_cxx = getenv("AFL_CXX");
cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++";
} else if (!strcmp(name, "afl-gcj")) {
u8* alt_cc = getenv("AFL_GCJ");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj";
} else {
u8* alt_cc = getenv("AFL_CC");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc";
}
...
cc_params[cc_par_cnt++] = "-B";
cc_params[cc_par_cnt++] = as_path;
...
总体逻辑比较简单,实际就是对参数做一些预处理,会用gcc
替换cc_params[0]
,利用-B as_path
替换汇编器。因而在调用execvp
执行命令时,本质上就是执行类似gcc ... -B as_path ...
这样的效果。
在理解afl-gcc.c
之后,我们就自然而然会想到,核心点在于关注afl-as
和默认as的区别。类似于afl-gcc,afl-as也是一个对as的封装
一些全局静态变量:
static u8** as_params; /* Parameters passed to the real 'as' */
static u8* input_file; /* Originally specified input file */
static u8* modified_file; /* Instrumented file for the real 'as' */
static u8 be_quiet, /* Quiet mode (no stderr output) */
clang_mode, /* Running in clang mode? */
pass_thru, /* Just pass data through? */
just_version, /* Just show version? */
sanitizer; /* Using ASAN / MSAN */
static u32 inst_ratio = 100, /* Instrumentation probability (%) */
as_par_cnt = 1; /* Number of params to 'as' */
main()
函数的核心主体如下,与afl-gcc.c中的main
十分类似。比较明显,都是通过先对参数进行处理,然后调用execvp()
去执行命令。
int main(int argc, char** argv) {
...
gettimeofday(&tv, &tz);
rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();
srandom(rand_seed);
edit_params(argc, argv);
...
if (!just_version) add_instrumentation();
if (!(pid = fork())) {
execvp(as_params[0], (char**)as_params);
FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);
}
...
}
edit_params()
编辑参数add_instrumentation()
插桩与afl-gcc的逻辑基本一致,替换参数,不过多赘述,核心部分如下:
static void edit_params(int argc, char** argv) {
...
as_params[0] = afl_as ? afl_as : (u8*)"as";
...
as_params[as_par_cnt++] = modified_file;
as_params[as_par_cnt] = NULL;
}
比较明显的看到其他部分与afl-gcc基本相同,因而我们关键来看看add_instrumentation()
函数这部分内容。这个函数主要用于处理输入的文件,然后生成modified_file.并在恰当的地方插桩。此函数的核心部分如下:
static void add_instrumentation(void) {
...
/*
匹配合适的插桩位置
*/
...
while (fgets(line, MAX_LINE, inf)) {
...
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
R(MAP_SIZE));
ins_lines++;
...
continue;
}
...
}
从源代码的注释中我们也能看到,AFL插桩的位置偏好,主要为函数入口,分支标签,条件分支标签…….
/* If we're in the right mood for instrumenting, check for function
names or conditional labels. This is a bit messy, but in essence,
we want to catch:
^main: - function entry point (always instrumented)
^.L0: - GCC branch label
^.LBB0_0: - clang branch label (but only in clang mode)
^\tjnz foo - conditional branches
...but not:
^# BB#0: - clang comments
^ # BB#0: - ditto
^.Ltmp0: - clang non-branch labels
^.LC0 - GCC non-branch labels
^.LBB0_0: - ditto (when in GCC mode)
^\tjmp foo - non-conditional jumps
Additionally, clang and GCC on MacOS X follow a different convention
with no leading dots on labels, hence the weird maze of #ifdefs
later on.
*/
在add_instrumentation()
函数中的变量trampoline_fmt_64
和trampoline_fmt_32
即为插桩代码,我们可以在文件afl-as.h中找到定义,这里我们直接看看64位的插桩代码
/* --- AFL TRAMPOLINE (64-BIT) --- */
.align 4
leaq -(128+24)(%%rsp), %%rsp
movq %%rdx, 0(%%rsp)
movq %%rcx, 8(%%rsp)
movq %%rax, 16(%%rsp)
movq $0x%08x, %%rcx
call __afl_maybe_log
movq 16(%%rsp), %%rax
movq 8(%%rsp), %%rcx
movq 0(%%rsp), %%rdx
leaq (128+24)(%%rsp), %%rsp
/* --- END --- */
流程非常简单易懂,大致如下:
call __afl_maybe_log
这里直接来看看__afl_maybe_log
函数,由于这段汇编逻辑过长,大概200多行。这里直接放ScUpax0s师傅的流程图
流程图中涉及到的一些变量及含义如下:
.lcomm __afl_area_ptr, 8 /*共享内存地址*/
.lcomm __afl_prev_loc, 8 /*上一个插桩位置(id为R(100)随机数的值)*/
.lcomm __afl_fork_pid, 4 /*由fork产生的子进程的pid*/
.lcomm __afl_temp, 4 /*缓冲区*/
.lcomm __afl_setup_failure, 1 /*标志位,如果置位则直接退出*/
.comm __afl_global_area_ptr, 8, 8 /*全局指针*/