AFL框架LLVM模式源码解析
AFL-FUZZ框架有一个LLVM模式,该模式下使用LLVM的一些特性分别实现了persistent mode
和trace-pc-guard mode
。这部分源码位于/AFL_PATH/llvm_mode
下,其中包含三个代码文件,接下来会逐一介绍:
afl-clang-fast.c
afl-llvm-pass.so.cc
afl-llvm-rt.o.c
afl-clang-fast.c
先看afl-clang-fast.c
,其主要功能是最终在/AFL_PATH
目录下编译出afl-clang-fast
和afl-clang-fast++
的ELF可执行文件。
从main函数看起,首先判断是否开启了AFL_QUIET
模式,该模式下在编译待测试的二进制文件过程中将不会产生AFL的相关输出。
然后会去检查是否存在afl-llvm-rt.o.c
文件产生的afl-llvm-rt.o运行时库以及afl-llvm-pass.so.cc
文件产生的afl-llvm-pass.so。
确定afl-llvm-rt.o库和afl-llvm-pass.so库存在后,便开始复制命令行参数并调用clang
和clang++
进行编译。此时会区分是否是trace-pc-guard mode
,如果是该模式,则使用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 ......
如果没有使用该模式,则:
clang -Xclang -load -Xclang /afl_path/afl-llvm-pass.so -Qunused-arguments ......
clang++ -Xclang -load -Xclang /afl_path/afl-llvm-pass.so -Qunused-arguments ......
此后会根据使用者具体的编译配置对编译命令的参数进行进一步改动(主要包括判断32位/64位环境,添加afl-llvm-rt.o库),并最后执行clang/clang++
。
afl-llvm-pass.so.cc
该库的主要作用是向LLVM编译中添加AFL所需的Pass模块。
LLVM的Pass
根据本人的理解,LLVM中包含了许多Pass。LLVM每次编译时的一些特殊处理如优化操作,都是由单个或多个Pass一起完成的。因此,可以把Pass认为是用LLVM编译中的一个节点。
所以AFL-LLVM中的所有操作都是通过Pass完成的。
添加Pass
通过查找LLVM官方文档中关于添加Pass的部分,可以知道Pass是通过LLVM::legacy::PassManagerBase
来添加的。
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);
随机插桩
与普通的AFL-FUZZ模式一样,本Pass对于代码也是进行分块插桩。同时还会根据环境变量AFL_INST_RETIO
来调整插桩的比例inst_ratio
,范围:1-100。
char* inst_ratio_str = getenv("AFL_INST_RATIO");
unsigned int inst_ratio = 100;
if (inst_ratio_str) {
if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || !inst_ratio ||
inst_ratio > 100)
FATAL("Bad value of AFL_INST_RATIO (must be between 1 and 100)");
}
其遍历所有代码块,然后根据设定的插桩覆盖比例对目标进行随机插桩:
for(所有代码块){
if(AFL_R(100) >= inst_ratio) continue;
......
插桩处理;
......
}
上面代码中宏AFL_R(x)
为random()%100
。
而插桩的内容包括加载共享内存以及更新bitmap,与afl-as.c中插入的汇编逻辑基本一致,在此不再赘述。
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本身的新功能,因此其具体实现由LLVM完成。AFL项目中仅实现了使用LLVM-trace-pc-guard功能的两个回调函数,通过阅读LLVM相关资料,可知:
1.__sanitizer_cov_trace_pc_guard(uint32_t* guard)
:每个代码块的尾部都会插入这个函数的代码,并且每个代码块都有独立的guard变量可以操作。此处通过guard变量的值来代表各个代码块的ID,从而使用如下代码来标记目标代码块被执行到,以此作为新路径的判断依据:
__afl_area_ptr[*guard]++;
2.__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中实现的功能一致,在此不再赘述。
结尾
总的来说,AFL-LLVM模式下persistent mode
的代码实现功能与之前afl-gcc.c
的基本一样,只是将插桩的操作作为了LLVM中Pass的形式添加进了clang/clang++的编译优化过程,而不是直接插入汇编代码。
而trace-pc-guard mode
则是自己额外又实现了一遍forkserver部分的功能,而其主要的trace-pc-guard功能是LLVM本身的,该模式利用LLVM-trace-pc-guard为每个代码块分配的guard指针变量来实现执行路径的追踪。