AFL二三事——源码分析(上篇)
字数 2814 2025-08-24 07:48:33
AFL源码分析教学文档(上篇)
一、AFL概述
AFL(American Fuzzy Lop)是由Michal Zalewski开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具。它通过记录输入样本的代码覆盖率(代码执行路径的覆盖情况),以此进行反馈,对输入样本进行调整以提高覆盖率,从而提升发现漏洞的可能性。
AFL主要特点
- 支持有源码和无源码程序的模糊测试
- 基于覆盖率引导的反馈机制
- 设计思想和实现方案在模糊测试领域具有重要意义
- 支持多种插桩模式:普通插桩、LLVM模式、QEMU模式
二、AFL源码结构
主要模块
-
插桩模块
afl-as.h,afl-as.c,afl-gcc.c:普通插桩模式,针对源码插桩llvm_mode:LLVM插桩模式,针对源码插桩qemu_mode:QEMU插桩模式,针对二进制文件插桩
-
Fuzzer模块
afl-fuzz.c:Fuzzer实现的核心代码,AFL的主体
-
辅助模块
afl-analyze:测试用例分析afl-plot:生成测试任务状态图afl-tmin:测试用例最小化afl-cmin:语料库精简afl-showmap:单个测试用例执行路径跟踪afl-whatsup:并行例程fuzzing结果统计afl-gotcpu:查看当前CPU状态
三、普通插桩模式分析
1. afl-gcc.c分析
主要功能
afl-gcc是GCC或clang的wrapper,主要作用是实现对于关键节点的代码插桩(汇编级),记录程序执行路径等关键信息。
关键变量
static u8* as_path; // AFL的as的路径
static u8** cc_params; // 传递给真实CC的参数
static u32 cc_par_cnt = 1; // 参数计数
static u8 be_quiet, // 静默模式
clang_mode; // 是否使用afl-clang*模式
主要函数
-
find_as(argv[0]):查找使用的汇编器- 检查环境变量
AFL_PATH - 检查
argv[0]路径 - 失败则抛出异常
- 检查环境变量
-
edit_params(argc, argv):处理传入的编译参数- 设置编译器参数(gcc/clang)
- 处理各种编译选项(-B, -integrated-as, -pipe等)
- 处理sanitizer相关选项(ASAN/MSAN)
- 设置优化选项(-g -O3 -funroll-loops等)
-
execvp(cc_params[0], (char**)cc_params):执行实际的编译命令
2. afl-as.c分析
主要功能
afl-as是GNU as的wrapper,目的是预处理由GCC/clang生成的汇编文件,并注入插桩代码。
关键变量
static u8** as_params; // 传递给真实'as'的参数
static u8* input_file; // 输入文件
static u8* modified_file; // 插桩后的文件
static u8 be_quiet, // 静默模式
clang_mode, // 是否在clang模式
pass_thru, // 只通过数据
just_version, // 只显示版本
sanitizer; // 是否使用ASAN/MSAN
static u32 inst_ratio = 100, // 插桩概率(%)
as_par_cnt = 1; // 传递给'as'的参数数量
主要函数
-
edit_params函数:- 设置
as_params参数数组 - 处理
--64/--32选项 - 设置输入/输出文件路径
- 设置
-
add_instrumentation函数(核心插桩函数):- 读取输入文件的每一行
- 只在
.text段插入桩代码 - 跳过标签、宏、注释
- 处理条件跳转指令(jnz等)
- 处理函数入口点和分支标签
插桩逻辑
-
插桩位置:
- 函数入口点(如
^main:) - GCC分支标签(如
^.L0:) - clang分支标签(如
^.LBB0_0:) - 条件跳转指令(如
\tjnz foo)
- 函数入口点(如
-
插桩代码(trampoline):
- 32位:
trampoline_fmt_32 - 64位:
trampoline_fmt_64
- 32位:
3. 插桩蹦床(trampoline)分析
32位插桩代码
.align 4
leal -16(%%esp), %%esp
movl %%edi, 0(%%esp)
movl %%edx, 4(%%esp)
movl %%ecx, 8(%%esp)
movl %%eax, 12(%%esp)
movl $0x%08x, %%ecx // 随机桩代码id
call __afl_maybe_log // 核心函数调用
movl 12(%%esp), %%eax
movl 8(%%esp), %%ecx
movl 4(%%esp), %%edx
movl 0(%%esp), %%edi
leal 16(%%esp), %%esp
64位插桩代码
.align 4
leaq -(128+24)(%%rsp), %%rsp
movq %%rdx, 0(%%rsp)
movq %%rcx, 8(%%rsp)
movq %%rax, 16(%%rsp)
movq $0x%08x, %%rcx // 随机桩代码id
call __afl_maybe_log // 核心函数调用
movq 16(%%rsp), %%rax
movq 8(%%rsp), %%rcx
movq 0(%%rsp), %%rdx
leaq (128+24)(%%rsp), %%rsp
__afl_maybe_log函数流程
- 保存寄存器状态
- 检查共享内存是否已映射
- 初始化fork server
- 记录执行路径
共享内存变量
__afl_area_ptr:共享内存地址__afl_prev_loc:上一个插桩位置ID__afl_fork_pid:fork产生的子进程pid__afl_temp:缓冲区__afl_setup_failure:标志位__afl_global_area_ptr:全局指针
四、LLVM模式分析
1. LLVM基础知识
LLVM核心设计:
- 统一的中间表示(LLVM IR)
- 模块化设计:前端、优化、后端分离
- Clang是LLVM的C/C++/Objective-C前端
编译流程:
源码 → Clang前端 → LLVM IR → LLVM Passes(优化/插桩) → 后端 → 机器码
2. afl-clang-fast.c分析
主要功能
LLVM模式下的编译器wrapper,实现编译器级别的插桩。
优势
- 编译器可以进行更多优化
- CPU架构无关
- 更好地处理多线程目标
关键函数
find_obj:查找运行时库afl-llvm-rt.oedit_params:处理编译参数- 设置Clang编译器
- 处理插桩模式(传统模式或trace-pc-guard模式)
- 处理各种编译选项
3. afl-llvm-pass.so.cc分析
主要功能
实现LLVM模式的插桩Pass,在IR级别插入覆盖率检测代码。
关键函数
runOnModule函数:
- 获取LLVMContext
- 设置插桩密度(
AFL_INST_RATIO) - 获取共享内存指针和上一个基本块ID
- 遍历每个基本块(BB):
- 寻找适合插桩的位置
- 随机创建当前BB的ID
- 插入load指令获取前一个BB的ID
- 插入共享内存访问指令
- 更新执行计数
- 更新
__afl_prev_loc
4. afl-llvm-rt.o.c分析
三种特殊功能
-
Deferred Instrumentation
- 延迟forkserver初始化
- 在大部分初始化完成后、二进制解析前进行克隆
- 使用
__AFL_INIT()宏
-
Persistent Mode
- 长期存活的进程测试多个用例
- 减少fork()调用和OS开销
- 使用
__AFL_LOOP(1000)宏
-
Trace-pc-guard Mode
- 在每个edge插入桩代码
- 使用
__sanitizer_cov_trace_pc_guard函数 - 需要设置
AFL_TRACE_PC=1和-fsanitize-coverage=trace-pc-guard参数
五、关键技术与设计思想
- 覆盖率引导:通过记录代码覆盖率指导fuzzing方向
- 高效插桩:在关键位置插入轻量级检测代码
- 进程复用:forkserver机制减少进程创建开销
- 多种模式:适应不同场景(普通、LLVM、QEMU)
- 反馈优化:共享内存记录执行路径,指导变异策略
六、总结
AFL的源码设计体现了以下核心思想:
- 最小化开销:通过精巧的插桩和进程复用减少性能损耗
- 最大化反馈:覆盖率信息指导变异策略
- 灵活性:支持多种插桩模式和运行方式
- 可扩展性:模块化设计便于功能扩展和定制
上篇主要分析了AFL的普通插桩模式和LLVM模式的核心实现,下篇将深入分析QEMU模式、fuzzer核心逻辑和变异策略等内容。