☰
AFL++ QEMU 模式原生 Hook 桥接(hooking_bridge)实战指南:在 QEMU 用户态模糊测试中安装自定义 Hook
2026/10/8 2:00:13 网站建设 项目流程
  • 应用安全
  • 测试
  • 漏洞扫描

【免费下载链接】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!

项目地址:https://gitcode.com/gh_mirrors/af/AFLplusplus
点击查看免费下载

导读

本指南讲解 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; };

两个字段的含义:

字段类型作用
addrunsigned long longhook 返回后要跳转到的地址(即新 PC)
remove_bpchar返回后是否移除当前安装的断点

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_numunsigned char指令指针寄存器编号,同样在<qemudir>/gdb-xml下的架构 XML 中查询
entry_addrunsigned long long入口地址。必须是main、init或任何 QEMU 在 hook 目标之前会执行的入口点;桥接以此判断何时初始化 gdbstub 并安装断点
hooksunsigned long long*被 hook 的地址列表
num_hooksunsigned long longhook 数量

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 明确列出三条限制,使用前需要知晓:

  1. 不能用于调试(-g选项):桥接在内部使用了 gdbstub,因此与 QEMU 的 gdb 调试模式冲突。但在与 AFL++ 配合使用时不受影响,问题不大;
  2. 不能把 hook 放在<entry point>之后的第一个基本块上:从源码看,断点是在入口基本块被翻译时才安装的(见上文entry_addr逻辑),因此该位置天然不可挂接,且通常也不是合适的 hook 位置;
  3. 仅支持 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。

小结:桥接的完整工作流

将以上内容串联起来,hooking_bridge 的完整工作流为:

  1. 用ENABLE_HOOKING=1 GLIB_H=... GLIB_CONFIG_H=... ./build_qemu_support.sh编译出 build/plugin.so;
  2. 编写hook.so:包含 inc/exports.h,按hook_<16位十六进制绝对地址>命名规则写 hook 函数(内部可用r_mem/w_mem/r_reg/w_reg读写内存与寄存器),返回struct ret控制跳转目标与断点去留,并导出struct conf* configure()声明入口地址与 hook 列表;
  3. 设置QEMU_PLUGIN="file=<plugin.so>,arg=<hook.so绝对路径>",以-Q模式启动 AFL++;
  4. 运行期间,桥接在入口基本块处初始化 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!

项目地址:https://gitcode.com/gh_mirrors/af/AFLplusplus
点击查看免费下载

相关推荐

上一篇:FanControl 风扇控制软件完整教程:3 步驯服电脑风扇噪音(Windows 免费)
下一篇:TPFanCtrl2 快速上手:ThinkPad 双风扇自定义调速曲线跑起来

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询