0x00 前言
为了解决使用AFL++进行Fuzzing的部分问题,个人决定对AFL的代码进行阅读,此为第二部分。
0x01 LLVM
1.1 起源
LLVM是底层虚拟机的英文缩写。
根据不同的场景,LLVM可能指示以下内容:
- LLVM项目/基础架构。此时,LLVM指代多个构建一个完整编译的项目,包括前端后端,优化器,汇编器,链接器,libc++。compiler-rt以及JIT引擎。
- 基于LLVM的编译器。此时LLVM指代部分或者完全采用LLVM基础架构构建的编译器。例如某个编译器可以采用LLVM作为前端或者后端,但是使用GCC以及GNU系统库函数进行最后的链接。
- LLVM库,此时,LLVM指代LLVM基础架构种可复用的代码部分。
- LLVM内核。此时LLVM指代在中间表示级别上所进行的优化以及后端算法。
- LLVM中间表示(LLVM IR)。此时,LLVM指代LLVM编译器的中间表示。
0x02 文件分析
2.1 afl-clang-fast.c
2.1.1 简介
该文件位于AFL的llvm_mode目录下 。AFL的 llvm_mode 可以实现编译器级别的插桩,可以替代 afl-gcc 或 afl-clang 使用的比较“粗暴”的汇编级别的重写的方法,且具备如下几个优势:
- 编译器可以进行很多优化以提升效率;
- 可以实现CPU无关,可以在非 x86 架构上进行fuzz;
- 可以更好地处理多线程目标。
在AFL的 llvm_mode 文件夹下包含3个文件: afl-clang-fast.c ,afl-llvm-pass.so.cc, afl-llvm-rt.o.c。
afl-llvm-rt.o.c 文件主要是重写了 afl-as.h 文件中的 main_payload 部分,方便调用;
afl-llvm-pass.so.cc 文件主要是当通过 afl-clang-fast 调用 clang 时,这个pass被插入到 LLVM 中,告诉编译器添加与 `afl-as.h 中大致等效的代码;
afl-clang-fast.c 文件本质上是 clang 的 wrapper,最终调用的还是 clang 。但是与 afl-gcc 一样,会进行一些参数处理。
llvm_mode 的插桩思路就是通过编写pass来实现信息记录,对每个基本块都插入探针,具体代码在 afl-llvm-pass.so.cc 文件中,初始化和forkserver操作通过链接完成。
2.1.2 函数
1.edit_params
该函数有两种插桩模式,一种是afl-llvm-pass.so注入插桩,一种是trace-pc-guard模式插桩,使用本地LLVM插桩回调来实现代码覆盖率。
首先判断传入第一个参数来选择编译器。然后遍历参数,根据参数设置bit_mode(bitmode决定调用的afl-llv-rt的位数,这里不知道是否和Compiler-RT项目有关,该项目是一个对目标架构提供硬件不支持的部分底层功能),x_set(有-x的话最后会设置-x none也即是不根据文件扩展名),asan_set,fortify_set参数,参数中如果有-Wl,-z,defs或 -Wl,--no-undefined则跳过,从而无视undefined reference to错误。
随后检查环境变量是否有AFL_HARDEN,如果有,添加 -fstack-protector-all 选项,同时如果设置了FORTIFY_SOURCE,添加-D_FORTIFY_SOURCE=2选项。
然后检查AFL_USET_ASAN,AFL_USE_MSAN环境变量,选择开启ASAN或者MSAN,ASAN,MSAN,AFL_HARDEN是互斥关系。
再然后根据环境变量AFL_DONT_OPTIMIZE设置编译优化选项,根据环境变量AFL_NO_BUILTIN设置内建函数优化。
接着设置宏变量__AFL_HAVE_MANUAL_CONTROL=1,__AFL_COMPILER=1,FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1。并且定义了__AFL_LOOP(),__AFL_INIT()两个宏函数,其中有意思的就是宏函数使用了__attribute__((used))防止了变量被编译器优化,volatile修饰变量防止被链接器优化,
/* When the user tries to use persistent or deferred forkserver modes by
appending a single line to the program, we want to reliably inject a
signature into the binary (to be picked up by afl-fuzz) and we want
to call a function from the runtime .o file. This is unnecessarily
painful for three reasons:
1) We need to convince the compiler not to optimize out the signature.
This is done with __attribute__((used)).
2) We need to convince the linker, when called with -Wl,--gc-sections,
not to do the same. This is done by forcing an assignment to a
'volatile' pointer.
3) We need to declare __afl_persistent_loop() in the global namespace,
but doing this within a method in a class is hard - :: and extern "C"
are forbidden and __attribute__((alias(...))) doesn't work. Hence the
__asm__ aliasing trick.
*/
cc_params[cc_par_cnt++] = "-D__AFL_LOOP(_A)="
"({ static volatile char *_B __attribute__((used)); "
" _B = (char*)\"" PERSIST_SIG "\"; "
"_L(_A); })";
没读懂第三条,可能与下面的链接有关
2.find_obj
该函数寻找运行时库(runtime libraries)具体是afl-llvm-rt.o文件
3.main
首先如果有宏定义__ANDROID__就调用find_obj,然后调用edit_params,最后execvp执行clang并传入处理完的参数。
2.2 afl-llvm-pass.so.cc
2.2.1 简介
实现一个遍。
2.2.2 前置知识
LLVM的每一遍处理都变成实现为Pass类的派生类。大多数的遍变成实现为单个cpp文件,Pass类的子类再匿名空间进行定义。为了使每一遍扫描的发挥实际作用,文件之外的代码也能够使用该类,文件中需要导出一个函数(创建Pass)。如图是一个创建遍的实例。
2.2.3 函数
1.registerAFLPass
static void registerAFLPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {
PM.add(new AFLCoverage());
}
static RegisterStandardPasses RegisterAFLPass(
PassManagerBuilder::EP_ModuleOptimizerEarly, registerAFLPass);
static RegisterStandardPasses RegisterAFLPass0(
PassManagerBuilder::EP_EnabledOnOptLevel0, registerAFLPass);
注册Pass
RegisterAFLPass和RegisterAFLPass0在不同的编译器优化级别(Optimization Level)下注册 AFLCoverage Pass。
2.runOnModule
该函数为关键函数,传入参数为Module &M。
其首先获取环境变量AFL_INST_RATIO,默认的插入比率为100。随后获取共享内存的指针和上一次插桩的ID。
接着循环遍历模块中的每一个函数,对函数中的每一个基本块(BB)进行如下操作:
- 在每一个BB的开头获取插入点
- 生成一个随机值,标识当前位置
- 然后获取上一个插桩id,共享内存,将上一个插桩id与当前的异或
- 然后再共享内存的对应地方+1
- 最后将现在的id右移一位并且设置为pre_loc
本质上就是和之前的add_instrumentation函数中插入的代码差不多。
2.3 afl-llvm-rt.o.c
2.3.1 简介
LLVM的插桩引导代码。重写afl-as.h中的main_payload部分。
2.3.2 全局变量
u8 __afl_area_initial[MAP_SIZE];
u8* __afl_area_ptr = __afl_area_initial; // 共享内存区域
__thread u32 __afl_prev_loc; // 上一个插桩ID
static u8 is_persistent; // 是否持续模式
2.3.3 函数
1.__afl_map_shm
该函数的目的是设置共享内存,首先获取环境变量__AFL_SHM_ID。然后将该id的共享内存附加进当前进程,并且返回给__afl_area_ptr。最后将共享内存区域第一个字节设置为1,防止插桩比率过低的时候,父进程忽略当前进程。
2.__afl_start_forkserver
该函数是fork server的逻辑。
首先向FORKSRV_FD + 1写入四字节的tmp数据,如果写入字节不等于4直接返回。告知fuzzer已准备完成。
随后进入一个循环首先从FORKSRV_FD读取4字节数据到was_killed。如果在持久模式下停止子进程,但存在条件竞争并且afl-fuzz已经发出了SIGKILL信号,那么就注销就进程,如果在持久模式下子进程只是暂停了就发送SIGCONT信号重启进程。
具体表现为如果child_stopped=1且was_killed=1(子进程被杀死)就会等待子进程结束,然后重新fork。否则重启子进程。
static void __afl_start_forkserver(void) {
static u8 tmp[4];
s32 child_pid;
u8 child_stopped = 0;
/* Phone home and tell the parent that we're OK. If parent isn't there,
assume we're not running in forkserver mode and just execute program. */
if (write(FORKSRV_FD + 1, tmp, 4) != 4) return;
while (1) {
u32 was_killed;
int status;
/* Wait for parent by reading from the pipe. Abort if read fails. */
if (read(FORKSRV_FD, &was_killed, 4) != 4) _exit(1);
/* If we stopped the child in persistent mode, but there was a race
condition and afl-fuzz already issued SIGKILL, write off the old
process. */
if (child_stopped && was_killed) {
child_stopped = 0;
if (waitpid(child_pid, &status, 0) < 0) _exit(1);
}
if (!child_stopped) {
/* Once woken up, create a clone of our process. */
child_pid = fork();
if (child_pid < 0) _exit(1);
/* In child process: close fds, resume execution. */
if (!child_pid) {
close(FORKSRV_FD);
close(FORKSRV_FD + 1);
return;
}
} else {
/* Special handling for persistent mode: if the child is alive but
currently stopped, simply restart it with SIGCONT. */
kill(child_pid, SIGCONT);
child_stopped = 0;
}
/* In parent process: write PID to pipe, then wait for child. */
if (write(FORKSRV_FD + 1, &child_pid, 4) != 4) _exit(1);
if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0)
_exit(1);
/* In persistent mode, the child stops itself with SIGSTOP to indicate
a successful run. In this case, we want to wake it up without forking
again. */
if (WIFSTOPPED(status)) child_stopped = 1;
/* Relay wait status to pipe, then loop back. */
if (write(FORKSRV_FD + 1, &status, 4) != 4) _exit(1);
}
}
然后forkserver向状态管道 FORKSRV_FD + 1 写入子进程的pid(告知子进程已启动),然后等待子进程结束(持久模式下子进程暂停也会立即返回)。如果子进程的返回状态时暂停就将child_stopped设置为1,随后将子进程的状态写入 FORKSRV_FD + 1 。
3.__afl_persistent_loop
该函数是持久模式的处理。
首先如果是第一次循环,清空共享内存空间,并将共享内存空间的第一个值设置为1,然后将__afl_pre_loc设置为0。cycle_cnt为max_cnt,由宏控制。然后返回1。
如果不是第一次循环则raise(SIGSTOP)停止当前进程,以便在下一次迭代开始时重新设置状态,并将共享内存空间的第一个值设置为1,然后将__afl_pre_loc设置为0。
当循环达到最大迭代次数后,会将覆盖率地图切换回初始状态以退出循环。
4.__afl_manual_init
该函数是初始化afl的一些内容,首先附加共享内存到该进程,然后启动fork server。
5.__afl_auto_init
/* Proper initialization routine. */
__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void) {
is_persistent = !!getenv(PERSIST_ENV_VAR);
if (getenv(DEFER_ENV_VAR)) return;
__afl_manual_init();
}
该函数使用了 __attribute__((constructor(CONST_PRIO))) 注释,表示它将在程序启动时自动执行。
首先获取环境变量__AFL_PERSISTENT。
然后获取环境变量__AFL_DEFER_FORKSRV的值,如果有,那么代表会延迟forkserver的初始化,直接return,如果没有那么就执行初始化。
6.__sanitizer_cov_trace_pc_guard
/* The following stuff deals with supporting -fsanitize-coverage=trace-pc-guard.
It remains non-operational in the traditional, plugin-backed LLVM mode.
For more info about 'trace-pc-guard', see README.llvm.
The first function (__sanitizer_cov_trace_pc_guard) is called back on every
edge (as opposed to every basic block). */
void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
__afl_area_ptr[*guard]++;
}
该函数是为了支持-fsanitize-coverage=trace-pc-guard,目的是让传入的参数所对应共享内存的地址处的值+1。
7.__sanitizer_cov_trace_pc_guard_init
该函数也是和trace-pc-guard mode有关,对于该模式并不特别了解,所以暂且放置。
文章评论