AFL二三事——源码分析(上篇)
字数 2814 2025-08-24 07:48:33

AFL源码分析教学文档(上篇)

一、AFL概述

AFL(American Fuzzy Lop)是由Michal Zalewski开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具。它通过记录输入样本的代码覆盖率(代码执行路径的覆盖情况),以此进行反馈,对输入样本进行调整以提高覆盖率,从而提升发现漏洞的可能性。

AFL主要特点

  • 支持有源码和无源码程序的模糊测试
  • 基于覆盖率引导的反馈机制
  • 设计思想和实现方案在模糊测试领域具有重要意义
  • 支持多种插桩模式:普通插桩、LLVM模式、QEMU模式

二、AFL源码结构

主要模块

  1. 插桩模块

    • afl-as.h, afl-as.c, afl-gcc.c:普通插桩模式,针对源码插桩
    • llvm_mode:LLVM插桩模式,针对源码插桩
    • qemu_mode:QEMU插桩模式,针对二进制文件插桩
  2. Fuzzer模块

    • afl-fuzz.c:Fuzzer实现的核心代码,AFL的主体
  3. 辅助模块

    • 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*模式

主要函数

  1. find_as(argv[0]):查找使用的汇编器

    • 检查环境变量AFL_PATH
    • 检查argv[0]路径
    • 失败则抛出异常
  2. edit_params(argc, argv):处理传入的编译参数

    • 设置编译器参数(gcc/clang)
    • 处理各种编译选项(-B, -integrated-as, -pipe等)
    • 处理sanitizer相关选项(ASAN/MSAN)
    • 设置优化选项(-g -O3 -funroll-loops等)
  3. 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'的参数数量

主要函数

  1. edit_params函数:

    • 设置as_params参数数组
    • 处理--64/--32选项
    • 设置输入/输出文件路径
  2. add_instrumentation函数(核心插桩函数):

    • 读取输入文件的每一行
    • 只在.text段插入桩代码
    • 跳过标签、宏、注释
    • 处理条件跳转指令(jnz等)
    • 处理函数入口点和分支标签

插桩逻辑

  1. 插桩位置:

    • 函数入口点(如^main:
    • GCC分支标签(如^.L0:
    • clang分支标签(如^.LBB0_0:
    • 条件跳转指令(如\tjnz foo
  2. 插桩代码(trampoline):

    • 32位:trampoline_fmt_32
    • 64位:trampoline_fmt_64

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函数流程

  1. 保存寄存器状态
  2. 检查共享内存是否已映射
  3. 初始化fork server
  4. 记录执行路径

共享内存变量

  • __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,实现编译器级别的插桩。

优势

  1. 编译器可以进行更多优化
  2. CPU架构无关
  3. 更好地处理多线程目标

关键函数

  1. find_obj:查找运行时库afl-llvm-rt.o
  2. edit_params:处理编译参数
    • 设置Clang编译器
    • 处理插桩模式(传统模式或trace-pc-guard模式)
    • 处理各种编译选项

3. afl-llvm-pass.so.cc分析

主要功能

实现LLVM模式的插桩Pass,在IR级别插入覆盖率检测代码。

关键函数

runOnModule函数:

  1. 获取LLVMContext
  2. 设置插桩密度(AFL_INST_RATIO
  3. 获取共享内存指针和上一个基本块ID
  4. 遍历每个基本块(BB):
    • 寻找适合插桩的位置
    • 随机创建当前BB的ID
    • 插入load指令获取前一个BB的ID
    • 插入共享内存访问指令
    • 更新执行计数
    • 更新__afl_prev_loc

4. afl-llvm-rt.o.c分析

三种特殊功能

  1. Deferred Instrumentation

    • 延迟forkserver初始化
    • 在大部分初始化完成后、二进制解析前进行克隆
    • 使用__AFL_INIT()
  2. Persistent Mode

    • 长期存活的进程测试多个用例
    • 减少fork()调用和OS开销
    • 使用__AFL_LOOP(1000)
  3. Trace-pc-guard Mode

    • 在每个edge插入桩代码
    • 使用__sanitizer_cov_trace_pc_guard函数
    • 需要设置AFL_TRACE_PC=1-fsanitize-coverage=trace-pc-guard参数

五、关键技术与设计思想

  1. 覆盖率引导:通过记录代码覆盖率指导fuzzing方向
  2. 高效插桩:在关键位置插入轻量级检测代码
  3. 进程复用:forkserver机制减少进程创建开销
  4. 多种模式:适应不同场景(普通、LLVM、QEMU)
  5. 反馈优化:共享内存记录执行路径,指导变异策略

六、总结

AFL的源码设计体现了以下核心思想:

  1. 最小化开销:通过精巧的插桩和进程复用减少性能损耗
  2. 最大化反馈:覆盖率信息指导变异策略
  3. 灵活性:支持多种插桩模式和运行方式
  4. 可扩展性:模块化设计便于功能扩展和定制

上篇主要分析了AFL的普通插桩模式和LLVM模式的核心实现,下篇将深入分析QEMU模式、fuzzer核心逻辑和变异策略等内容。

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,主要作用是实现对于关键节点的代码插桩(汇编级),记录程序执行路径等关键信息。 关键变量 主要函数 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生成的汇编文件,并注入插桩代码。 关键变量 主要函数 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 3. 插桩蹦床(trampoline)分析 32位插桩代码 64位插桩代码 __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.o edit_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核心逻辑和变异策略等内容。