AFL源码学习(三)
字数 3893 2025-08-29 08:30:30

AFL源码学习与Fuzzing技术详解

前言

本文基于AFL-2.57b版本源码,深入分析AFL(American Fuzzy Lop)的实现原理和关键技术点。AFL是一款革命性的模糊测试工具,通过编译时插桩和覆盖率引导,实现了高效的漏洞挖掘能力。

AFL整体架构

AFL运行时包含三个主要进程:

  1. fuzzer:主控制进程,负责变异策略和调度
  2. fork server:用于快速fork子进程
  3. child:实际执行目标程序的进程

初始化流程

1. 参数处理与环境设置

main() -> 随机化种子 -> 处理argv参数 -> 信号处理设置

信号处理设置:

  • SIGHUP/SIGINT/SIGTERM:设置stop_soon标志,关闭fork server和child
  • SIGALRM:处理超时,关闭child或fork server
  • SIGWINCH:UI显示警告
  • SIGUSR1:设置skip_requested标志,跳过当前样例变异

2. 安全检查

check_asan_opts()检查ASAN和MSAN配置:

  • ASAN需要设置:abort_on_error=1, symbolize=0
  • MSAN需要设置:exit_code=MSAN_ERROR, symbolize=0

3. 并行模式处理

并行模式(-S -M)下:

  • 检查sync_id合法性
  • 设置sync_dir(工作目录)和out_dir(工作目录/sync_id)
  • 非并行模式直接使用"-o out_dir"

4. CPU绑定与性能设置

Linux下:

  • 绑定CPU核心
  • 检查并设置CPU为performance模式(最高频率)

5. 后处理模块设置

setup_post()

  • 检查AFL_POST_LIBRARY环境变量
  • 使用dlopen加载指定库
  • 设置afl_postprocess指向库中的post_handler函数

post_handler签名:u8* post_handler(u8* data, u32* len)

  • 在变异出新用例后、执行前被调用
  • 可用于记录或修改变异用例

6. 共享内存初始化

setup_shm()

  • 初始化2字节(16bit)的共享内存
  • 相比之前版本(8bit)有显著优化

7. 文件系统准备

  • 创建queue等必要目录
  • 打开/dev/null等备用文件描述符
  • 读取测试用例到queue链表
  • 读取extra token(可自动生成或通过-x指定字典)
  • 复制初始输入(-i指定的文件)
  • 创建.cur_input文件用于存储变异用例

8. 初始测试执行

perform_dry_run()

  • 测试queue中的所有用例
  • 使用calibrate_case()校准每个用例
  • 首次执行时调用check_map_coverage()检查覆盖率均匀性

关键函数分析

1. calibrate_case() - 用例校准

主要工作:

  1. 如果fork server未准备好,调用init_forkserver()
  2. 多次调用run_target()测试用例路径
    • 发现无插桩则退出
    • 发现新路径则提升测试次数到40次
  3. 更新用例信息
  4. 调用update_bitmap_score()进行打分

2. init_forkserver() - 初始化fork server

通讯流程:

  1. 初始化管道

  2. fork子进程:

    • 设置内存限制
    • 关闭core dump
    • 开启新进程组
    • 重定向流
    • 绑定管道文件描述符(198: supervisor→fork server, 199: fork server→supervisor)
    • 关闭无用fd
    • 设置ASan/MSan
    • 执行程序并在第一个节点暂停
    • 向fd 199写入4字节hello数据包
    • 阻塞等待supervisor指令
  3. 主进程:

    • 绑定管道文件描述符
    • 等待fork server的4字节数据
    • 接收成功则返回,否则检查问题

3. run_target() - 执行目标程序

流程:

  1. 清空trace_bits
  2. 发送4字节启动child
  3. 从fork server接收4字节获取child pid
  4. 再次接收4字节判断退出原因
  5. 对trace_bits进行分桶操作
  6. 返回状态码

4. update_bitmap_score() - 用例打分

打分标准:

  • 分数 = 执行时间(exec_us) * 文件大小(len)
  • 分数越小表示用例越好

favored集合构建:

  • 对于共享内存的每个位置(代表一条边)
  • 记录top_rated指针,指向覆盖该边且分数最小的用例
  • 最终形成的favored集合比完整corpus小5-10倍

主fuzzing循环

循环遍历queue,对每个用例:

  1. 调用cull_queue()精简队列
  2. 调用fuzz_one()变异当前用例
  3. 并行模式下每5次调用sync_fuzzers()同步
  4. 如果遍历完无新发现:
    • 开启use_splicing模式
    • 增加cycles_wo_finds计数器

1. cull_queue() - 队列精简

流程:

  1. 初始化temp_v为0xff
  2. 清空队列的favorable标记
  3. 遍历trace_bits,去除冗余路径用例
  4. 重构favored集合

2. fuzz_one() - 用例变异

准备阶段

  • 判断是否跳过变异:
    • 有全新favored用例:99%跳过
    • 已fuzz过或不受青睐用例:尽快安排青睐用例
    • 无全新favored用例:95%跳过已fuzz,75%跳过未fuzz
  • 环境准备:mmap映射用例,为out_buf分配空间

校准阶段

  • 用例校准失败时尝试重新校准(最多3次)

修剪阶段

  • 未修剪用例调用trim_case()
  • 类似afl-tmin的block deletion:
    1. 分16块
    2. 遍历删除块
    3. 测试路径是否相同
    4. 相同则覆盖原用例

性能评分

  • 调用calculate_score()基于耗时、覆盖率、深度等因素
  • 决定是否跳过deterministic变异:
    1. -d参数设置(skip_deterministic为true)
    2. 用例已fuzz过
    3. 用例已完成deterministic阶段
    4. 并行模式(-M)自行处理deterministic

Deterministic变异阶段

bitflip(字节翻转)
  • bitflip 1/1:每个bit翻转
    • 调用common_fuzz_stuff()测试
    • 调用maybe_add_auto()构建自动词典
  • bitflip 8/8:每个字节翻转
    • 进行敏感分析:标记路径变化的位置
    • 敏感部分>90%则全标记
    • 长度<128则全标记
arith(简单加减)
  • arith 8/8、16/8、32/8
  • 给uint8/16/32加/减[1,35]
  • 跳过bitflip已覆盖位置
interest(有趣替换)
  • interest 8/8、16/8、32/8
  • 替换为内置interesting_n[]值
  • 跳过非敏感位置和已覆盖位置
extras(词典替换)
  • user extras (over):用户词典覆盖
  • user extras (insert):用户词典插入
  • auto extras (over):自动词典覆盖(最多前50个)

Havoc随机变异阶段

  • 执行次数受用例得分和havoc_div影响
  • 随机组合2-128个变异算子
  • 17个变异算子:
    1. 随机bit翻转
    2. 随机字节替换为interesting_8[]
    3. 随机word替换为interesting_16[]
    4. 随机dword替换为interesting_32[]
    5. 随机字节减[1,35]
    6. 随机字节加[1,35]
    7. 随机word减[1,35]
    8. 随机word加[1,35]
    9. 随机dword减[1,35]
    10. 随机dword加[1,35]
    11. 随机字节替换为随机值
    12. 随机删除块(概率较高)
    13. 随机插入块(75%复制用例块,25%随机相同字节块)
    14. 随机覆盖块(75%复制用例块,25%随机相同字节块)
    15. 随机覆写词典条目
    16. 随机插入词典条目

Splicing拼接变异阶段

  • 最多执行15次
  • 每次执行后调用havoc再次变异
  • 随机挑选另一用例,随机分割点拼接
  • 生成新用例后调用havoc变异

3. sync_fuzzers() - 并行同步

流程:

  1. 遍历其他fuzzer的queue文件
  2. 测试所有用例
  3. 调用save_if_interesting()尝试加入队列
  4. 通过sync_id比较避免重复测试

关键技术点

1. 覆盖率引导

  • 使用共享内存trace_bits记录边覆盖
  • 分桶操作减少碰撞
  • 通过覆盖率变化判断用例价值

2. 变异策略

  • 分层变异:deterministic -> havoc -> splicing
  • 多种变异算子组合
  • 敏感度分析优化变异效率

3. 队列管理

  • favored集合构建
  • 用例打分与优先级调度
  • 精简策略减少冗余测试

4. 性能优化

  • fork server减少进程创建开销
  • 共享内存快速通信
  • 文件级操作简化并行同步
  • 短用例优先策略

总结

AFL通过创新的覆盖率引导、高效的变异策略和精细的队列管理,实现了模糊测试领域的重大突破。其关键技术包括:

  1. 编译时插桩获取精确覆盖率
  2. 分层变异策略平衡探索与利用
  3. fork server机制优化执行效率
  4. 敏感度分析和精简队列减少冗余
  5. 文件级并行同步简化分布式部署

这些设计思想不仅适用于安全测试,也为其他领域的自动化测试提供了宝贵参考。

AFL源码学习与Fuzzing技术详解 前言 本文基于AFL-2.57b版本源码,深入分析AFL(American Fuzzy Lop)的实现原理和关键技术点。AFL是一款革命性的模糊测试工具,通过编译时插桩和覆盖率引导,实现了高效的漏洞挖掘能力。 AFL整体架构 AFL运行时包含三个主要进程: fuzzer :主控制进程,负责变异策略和调度 fork server :用于快速fork子进程 child :实际执行目标程序的进程 初始化流程 1. 参数处理与环境设置 信号处理设置: SIGHUP/SIGINT/SIGTERM:设置stop_ soon标志,关闭fork server和child SIGALRM:处理超时,关闭child或fork server SIGWINCH:UI显示警告 SIGUSR1:设置skip_ requested标志,跳过当前样例变异 2. 安全检查 check_asan_opts() 检查ASAN和MSAN配置: ASAN需要设置: abort_on_error=1 , symbolize=0 MSAN需要设置: exit_code=MSAN_ERROR , symbolize=0 3. 并行模式处理 并行模式(-S -M)下: 检查sync_ id合法性 设置sync_ dir(工作目录)和out_ dir(工作目录/sync_ id) 非并行模式直接使用"-o out_ dir" 4. CPU绑定与性能设置 Linux下: 绑定CPU核心 检查并设置CPU为performance模式(最高频率) 5. 后处理模块设置 setup_post() : 检查AFL_ POST_ LIBRARY环境变量 使用dlopen加载指定库 设置afl_ postprocess指向库中的post_ handler函数 post_ handler签名: u8* post_handler(u8* data, u32* len) 在变异出新用例后、执行前被调用 可用于记录或修改变异用例 6. 共享内存初始化 setup_shm() : 初始化2字节(16bit)的共享内存 相比之前版本(8bit)有显著优化 7. 文件系统准备 创建queue等必要目录 打开/dev/null等备用文件描述符 读取测试用例到queue链表 读取extra token(可自动生成或通过-x指定字典) 复制初始输入(-i指定的文件) 创建.cur_ input文件用于存储变异用例 8. 初始测试执行 perform_dry_run() : 测试queue中的所有用例 使用 calibrate_case() 校准每个用例 首次执行时调用 check_map_coverage() 检查覆盖率均匀性 关键函数分析 1. calibrate_ case() - 用例校准 主要工作: 如果fork server未准备好,调用 init_forkserver() 多次调用 run_target() 测试用例路径 发现无插桩则退出 发现新路径则提升测试次数到40次 更新用例信息 调用 update_bitmap_score() 进行打分 2. init_ forkserver() - 初始化fork server 通讯流程: 初始化管道 fork子进程: 设置内存限制 关闭core dump 开启新进程组 重定向流 绑定管道文件描述符(198: supervisor→fork server, 199: fork server→supervisor) 关闭无用fd 设置ASan/MSan 执行程序并在第一个节点暂停 向fd 199写入4字节hello数据包 阻塞等待supervisor指令 主进程: 绑定管道文件描述符 等待fork server的4字节数据 接收成功则返回,否则检查问题 3. run_ target() - 执行目标程序 流程: 清空trace_ bits 发送4字节启动child 从fork server接收4字节获取child pid 再次接收4字节判断退出原因 对trace_ bits进行分桶操作 返回状态码 4. update_ bitmap_ score() - 用例打分 打分标准: 分数 = 执行时间(exec_ us) * 文件大小(len) 分数越小表示用例越好 favored集合构建: 对于共享内存的每个位置(代表一条边) 记录top_ rated指针,指向覆盖该边且分数最小的用例 最终形成的favored集合比完整corpus小5-10倍 主fuzzing循环 循环遍历queue,对每个用例: 调用 cull_queue() 精简队列 调用 fuzz_one() 变异当前用例 并行模式下每5次调用 sync_fuzzers() 同步 如果遍历完无新发现: 开启use_ splicing模式 增加cycles_ wo_ finds计数器 1. cull_ queue() - 队列精简 流程: 初始化temp_ v为0xff 清空队列的favorable标记 遍历trace_ bits,去除冗余路径用例 重构favored集合 2. fuzz_ one() - 用例变异 准备阶段 判断是否跳过变异: 有全新favored用例:99%跳过 已fuzz过或不受青睐用例:尽快安排青睐用例 无全新favored用例:95%跳过已fuzz,75%跳过未fuzz 环境准备:mmap映射用例,为out_ buf分配空间 校准阶段 用例校准失败时尝试重新校准(最多3次) 修剪阶段 未修剪用例调用 trim_case() 类似afl-tmin的block deletion: 分16块 遍历删除块 测试路径是否相同 相同则覆盖原用例 性能评分 调用 calculate_score() 基于耗时、覆盖率、深度等因素 决定是否跳过deterministic变异: -d参数设置(skip_ deterministic为true) 用例已fuzz过 用例已完成deterministic阶段 并行模式(-M)自行处理deterministic Deterministic变异阶段 bitflip(字节翻转) bitflip 1/1:每个bit翻转 调用 common_fuzz_stuff() 测试 调用 maybe_add_auto() 构建自动词典 bitflip 8/8:每个字节翻转 进行敏感分析:标记路径变化的位置 敏感部分>90%则全标记 长度 <128则全标记 arith(简单加减) arith 8/8、16/8、32/8 给uint8/16/32加/减[ 1,35 ] 跳过bitflip已覆盖位置 interest(有趣替换) interest 8/8、16/8、32/8 替换为内置interesting_ n[ ]值 跳过非敏感位置和已覆盖位置 extras(词典替换) user extras (over):用户词典覆盖 user extras (insert):用户词典插入 auto extras (over):自动词典覆盖(最多前50个) Havoc随机变异阶段 执行次数受用例得分和havoc_ div影响 随机组合2-128个变异算子 17个变异算子: 随机bit翻转 随机字节替换为interesting_ 8[ ] 随机word替换为interesting_ 16[ ] 随机dword替换为interesting_ 32[ ] 随机字节减[ 1,35 ] 随机字节加[ 1,35 ] 随机word减[ 1,35 ] 随机word加[ 1,35 ] 随机dword减[ 1,35 ] 随机dword加[ 1,35 ] 随机字节替换为随机值 随机删除块(概率较高) 随机插入块(75%复制用例块,25%随机相同字节块) 随机覆盖块(75%复制用例块,25%随机相同字节块) 随机覆写词典条目 随机插入词典条目 Splicing拼接变异阶段 最多执行15次 每次执行后调用havoc再次变异 随机挑选另一用例,随机分割点拼接 生成新用例后调用havoc变异 3. sync_ fuzzers() - 并行同步 流程: 遍历其他fuzzer的queue文件 测试所有用例 调用 save_if_interesting() 尝试加入队列 通过sync_ id比较避免重复测试 关键技术点 1. 覆盖率引导 使用共享内存trace_ bits记录边覆盖 分桶操作减少碰撞 通过覆盖率变化判断用例价值 2. 变异策略 分层变异:deterministic -> havoc -> splicing 多种变异算子组合 敏感度分析优化变异效率 3. 队列管理 favored集合构建 用例打分与优先级调度 精简策略减少冗余测试 4. 性能优化 fork server减少进程创建开销 共享内存快速通信 文件级操作简化并行同步 短用例优先策略 总结 AFL通过创新的覆盖率引导、高效的变异策略和精细的队列管理,实现了模糊测试领域的重大突破。其关键技术包括: 编译时插桩获取精确覆盖率 分层变异策略平衡探索与利用 fork server机制优化执行效率 敏感度分析和精简队列减少冗余 文件级并行同步简化分布式部署 这些设计思想不仅适用于安全测试,也为其他领域的自动化测试提供了宝贵参考。