初窥 fuzz 门径:AFL源码研究二
本文最后更新于 2025年3月2日 中午
通过AFL源码的综合阅读和理解,我们已经知道了相关重点,AFL 在注释中已经对通过afl-gcc来插桩这种做法不建议了,故我们重点要学习通过llvm pass 来插桩。
这篇文章重点之意也是在此。
llvm_mode 文件夹架构:
llvm_mode
- afl-fast-clang
- afl-llvm-pass.so.cc
- afl-llvm-rt.o.c
- Makefile
- README.llvm
我们依次查看分析看看
afl-fast-clang
它是针对前端clang的一个wrapper, 原理和结构都类似于afl-gcc.c ,故在这里简要分析
main 函数核心代码如下:
find_obj(argv[0]);
edit_params(argc, argv);
execvp(cc_params[0], (char**)cc_params);
其中主要有如下三个函数的调用:
find_as(argv[0])
:查找汇编器路径AFL_PATH,且会查看AFL_PATH/afl-llvm-rt.o
是否可以访问,如果可以就设置这个目录为obj_path,然后直接返回edit_params(argc, argv)
:- 决定
cc_params[0]
的值是clang++
还是clang
; - 默认情况下,通过
afl-llvm-pass.so
来注入instrumentation,也支持trace-pc-guard
模式; - 然后如果定义了
USE_TRACE_PC
宏,就将-fsanitize-coverage=trace-pc-guard -mllvm -sanitizer-coverage-block-threshold=0
添加到参数里; - 如果没有定义,就依次将
-Xclang -load -Xclang obj_path/afl-llvm-pass.so -Qunused-arguments
; - 进一步处理传入的编译参数,将处理好的参数放入
cc_params[]
数组
- 决定
execvp(cc_params[0], (cahr**)cc_params)
: 执行真正的编译器: CLANG
afl-llvm-pass.so.cc
afl-llvm-pass里只有一个Transform pass AFLCoverage,其继承自ModulePass,所以我们主要分析一下它的runOnModule函数,这里简单的介绍一下llvm里的一些层次关系,粗略理解就是Module相当于你的程序,里面包含所有Function和全局变量,而Function里包含所有BasicBlock和函数参数,BasicBlock里包含所有Instruction,Instruction包含Opcode和Operands。
如果熟悉第一部门的插桩汇编代码,可以看出这里是llvm Pass 的对其的相关实现
总的来说,这里干的事情是通过遍历每一个基本快,向基本快首部插入实现了如下代码的instruction ir
cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;
AFLCoverage::runOnModule
AFLCoverage::runOnModule 函数的实现如下,关键部分已通过注释解释
bool AFLCoverage::runOnModule(Module &M) {
// 通过getContext来获取LLVMContext,其保存了整个程序里分配的类型和常量信息。
LLVMContext &C = M.getContext();
// 通过这个Context来获取type实例Int8Ty和Int32Ty
IntegerType *Int8Ty = IntegerType::getInt8Ty(C);
IntegerType *Int32Ty = IntegerType::getInt32Ty(C);
/* Show a banner */
char be_quiet = 0;
if (isatty(2) && !getenv("AFL_QUIET")) {
SAYF(cCYA "afl-llvm-pass " cBRI VERSION cRST " by <lszekeres@google.com>\n");
} else be_quiet = 1;
/* Decide instrumentation ratio */
/*
读取环境变量AFL_INST_RATIO给变量inst_ratio, ,其值默认为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)");
}
/* Get globals for the SHM region and the previous location. Note that
__afl_prev_loc is thread-local. */
// 获取全局变量中指向共享内存的指针,以及上一个基础块的编号
GlobalVariable *AFLMapPtr =
new GlobalVariable(M, PointerType::get(Int8Ty, 0), false,
GlobalValue::ExternalLinkage, 0, "__afl_area_ptr");
GlobalVariable *AFLPrevLoc = new GlobalVariable(
M, Int32Ty, false, GlobalValue::ExternalLinkage, 0, "__afl_prev_loc",
0, GlobalVariable::GeneralDynamicTLSModel, 0, false);
/* Instrument all the things! */
int inst_blocks = 0;
for (auto &F : M) // 遍历每个函数
for (auto &BB : F) { // 遍历每个函数中的每个基本块
/*
找到此基本块中适合插入instrument的位置,
后续通过初始化IRBuilder的一个实例进行插入,插在基本块首部
*/
BasicBlock::iterator IP = BB.getFirstInsertionPt();
IRBuilder<> IRB(&(*IP));
if (AFL_R(100) >= inst_ratio) continue;
/* Make up cur_loc */
// 随机创建一个当前基本块的编号
unsigned int cur_loc = AFL_R(MAP_SIZE);
ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);
/* Load prev_loc */
// 通过插入load指令来获取前一个基本块的编号。
LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());
/* Load SHM pointer */
/*
通过插入load指令来获取共享内存的地址,
并通过CreateGEP函数来获取共享内存里指定index的地址,
这个index通过cur_loc和prev_loc取xor计算得到。
*/
LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);
// 为MapPtr设置了名为"nosanitize"的元数据标记
MapPtr->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *MapPtrIdx =
IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));
/* Update bitmap */
/*
通过插入load指令来读取对应index地址,并通过插入add指令来将其加一,
然后通过创建store指令将新值写入,更新共享内存
*/
LoadInst *Counter = IRB.CreateLoad(MapPtrIdx);
Counter->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *Incr = IRB.CreateAdd(Counter, ConstantInt::get(Int8Ty, 1));
IRB.CreateStore(Incr, MapPtrIdx)
->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
/* Set prev_loc to cur_loc >> 1 */
// 将当前cur_loc的值右移一位,然后通过插入store指令,更新__afl_prev_loc的值
StoreInst *Store =
IRB.CreateStore(ConstantInt::get(Int32Ty, cur_loc >> 1), AFLPrevLoc);
Store->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
inst_blocks++;
}
/* Say something nice. */
if (!be_quiet) {
if (!inst_blocks) WARNF("No instrumentation targets found.");
else OKF("Instrumented %u locations (%s mode, ratio %u%%).",
inst_blocks, getenv("AFL_HARDEN") ? "hardened" :
((getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) ?
"ASAN/MSAN" : "non-hardened"), inst_ratio);
}
return true;
}
afl-llvm-rt.o.c
afl-llvm-rt.o.c 实现了fork server
、persistent mode
、 trace-pc-guard mode
这三个关键的功能
先来看初始化函数,其中会执行初始化共享内存、执行fork server
void __afl_manual_init(void) {
static u8 init_done;
if (!init_done) {
__afl_map_shm();
__afl_start_forkserver();
init_done = 1;
}
}
__afl_start_forkserver
深入看看 __afl_start_forkserver
的执行逻辑:
如果熟悉GCC编译模式下的插桩代码逻辑,可以相互对应查看,逻辑基本一致。
/* Fork server logic. */
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. */
// parent are ready!
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. */
// 没读到,阻塞(read); 如果读到了,就代表AFL命令我们fork server去执行一次fuzz
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. */
/*
persistent mode 持续模式: 认为API 是无状态的,API重置后,一个进程可以被重复使用
while (__AFL_LOOP(1000)) {
Read input data.
Call library code to be fuzzed.
Reset state.
}
Exit normally
*/
if (child_stopped && was_killed) {
child_stopped = 0;
if (waitpid(child_pid, &status, 0) < 0) _exit(1);
}
// 如果child_stopped为0,则直接fork出一个子进程去进行fuzz
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);
}
}
函数逻辑
逻辑如下:
- 设置
child_stopped
为0,此时父进程(fuzzer)已经准备好了,通过向FORKSRV_FD + 1
状态管道写入4个字节,通知完毕 - 进入死循环
- 尝试去读
FORKSRV_FD
状态管道, 没读到,阻塞(read); 如果读到了,就代表AFL命令我们fork server
去执行一次fuzz - 处理
persistent mode
情况,如果我们在持久模式下停止了子进程,但存在竞争条件,并且afl-fuzz已经发出SIGKILL,退出旧进程 - 如果
child_stopped
为0,则直接fork出一个子进程去进行fuzz- 创建一个与父进程一样的进程,但是关闭了
FORKSRV_FD
和FORKSRV_FD + 1
两个文件描述符,然后return跳出fuzz loop
,恢复正常执行
- 创建一个与父进程一样的进程,但是关闭了
- 否则,如果
child_stopped
不为0,进入persistent mode
持久模式的特殊处理:如果子模式还活着,但当前已停止,只需使用SIGCONT重新启动它 - 父进程:将PID写入管道,然后等待子进程
- 父进程:WIFSTOPPED(status)宏确定返回值是否对应于一个暂停子进程,在
persistent mode
持久模式下,子进程会用SIGSTOP停止自己,以表示运行成功。在这种情况下,我们希望在不再次分叉的情况下唤醒它 - 将等待状态写到管道
FORKSRV_FD + 1
,然后循环返回
persistent mode
它并不是通过fork出子进程去进行fuzz的,而是认为当前我们正在fuzz的API是无状态的,当API重置后,一个长期活跃的进程就可以被重复使用,这样可以消除重复执行fork函数以及OS相关所需要的开销。
while (__AFL_LOOP(1000)) {
/* Read input data. */
/* Call library code to be fuzzed. */
/* Reset state. */
}
/* Exit normally */
trace-pc-guard mode
要使用这个功能,需要先通过AFL_TRACE_PC=1
来定义 DUSE_TRACE_PC
宏,从而在执行afl-clang-fast的时候传入 -fsanitize-coverage=trace-pc-guard
参数,来开启这个功能,和之前我们的插桩不同,开启了这个功能之后,我们不再是仅仅只对每个基本块插桩,而是对每条edge都进行了插桩。
afl-fuzz
这是一个有 8k 余行的源码,函数数量不少,那么分析的思路是通过解释调用的各个函数的意义来表达它的实现思想。篇幅有限,有些函数可能不会展开。
main 函数核心代码如下:
setup_signal_handlers();
check_asan_opts();
if (sync_id) fix_up_sync();
save_cmdline(argc, argv);
fix_up_banner(argv[optind]);
check_if_tty();
get_core_count();
#ifdef HAVE_AFFINITY
bind_to_free_cpu();
#endif /* HAVE_AFFINITY */
check_crash_handling();
check_cpu_governor();
setup_post();
setup_shm();
init_count_class16();
setup_dirs_fds();
read_testcases();
load_auto();
pivot_inputs();
if (extras_dir) load_extras(extras_dir);
if (!timeout_given) find_timeout();
detect_file_args(argv + optind + 1);
if (!out_file) setup_stdio_file();
check_binary(argv[optind]);
perform_dry_run(use_argv);
cull_queue();
show_init_stats();
seek_to = find_start_position();
...
main 函数逻辑
以函数为线索,开始分析各个函数的调用逻辑。
setup_signal_handlers()
- 注册必要的信号处理函数
- SIGHUP/SIGINT/SIGTERM
- 主要是
stop
的处理函数
- 主要是
- SIGALRM
- 处理超时的情况
- SIGWINCH
- 处理窗口大小的变化信号
- SIGUSR1
- 留给用户自定义的信号
- SIGTSTP/SIGPIPE
check_asan_opts()
- 读取环境变量ASAN_OPTIONS和MSAN_OPTIONS,做一些检查
fix_up_sync()
- 如果通过-M或者-S指定了sync_id,则更新out_dir和sync_dir的值
设置sync_dir的值为out_dir
设置out_dir的值为out_dir/sync_id
- 如果通过-M或者-S指定了sync_id,则更新out_dir和sync_dir的值
save_cmdline(argc, argv)
fix_up_banner(argv[optind])
参考
非常感谢前人分析的优秀文章!这对我的学习帮助很大