基于 QEMU 的闭源二进制 Fuzzing:AFL++ QEMU Mode 踩坑实录

在进行闭源商业软件、嵌入式固件提取程序或第三方闭源二进制库的安全审计时,安全研究员无法拿到源码进行 ASan/AFL 源码级插桩编译。AFL++ 提供的 QEMU 模式(基于 QEMU 用户态模拟器)通过在 TCG(Tiny Code Generator)将目标架构机器码翻译为宿主机器码时动态插入覆盖率统计桩(Bitmap Logging),成为无源码模糊测试的核心手段。
然而,在实际打靶过程中,QEMU 模式经常面临执行速度极慢、动态链接库缺失、Forkserver 死锁以及持久化模式挂钩失败等问题。
QEMU 模式插桩原理与执行流
AFL++ QEMU Mode(QEMU User Space Emulation)在翻译每个基本块(Basic Block)时,向生成的中介码中插入一段反馈逻辑:
由于每个指令基本块都需要经过 JIT 动态翻译,默认 QEMU 模式的吞吐量通常只有每秒 100~300 次(exec/s),相比源码插桩的几千上万次差距巨大。
典型排坑实录与调优方案
坑点一:动态链接库加载失败与根目录映射
现象:直接运行 afl-fuzz -Q — ./target @@ 时报错 /lib/ld-uClibc.so.0: No such file or directory 或找不到宿主 libc。
排障与解决:QEMU 用户态需要正确的根文件系统路径支持。通过环境变量 QEMU_LD_PREFIX 或 AFL_INST_LIBS 指定目标架构的库目录:
export QEMU_LD_PREFIX=/usr/gnemul/qemu-arm
export QEMU_SETENV="LD_LIBRARY_PATH=/lib:/usr/lib"
坑点二:执行效率低下与 Persistent Mode(持久化循环)改造
现象:目标二进制启动耗时长(初始化大量全局状态、配置解析),每次 fork 重新执行成本极高。
排障与解决:AFL++ 支持 QEMU 持久化模式(In-memory Persistent Mode)。我们只需在反汇编器(IDA/Ghidra)中定位核心处理函数的入口地址和返回地址,让 QEMU 循环复用该进程内存,而无需频繁退出和重新加载进程。
# 指定持久化入口地址
export AFL_QEMU_PERSISTENT_ADDR=0x401250
# 指定执行返回点
export AFL_QEMU_PERSISTENT_RET=0x401290
# 单个 Fork 进程循环复用执行 1000 次后再重新重启
export AFL_QEMU_PERSISTENT_CNT=1000
# 开启标准输入读取加速
export AFL_QEMU_PERSISTENT_GPR=1
启用 Persistent Mode 后,测试吞吐量通常可从 200 exec/s 暴增至 3000~8000 exec/s。
坑点三:地址随机化(ASLR)与 PIE 导致的地址漂移
现象:目标二进制启用了 PIE,设置的 AFL_QEMU_PERSISTENT_ADDR 每次运行发生基址偏移,导致 Hook 点失效。
排障与解决:强制 QEMU 禁用基址随机化,指定固定的加载基址:
export AFL_NO_AFFINITY=1
export AFL_QEMU_CUSTOM_BIN=1
export QEMU_RESERVED_VA=0x0
实战启动脚本与目标 Harness 模板
下面是一个完整的闭源二进制 AFL++ QEMU 测试启动脚本,包含输入语料预热、字典生成与核心参数配置:
#!/usr/bin/env bash
set -euo pipefail
TARGET_BIN="./vulnerable_service.bin"
INPUT_CORPUS="./in"
OUTPUT_DIR="./out"
DICT_FILE="./protocol.dict"
# 1. 检查 AFL++ QEMU 构建环境
if ! command -v afl-fuzz &> /dev/null; then
echo "[-] 错误: 未检测到 afl-fuzz 可执行路径,请确认 AFL++ 安装。"
exit 1
fi
# 2. 准备最小种子语料
mkdir -p "${INPUT_CORPUS}"
if [ ! "$(ls -A "${INPUT_CORPUS}")" ]; then
echo -ne "MAGIC\\x01\\x00\\x00\\x00\\x08TESTDATA" > "${INPUT_CORPUS}/seed1.bin"
echo "[+] 成功初始化首个探测种子: seed1.bin"
fi
# 3. 配置 QEMU 性能加速与限制参数
export AFL_SKIP_CPUFREQ=1 # 跳过 CPU 调频检查
export AFL_DEBUG_CHILD=0 # 调试时可设为 1 查看子进程输出
export AFL_QEMU_PERSISTENT_ADDR="0x00401820"
export AFL_QEMU_PERSISTENT_CNT="5000"
export AFL_ENTRYPOINT="0x00401820" # 忽略之前的初始化代码,直接在此地址开启分支统计
echo "[*] 开始启动 AFL++ QEMU Mode Fuzzing 实例…"
# 4. 执行 Fuzzing (单核主实例)
afl-fuzz \\
-Q \\
-i "${INPUT_CORPUS}" \\
-o "${OUTPUT_DIR}" \\
-m none \\
-x "${DICT_FILE}" \\
-M master-fuzzer \\
— "${TARGET_BIN}" @@
异常崩溃定位与复现验证
当 Fuzzing 在 out/master-fuzzer/crashes/ 目录下捕获到崩溃用例时,由于闭源程序没有 ASan 报告,必须通过 GDB 配合 QEMU 进行崩溃点分析:
# 启动 QEMU 调试模式,开放 1234 端口
qemu-x86_64 -g 1234 ./vulnerable_service.bin out/master-fuzzer/crashes/id:000000,… &
# 本地 GDB 远程连接分析
gdb-multiarch ./vulnerable_service.bin -ex "target remote localhost:1234" -ex "continue"
在 GDB 中定位崩溃现场的寄存器状态(如 RIP/EIP 覆写、非法内存指针访问),即可快速判定该崩溃是否属于可利用的内存破坏漏洞。

