- 应用安全
- 测试
- 漏洞扫描
【免费下载链接】AFLplusplus
AFL++ is a state-of-the-art fuzzer, and #1 in benchmarks. It was originally based on AFL. Today it comes with qemu 5.1, collision-free coverage, enhanced laf-intel & redqueen, AFLfast++ power schedules, MOpt mutators, unicorn_mode, and a lot more!
导读
本指南讲解 AFL++ 提供的 QEMU 原生 hook 支持——qemu_mode/hooking_bridge桥接插件。它把用户编写的 hook 函数(以共享库.so形式提供)通过一个 QEMU 插件桥接到 QEMU 用户态(usermode)生态中,从而在模糊测试(fuzzing)过程中对目标程序的指定指令地址进行拦截、读写内存/寄存器、改写执行流,而不必依赖代价更高的 UNICORN 方案及其衍生工具。读完本文,你将掌握:桥接插件的编译方法、hook 共享库的编写规范(函数命名、struct ret/struct conf协议、r_mem/w_mem/r_reg/w_reg四个 API)、运行时的QEMU_PLUGIN环境变量配置,以及当前实现的已知限制。
设计动机:为什么要在 QEMU 内建 hook
The essential idea is to have inbuilt hooking support into QEMU, instead of relying on the more expensive options UNICORN and its children.
AFL++ 的 QEMU 模式(QEMUAFL)本身已经提供了基于 TCG 翻译的插桩能力。但在某些场景下(例如需要精确改写被测试程序的内存、寄存器或指令执行流),用户希望以更轻量的方式注入自定义逻辑。hooking_bridge 的核心思路是:
- 复用 QEMU 用户态模拟生态,而不是单独启动一个 UNICORN 模拟器实例;
- 通过 QEMU 插件机制(
qemu-plugin.h接口)在翻译基本块、初始化 vCPU、进程退出等时机挂接回调; - 在内部借用 QEMU 的 gdbstub 服务(内存读写、断点管理、寄存器读写),实现"按地址断点 -> 执行用户 hook -> 改写 PC 并继续执行"的完整闭环。
从源码结构看,整个桥接由两个 C 文件组成:
- src/main.c:定义
qemu_plugin_install插件入口,注册vcpu_init_cb、tb_trans_cb(基本块翻译回调)与atexit_cb; - src/patching.c:实现 hook 库的加载(
dlopen)、configure函数解析(dlsym)、断点安装与信号回调处理,以及对外提供的四个 API。
两个文件共享头文件 inc/common.h(插件回调声明)和 inc/exports.h(用户侧 API 与数据结构定义)。
当前实现仅支持 Linux(Currently, LINUX only),且官方已在 Ubuntu 22.04.3 LTS + 6.5.0-28-generic 内核的 x86_64 环境上验证。
编译桥接插件
桥接插件的编译被整合进了 QEMU 模式的构建脚本 build_qemu_support.sh。与普通编译 qemuafl 相同,只需额外传入三个参数:
ENABLE_HOOKING=1:启用并编译 hooking_bridge 插件;GLIB_H:指向系统中glib.h头文件所在目录;GLIB_CONFIG_H:指向系统中glibconfig.h头文件所在目录。
脚本中的对应逻辑位于 build_qemu_support.sh:
if [ "$ENABLE_HOOKING" = "1" ];then echo "[+] ENABLING HOOKING" set -e cd ./hooking_bridge || exit 255 mkdir -p ./build echo "[+] Hook compiler = $CROSS" make CC="$CROSS $CROSS_FLAGS" GLIB_H="$GLIB_H" GLIB_CONFIG_H="$GLIB_CONFIG_H" set +e cd .. fi即当ENABLE_HOOKING=1时,脚本进入 qemu_mode/hooking_bridge 目录并执行其 Makefile。该 Makefile 的编译参数为:
INC=-I./inc -I../qemuafl/include -I$(GLIB_H) -I$(GLIB_CONFIG_H) $(BLD)/patching.o:$(SRC)/patching.c $(CC) -c -fPIC $(INC) -o $(BLD)/patching.o $(SRC)/patching.c plugin:$(SRC)/main.c $(BLD)/patching.o $(CC) -c -fPIC $(INC) -o $(BLD)/plugin.o $(SRC)/main.c $(CC) -shared -o $(BLD)/plugin.so $(BLD)/plugin.o $(BLD)/patching.o可以看到,插件被编译为hooking_bridge/build/plugin.so(位置无关代码-fPIC+ 共享库链接),编译时需要:
qemu_mode/qemuafl/include(QEMU 插件头文件路径);- 由
GLIB_H/GLIB_CONFIG_H提供的 glib 头文件路径(patching.c依赖glib.h的GByteArray来承载寄存器读出的字节)。
一个完整的构建命令示例(具体路径以实际系统为准):
cd qemu_mode GLIB_H=/usr/include/glib-2.0 \ GLIB_CONFIG_H=/usr/lib/x86_64-linux-gnu/glib-2.0/include \ ENABLE_HOOKING=1 ./build_qemu_support.sh构建成功后,产物位于qemu_mode/hooking_bridge/build/plugin.so,它就是后续通过QEMU_PLUGIN环境变量加载的 QEMU 插件本体。
编写 Hook:核心规范
编写 hook 的完整流程分四步,下面逐一展开(对应 README 的Writing hooks一节)。
第一步:创建 hook 函数并包含 exports.h
在一个共享库(如hook.so)中编写一个或多个 hook 函数,并在编译时包含 inc/exports.h:
#include "exports.h"该头文件同时定义了四个 API 函数以及struct conf/struct ret两个协议结构,是编写 hook 的唯一官方头文件。你的构建命令大致为:
gcc -shared -fPIC -I<AFLplusplus路径>/qemu_mode/hooking_bridge/inc -o hook.so hook.c第二步:函数命名规则hook_<左填充的 hook 地址>
README 中给出的示例 hook 函数如下:
struct ret* hook_000000400deadc08(){ memset (buf, 0, 8); scanf("%s",buf); r_reg(RSI,(void *)&h_addr); w_mem(h_addr,8, buf); to_ret = (struct ret){0x400deadcab, 0}; return &to_ret; }命名规则(关键,必须严格遵守):
- hook 函数必须命名为
hook_<left padded hook location>; <hook location>是 hook 要放置的绝对地址,即文件基地址(file base address,QEMU 下当前不会改变)加上指令偏移;- 地址要左填充 0 直到满16 个十六进制字符。因此示例中的
0x400deadc08被写成000000400deadc08(前缀000000补齐到 16 位); - hook 函数必须返回
struct ret *(见第四步)。
从源码角度看,这个命名规则在 src/patching.c 中由信号回调强制执行:
r_reg(config->IP_reg_num, cbuf); gen_addr = *(unsigned long long *)cbuf; sprintf(cbuf, "hook_%016llx", gen_addr); *(unsigned long long **)(&hook) = dlsym(handle, cbuf); if (!hook) { exit(-1); }即:当命中断点时,桥接先读出指令指针(IP)寄存器,格式化成hook_%016llx字符串,再通过dlsym在你的 hook 共享库中查找同名符号。找不到对应符号会直接exit(-1)终止进程,因此函数命名必须与绝对地址严格一一对应。
第三步:使用四个内存/寄存器访问 API
hook 中大概率需要访问目标的内存与寄存器,exports.h提供四个 API(README 中的签名与注释):
// Read memory (from address, num. bytes, destination buffer) -> returns 0 on success int r_mem(unsigned long long addr, unsigned long long len, void *dest); // Write memory (to address, num. bytes, source buffer) -> returns 0 on success int w_mem(unsigned long long addr, unsigned long long len, void *src); // Read register (identifier, destination buffer) -> returns number of bytes read int r_reg(unsigned char reg, void *dest); // Read register (identifier, source buffer) -> returns number of bytes written int w_reg(unsigned char reg, char *src);它们的底层实现(src/patching.c)直接复用了 QEMU gdbstub 的能力:
r_mem/w_mem调用target_memory_rw_debug(cpu, addr, buf, len, is_write),分别对应读(is_write=0)与写(is_write=1),成功返回 0;r_reg先清空GByteArray,再调用gdb_read_register(cpu, out, reg)并把读出字节拷贝到dest,返回读出的字节数;w_reg直接调用gdb_write_register(cpu, src, reg),返回写入的字节数。
注意r_mem/w_mem需要 8 个字节的寄存器存储(64 位目标),而r_reg的dest缓冲区大小要能容纳目标寄存器宽度。
寄存器标识符(reg):这里的reg本质上是 gdb 用于查询寄存器的编号。它可以在 QEMU(qemuafl)源码树的gdb-xml目录下按架构区分的 XML 文件中查到。以 README 的示例为例,RSI的编号是 4,取自i386-64bit.xml。同理,struct conf中的IP_reg_num也是从同一组 XML 文件中取得的指令指针寄存器编号(详见下文configure一节)。
第四步:返回struct ret控制执行流
处理完成后,hook 必须返回struct ret *指针。该结构定义在 inc/exports.h:
struct ret{ unsigned long long addr; char remove_bp; };两个字段的含义:
| 字段 | 类型 | 作用 |
|---|---|---|
addr | unsigned long long | hook 返回后要跳转到的地址(即新 PC) |
remove_bp | char | 返回后是否移除当前安装的断点 |
remove_bp字段在 hook 处于一个持续循环中时尤为关键:如果希望该 hook 保留以便后续轮次继续被触发,就不要移除断点;如果是一次性改写,则应设置为非零以便下次不再触发。
源码中(src/patching.c)对该返回值的处理逻辑是:
- 调用
hook()得到returned; - 若
returned->remove_bp为真,或returned->addr == gen_addr(返回地址仍是原断点地址),则先移除断点,避免无限循环触发; - 若
returned->addr == gen_addr,进入单步模式(single_stepped = 1,cpu_single_step),执行完当前指令后再把断点装回去; - 否则(跳转到其他地址),直接通过
gdb_set_cpu_pc(returned->addr)改写指令指针,不再重新执行 IP 处的原指令。
第五步:定义configure函数安装 hook 列表
最后,你的共享库中必须导出一个签名固定的configure函数,桥接插件在加载时通过dlsym(handle, "configure")找到它(见 src/patching.c)。README 示例:
struct conf config; struct conf* configure(){ config.IP_reg_num = 16; config.entry_addr = 0x4000001000; config.num_hooks = NUMHOOKS; //1,2,3... hooks[0] = 0x400deadc08; // hooks[1] = 0xcafecace // .... config.hooks = hooks; //Any other processing stuff you need done before fuzztime return &config; }struct conf的完整格式(inc/exports.h):
struct conf{ unsigned char IP_reg_num; unsigned long long entry_addr; unsigned long long* hooks; unsigned long long num_hooks; };各字段说明:
| 字段 | 类型 | 含义 |
|---|---|---|
IP_reg_num | unsigned char | 指令指针寄存器编号,同样在<qemudir>/gdb-xml下的架构 XML 中查询 |
entry_addr | unsigned long long | 入口地址。必须是main、init或任何 QEMU 在 hook 目标之前会执行的入口点;桥接以此判断何时初始化 gdbstub 并安装断点 |
hooks | unsigned long long* | 被 hook 的地址列表 |
num_hooks | unsigned long long | hook 数量 |
entry_addr的作用在 src/patching.c 中体现:每当一个基本块被翻译时(patch_block_trans_cb),桥接会取出该基本块的虚拟地址qemu_plugin_tb_vaddr(tb),一旦发现它等于config->entry_addr,就调用gdb_accept_init(-1)初始化 gdbstub,并遍历config->hooks为每个地址安装断点(gdb_breakpoint_insert(0, config->hooks[i], 1))。这也解释了 README 限制中"无法在入口点之后的第一个基本块上放置 hook"的原因——断点的安装发生在入口基本块被翻译的时刻,此时该基本块尚未(也来不及)被打上断点。
运行:通过 QEMU_PLUGIN 加载
在运行 AFL++ 的 QEMU 模式之前,设置环境变量:
export QEMU_PLUGIN="file=<AFL下载路径>/qemu_mode/hooking_bridge/build/plugin.so,arg=<你的hook.so>"参数说明:
file=后是桥接插件本体plugin.so的路径(上文编译产物);arg=后是你的 hook 共享库的绝对路径(README 明确要求 absolute path)。
随后正常以 QEMU 模式启动 AFL++(-Q选项)即可,例如:
QEMU_PLUGIN="file=/path/to/AFLplusplus/qemu_mode/hooking_bridge/build/plugin.so,arg=/path/to/hook.so" \ afl-fuzz -Q -i in -o out -- ./target @@插件加载链路(src/main.c)中,qemu_plugin_install接收插件参数argv[0](即arg=传入的 hook 库路径),把它交给patch_init;随后注册 vCPU 初始化回调、基本块翻译回调和进程退出回调。patch_init(src/patching.c)内部执行dlopen(hook_lib, RTLD_NOW)(失败则打印DLOPEN Error并退出)、解析configure、安装信号回调set_signal_callback(handle_signal_callback),并创建用于寄存器读写的GByteArray。
当前限制
README 明确列出三条限制,使用前需要知晓:
- 不能用于调试(
-g选项):桥接在内部使用了 gdbstub,因此与 QEMU 的 gdb 调试模式冲突。但在与 AFL++ 配合使用时不受影响,问题不大; - 不能把 hook 放在
<entry point>之后的第一个基本块上:从源码看,断点是在入口基本块被翻译时才安装的(见上文entry_addr逻辑),因此该位置天然不可挂接,且通常也不是合适的 hook 位置; - 仅支持 Linux:
dlopen/dlsym等加载机制与 gdbstub 集成方式目前都是面向 Linux 的实现(源码中patch_init也有TODO make OS agnostic, remove dlopen的注释)。官方验证环境为:- Ubuntu 22.04.3 LTS(jammy),
lsb_release -a输出Distributor ID: Ubuntu / Description: Ubuntu 22.04.3 LTS / Release: 22.04 / Codename: jammy; - 内核
6.5.0-28-generic #29~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC ... x86_64。
- Ubuntu 22.04.3 LTS(jammy),
小结:桥接的完整工作流
将以上内容串联起来,hooking_bridge 的完整工作流为:
- 用
ENABLE_HOOKING=1 GLIB_H=... GLIB_CONFIG_H=... ./build_qemu_support.sh编译出 build/plugin.so; - 编写
hook.so:包含 inc/exports.h,按hook_<16位十六进制绝对地址>命名规则写 hook 函数(内部可用r_mem/w_mem/r_reg/w_reg读写内存与寄存器),返回struct ret控制跳转目标与断点去留,并导出struct conf* configure()声明入口地址与 hook 列表; - 设置
QEMU_PLUGIN="file=<plugin.so>,arg=<hook.so绝对路径>",以-Q模式启动 AFL++; - 运行期间,桥接在入口基本块处初始化 gdbstub 并安装断点,命中断点后通过
dlsym查找对应 hook 符号、执行 hook、按struct ret改写 PC 并继续执行,从而把自定义逻辑无缝织入 QEMU 用户态模糊测试流程。
如果需要深入源码细节,可以继续阅读 qemu_mode/hooking_bridge/src/patching.c(gdbstub 导入与断点/信号处理)、qemu_mode/hooking_bridge/src/main.c(插件入口与回调注册)以及 qemu_mode/hooking_bridge/Makefile(构建参数)。
- 应用安全
- 测试
- 漏洞扫描
【免费下载链接】AFLplusplus
AFL++ is a state-of-the-art fuzzer, and #1 in benchmarks. It was originally based on AFL. Today it comes with qemu 5.1, collision-free coverage, enhanced laf-intel & redqueen, AFLfast++ power schedules, MOpt mutators, unicorn_mode, and a lot more!
相关推荐
AFL QEMU模式:跨架构二进制模糊测试实战指南
American Fuzzy Lop AFL 作为业界领先的安全导向模糊测试工具,其QEMU模式为二进制程序的安全测试提供了革命性的解决方案。AFL QEMU模
网络安全应用安全测试开发工具AFL++ QEMU 模式持久化(Persistent Mode)模糊测试实战指南:地址配置、寄存器恢复与内存内 Hook 提速
AFL++ QEMU 模式持久化(Persistent Mode)模糊测试实战指南:地址配置、寄存器恢复与内存内 Hook 提速 AFL++ 的 QEMU 模式
应用安全测试漏洞扫描AFL++ Nyx 模式实战指南:基于 KVM/QEMU 的全系统模拟快照式模糊测试
AFL++ Nyx 模式实战指南:基于 KVM/QEMU 的全系统模拟快照式模糊测试 Nyx 是 AFL++ 提供的一种 全系统模拟(full system e
应用安全测试漏洞扫描
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考