AFL原码分析----编译和插桩
字数 2824 2025-08-06 18:07:59
AFL源码分析:编译和插桩机制详解
一、AFL整体架构概述
AFL(American Fuzzy Lop)是一款著名的模糊测试工具,其核心创新在于通过编译时插桩来收集代码覆盖率信息。AFL主要包含三个关键组件:
- afl-gcc/afl-clang:编译器封装器,负责在编译过程中插入代码覆盖率检测
- afl-as:汇编器封装器,实际执行插桩操作
- afl-fuzz:模糊测试引擎,利用收集的覆盖率信息指导测试
AFL支持三种工作模式:
- 普通模式:通过gcc/clang在汇编层面插桩
- LLVM模式:在编译层面为程序插桩,适用于clang
- QEMU模式:通过修改QEMU代码在模拟执行时记录路径
二、afl-gcc源码分析
2.1 主要功能
afl-gcc本质上是一个编译器wrapper,它在系统编译器(gcc/clang)上层添加了一层封装,主要功能是:
- 定位afl-as的位置
- 修改编译参数
- 调用真正的编译器进行编译
2.2 核心函数
2.2.1 find_as函数
static void find_as(u8* argv0)
功能:查找afl-as的位置
查找顺序:
- 检查
AFL_PATH环境变量指定的路径 - 检查argv0所在目录(即afl-gcc所在目录)
- 检查默认安装路径
AFL_PATH
2.2.2 edit_params函数
static void edit_params(u32 argc, char** argv)
功能:编辑传递给编译器的参数
主要操作:
- 识别编译器类型(gcc/g++/clang/clang++)
- 处理特殊编译选项(-B, -integrated-as, -pipe等)
- 根据环境变量设置编译标志:
AFL_HARDEN:添加-fstack-protector-allAFL_USE_ASAN:添加-fsanitize=addressAFL_USE_MSAN:添加-fsanitize=memoryAFL_DONT_OPTIMIZE:禁用优化选项
- 添加
-B参数指定afl-as路径
2.2.3 main函数
int main(int argc, char** argv)
执行流程:
- 调用
find_as定位afl-as - 调用
edit_params编辑编译参数 - 调用
execvp执行真正的编译器
三、afl-as源码分析
3.1 主要功能
afl-as是汇编器的wrapper,负责在实际汇编过程中插入覆盖率检测代码。其核心功能包括:
- 处理汇编文件
- 识别需要插桩的位置
- 插入桩代码(trampoline)
- 调用真正的汇编器完成汇编
3.2 核心函数
3.2.1 edit_params函数
static void edit_params(u32 argc, char** argv)
功能:编辑传递给汇编器的参数
主要操作:
- 确定临时文件目录(检查
TMPDIR/TMP/TEMP环境变量) - 确定汇编器路径(检查
AFL_AS环境变量) - 处理
-m32/-m64标志 - 创建临时汇编文件路径:
tmp_dir/.afl-pid-time.s
3.2.2 add_instrumentation函数
static void add_instrumentation(void)
功能:向汇编代码中插入桩代码
插桩位置判断条件:
- 当前处于代码段(由
.text等section标记) - 行以制表符开头后跟字母
- 是以下类型之一:
- 函数入口点(如
main:) - GCC分支标签(如
.L0:) - Clang分支标签(如
.LBB0_0:) - 条件分支指令(如
\tjnz foo)
- 函数入口点(如
不插桩的情况:
- Clang注释(如
# BB#0:) - 非分支标签(如
.Ltmp0:,.LC0) - 无条件跳转(如
\tjmp foo)
3.2.3 桩代码分析
AFL插入的桩代码主要有两部分:
-
trampoline代码:保存寄存器状态,调用覆盖率记录函数
/* --- AFL TRAMPOLINE (64-BIT) */ .align 4 leaq -(128+24)(%rsp), %rsp movq %rdx, 0(%rsp) movq %rcx, 8(%rsp) movq %rax, 16(%rsp) movq $0x%08x, %rcx call __afl_maybe_log movq 16(%rsp), %rax movq 8(%rsp), %rcx movq 0(%rsp), %rdx leaq (128+24)(%rsp), %rsp /* --- END --- */ -
main_payload:包含覆盖率记录的核心逻辑
__afl_maybe_log: lahf seto %al movq __afl_area_ptr(%rip), %rdx testq %rdx, %rdx je __afl_setup __afl_store: xorq __afl_prev_loc(%rip), %rcx xorq %rcx, __afl_prev_loc(%rip) shrq $1, __afl_prev_loc(%rip) incb (%rdx, %rcx, 1) __afl_return: addb $127, %al sahf ret
四、AFL的forkserver机制
4.1 两种执行模式
AFL提供两种fuzzer与被测程序通信的方式:
-
execv模式:
- 每次测试都调用execv运行目标程序
- 效率低下,仅在使用
AFL_NO_FORKSRV或dumb_mode=1时启用
-
forkserver模式(默认):
- fuzzer和forkserver通过两个管道通信
- 控制管道(forkserver读,fuzzer写)
- 数据管道(fuzzer读,forkserver写)
4.2 forkserver工作流程
-
初始化阶段:
- fuzzer调用
execve()运行目标程序作为forkserver - forkserver执行到第一个桩代码处,进入
__afl_maybe_log - 初始化共享内存(
__afl_setup)
- fuzzer调用
-
测试执行阶段:
- forkserver通过
fork()创建子进程 - 子进程恢复执行被测程序
- 父进程等待子进程结束并报告状态
- forkserver通过
-
覆盖率记录:
- 子进程执行到桩代码时,调用
__afl_maybe_log - 通过
hare_memory[rcx^afl_prev_loc]++记录分支执行次数
- 子进程执行到桩代码时,调用
4.3 共享内存机制
-
共享内存初始化:
- fuzzer通过
shmget创建共享内存 - forkserver通过
shmat附加到共享内存 - 共享内存ID通过
AFL_SHM_ENV环境变量传递
- fuzzer通过
-
覆盖率存储:
- 共享内存(
trace_bits)用于存储分支命中次数 - 每个分支对应共享内存中的一个字节
- 分支ID计算方式:
current_block_id ^ previous_block_id
- 共享内存(
五、编译插桩实践指南
5.1 基本使用
-
使用afl-gcc编译目标程序:
afl-gcc -g -o target target.c -
环境变量控制:
AFL_HARDEN=1:启用强化编译选项AFL_USE_ASAN=1:启用AddressSanitizerAFL_INST_RATIO=100:设置插桩比例(0-100)
5.2 高级配置
-
LLVM模式:
afl-clang-fast -g -o target target.c -
持久模式:
- 在目标代码中手动标记循环:
while (__AFL_LOOP(1000)) { /* Test case processing */ } -
自定义插桩:
- 修改
afl-as.h中的桩代码模板 - 调整插桩判断逻辑
- 修改
六、性能优化建议
-
ASAN优化:
- 当使用ASAN时,设置
AFL_INST_RATIO=30降低插桩密度 - 避免同时启用ASAN和MSAN
- 当使用ASAN时,设置
-
共享内存优化:
- 确保
/dev/shm有足够空间 - 考虑使用
AFL_SHM_ENV指定自定义共享内存ID
- 确保
-
forkserver调优:
- 对于短时间运行的程序,考虑禁用forkserver
- 监控forkserver子进程数量避免资源耗尽
七、常见问题排查
-
插桩失败:
- 检查
afl-as是否在PATH中 - 验证
AFL_PATH环境变量设置正确
- 检查
-
共享内存错误:
- 检查
/proc/sys/kernel/shmmax设置 - 验证用户是否有足够的共享内存权限
- 检查
-
forkserver挂起:
- 检查管道通信是否正常
- 验证目标程序是否在第一个桩代码处停止
通过深入理解AFL的编译插桩机制,可以更好地利用其进行高效的模糊测试,并能够根据实际需求进行定制和优化。