概述 by ChatGPT
本文从
qemuafl
的初始化、持久模式到插桩流程详细解析了其核心机制,包括afl_setup
的环境变量配置、afl_forkserver
的与 AFL++ 的交互,及afl_gen_trace
的插桩逻辑。还分析了持久模式核心函数如afl_persistent_loop
的运行逻辑及其与共享内存和寄存器快照的关系,揭示了qemuafl
高效模糊测试的实现原理。
初始化流程
在 qemuafl/accel/tcg/translator.c
的 translator_loop
函数中,如果要翻译的基本块中包含 afl_entry_point
则调用 afl_setup
函数进行初始化:
void translator_loop(const TranslatorOps *ops, DisasContextBase *db,
CPUState *cpu, TranslationBlock *tb, int max_insns)
{
... ...
if (db->pc_next == afl_entry_point) {
static bool first = true;
...
if (first) {
afl_setup();
...
tb_flush(cpu);
first = false;
}
gen_helper_afl_entry_routine(cpu_env);
}
在初始化之后会刷新缓存,结合注释信息,我的理解是在初始化之前有可能已经对一些基本块做了翻译,而 qemuafl 初始化之后会加入一些其他信息,因此在这里刷新 CPU 缓存且只刷新一次。之后调用 gen_helper_afl_entry_routine
生成入口点代码,该函数留在后面分析。
afl_setup
afl_setup
会进行一些初始化操作,包括:
AFL_INST_RATIO
:设置插桩比例(afl_inst_rms
变量);__AFL_SHM_ID
:初始化共享内存;AFL_QEMU_DISABLE_CACHE
:判断是否禁用缓存;___AFL_EINS_ZWEI_POLIZEI___
:判断是否开启 cmplog forkserver;- 设置插桩范围
AFL_INST_LIBS
:设置是否完全插桩;AFL_CODE_START
、AFL_CODE_END
:设置特定起始结束位置;AFL_QEMU_INST_RANGES
:设置自定义的插桩范围;AFL_QEMU_EXCLUDE_RANGES
:设置自定义的插桩排除范围;
AFL_DEBUG
:是否开启调试;AFL_QEMU_COMPCOV
:是否启用 x86 和 x86_64 中所有 cmp 和 sub 的 CompareCoverage 跟踪。- 从
AFL_COMPCOV_LEVEL
获取 CompareCoverage 等级,默认为 1。
- 从
AFL_QEMU_PERSISTENT_HOOK
:是否开启 persistent mode- 如果上文开启了
AFL_QEMU_COMPCOV
会和 persistent 冲突; - 如果是静态编译的 QEMU,无法使用持久测试,退出;
- 从持久测试的 lib 库中获取一些关键函数:
afl_persistent_hook_init
:声明是否使用内存进行模糊测试(sharedmem_fuzzing
);afl_persistent_hook
:用户自定义的函数,在每次模糊测试之前可以覆盖要解析的缓冲区和内存,并正确设置长度;
AFL_QEMU_PERSISTENT_ADDR
:persisent 模式下需要有一个起始地址;AFL_QEMU_PERSISTENT_RET
:在执行到这个地址时恢复(保存的)状态;AFL_QEMU_PERSISTENT_GPR
:是否保持寄存器一致;AFL_QEMU_PERSISTENT_MEM
:是否保持内存一致;AFL_QEMU_PERSISTENT_RETADDR_OFFSET
:是否设置返回地址相对起始地址的偏移;AFL_QEMU_PERSISTENT_CNT
:设置持久测试的次数;AFL_QEMU_PERSISTENT_EXITS
:是否强制 QEMU 将 pc 设置为 START,而不是执行exit_group
系统调用并退出程序;AFL_QEMU_SNAPSHOT
:等价于开启了上面的AFL_QEMU_PERSISTENT_GPR
AFL_QEMU_PERSISTENT_MEM
AFL_QEMU_PERSISTENT_EXITS
- 如果上文开启了
在 afl_setup
后会执行 gen_helper_afl_entry_routine
生成入口点的 TCG 代码。在执行到该代码时,最终会调用 afl_forkserver
函数。
afl_forkserver
afl_forkserver
主要流程如下:
- 如果不在老版本 forkserver 下(
AFL_OLD_FORKSERVER
),则:- 设置
lkm_snapshot
状态; - 设置
sharedmem_fuzzing
状态;
- 设置
- 和 AFL++ 交互,这一部分可以看我之前绘制的通信流程图,这里不再赘述。
- 在这一部分多出了一块
sharedmem_fuzzing
的逻辑,如果开启了共享内存模糊测试,则会调用到afl_map_shm_fuzz
函数中从共享内存中获得输入。
- 在这一部分多出了一块
persistent mode
在持久模式下,初始化的位置在每个架构实现的 disas_insn
最开始,例如 i386/x64 的实现就位于 qemuafl/target/i386/tcg/translate.c
:
static target_ulong disas_insn(DisasContext *s, CPUState *cpu)
{
...
AFL_QEMU_TARGET_I386_SNIPPET
在这个位置插入了一段 SNIPPET,用于持久模式的实现,它的逻辑为:
- 在持久模式起始地址(
afl_persistent_addr
)的操作:- 生成持久 fuzz 栈帧恢复的 TCG 代码;
- 在未保存 GPR 且未设置
afl_persistent_ret_addr
的条件下恢复栈指针;
- 在未保存 GPR 且未设置
- 生成持久测试的 TCG 代码(
afl_persistent_routine
) - 如果没有设置持久化的返回地址,则将
afl_persistent_addr
这个持久化函数入口地址写入栈中作为返回地址;
- 生成持久 fuzz 栈帧恢复的 TCG 代码;
- 如果设置了持久化的返回地址,且执行到了返回地址,则生成跳转到返回地址的 TCG 代码。
其他架构的处理逻辑类似,这里不再赘述。
afl_persistent_loop
上文的 HELPER(afl_persistent_routine)
最终会在代码运行时执行到 afl_persistent_loop
函数,在这里对每次持久模式都会做一些初始化操作。该函数的逻辑为:
- 如果是第一次持久测试:
- 重置共享内存
afl_area_ptr
; - 设置内存或寄存器快照;
- 执行用户定义的
afl_persistent_hook
函数;
- 重置共享内存
- 如果是第二次或之后的持久测试:
- 调用
afl_persistent_iter
,这个函数主要功能为:- 如果到达了用户设定的持久化循环次数,则:
- 恢复内存或寄存器快照
- 处理未禁用 cache 的逻辑,本质上还是通信流程图中向 AFL 发送返回值的流程;
- 在上述过程后,可以开启新一轮 fuzz 了,重新执行
afl_persistent_hook
并恢复寄存器快照。
- 如果到达了用户设定的持久化循环次数,则:
- 调用
插桩流程
afl_gen_trace
QEMU 会在 tb_gen_code
函数中执行翻译流程,qemuafl 会在 qemu 翻译一个新的代码块之前调用 afl_gen_trace
函数生成 TCG 代码:
tcg_func_start(tcg_ctx);
tcg_ctx->cpu = env_cpu(env);
afl_gen_trace(pc);
gen_intermediate_code(cpu, tb, max_insns);
tcg_ctx->cpu = NULL;
max_insns = tb->icount;
trace_translate_block(tb, tb->pc, tb->tc.ptr);
afl_gen_trace
会进行以下操作:
- 检查是否需要对当前位置进行插桩;
- 注:这一步可以避免 qemuafl 初始化之前的插桩,也是初始化后需要刷新 cpu 缓存的原因。
- 计算当前地址哈希作为插桩 ID,检查当前地址是否超过插桩比例;
- 判断是否记录不稳定的基本块,通过
gen_helper_afl_maybe_log*
生成插桩代码。
afl_maybe_log*
在调用到 afl_maybe_log*
函数时,该函数会向共享内存中写入覆盖率信息:
void HELPER(afl_maybe_log)(target_ulong cur_loc) {
register uintptr_t afl_idx = cur_loc ^ afl_prev_loc;
INC_AFL_AREA(afl_idx);
// afl_prev_loc = ((cur_loc & (MAP_SIZE - 1) >> 1)) |
// ((cur_loc & 1) << ((int)ceil(log2(MAP_SIZE)) -1));
afl_prev_loc = cur_loc >> 1;
}
void HELPER(afl_maybe_log_trace)(target_ulong cur_loc) {
register uintptr_t afl_idx = cur_loc;
INC_AFL_AREA(afl_idx);
}
可以看到,在正常记录时 afl_idx=cur_loc ^ afl_prev_loc
,而记录不稳定的代码块时只会使用 afl_idx=cur_loc
。除此之外,原本的 afl_prev_loc
计算方式较为复杂,而后来实现的则更为简单,查看 git 日志时说明的原因是为了提高执行效率。
INC_AFL_AREA
就是写入覆盖率信息的具体实现:
#if (defined(__x86_64__) || defined(__i386__)) && defined(AFL_QEMU_NOT_ZERO)
#define INC_AFL_AREA(loc) \
asm volatile( \
"addb $1, (%0, %1, 1)\n" \
"adcb $0, (%0, %1, 1)\n" \
: /* no out */ \
: "r"(afl_area_ptr), "r"(loc) \
: "memory", "eax")
#else
#define INC_AFL_AREA(loc) afl_area_ptr[loc]++
#endif
总结
根据附录 A,在上文分析了 afl_entry_routine、afl_persistent_routine、afl_maybe_log* 系列函数,但还有一个功能未分析,也就是 CompareCoverage 功能,可以参考 Compare coverage for AFL++ QEMU 这篇文章。
附录
A. qemu TCG runtime
tcg-runtime.h
中包含一些 tcg 中使用的 afl 函数:
DEF_HELPER_FLAGS_1(afl_entry_routine, TCG_CALL_NO_RWG, void, env)
DEF_HELPER_FLAGS_1(afl_persistent_routine, TCG_CALL_NO_RWG, void, env)
DEF_HELPER_FLAGS_1(afl_maybe_log, TCG_CALL_NO_RWG, void, tl)
DEF_HELPER_FLAGS_1(afl_maybe_log_trace, TCG_CALL_NO_RWG, void, tl)
DEF_HELPER_FLAGS_3(afl_compcov_16, TCG_CALL_NO_RWG, void, tl, tl, tl)
DEF_HELPER_FLAGS_3(afl_compcov_32, TCG_CALL_NO_RWG, void, tl, tl, tl)
DEF_HELPER_FLAGS_3(afl_compcov_64, TCG_CALL_NO_RWG, void, tl, tl, tl)
DEF_HELPER_FLAGS_3(afl_cmplog_8, TCG_CALL_NO_RWG, void, tl, tl, tl)
DEF_HELPER_FLAGS_3(afl_cmplog_16, TCG_CALL_NO_RWG, void, tl, tl, tl)
DEF_HELPER_FLAGS_3(afl_cmplog_32, TCG_CALL_NO_RWG, void, tl, tl, tl)
DEF_HELPER_FLAGS_3(afl_cmplog_64, TCG_CALL_NO_RWG, void, tl, tl, tl)
DEF_HELPER_FLAGS_1(afl_cmplog_rtn, TCG_CALL_NO_RWG, void, env)
B. QEMU helper 机制
在附录 A 中出现了很多类似 DEF_HELPER_FLAG_*
的宏,后面的数字显然是参数的数量,以 gen_helper_<function name>
的形式调用。这类 helper 可以用来生成 TCG 代码,这里简单研究一下它的实现逻辑。
以 DEF_HELPER_FLAGS_1
为例,它实际上有三处定义位置:
helper-proto.h
- 生成辅助函数的原型声明,用于编译器类型检查和链接。
#define DEF_HELPER_FLAGS_1(name, flags, ret, t1) \
dh_ctype(ret) HELPER(name) (dh_ctype(t1));
helper-gen.h
- 生成创建 TCG 代码的函数
#define DEF_HELPER_FLAGS_1(name, flags, ret, t1) \
static inline void glue(gen_helper_, name)(dh_retvar_decl(ret) \
dh_arg_decl(t1, 1)) \
{ \
TCGTemp *args[1] = { dh_arg(t1, 1) }; \
tcg_gen_callN(HELPER(name), dh_retvar(ret), 1, args); \
}
helper-tcg.h
- 定义了
DEF_HELPER_FLAGS_1
的数据结构:指针、名称、标志和大小掩码,主要用于 TCG 内部管理。
- 定义了
#define DEF_HELPER_FLAGS_1(NAME, FLAGS, ret, t1) \
{ .func = HELPER(NAME), .name = str(NAME), \
.flags = FLAGS | dh_callflag(ret), \
.sizemask = dh_sizemask(ret, 0) | dh_sizemask(t1, 1) },
为什么会有三处定义呢?前两处很好理解,分别是函数的定义和实现,而第三处则会用于 helper table 的实现:
static const TCGHelperInfo all_helpers[] = {
#include "exec/helper-tcg.h"
};
这一处是为了在实际执行时找到对应的 helper,避免无法执行。