AFL++(一)

176 阅读8分钟

AFL++(一)

本文信息量较少,我是刚开始看AFL++,啥也不懂,这个文章主要是理思路明确之后怎么精进,如果感兴趣可以看后续文章(如果有的话)。

AFL++ 是 Google 的 AFL 的一个分支,具有更快的速度,更多的突变,更多的自定义模块等

AFL++的github地址:github.com/AFLplusplus…

主要的目标是使用AFL++ Fuzzing 测试MCU固件,当我们解压提取一个固件时,能够获得大量的IOT二进制应用,如果要进行漏洞挖掘则需要将二进制文件进行逆向分析,然后查找危险函数以及输入接口,对于一个大型的应用,直接进行二进制分析由于代码量过大会大大降低我们的效率,所以可以使用Fuzzing技术进行模糊测试,提高漏洞挖掘效率。

对于固件通常由于架构的不同,不能直接进行Fuzzing,AFL++可以使用Qemu、Unicorn或Frida 三种模式进行Fuzzing,虽然不如有源码进行Fuzzing高效,但是其适用面广,并且同样能提高漏洞挖掘效率。

这里我核心聚焦于使用Qemu的Fuzzing。

AFL++使用qemu用户模式模拟仿真来运行二进制文件,其使用的qemu是进行修改的版本,在程序执行时检测基本块,根据收集的信息生成测试用例,通过生成的大量测试用例触发不同的代码路径,从而提高代码的覆盖率,提高触发Crash的概率。

AFL++和其他的类似的Fuzzing工具一样,仅适用于文件输入的Fuzzing。

本文的最终目标是修改AFL++使其适配经过我们修改后的qemu,使其能够更好更快速地对MCU固件进行fuzzing。

一、 AFL++简述

1. AFL++运行逻辑

首先想要修改AFL++先要了解其大致运行逻辑。

核心组件:

组件功能
afl-cc / afl-clang-fast编译时插桩工具(LLVM/Clang 插件)
afl-qemu-traceQEMU 模式下的动态插桩工具
afl-fuzz主模糊测试引擎(fuzzer)
afl-showmap覆盖率分析工具
afl-cmin最小化测试用例集
afl-tmin测试用例最小化(简化崩溃样本)

2. AFL++ 的模糊测试流程

  1. 准备阶段

    • 获取目标程序
    • 使用 QEMU 模式进行动态插桩
    • 准备初始种子(seed)输入文件
    • (可选)准备字典(dictionary)以提升语法感知能力
  2. 初始化阶段

    • afl-fuzz 启动后:
      • 读取种子目录中的输入
      • 使用 afl-showmap 或直接执行方式测量每个种子的执行路径(edge coverage)
      • 构建初始的“覆盖图”(coverage map),记录哪些边(edges)已被触发
      • 将未覆盖的路径加入待处理队列(queue)
  3. 模糊测试主循环(Fuzzing Loop)

    • 从队列中取出一个测试用例(test case)
    • 应用一系列变异策略(mutations)
      • 位翻转、字节增减、块复制/移动、arith 变异等
      • RedQueen(基于符号执行的语义感知变异)
      • MOpt(机器学习启发式变异调度)
    • 执行变异后的输入:
      • 使用 QEMU 模式 → 通过 afl-qemu-trace 运行
    • 收集执行反馈(通过共享内存中的 trace bitmap)
    • 分析新覆盖:
      • 是否发现了新的基本块转移路径(edge)?
      • 是否导致崩溃(crash)或超时(hang)?
    • 更新内部状态:
      • 若发现新路径 → 将该输入加入队列
      • 若崩溃 → 保存到 crashes/ 目录
      • 若 hang → 保存到 hangs/ 目录
    • 调度策略(Power Schedules)决定下一个优先 fuzz 的测试用例(如 AFLFast 的指数调度)
  4. 持续迭代

    • 直到用户手动停止或达到预设时间/迭代次数

二、AFL++ 与 QEMU 的交互逻辑(QEMU Mode)

当目标程序没有源码时,无法使用 LLVM 插桩,AFL++ 提供了 QEMU 用户态模拟模式(User Mode Emulation) 来实现动态二进制插桩。

QEMU Mode 是 AFL++ 中用于二进制模糊测试的核心技术之一。

1. QEMU Mode 的工作原理

AFL++ 的 QEMU 模式基于修改版的 QEMU(版本 ~5.1+),在模拟执行目标二进制文件时,插入轻量级的基本块边界探针,从而收集覆盖率信息。

核心思想:
  • 利用 QEMU 的 TCG 在 JIT 编译过程中动态插入插桩代码,TCG是 QEMU 的核心组件之一。它负责把目标程序的原始指令翻译成当前 CPU 能运行的中间代码,然后再转成本地指令执行,这里插桩是为了记录运行到哪里了
  • 记录每次基本块之间的跳转(edge),形成类似插桩版本的 trace bitmap
  • 通过共享内存(shared memory)将覆盖率数据传递给 afl-fuzz

2. QEMU Mode 的构建与启用

编译 QEMU 模式支持:
make source-only
cd qemu_mode
./build_qemu_support.sh

这会编译一个打了 AFL++ 补丁的 QEMU,生成 afl-qemu-trace 可执行文件。

补丁内容包括:

  • 在 TCG 中插入 __afl_area_ptr 共享内存访问
  • 插入 __afl_prev_loc 用于边哈希
  • 去除无关组件,优化性能
补丁具体内容(***)(github.com/AFLplusplus…

qemuafl是为了适配afl++而针对性重构后的qemu。从 AFL++ 3.0+ 版本开始,afl++项目弃用了传统的 QEMU 补丁方式(即 patches/ 目录下给标准 QEMU 打 patch),转而采用一个完全独立重构的 QEMU 分支。

我有一个想法,获取可以先看有patches的版本了解到底改了哪些,这样有对照也更容易理解为什么要改,再看比较新的版本看看相较于有patches的版本又改了哪些优化的原因是什么。(关注的是最后一版有patches文件夹的版本2.68c)

3. AFL++ 与 QEMU 的交互流程

以下是 afl-fuzz 启动并使用 QEMU 模式的完整交互过程:

Step 1: afl-fuzz 初始化共享内存
// afl-fuzz.c
u8 *shm = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
setenv("__AFL_SHM_ID", shm_id_str, 1);  // 传递给子进程
  • 分配一块大小为 MAP_SIZE(默认 64KB)的共享内存区域,称为 trace bitmap
  • 设置环境变量 __AFL_SHM_ID,告知子进程使用哪个共享内存段
Step 2: afl-qemu-trace 启动并连接共享内存
afl-qemu-trace -- /path/to/binary @@ 
  • afl-qemu-trace 是修改版 QEMU 的 wrapper
  • 它读取 __AFL_SHM_ID,attach 到同一块共享内存
  • 初始化全局变量:
    u8 * __afl_area_ptr = shm_addr;
    u32 __afl_prev_loc = 0;
    
Step 3: QEMU 在 TCG 层插入插桩代码

在 QEMU 编译每一个基本块(basic block)时,TCG 会在块末尾插入如下伪代码:

uint32_t cur_location = (current_bb_addr >> 1) & (MAP_SIZE - 1);
__afl_area_ptr[cur_location] ^= 1;                     // 标记此位置
__afl_area_ptr[cur_location ^ __afl_prev_loc] += 1;    // 哈希前后块形成 edge
__afl_prev_loc = cur_location >> 1;                    // 更新前一个位置

注:cur_location 是当前基本块地址的哈希,__afl_prev_loc 保存上一个块的位置,两者异或构成“边”(edge),避免碰撞。

这样就能模拟出类似 LLVM 插桩的 edge coverage

Step 4: 执行测试用例
  • afl-fuzz fork 出一个子进程
  • 子进程执行:afl-qemu-trace /target_binary input_file
  • QEMU 模拟执行整个程序,期间不断更新共享内存中的 trace bitmap
  • 程序退出后,子进程结束,但共享内存保留
Step 5: afl-fuzz 读取覆盖率数据
// afl-fuzz.c
u8* trace_bits = __afl_area_ptr;
u8* virgin_map = 初始化为全 1 的位图,表示尚未覆盖的边

// 比较当前 trace_bits 与 virgin_map
if (has_new_bits(virgin_map, trace_bits)) {
    save_input_to_queue();   // 发现新路径,加入队列
}
  • 每次执行完后,afl-fuzz 扫描 trace_bits,判断是否触发了新的边
  • 清空 trace_bits(设为 0)以便下一次使用
Step 6: 变异 → 执行 → 判断 → 循环

继续从队列中选择新的种子进行变异,重复上述过程。

4. QEMU Mode 的性能优化技巧

由于 QEMU 模拟本身较慢,AFL++ 做了大量优化:

优化技术说明
Persistent Mode in QEMU支持 __afl_loop() 接口,允许在单个 QEMU 实例中多次执行目标函数(需目标支持)
Instruction Lifting Cache缓存已翻译的基本块,避免重复翻译
Selective Instrumentation可只对特定模块(如 libpng)插桩,跳过无关代码
COVIR(Coverage Inference Rewriting)减少插桩密度,提升速度
CPU Emulation Optimization禁用浮点、SIMD 等非必要功能

三、AFL++ QEMU Mode 的限制与挑战

限制说明
性能开销大QEMU 模拟通常比原生运行慢 5–20 倍
不支持系统调用 Hook无法轻易拦截 read()/write() 修改输入流
多线程支持差QEMU 用户态模拟对多线程支持有限
架构支持有限目前主要支持 x86/x86_64/arm/mips 等常见架构
地址空间随机化(ASLR)需关闭否则基本块地址变化影响插桩稳定性
速度太慢(***)仿真慢fuzz慢

四、与其他模式的对比

模式是否需要源码性能覆盖精度适用场景
LLVM 插桩(afl-clang-fast)✅ 是⚡⚡⚡ 高推荐首选
GCC 插桩(afl-gcc)✅ 是⚡⚡ 中兼容旧项目
QEMU Mode❌ 否⚡ 低闭源二进制
Unicorn Mode❌ 否⚡⚡ 中高(局部)固定代码片段
Frida Mode❌ 否⚡ 低Android / JS Hook