AFL++ QEMU mode (aarch64)

AFL++ QEMU mode (aarch64)高级应用

参考资料:

AFL++ 的 QEMU 模式在物联网(IoT)和车联网(V2X)领域有着重要的应用。由于这些设备和系统通常包含大量闭源软件,且运行在多种不同架构上(如 ARM、MIPS 等),传统的源代码插桩方法往往不可行。利用 QEMU 模式,AFL++ 能够在不需要源代码的情况下直接对这些设备的固件或二进制文件进行模糊测试。

但 QEMU 模式的执行速度比编译时插桩模式慢得多,因为 QEMU 模式需要进行二进制翻译和仿真,而这些操作比直接运行编译过的插桩代码要耗费更多的时间和资源。

为了优化提升 QEMU mode 测试效率,我们可以通过对下述环境变量进行配置,具体内容可参考官方说明文档

  • 插桩和覆盖率:

    • AFL_INST_LIBS (如果需要对使用的库进行插桩,可启用该选项)
    • AFL_QEMU_INST_RANGES (用于指定插桩的地址范围)
  • 突变:

    • AFL_CUSTOM_MUTATOR_LIBRARY
    • AFL_CUSTOM_MUTATOR_ONLY
  • 变异:

    • AFL_ENTRYPOINT
    • AFL_QEMU_PERSISTENT_ADDR/ AFL_QEMU_PERSISTENT_ADDR_RET
    • AFL_QEMU_PERSISTENT_HOOK
    • AFL_DISABLE_TRIM
    • AFL_DEBUG/ AFL_DEBUG_CHILD

本文将以 libjpeg 为例,在 aarch64 架构下进行编译与测试,测试目标是 libjpeg 库中自带的 cjpeg 程序。它是一个二进制文件,能够将输入的图片文件转换为 jpeg 文件。

编译目标程序:

1
2
3
4
5
# 按照交叉编译环境 
$ sudo apt install gcc-aarch64-linux-gnu
# 生成 Makefile
$ ./configure --prefix="$(pwd)/install" --enable-shared --enable-static CC=aarch64-linux-gnu-gcc --host=aarch64-linux
$ make -j && make install

运行(AFL++ 的 QEMU mode 已经预先编译安装完成(aarch64)):

1
2
3
$ QEMU_LD_PREFIX=/usr/aarch64-linux-gnu LD_LIBRARY_PATH=./install/lib afl-qemu-trace ./install/bin/cjpeg -h
usage: ./cjpeg [switches] [inputfile]
......
  • 种子应当寻找对应的合法类型文件,例如这里可以寻找 ppm 类型样例文件
    • 这里由于程序较简单,种子只写了一个 a,任其随机变异
1
2
3
cd install
# 创建种子文件
mkdir input && echo a > input/test
  • 由于该程序是动态链接的,且使用了 libjpeg.so 库,在对其进行模糊测试时需要装载:
1
2
3
4
5
6
7
#!/bin/bash
# fuzz.sh 文件内容:
# 配置 ld 路径
export QEMU_LD_PREFIX=/usr/aarch64-linux-gnu
# 指定 libjpeg.so 所在的路径
export LD_LIBRARY_PATH=./lib
afl-fuzz -i ./input -o ./output -Q -m 10240 -- ./bin/cjpeg @@

发现第一个 crash 用时 44s,exec speed 在 700 ~ 1000/sec 浮动:

image-20240617100028263

入口点更改

默认情况下,AFL++ 会自动将程序的入口点设置为 AFL 的入口点( _start 函数),这种情况下,每次迭代都会完整地运行整个目标

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
# 可以通过开启 AFL_DEBUG 选项查看入口点:
[AFL++ ebb70fcf6696] ~/libjpeg/install # AFL_DEBUG=1 ./fuzz.sh | grep entrypoint
AFL forkserver entrypoint: 0x5500001840
# 使用 r2 查询 _start 函数地址:
$ r2 -c "aa; pdf@0x1840" bin/cjpeg
;-- _start:
;-- pc:
; XREFS(30)
┌ 48: entry0 (func rtld_fini, int64_t argc, char **ubp_av); // noreturn
│ ; arg func rtld_fini @ x0
│ ; arg int64_t argc @ sp+0x0
│ ; arg char **ubp_av @ sp+0x8
│ 0x00001840 1f2003d5 nop
│ 0x00001844 1d0080d2 mov x29, 0
│ 0x00001848 1e0080d2 mov x30, 0
│ 0x0000184c e50300aa mov x5, x0 ; func rtld_fini
│ 0x00001850 e10340f9 ldr x1, [sp] ; pstate ; int argc
│ 0x00001854 e2230091 add x2, ubp_av ; char **ubp_av
│ 0x00001858 e6030091 mov x6, sp ; void *stack_end
│ 0x0000185c a00000d0 adrp x0, 0x17000
│ 0x00001860 00f847f9 ldr x0, [x0, 0xff0] ; 0x1480
│ ; dbg.main ; func main
│ 0x00001864 030080d2 mov x3, 0 ; func init
│ 0x00001868 040080d2 mov x4, 0 ; func fini
└ 0x0000186c 89feff97 bl sym.imp.__libc_start_main ; int __libc_start_main(func main, int argc, char **ubp_av, func init, func fini, func rtld_fini, void *stack_end)

接下来使用 AFL_ENTRYPOINT 指定入口点到程序的主函数,跳过 _start 函数中的初始化工作:

1
2
3
4
# 找到 main 函数偏移地址: 0x1480
$ r2 -c "aa; afl" bin/cjpeg 2> /dev/null | grep main
0x00001290 1 16 sym.imp.__libc_start_main
0x00001480 35 896 main

将偏移地址加上基地址得到最终的目标函数地址(aarch64 架构下,base addr 为 0x5500000000),那么 main 函数的目标地址为:0x5500001480

  • 对于 amd64: 添加 0x4000000000
  • 对于 x86: 添加 0x40000000
  • 对于 aarch64: 添加 0x5500000000
  • 对于 arm: 无需添加
  • 具体的实际加载地址可以通过 AFL_QEMU_DEBUG_MAPS=1 afl-qemu-trace TARGET-BINARY 查看
1
2
# 在 fuzz.sh 中添加 AFL_ENTRYPOINT 选项
export AFL_ENTRYPOINT=0x5500001480

使用同样的种子,发现第一个 crash 用时 28s,exec speed 在 1000~1500/sec 浮动

51ygiak3.y5b

持续模式

“持续模式”是允许 AFL++ 避免每个迭代都调用 fork 的特性。相反,它在到达某个地址(AFL_QEMU_PERSISTENT_ADDR)时保存子节点的状态,并在到达另一个地址( AFL_QEMU_PERSISTENT_RET)时恢复此状态。(aarch64 架构下无法使用 AFL_QEMU_PERSISTENT_RETADDR_OFFSET

  • 需要注意,AFL_QEMU_PERSISTENT_ADDR 必须指向程序中一个能够循环调用的函数,并且该函数返回后能够再次被调用。
  • 99% 的情况下,需要附加 AFL_QEMU_PERSISTENT_GPR=1(用于恢复通用寄存器的状态,如果不添加该环境变量,第二次迭代循环开始之后会丢失参数值,例如 main 函数的 argc 值将丢失)

在 cjpeg 中,我们目前需要重点关注的是解析文件、处理文件部分的函数,计算偏移后指定持续模式的起始地址与返回地址,期望能够持续测试目标部分如下:

ytnr04bx.hkh
  • 要注意,持续模式起始地址需要囊括读取文件部分(否则无法获取到模糊测试输入数据)

在本样例程序中,文件读取、解析操作都在 main 函数中,因此起始地址仍需配置为 main 函数开始,返回地址可以适当前移(文件关闭句柄之前)

1
2
export AFL_QEMU_PERSISTENT_ADDR=0x5500001480
export AFL_QEMU_PERSISTENT_RET=0x5500001668

另外需要注意本程序中某个分支下有一个能够导致程序退出的 return,持续模式下如果包含能够导致程序退出的分支,需要指定:

1
export AFL_QEMU_PERSISTENT_EXITS=1
  • 启用该选项后,如果遇到 exit 不会退出程序,而是返回到 START 重新执行
tptd4hcc.nan

如果执行时如果遇到下述问题,需要考虑修改起始地址、返回地址及上述相关配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[AFL] ERROR: no persistent iteration executed

[-] Unable to communicate with fork server. Some possible reasons:

- You've run out of memory. Use -m to increase the the memory limit
to something higher than 10240.
- The binary or one of the libraries it uses manages to create
threads before the forkserver initializes.
- The binary, at least in some circumstances, exits in a way that
also kills the parent process - raise() could be the culprit.
- If using persistent mode with QEMU, AFL_QEMU_PERSISTENT_ADDR is
probably not valid (hint: add the base address in case of PIE)

If all else fails you can disable the fork server via AFL_NO_FORKSRV=1.

[-] PROGRAM ABORT : Unable to communicate with fork server
Location : afl_fsrv_run_target(), src/afl-forkserver.c:1990

对样例程序进行模糊测试:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
export QEMU_LD_PREFIX=/usr/aarch64-linux-gnu
export LD_LIBRARY_PATH=./lib
# export AFL_INST_LIBS=1
export AFL_ENTRYPOINT=0x5500001480
export AFL_QEMU_PERSISTENT_ADDR=0x5500001480
export AFL_QEMU_PERSISTENT_RET=0x5500001668
export AFL_QEMU_PERSISTENT_GPR=1
export AFL_QEMU_PERSISTENT_EXITS=1
export AFL_QEMU_PERSISTENT_CNT=1000
# export AFL_DEBUG=1
afl-fuzz -i ./input -o ./output -Q -m 10240 -- ./bin/cjpeg @@
  • 目标中的循环越稳定,可以运行的时间越长,循环越不稳定,循环计数应该越低。较低值为 100,最大值应为 10000。默认值为 1000。可以使用 AFL_QEMU_PERSISTENT_CNT 设置该值(根据具体情况适当调整该值即可,这里设为 1000)

发现第一个 crash 用时 8s,exec speed 在 6000+/sec

aqexs3jf.v4a

内存模糊测试

目标:直接从模糊器的内存中读取输入,跳过文件打开读取操作

钩子:

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
/*
* Inspired by https://github.com/AFLplusplus/AFLplusplus/blob/stable/utils/qemu_persistent_hook/read_into_rdi.c
*/
#include "api.h"
#include <string.h>

#define g2h(x) ((void *)((unsigned long)(x) + guest_base))
#define h2g(x) ((uint64_t)(x) - guest_base)

void afl_persistent_hook(struct arm64_regs *regs, uint64_t guest_base, uint8_t *input_buf, uint32_t input_buf_len) {
// Make sure we don't overflow the target buffer
if (input_buf_len > 4096)
input_buf_len = 4096;

// Copy the fuzz data to the target's memory
memcpy(g2h(regs->x0), input_buf, input_buf_len);

// 根据实际情况修改寄存器的数据
}

int afl_persistent_hook_init(void) {
// 1 for shared memory input (faster), 0 for normal input (you have to use
// read(), input_buf will be NULL)
return 1;
}
  • 注意: api.h 来自 AFLplusplus/qemu_mode/qemuafl/qemuafl/api.h
  • 编译 hook 代码:gcc -shared -fPIC -o libhook.so hook.c
  • 加载 hook 代码:export AFL_QEMU_PERSISTENT_HOOK="./libhook.so"

修改持续模式的 START 地址到待测试的目标函数,根据反汇编信息,修改各参数对应的寄存器数据,启动模糊测试(暂无实例演示)