OneShell

I fight for a brighter tomorrow

0%

afl qemu mode简介

afl qemu 模式简介

这篇文章主要是参考了 afl-qemu,并简单扩展了自己对 QEMU 的理解和最近对于 AFL 源码阅读的一些理解。

qemu 简介

qemu 在 IoT 漏洞挖掘和复现中被使用得很多用来进行固件的模拟,根据模拟的级别可以分为用户程序模拟和系统虚拟化模拟。

用户程序模拟就是 QEMU 能够将一个平台编译的二进制文件运行在另外一个不同的平台,例如一个 ARM 指令集的二进制程序,通过 QEMU 的 TCG(Tiny Code Generator)处理之后,ARM 指令被转换成 TCG 的中间代码,然后再转换成目的平台的代码。

系统虚拟化模拟指的是 QEMU 能够模拟一个完整的操作系统虚拟机,该虚拟机有自己的虚拟 CPU、芯片组、虚拟内存以及其他的虚拟外设例如网卡,能够给虚拟机中运行的操作系统提供和物理硬件平台一致的硬件视图。

QEMU 能够模拟的平台也很多,常见的 x86/64、ARM、MIPS、PPC 等,早期的 QEMU 都是通过 TCG 来完成对硬件平台的模拟,所有的虚拟机指令也需要通过 QEMU 来进行转换,这个地方不继续深入说明,就简单知道有这个基本 TCG 进行指令翻译的流程。回到正式话题,如果我们要对某个其他指令集的二进制文件进行模糊测试,就可以使用 afl 的 qemu mode。

当我们使用 qemu 来加载一个其他指令集的可执行文件(ELF 为例)时,基本的流程如下:

  • qemu 初始化
  • TCG 初始化
  • CPU 初始化
  • 加载可执行文件,以 ELF 为例,有对 ELF 的解析过程
  • syscall 初始化,qemu 是将可执行文件的系统调用转发到宿主机的系统调用来实现
  • signal 初始化,在 afl 中也是根据 target 执行过程中的 signal 或者结束代码来判断 crash
  • gdbserver 初始化,如果启动 qemu 的时候选择调试 -g
  • cpu_loop 开始模拟

如上,重要的流程就是 QEMU 分析目标程序的 ELF 结构,分配必要的内存,装载所需要的库,然后开始进行指令集的翻译、执行,并且将遇见的 syscall 系统调用转发到宿主机进行模拟。CPU 执行的基本流程大概是:

1
2
3
4
for (;;) {
cpu_exec();
switch(处理退出事件)
}

afl qemu mode

在 afl 源码的 qemu mode 文件夹,目录结构如下,其中有编译 qemu mode 的脚本,qemu-2.10.0 的源码,以及一个 readme 文档。

1
2
3
4
5
6
7
8
9
10
# kali @ kali in ~/Code/AFL/qemu_mode [22:38:50] 
$ tree -L 1
.
├── build_qemu_support.sh
├── patches
├── qemu-2.10.0
├── qemu-2.10.0.tar.xz
└── README.qemu

2 directories, 3 files

在 readme 文档中,有对 qemu mode 的简单介绍,是基于 QEMU 的用户级模拟,用于仿真黑盒闭源二进制文件。

  1. Introduction

The code in this directory allows you to build a standalone feature that
leverages the QEMU “user emulation” mode and allows callers to obtain
instrumentation output for black-box, closed-source binaries. This mechanism
can be then used by afl-fuzz to stress-test targets that couldn’t be built
with afl-gcc.

The usual performance cost is 2-5x, which is considerably better than
seen so far in experiments with tools such as DynamoRIO and PIN.

The idea and much of the implementation comes from Andrew Griffiths.

接下来就是看看 qemu mode 的编译流程,其中大概做了一些注释

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#!/bin/sh
#
# Copyright 2015 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -----------------------------------------
# american fuzzy lop - QEMU build script
# --------------------------------------
#
# Written by Andrew Griffiths <agriffiths@google.com> and
# Michal Zalewski <lcamtuf@google.com>
#
# This script downloads, patches, and builds a version of QEMU with
# minor tweaks to allow non-instrumented binaries to be run under
# afl-fuzz.
#
# The modifications reside in patches/*. The standalone QEMU binary
# will be written to ../afl-qemu-trace.
#


VERSION="2.10.0"
# QEMU 2.10.0 源码下载
QEMU_URL="http://download.qemu-project.org/qemu-${VERSION}.tar.xz"
QEMU_SHA384="68216c935487bc8c0596ac309e1e3ee75c2c4ce898aab796faa321db5740609ced365fedda025678d072d09ac8928105"

echo "================================================="
echo "AFL binary-only instrumentation QEMU build script"
echo "================================================="
echo

echo "[*] Performing basic sanity checks..."

if [ ! "`uname -s`" = "Linux" ]; then

echo "[-] Error: QEMU instrumentation is supported only on Linux."
exit 1

fi
# 进行patch
if [ ! -f "patches/afl-qemu-cpu-inl.h" -o ! -f "../config.h" ]; then

echo "[-] Error: key files not found - wrong working directory?"
exit 1

fi

if [ ! -f "../afl-showmap" ]; then

echo "[-] Error: ../afl-showmap not found - compile AFL first!"
exit 1

fi


for i in libtool wget python automake autoconf sha384sum bison iconv; do

T=`which "$i" 2>/dev/null`

if [ "$T" = "" ]; then

echo "[-] Error: '$i' not found, please install first."
exit 1

fi

done

if [ ! -d "/usr/include/glib-2.0/" -a ! -d "/usr/local/include/glib-2.0/" ]; then

echo "[-] Error: devel version of 'glib2' not found, please install first."
exit 1

fi

if echo "$CC" | grep -qF /afl-; then

echo "[-] Error: do not use afl-gcc or afl-clang to compile this tool."
exit 1

fi

echo "[+] All checks passed!"

ARCHIVE="`basename -- "$QEMU_URL"`"

CKSUM=`sha384sum -- "$ARCHIVE" 2>/dev/null | cut -d' ' -f1`

if [ ! "$CKSUM" = "$QEMU_SHA384" ]; then

echo "[*] Downloading QEMU ${VERSION} from the web..."
rm -f "$ARCHIVE"
wget -O "$ARCHIVE" -- "$QEMU_URL" || exit 1

CKSUM=`sha384sum -- "$ARCHIVE" 2>/dev/null | cut -d' ' -f1`

fi

if [ "$CKSUM" = "$QEMU_SHA384" ]; then

echo "[+] Cryptographic signature on $ARCHIVE checks out."

else

echo "[-] Error: signature mismatch on $ARCHIVE (perhaps download error?)."
exit 1

fi

echo "[*] Uncompressing archive (this will take a while)..."

rm -rf "qemu-${VERSION}" || exit 1
tar xf "$ARCHIVE" || exit 1

echo "[+] Unpacking successful."
# 根据脚本执行的环境变量CPU_TARGET编译对应架构的QEMU
echo "[*] Configuring QEMU for $CPU_TARGET..."

ORIG_CPU_TARGET="$CPU_TARGET"

test "$CPU_TARGET" = "" && CPU_TARGET="`uname -m`"
test "$CPU_TARGET" = "i686" && CPU_TARGET="i386"

cd qemu-$VERSION || exit 1
# 对QEMU源码进行patch
echo "[*] Applying patches..."

patch -p1 <../patches/elfload.diff || exit 1
patch -p1 <../patches/cpu-exec.diff || exit 1
patch -p1 <../patches/syscall.diff || exit 1
patch -p1 <../patches/configure.diff || exit 1
patch -p1 <../patches/memfd.diff || exit 1

echo "[+] Patching done."

# --enable-pie seems to give a couple of exec's a second performance
# improvement, much to my surprise. Not sure how universal this is..

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 || exit 1

echo "[+] Configuration complete."

echo "[*] Attempting to build QEMU (fingers crossed!)..."

make || exit 1

echo "[+] Build process successful!"

echo "[*] Copying binary..."
# 将编译出来的qemu可执行文件复制为afl-qemu-trace
cp -f "${CPU_TARGET}-linux-user/qemu-${CPU_TARGET}" "../../afl-qemu-trace" || exit 1

cd ..
ls -l ../afl-qemu-trace || exit 1

echo "[+] Successfully created '../afl-qemu-trace'."

if [ "$ORIG_CPU_TARGET" = "" ]; then

echo "[*] Testing the build..."

cd ..

make >/dev/null || exit 1

gcc test-instr.c -o test-instr || exit 1

unset AFL_INST_RATIO

# We shouldn't need the /dev/null hack because program isn't compiled with any
# optimizations.
echo 0 | ./afl-showmap -m none -Q -q -o .test-instr0 ./test-instr || exit 1
echo 1 | ./afl-showmap -m none -Q -q -o .test-instr1 ./test-instr || exit 1

rm -f test-instr

cmp -s .test-instr0 .test-instr1
DR="$?"

rm -f .test-instr0 .test-instr1

if [ "$DR" = "0" ]; then

echo "[-] Error: afl-qemu-trace instrumentation doesn't seem to work!"
exit 1

fi

echo "[+] Instrumentation tests passed. "
echo "[+] All set, you can now use the -Q mode in afl-fuzz!"

else

echo "[!] Note: can't test instrumentation when CPU_TARGET set."
echo "[+] All set, you can now (hopefully) use the -Q mode in afl-fuzz!"

fi

exit 0

可以看到编译脚本中是先下载指定 QEMU 版本的源码,进行一些检测,然后进行 patch。patch 是对 QEMU 的 ELF 装载、CPU 执行、系统调用、内存描述符等相关的代码进行了修改,以及对 configure 文件也进行修改。patch 完成之后进行 make,然后将编译完成的 qemu-${CPU_TARGET} 复制到 afl 的目录下,重命名为 afl-qemu-trace,在之后会从 afl-fuzz.c 简单说下 afl-qemu-trace 是如何进行调用的。这个地方说一下,如果要 fuzz 指定架构的二进制文件,那么需要在编译的时候指定编译 qemu 可执行文件针对的架构,例如编译 mips 的命令:CPU_TARGET=mips ./build_qemu_support.sh。然后就是在脚本中测试编译出来的 afl-qemu-trace 能否正常使用。

可见最为关键的地方是进行的几个 patch,patch 文件内容不多,下面来简单分析一下 diff 文件。

1
2
3
4
5
6
7
8
9
10
11
# kali @ kali in ~/Code/AFL/qemu_mode/patches [0:49:08] 
$ tree -L 1
.
├── afl-qemu-cpu-inl.h
├── configure.diff
├── cpu-exec.diff
├── elfload.diff
├── memfd.diff
└── syscall.diff

0 directories, 6 files

elfload.diff

如下是 elfload.diff 文件,新增了三个外部引用,在 QEMU 执行 ELF 文件解析的时候获取这些地址。

  • afl_entry_point:ELF 的入口点
  • afl_start_code:ELF 代码段的 start
  • afl_end_code:ELF 代码段的 end
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) {

cpu-exec.diff

如下是 cpu-exec.diff 文件,新增了一个头文件 afl-qemu-cpu-inl.h,在 CPU 执行 QEMU 翻译代码的前后位置分别加入了宏定义 AFL_QEMU_CPU_SNIPPET2 和 AFL_QEMU_CPU_SNIPPET1。TB(Translation Block)是 QEMU 进行指令翻译的基本单位。

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_SNIPPET1 是用来实现翻译的加速,AFL_QEMU_CPU_SNIPPET2 位于执行 TB 的函数,在待 fuzz 程序的入口处,进行 afl_setup,然后执行 afl_forkserver,其中有 fork 执行过程。对于覆盖率的计算则是常见的 afl_maybe_log 函数,在使用 afl-gcc 等编译的程序中也会在基本块插入 afl_maybe_log 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* A snippet patched into tb_find_slow to inform the parent process that
we have hit a new block that hasn't been translated yet, and to tell
it to translate within its own context, too (this avoids translation
overhead in the next forked-off copy). */

#define AFL_QEMU_CPU_SNIPPET1 do { \
afl_request_tsl(pc, cs_base, flags); \
} while (0)

/* This snippet kicks in when the instruction pointer is positioned at
_start and does the usual forkserver stuff, not very different from
regular instrumentation injected via afl-as.h. */

#define AFL_QEMU_CPU_SNIPPET2 do { \
if(itb->pc == afl_entry_point) { \
afl_setup(); \
afl_forkserver(cpu); \
} \
afl_maybe_log(itb->pc); \
} while (0)

syscall.diff

如下是 syscall.diff,定义了全局变量 qemu.h,新增外部引用 afl_forksrv_pid,也就是 forkserver 的 pid,新增了对于 TARGET_NR_tgkill 的判定,具体的含义不是很懂,但应该和 target 的执行返回错误码有关。

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
--- 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
@@ -116,6 +116,8 @@

#include "qemu.h"

+extern unsigned int afl_forksrv_pid;
+
#ifndef CLONE_IO
#define CLONE_IO 0x80000000 /* Clone io context */
#endif
@@ -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

综上所述,三个 patch 文件所作的工作就是在 qemu 代码翻译的阶段,在 QEMU 翻译基本块 TB 加入指令插桩代码,从 ELF 的入口开始执行,然后进行代码覆盖率和错误代码的相关计算。

afl-fuzz.c 中使用 qemu mode

假设已经简单阅读过 afl-fuzz.c 的源码,在 main 函数参数解析时,会判定 -Q 使用了 qemu mode。

1
2
3
4
5
6
7
8
case 'Q': /* QEMU mode */

if (qemu_mode) FATAL("Multiple -Q options not supported");
qemu_mode = 1;

if (!mem_limit_given) mem_limit = MEM_LIMIT_QEMU;

break;

如果是使用了 qemu mode,会根据 afl-fuzz 的命令参数构造 qemu 的启动参数。在 get_qemu_argv 函数中,主要执行的工作就是为 QEMU 设置相关环境变量,搜索 afl-qemu-trace 程序,构造 afl-qemu-trace 启动的命令参数,也就是将 target_path 修改为如何使用 afl-qemu-trace 执行待 fuzz 程序。

1
2
3
4
if (qemu_mode)
use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind);
else
use_argv = argv + optind;

那么最终 qemu 模式是如何启动起来的,我根据源码的理解大概是,在 afl-fuzz.c 的 main 函数,在最后的循环中,调用 fuzz_one -> calibrate_case -> init_forkserver -> execv

1
2
3
4
5
6
static u8 fuzz_one(char** argv);
static u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem, u32 handicap, u8 from_queue);
EXP_ST void init_forkserver(char** argv);
...
execv(target_path, argv);
...

在 init_forkserver 函数中,有将变异数据的文件描述符和待 fuzz 程序的标准输入绑定的过程。当使用 execv 执行 afl-qemu-trace,此时 patch 了的 afl-qemu-trace 会在 ELF 的入口处执行 afl_setup() 函数和 afl_forkserver() 函数,进行真正的待 fuzz 程序的执行。

小结

这篇文章先简单介绍了 QEMU 执行用户级程序的基本流程,然后通过分析 afl qemu mode 的编译脚本和 diff 文件,简单说明了在 afl 中使用 qemu fuzz 闭源程序的基本流程,然后分析 afl-fuzz.c 源码,简要说明 afl-fuzz 是如何将进行一个变异数据送到待 fuzz 程序中去执行。

如果有问题,或者看到这篇文章的师傅有其他的见解讨论,请联系我,谢谢!

这里有一个简单的对于 AFL qemu mode 的使用,参见使用afl对无源码程序进行fuzz