关于QEMU和AFL-QEMU模式那些事

关于QEMU和AFL-QEMU模式那些事

1. QEMU是什么?

  • QEMU(Quick EMUlator)是一个仿真和模拟虚拟环境的开源软件,其本质上还是一个软件,通过动态翻译实现模拟[1]。
  • 软件虚拟化
  • 跨平台
  • 仿真多种架构的处理器

动态翻译

  • 以模拟x86下的指令为例,动态翻译的基本思想就是把每一条x86指令切分为若干条微操作,而每条微操作都是由QEMU中的一段简单的C代码来实现
  • QEMU的动态翻译后端是TCG[2],Tiny Code Generator

CPU 状态优化

  • 状态记录在翻译块(Translation Block,TB)中
  • 如果状态发生改变,那么新TB将会生成,而之前的TB将不再使用直到状态与之前的TB相匹配
  • e.g. x86 SS-堆栈段寄存器 DS-数据段寄存器 ES-附加段寄存器 为0时,将不再生成一个新的段基址

块直接链接

  • 执行完每一个翻译块之后,QEMU根据模拟的PC和CPU状态信息来找到下一个基本块(基于哈希表?)

    • 如果下一个块还没被翻译过,那么将载入一个新的块
    • 否则,跳转到下一个已经翻译过的块中
  • 对于模拟的PC已知的情况下,QEMU直接patch一个TB:直接跳转

自修改代码和翻译代码非法检查

  • 当一个TB生成后,相应的页是写保护的。如果对该页进行写操作,那么QEMU将使得所有已翻译的块失效,并重新启用写权限
  • 给定一个页维护一个由每一个TB块构成的链表

异常处理

  • longjmp():FPE
  • SIGSEGV、SIGBUS

MMU 模拟

2. AFL QEMU模式

2.1 安装

  • 安装相关依赖:
1
2
$ # 缺少啥依赖补充啥即可,这里仅展示名字差异较大的安装包
$ sudo ap-get install libtool-bin libglib2.0-dev
1
$ bash build_qemu_support.sh

错误1:如果出现下面的报错信息,则将build_qemu_support.sh脚本的第148-150行的./configure末尾添加一个--python=/path/to/python_bin即可:

1
2
3
4
...
ERROR: Cannot use 'python', Python 2.6 or later is required.
Note that Python 3 or later is not yet supported.
Use --python=/path/to/python to specify a supported Python.

e.g.

1
2
3
CFLAGS="-O3 -ggdb" ./configure --disable-system \
--enable-linux-user --disable-gtk --disable-sdl --disable-vnc \
--target-list="${CPU_TARGET}-linux-user" --enable-pie --enable-kvm --python=/usr/bin/python2.7 || exit 1

错误2:如果出现下面的报错信息,原因是glibc wrapper更改所致,解决办法是将下述补丁添加到patches/syscall.path中:

1
2
3
error: static declaration of ‘gettid’ follows non-static declaration
error: ‘SIOCGSTAMP’ undeclared here (not in a function); did you mean ‘SIOCSRARP’?
error: ‘SIOCGSTAMPNS’ undeclared here (not in a function); did you mean ‘SIOCGSTAMP_OLD’?

Patch(syscall1.patch):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
--- qemu-2.10.0-rc3-clean/linux-user/syscall.c	2017-08-15 11:39:41.000000000 -0700
+++ qemu-2.10.0-rc3/linux-user/syscall.c 2017-08-22 14:34:03.193088186 -0700
@@ -111,6 +111,7 @@
#include <linux/if_bridge.h>
#endif
#include <linux/audit.h>
+#include <linux/sockios.h>
#include "linux_loop.h"
#include "uname.h"

@@ -116,6 +116,8 @@

#include "qemu.h"

+extern unsigned int afl_forksrv_pid;
+
#ifndef CLONE_IO
#define CLONE_IO 0x80000000 /* Clone io context */
#endif
@@ -256,6 +257,7 @@
#endif

#ifdef __NR_gettid
+#define __NR_sys_gettid __NR_gettid
-_syscall0(int, gettid)
+_syscall0(int, sys_gettid)
#else
/* This is a replacement for the host gettid() and must return a host
@@ -11688,8 +11690,21 @@
break;

case TARGET_NR_tgkill:
- ret = get_errno(safe_tgkill((int)arg1, (int)arg2,
- target_to_host_signal(arg3)));
+
+ {
+ int pid = (int)arg1,
+ tgid = (int)arg2,
+ sig = (int)arg3;
+
+ /* Not entirely sure if the below is correct for all architectures. */
+
+ if(afl_forksrv_pid && afl_forksrv_pid == pid && sig == SIGABRT)
+ pid = tgid = getpid();
+
+ ret = get_errno(safe_tgkill(pid, tgid, target_to_host_signal(sig)));
+
+ }
+
break;

#ifdef TARGET_NR_set_robust_list

重新运行脚本!

Now, just enjoy it!:happy:

2.2 使用

使用方法很简单,仅需在afl-fuzz中添加-Q参数即可。例如,对ubuntu操作系统自带的readelf进行模糊测试,执行如下命令:

1
afl-fuzz -m none -Q -i /home/xiechen/fuzzing-corpus/exe/mopt/ -o out_readelf -d readelf -a @@

2.3 原理[4]

qemu 在执行一个程序时,从被执行程序的入口点开始对基本块翻译并执行,为了提升效率,qemu会把翻译出来的基本块存放到 cache 中,当 qemu 要执行一个基本块时首先判断基本块是否在 cache 中,如果在 cache 中则直接执行基本块,否则会翻译基本块并执行。

cpu-exec.diff中,afl-qemu-cpu-inl.h是AFL为适用于QEMU的forkserver和覆盖率统计的头文件:

2.3.1 AFL_QEMU_CPU_SNIPPET2

  • AFL_QEMU_CPU_SNIPPET2被植入到cpu-exec.ccpu_tb_exec()函数中,该函数用于执行一个TB,并根据需要修复CPU状态;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
--- qemu-2.10.0-rc3-clean/accel/tcg/cpu-exec.c	2017-08-15 11:39:41.000000000 -0700
+++ qemu-2.10.0-rc3/accel/tcg/cpu-exec.c 2017-08-22 14:34:55.868730680 -0700
@@ -36,6 +36,8 @@
#include "sysemu/cpus.h"
#include "sysemu/replay.h"

+#include "../patches/afl-qemu-cpu-inl.h"
+
/* -icount align implementation. */

typedef struct SyncClocks {
@@ -144,6 +146,8 @@
int tb_exit;
uint8_t *tb_ptr = itb->tc_ptr;

+ AFL_QEMU_CPU_SNIPPET2;
+
qemu_log_mask_and_addr(CPU_LOG_EXEC, itb->pc,
"Trace %p [%d: " TARGET_FMT_lx "] %s\n",
itb->tc_ptr, cpu->cpu_index, itb->pc,
@@ -365,6 +369,7 @@
if (!tb) {
/* if no translated code available, then translate it now */
tb = tb_gen_code(cpu, pc, cs_base, flags, 0);
+ AFL_QEMU_CPU_SNIPPET1;
}

mmap_unlock();
  • AFL_QEMU_CPU_SNIPPET2:这里的思路和AFL基于汇编的插桩一致,就是在QEMU执行每一个TB之前,判断当前的PC是否为入口点(afl_entry_pointelfload.c中被赋值)。如果是入口点,则在此处获取共享内存区域地址,并启用forkserver。请注意,这里的afl_forkserver其实是一个死循环,也就是整个elf程序在这里被阻塞,直到fuzzer有数据需要程序运行时才会fork出一个子进程

    1
    2
    3
    4
    5
    6
    7
    #define AFL_QEMU_CPU_SNIPPET2 do { \
    if(itb->pc == afl_entry_point) { \
    afl_setup(); \
    afl_forkserver(cpu); \
    } \
    afl_maybe_log(itb->pc); \
    } while (0)
    • afl_entry_point
      • elfload.c可以看作是包含QEMU载入ELF文件相关操作的一个c文件,其中load_elf_image()函数用以将ELF镜像载入到地址空间中。这里的info->entry其实就是ELF文件入口地址,其值为ehdr->e_entry + load_bias。注意到这里有一个load_bias,这个值其实上是ELF文件加载到内存中的一个偏差值。
      • elfload.c处将afl_entry_point赋值为ELF文件的入口地址,然后在翻译每个TB之前判断该TB是否为初始块,如果为初始块,则在此处设置forkserver,这里的想法与AFL的插桩逻辑是一致的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    --- qemu-2.10.0-rc3-clean/linux-user/elfload.c	2017-08-15 11:39:41.000000000 -0700
    +++ qemu-2.10.0-rc3/linux-user/elfload.c 2017-08-22 14:33:57.397127516 -0700
    @@ -20,6 +20,8 @@

    #define ELF_OSABI ELFOSABI_SYSV

    +extern abi_ulong afl_entry_point, afl_start_code, afl_end_code;
    +
    /* from personality.h */

    /*
    @@ -2085,6 +2087,8 @@
    info->brk = 0;
    info->elf_flags = ehdr->e_flags;

    + if (!afl_entry_point) afl_entry_point = info->entry; //
    +
    for (i = 0; i < ehdr->e_phnum; i++) {
    struct elf_phdr *eppnt = phdr + i;
    if (eppnt->p_type == PT_LOAD) {
    @@ -2118,9 +2122,11 @@
    if (elf_prot & PROT_EXEC) {
    if (vaddr < info->start_code) {
    info->start_code = vaddr;
    + if (!afl_start_code) afl_start_code = vaddr;
    }
    if (vaddr_ef > info->end_code) {
    info->end_code = vaddr_ef;
    + if (!afl_end_code) afl_end_code = vaddr_ef;
    }
    }
    if (elf_prot & PROT_WRITE) {

2.3.2 AFL_QEMU_CPU_SNIPPET1

  • AFL_QEMU_CPU_SNIPPET1被植入到QEMU的tb_find()函数中。tb_find()函数用于在哈希表中寻找当前PC所对应的TB,如果没找着该TB,则调用tb_gen_code()来进行动态翻译,之后便附加上了AFL_QEMU_CPU_SNIPPET1所对应的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #define AFL_QEMU_CPU_SNIPPET1 do { \
    afl_request_tsl(pc, cs_base, flags); \
    } while (0)

    ...

    /* This code is invoked whenever QEMU decides that it doesn't have a
    translation of a particular block and needs to compute it. When this happens,
    we tell the parent to mirror the operation, so that the next fork() has a
    cached copy. */

    static void afl_request_tsl(target_ulong pc, target_ulong cb, uint64_t flags) {

    struct afl_tsl t;

    if (!afl_fork_child) return;

    t.pc = pc;
    t.cs_base = cb;
    t.flags = flags;

    if (write(TSL_FD, &t, sizeof(struct afl_tsl)) != sizeof(struct afl_tsl))
    return;

    }

    这里将当前TB的信息保存到结构体afl_tsl中,并将该信息发送回fuzzer。而afl_wait_tsl()函数就是用来等待fork子进程通过管道传递回afl_tsl的信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    static void afl_wait_tsl(CPUState *cpu, int fd) {

    struct afl_tsl t;
    TranslationBlock *tb;

    while (1) {

    /* Broken pipe means it's time to return to the fork server routine. */
    /* 注意到这里是一个死循环,不断地获取TB的相关信息,那这里在什么时候break呢?子进程杀死或者关闭了管道之后就会break */
    if (read(fd, &t, sizeof(struct afl_tsl)) != sizeof(struct afl_tsl))
    break;
    /* 下面这部分的代码是不是有点多余?因为在cpu-exec.c中其实已经存在有相同的处理逻辑了 */
    tb = tb_htable_lookup(cpu, t.pc, t.cs_base, t.flags);

    if(!tb) {
    mmap_lock();
    tb_lock();
    tb_gen_code(cpu, t.pc, t.cs_base, t.flags, 0);
    mmap_unlock();
    tb_unlock();
    }

    }

    close(fd);

    }

    这里的struct afl_tsl t就是象征性的获取一下,在后续的流程中并没有使用,这里其实就是监控子进程状态的一个处理过程。

2.3.3 afl_maybe_log()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static inline void afl_maybe_log(abi_ulong cur_loc) {

static __thread abi_ulong prev_loc;

/* Optimize for cur_loc > afl_end_code, which is the most likely case on
Linux systems. */

if (cur_loc > afl_end_code || cur_loc < afl_start_code || !afl_area_ptr)
return;

/* Looks like QEMU always maps to fixed locations, so ASAN is not a
concern. Phew. But instruction addresses may be aligned. Let's mangle
the value to get something quasi-uniform. */

cur_loc = (cur_loc >> 4) ^ (cur_loc << 8); // 注意这里的转换操作!
cur_loc &= MAP_SIZE - 1;

/* Implement probabilistic instrumentation by looking at scrambled block
address. This keeps the instrumented locations stable across runs. */

if (cur_loc >= afl_inst_rms) return;

afl_area_ptr[cur_loc ^ prev_loc]++;
prev_loc = cur_loc >> 1;

}
  • 🤔 cur_loc是PC,注意到这里的索引值为(cur_loc >> 4) ^ (cur_loc << 8):代码注释中说指令地址可能对齐了,因此对该索引值进行了修改。这里其实关键的问题在于指令地址空间远大于哈希位图的大小,换言之很容易发生碰撞。我们这里假设PC值(32位)为0x04001234,假设下一次PC值(32位)为0x04011234,那么如果不进行上述的修改,与MAP_SIZE与的结果是相同的,因此会造成很严重的哈希位图碰撞问题,而且这个由于PC值本身空间范围很大,因此这种哈希位图碰撞问题格外的严重。所以这里必须进行相关的转换操作来避免PC值的碰撞,从而有效区分不同PC值(i.e.不同的基本块)。

2.4 改进

  • 参考资料[4]中针对AFL中QEMU模式存在的问题进行了阐述,如下:

在原始的 AFL qemu 版本中获取覆盖率的方式是在每次翻译基本块前调用 afl_maybe_logafl-fuzz 同步覆盖率信息,这种方式有一个问题就是由于 qemu 会把顺序执行的基本块 chain 一起,这样可以提升执行速度。但是在这种方式下有的基本块就会由于 chain 的原因导致追踪不到基本块的执行, afl 的处理方式是禁用 qemuchain 功能,这样则会削减 qemu 的性能(🤔 应该不太会影响qemu的性能吧,但确实会额外添加一些AFL本身的执行)。

  • 我们在第一节中阐述的QEMU特性中,其中有一个就是Direct block chaining。其实就是说,原本能够顺序在一起的基本块,比如A->B->C,QEMU可以将这三个块链起来,从而整合为一个块A’。然而这样的操作会导致AFL跟踪基本块执行的不精确,因此AFL禁用了QEMU的chain功能
  • AFL++的QEMU模式如何进行了改机呢?
  1. 将统计覆盖率插桩的代码插入到每个翻译的基本块前面,=>代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* Generates TCG code for AFL's tracing instrumentation. */
static void afl_gen_trace(target_ulong cur_loc) {

/* Optimize for cur_loc > afl_end_code, which is the most likely case on
Linux systems. */

cur_block_is_good = afl_must_instrument(cur_loc);

if (!cur_block_is_good)
return;

/* Looks like QEMU always maps to fixed locations, so ASLR is not a
concern. Phew. But instruction addresses may be aligned. Let's mangle
the value to get something quasi-uniform. */

// cur_loc = (cur_loc >> 4) ^ (cur_loc << 8);
// cur_loc &= MAP_SIZE - 1;
cur_loc = (uintptr_t)(afl_hash_ip((uint64_t)cur_loc)); // <==== 这里使用了一种更复杂的hash算法
cur_loc &= (MAP_SIZE - 1);

/* Implement probabilistic instrumentation by looking at scrambled block
address. This keeps the instrumented locations stable across runs. */

if (cur_loc >= afl_inst_rms) return;

TCGv cur_loc_v = tcg_const_tl(cur_loc);
if (unlikely(afl_track_unstable_log_fd() >= 0)) {
gen_helper_afl_maybe_log_trace(cur_loc_v);
} else {
gen_helper_afl_maybe_log(cur_loc_v);
}
tcg_temp_free(cur_loc_v);

}

/* Called with mmap_lock held for user mode emulation. */
TranslationBlock *tb_gen_code(CPUState *cpu,
target_ulong pc, target_ulong cs_base,
uint32_t flags, int cflags)
...
tcg_ctx->cpu = env_cpu(env);
afl_gen_trace(pc); // <==== 在每个基本块前插入覆盖率统计的代码(这个idea其实和clang-fast插桩方式是一致的)
gen_intermediate_code(cpu, tb, max_insns);
tcg_ctx->cpu = NULL;
max_insns = tb->icount;
...

这里需要注意的是gen_helper_afl_maybe_loggen_helper_afl_maybe_log_trace这两个函数,这两个函数用于创建一个TCG的call调用,并调用afl_maybe_logafl_maybe_log_trace这两个函数。简而言之,这两个函数就是用于生成调用AFL进行覆盖率统计的相关函数,这些函数定义在accel/tcg/translate-all.c中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

include/exec/helper-head.h:

1
#define HELPER(name) glue(helper_, name)

include/exec/helper-gen.h:

1
2
3
4
5
6
7
#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); \
}

afl_gen_trace()用于在每个基本块前插入afl_maybe_logafl_maybe_log_trace的函数调用,跟踪程序的覆盖情况。

  1. 与forkserver的同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static inline TranslationBlock *tb_find(CPUState *cpu,
TranslationBlock *last_tb,
int tb_exit, uint32_t cf_mask)
{
...
if (tb == NULL) {
mmap_lock();
tb = tb_gen_code(cpu, pc, cs_base, flags, cf_mask);
was_translated = true; // 新翻译的基本块
mmap_unlock();
/* We add the TB in the virtual pc hash table for the fast lookup */
qatomic_set(&cpu->tb_jmp_cache[tb_jmp_cache_hash_func(pc)], tb);
}
#ifndef CONFIG_USER_ONLY
/* We don't take care of direct jumps when address mapping changes in
* system emulation. So it's not safe to make a direct jump to a TB
* spanning two pages because the mapping for the second page can change.
*/
if (tb->page_addr[1] != -1) {
last_tb = NULL;
}
#endif
/* See if we can patch the calling TB. */
if (last_tb) {
tb_add_jump(last_tb, tb_exit, tb);
was_chained = true;
}
if (was_translated || was_chained) {
afl_request_tsl(pc, cs_base, flags, cf_mask,
was_chained ? last_tb : NULL, tb_exit); // <=== 如果有新翻译的基本块或者新构建的chain,则通知forkserver更新缓存
}
return tb;
}

afl_request_tsl()相匹配的是afl_wait_tsl(),该函数检查当前缓存的TB,如果是新TB,则判断该TB的PC是否合法。这里源码注释中描述到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (!tb) {

/* The child may request to transate a block of memory that is not
mapped in the parent (e.g. jitted code or dlopened code).
This causes a SIGSEV in gen_intermediate_code() and associated
subroutines. We simply avoid caching of such blocks. */

if (is_valid_addr(t.tb.pc)) {

mmap_lock();
tb = tb_gen_code(cpu, t.tb.pc, t.tb.cs_base, t.tb.flags, t.tb.cf_mask);
mmap_unlock();

} else {

invalid_pc = 1;

}

}

也就是说,如果子进程请求翻译一个没有在父进程中映射的内存块时,这会导致在gen_intermediate_code()函数以及后续相关联的程序中发生SEGV的错误因此应当避免缓存这类块,因此这里做了一个过滤操作。

此外,该版本的改进还额外支持了AFL中的persistent模式,此节不再详细介绍,感兴趣的读者可以阅读[qemuafl的源码](AFLplusplus/qemuafl: This fork of QEMU enables fuzzing userspace ELF binaries under AFL++. (github.com))

3. 小结

  1. https://github.com/lishuhuakai/qemu_reading/tree/main/Document/qemu-0.1.6
  2. [Translator Internals — QEMU documentation](https://www.qemu.org/docs/master/devel/tcg.html#:~:text=QEMU is a dynamic translator,simple while achieving good performances.)
  3. Bellard, Fabrice. “QEMU, a fast and portable dynamic translator.” USENIX annual technical conference, FREENIX Track. Vol. 41. 2005.
  4. 基于qemu和unicorn的Fuzz技术分析 - 先知社区 (aliyun.com)
  5. Linux中ELF文件启动过程 - 怎么可以吃突突 - 博客园 (cnblogs.com)

关于QEMU和AFL-QEMU模式那些事
http://bladchan.github.io/2023/07/20/关于QEMU和AFL-QEMU模式那些事/
作者
bladchan
发布于
2023年7月20日
许可协议