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-trace | QEMU 模式下的动态插桩工具 |
afl-fuzz | 主模糊测试引擎(fuzzer) |
afl-showmap | 覆盖率分析工具 |
afl-cmin | 最小化测试用例集 |
afl-tmin | 测试用例最小化(简化崩溃样本) |
2. AFL++ 的模糊测试流程
-
准备阶段
- 获取目标程序
- 使用 QEMU 模式进行动态插桩
- 准备初始种子(seed)输入文件
- (可选)准备字典(dictionary)以提升语法感知能力
-
初始化阶段
afl-fuzz启动后:- 读取种子目录中的输入
- 使用
afl-showmap或直接执行方式测量每个种子的执行路径(edge coverage) - 构建初始的“覆盖图”(coverage map),记录哪些边(edges)已被触发
- 将未覆盖的路径加入待处理队列(queue)
-
模糊测试主循环(Fuzzing Loop)
- 从队列中取出一个测试用例(test case)
- 应用一系列变异策略(mutations):
- 位翻转、字节增减、块复制/移动、arith 变异等
- RedQueen(基于符号执行的语义感知变异)
- MOpt(机器学习启发式变异调度)
- 执行变异后的输入:
- 使用 QEMU 模式 → 通过
afl-qemu-trace运行
- 使用 QEMU 模式 → 通过
- 收集执行反馈(通过共享内存中的 trace bitmap)
- 分析新覆盖:
- 是否发现了新的基本块转移路径(edge)?
- 是否导致崩溃(crash)或超时(hang)?
- 更新内部状态:
- 若发现新路径 → 将该输入加入队列
- 若崩溃 → 保存到
crashes/目录 - 若 hang → 保存到
hangs/目录
- 调度策略(Power Schedules)决定下一个优先 fuzz 的测试用例(如 AFLFast 的指数调度)
-
持续迭代
- 直到用户手动停止或达到预设时间/迭代次数
二、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-fuzzfork 出一个子进程- 子进程执行:
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 |