吃透重点:AFL源码研究二
本文最后更新于 2025年3月2日 中午
通过AFL源码的综合阅读和理解,我们已经知道了相关重点,AFL 在注释中已经对通过afl-gcc来插桩这种做法不建议了,故我们重点要学习通过llvm pass 来插桩。
这篇文章重点之意也是在此。
llvm_mode 文件夹架构:
1 |
|
我们依次查看分析看看
afl-fast-clang
它是针对前端clang的一个wrapper, 原理和结构都类似于afl-gcc.c ,故在这里简要分析
main 函数核心代码如下:
1 |
|
其中主要有如下三个函数的调用:
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
1 |
|
AFLCoverage::runOnModule
AFLCoverage::runOnModule 函数的实现如下,关键部分已通过注释解释
1 |
|
afl-llvm-rt.o.c
afl-llvm-rt.o.c 实现了fork server
、persistent mode
、 trace-pc-guard mode
这三个关键的功能
先来看初始化函数,其中会执行初始化共享内存、执行fork server
1 |
|
__afl_start_forkserver
深入看看 __afl_start_forkserver
的执行逻辑:
如果熟悉GCC编译模式下的插桩代码逻辑,可以相互对应查看,逻辑基本一致。
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相关所需要的开销。
1 |
|
trace-pc-guard mode
要使用这个功能,需要先通过AFL_TRACE_PC=1
来定义 DUSE_TRACE_PC
宏,从而在执行afl-clang-fast的时候传入 -fsanitize-coverage=trace-pc-guard
参数,来开启这个功能,和之前我们的插桩不同,开启了这个功能之后,我们不再是仅仅只对每个基本块插桩,而是对每条edge都进行了插桩。
afl-fuzz
这是一个有 8k 余行的源码,函数数量不少,那么分析的思路是通过解释调用的各个函数的意义来表达它的实现思想。篇幅有限,有些函数可能不会展开。
main 函数核心代码如下:
1 |
|
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])
参考
非常感谢前人分析的优秀文章!这对我的学习帮助很大