AFL原码分析----编译和插桩
字数 2824 2025-08-06 18:07:59

AFL源码分析:编译和插桩机制详解

一、AFL整体架构概述

AFL(American Fuzzy Lop)是一款著名的模糊测试工具,其核心创新在于通过编译时插桩来收集代码覆盖率信息。AFL主要包含三个关键组件:

  1. afl-gcc/afl-clang:编译器封装器,负责在编译过程中插入代码覆盖率检测
  2. afl-as:汇编器封装器,实际执行插桩操作
  3. afl-fuzz:模糊测试引擎,利用收集的覆盖率信息指导测试

AFL支持三种工作模式:

  • 普通模式:通过gcc/clang在汇编层面插桩
  • LLVM模式:在编译层面为程序插桩,适用于clang
  • QEMU模式:通过修改QEMU代码在模拟执行时记录路径

二、afl-gcc源码分析

2.1 主要功能

afl-gcc本质上是一个编译器wrapper,它在系统编译器(gcc/clang)上层添加了一层封装,主要功能是:

  1. 定位afl-as的位置
  2. 修改编译参数
  3. 调用真正的编译器进行编译

2.2 核心函数

2.2.1 find_as函数

static void find_as(u8* argv0)

功能:查找afl-as的位置

查找顺序:

  1. 检查AFL_PATH环境变量指定的路径
  2. 检查argv0所在目录(即afl-gcc所在目录)
  3. 检查默认安装路径AFL_PATH

2.2.2 edit_params函数

static void edit_params(u32 argc, char** argv)

功能:编辑传递给编译器的参数

主要操作:

  1. 识别编译器类型(gcc/g++/clang/clang++)
  2. 处理特殊编译选项(-B, -integrated-as, -pipe等)
  3. 根据环境变量设置编译标志:
    • AFL_HARDEN:添加-fstack-protector-all
    • AFL_USE_ASAN:添加-fsanitize=address
    • AFL_USE_MSAN:添加-fsanitize=memory
    • AFL_DONT_OPTIMIZE:禁用优化选项
  4. 添加-B参数指定afl-as路径

2.2.3 main函数

int main(int argc, char** argv)

执行流程:

  1. 调用find_as定位afl-as
  2. 调用edit_params编辑编译参数
  3. 调用execvp执行真正的编译器

三、afl-as源码分析

3.1 主要功能

afl-as是汇编器的wrapper,负责在实际汇编过程中插入覆盖率检测代码。其核心功能包括:

  1. 处理汇编文件
  2. 识别需要插桩的位置
  3. 插入桩代码(trampoline)
  4. 调用真正的汇编器完成汇编

3.2 核心函数

3.2.1 edit_params函数

static void edit_params(u32 argc, char** argv)

功能:编辑传递给汇编器的参数

主要操作:

  1. 确定临时文件目录(检查TMPDIR/TMP/TEMP环境变量)
  2. 确定汇编器路径(检查AFL_AS环境变量)
  3. 处理-m32/-m64标志
  4. 创建临时汇编文件路径:tmp_dir/.afl-pid-time.s

3.2.2 add_instrumentation函数

static void add_instrumentation(void)

功能:向汇编代码中插入桩代码

插桩位置判断条件:

  1. 当前处于代码段(由.text等section标记)
  2. 行以制表符开头后跟字母
  3. 是以下类型之一:
    • 函数入口点(如main:)
    • GCC分支标签(如.L0:)
    • Clang分支标签(如.LBB0_0:)
    • 条件分支指令(如\tjnz foo)

不插桩的情况:

  • Clang注释(如# BB#0:)
  • 非分支标签(如.Ltmp0:, .LC0)
  • 无条件跳转(如\tjmp foo)

3.2.3 桩代码分析

AFL插入的桩代码主要有两部分:

  1. 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 --- */
    
  2. 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与被测程序通信的方式:

  1. execv模式

    • 每次测试都调用execv运行目标程序
    • 效率低下,仅在使用AFL_NO_FORKSRVdumb_mode=1时启用
  2. forkserver模式(默认):

    • fuzzer和forkserver通过两个管道通信
    • 控制管道(forkserver读,fuzzer写)
    • 数据管道(fuzzer读,forkserver写)

4.2 forkserver工作流程

  1. 初始化阶段

    • fuzzer调用execve()运行目标程序作为forkserver
    • forkserver执行到第一个桩代码处,进入__afl_maybe_log
    • 初始化共享内存(__afl_setup)
  2. 测试执行阶段

    • forkserver通过fork()创建子进程
    • 子进程恢复执行被测程序
    • 父进程等待子进程结束并报告状态
  3. 覆盖率记录

    • 子进程执行到桩代码时,调用__afl_maybe_log
    • 通过hare_memory[rcx^afl_prev_loc]++记录分支执行次数

4.3 共享内存机制

  1. 共享内存初始化

    • fuzzer通过shmget创建共享内存
    • forkserver通过shmat附加到共享内存
    • 共享内存ID通过AFL_SHM_ENV环境变量传递
  2. 覆盖率存储

    • 共享内存(trace_bits)用于存储分支命中次数
    • 每个分支对应共享内存中的一个字节
    • 分支ID计算方式:current_block_id ^ previous_block_id

五、编译插桩实践指南

5.1 基本使用

  1. 使用afl-gcc编译目标程序:

    afl-gcc -g -o target target.c
    
  2. 环境变量控制:

    • AFL_HARDEN=1:启用强化编译选项
    • AFL_USE_ASAN=1:启用AddressSanitizer
    • AFL_INST_RATIO=100:设置插桩比例(0-100)

5.2 高级配置

  1. LLVM模式

    afl-clang-fast -g -o target target.c
    
  2. 持久模式

    • 在目标代码中手动标记循环:
    while (__AFL_LOOP(1000)) {
      /* Test case processing */
    }
    
  3. 自定义插桩

    • 修改afl-as.h中的桩代码模板
    • 调整插桩判断逻辑

六、性能优化建议

  1. ASAN优化

    • 当使用ASAN时,设置AFL_INST_RATIO=30降低插桩密度
    • 避免同时启用ASAN和MSAN
  2. 共享内存优化

    • 确保/dev/shm有足够空间
    • 考虑使用AFL_SHM_ENV指定自定义共享内存ID
  3. forkserver调优

    • 对于短时间运行的程序,考虑禁用forkserver
    • 监控forkserver子进程数量避免资源耗尽

七、常见问题排查

  1. 插桩失败

    • 检查afl-as是否在PATH中
    • 验证AFL_PATH环境变量设置正确
  2. 共享内存错误

    • 检查/proc/sys/kernel/shmmax设置
    • 验证用户是否有足够的共享内存权限
  3. forkserver挂起

    • 检查管道通信是否正常
    • 验证目标程序是否在第一个桩代码处停止

通过深入理解AFL的编译插桩机制,可以更好地利用其进行高效的模糊测试,并能够根据实际需求进行定制和优化。

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函数 功能:查找afl-as的位置 查找顺序: 检查 AFL_PATH 环境变量指定的路径 检查argv0所在目录(即afl-gcc所在目录) 检查默认安装路径 AFL_PATH 2.2.2 edit_ params函数 功能:编辑传递给编译器的参数 主要操作: 识别编译器类型(gcc/g++/clang/clang++) 处理特殊编译选项(-B, -integrated-as, -pipe等) 根据环境变量设置编译标志: AFL_HARDEN :添加 -fstack-protector-all AFL_USE_ASAN :添加 -fsanitize=address AFL_USE_MSAN :添加 -fsanitize=memory AFL_DONT_OPTIMIZE :禁用优化选项 添加 -B 参数指定afl-as路径 2.2.3 main函数 执行流程: 调用 find_as 定位afl-as 调用 edit_params 编辑编译参数 调用 execvp 执行真正的编译器 三、afl-as源码分析 3.1 主要功能 afl-as是汇编器的wrapper,负责在实际汇编过程中插入覆盖率检测代码。其核心功能包括: 处理汇编文件 识别需要插桩的位置 插入桩代码(trampoline) 调用真正的汇编器完成汇编 3.2 核心函数 3.2.1 edit_ params函数 功能:编辑传递给汇编器的参数 主要操作: 确定临时文件目录(检查 TMPDIR / TMP / TEMP 环境变量) 确定汇编器路径(检查 AFL_AS 环境变量) 处理 -m32 / -m64 标志 创建临时汇编文件路径: tmp_dir/.afl-pid-time.s 3.2.2 add_ instrumentation函数 功能:向汇编代码中插入桩代码 插桩位置判断条件: 当前处于代码段(由 .text 等section标记) 行以制表符开头后跟字母 是以下类型之一: 函数入口点(如 main: ) GCC分支标签(如 .L0: ) Clang分支标签(如 .LBB0_0: ) 条件分支指令(如 \tjnz foo ) 不插桩的情况: Clang注释(如 # BB#0: ) 非分支标签(如 .Ltmp0: , .LC0 ) 无条件跳转(如 \tjmp foo ) 3.2.3 桩代码分析 AFL插入的桩代码主要有两部分: trampoline代码 :保存寄存器状态,调用覆盖率记录函数 main_ payload :包含覆盖率记录的核心逻辑 四、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 ) 测试执行阶段 : forkserver通过 fork() 创建子进程 子进程恢复执行被测程序 父进程等待子进程结束并报告状态 覆盖率记录 : 子进程执行到桩代码时,调用 __afl_maybe_log 通过 hare_memory[rcx^afl_prev_loc]++ 记录分支执行次数 4.3 共享内存机制 共享内存初始化 : fuzzer通过 shmget 创建共享内存 forkserver通过 shmat 附加到共享内存 共享内存ID通过 AFL_SHM_ENV 环境变量传递 覆盖率存储 : 共享内存( trace_bits )用于存储分支命中次数 每个分支对应共享内存中的一个字节 分支ID计算方式: current_block_id ^ previous_block_id 五、编译插桩实践指南 5.1 基本使用 使用afl-gcc编译目标程序: 环境变量控制: AFL_HARDEN=1 :启用强化编译选项 AFL_USE_ASAN=1 :启用AddressSanitizer AFL_INST_RATIO=100 :设置插桩比例(0-100) 5.2 高级配置 LLVM模式 : 持久模式 : 在目标代码中手动标记循环: 自定义插桩 : 修改 afl-as.h 中的桩代码模板 调整插桩判断逻辑 六、性能优化建议 ASAN优化 : 当使用ASAN时,设置 AFL_INST_RATIO=30 降低插桩密度 避免同时启用ASAN和MSAN 共享内存优化 : 确保 /dev/shm 有足够空间 考虑使用 AFL_SHM_ENV 指定自定义共享内存ID forkserver调优 : 对于短时间运行的程序,考虑禁用forkserver 监控forkserver子进程数量避免资源耗尽 七、常见问题排查 插桩失败 : 检查 afl-as 是否在PATH中 验证 AFL_PATH 环境变量设置正确 共享内存错误 : 检查 /proc/sys/kernel/shmmax 设置 验证用户是否有足够的共享内存权限 forkserver挂起 : 检查管道通信是否正常 验证目标程序是否在第一个桩代码处停止 通过深入理解AFL的编译插桩机制,可以更好地利用其进行高效的模糊测试,并能够根据实际需求进行定制和优化。