先看afl-gcc的实现
当调用afl-g++或afl-gcc对目标代码进行插桩编译时,首先调用g++或gcc对目前程序进行编译,
如下,通过cc_params[0]指定编译器
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";
}
编译过程中通过-B参数指定汇编器afl-as所在的路径as_path,正是通过afl-as在汇编层面对g++或gcc编译生成的汇编代码进行插桩(汇编器afl-as的路径as_path通过find_as()函数找到,一般和afl-g++或afl-gcc在同一个目录下)。
cc_params[cc_par_cnt++] = "-B";
cc_params[cc_par_cnt++] = as_path;
执行cc_params[0],编译完成后,就会找到afl-as进行插桩处理:
execvp(cc_params[0], (char**)cc_params);
再看afl-as的实现
首先,调用add_instrumentation()进行插桩
if (!just_version) add_instrumentation();
add_instrumentation()的实现大致如下,在汇编代码中找到.text代码,然后在函数入口、分支标签、条件跳转语句的下面(这里也是分支,条件不成立会走这里)调用fprintf()进行插桩,插入的语句在变量trampoline_fmt_64或trampoline_fmt_32中定义
/* 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
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
R(MAP_SIZE));
这里outf为待插桩的汇编文件,R(MAP_SIZE)生成一个随机数,用来表示当前的插桩的位置(分支)的标识符。虽然是随机生成,但是插桩后就是个确定值了,后面进项fuzz时对代码路径的追踪就是通过记录代码执行过程中的遇到的每个标识符来实现的。
对所有分支插桩完后,后面还会插入一段代码main_payload_64或者main_payload_32,前面插入的trampoline_fmt_64或trampoline_fmt_32会调用到这段代码。
if (ins_lines)
fputs(use_64bit ? main_payload_64 : main_payload_32, outf);
插桩完成后,最后,启动一个子进程,调用as将插桩后的汇编代码翻译成机器码,完成整个插桩编译流程。
as_params[0] = afl_as ? afl_as : (u8*)"as";
if (!(pid = fork())) {
execvp(as_params[0], (char**)as_params);
FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);
}
后面的篇章,我们以64位为例,分析trampoline_fmt_64和main_payload_64的内容。