原文链接:https://airbus-seclab.github.io/AFLplusplus-blogpost/

想象一下,你在一个没有源代码的二进制文件中发现了一个可能存在漏洞的函数。为了帮助你识别漏洞,你需要尽可能使用最相关的 AFL++ 配置进行模糊处理。然而,由于在实践中实现这样的工具并不容易,我们决定在一篇博客文章中总结我们的经验和方法,以帮助未来的工作。

这篇博文介绍了如何利用 AFL++-QEMU 的高级功能,在实际案例中逐步开始语法感知和内存中持续模糊测试。我们提供了所有脚本和数据(以及 ELF 目标),您可以在阅读本博文的同时自己进行实验。尽管我们鼓励您这样做,但这完全是可选的;您也可以只通过通读来欣赏这篇文章,我们希望您仍能从中学到东西!

二进制模糊:一些反复出现的问题

QEMU 是 AFL++ 支持的后端之一,用于处理纯二进制程序的插装。

在实践中,这意味着与源代码可用的目标相反,您不需要重新编译源代码来获得检测二进制文件。相反,AFL++ 会使用 QEMU 的补丁版用户模式仿真执行原始二进制文件,以收集覆盖信息。

注意事项:

  • 如果你想了解有关 QEMU 内部的更多信息,请务必查看本系列文章
  • 在本文中,我们只探讨 QEMU 后端。然而,这里详细描述的大多数概念应该适用于其他可用的AFL++ 后端

使用 QEMU 模式(使用 -Q 标志)执行 Fuzzing 的基本操作如下图所示:

图解命令行

使用 QEMU 模式,可以配置不同的方面来优化 Fuzzing 性能和覆盖范围。官方文档介绍了所有可用的功能。其中:

  • 插桩和覆盖率:
    • 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

本文旨在介绍我们如何在实际案例中回答这些问题,从基本配置到针对目标进行优化的设置,这些都可以重复使用并应用于其他类似项目。

然而,从理论到实践有时显得枯燥乏味,而且经常会产生一些反复出现的问题,例如:

  • 我们希望插桩检测覆盖哪段代码?
  • Fuzzer 入口点的最佳选择是什么?
  • 移动入口点对测试用例的格式意味着什么?
  • 我们的工作如何从 AFL++ 中提供的高级功能中获益,以提高性能?

本文旨在介绍我们如何在实际案例中回答这些问题,从基本配置到针对目标进行优化的设置,并且这些都可以重复使用并应用于其他类似项目。

目标

弱 X509 解析器

我们选择的示例灵感来自于我们在安全评估期间遇到的现实生活中的目标(但由于显而易见的原因,无法重新分配)。它是一个二进制文件,需要一个文件名作为输入,并尝试将相应文件的内容解析为 X509 证书。

它只包含几个基本功能:

  • main:main 函数,该函数将文件作为输入并以该文件作为参数进行调用 parse_cert
  • parse_cert:调用 read_file 并将读取缓冲区作为参数提供给 parse_cert_buf
  • read_file:打开文件,读取并返回其内容;
  • parse_cert_buf:将缓冲区解析 openssl 为 C 库中的 X509 证书 d2i_X509,尝试获取 CN 并打印它。

此目标故意包含一个我们希望在 Fuzzing 活动期间触及的小漏洞:中的基于栈的缓冲区溢出 parse_cert_buf

1
2
3
4
5
6
7
8
9
10
11
int parse_cert_buf(const unsigned char *buf, size_t len) {
X509 *cert;
char cn[MAX_CN_SIZE];

cert = d2i_X509(NULL, &buf, len);
...
char *subj = X509_NAME_oneline(X509_get_subject_name(cert), NULL, 0);
strcpy(cn, subj); // Oops
printf("Got CN='%s' from certificate\n", cn);
...
}

此外,还特意在主程序开始时添加了一个虚拟 init 函数,以模拟初始化阶段,该阶段将花费时间并使目标缓慢启动。

探索目标

在现实生活中,目标显然不像我们的弱 X509 解析器那么简单。事实上,一个好的仅有二进制目标的模糊活动总是从逆向工程阶段开始:

  • 了解目标,它是如何工作的,它如何与环境交互等。
  • 确定要研究的有趣特征;
  • 找到可能被证明是好的模糊目标的函数;
  • 分析调用上下文、结构、用户控制的参数等。
  • 使用适当的参数和模糊化的输入构建一个工具或工具链来调用目标函数。
  • 生成初始语料库以启动 Fuzzer.

尽管有一些工具(如fuzzable)可以帮助完成其中的一些步骤,但它们通常仍然是模糊二进制目标的必需的、繁琐的和手动的部分。

由于我们的示例很简单,因此你应该不会花费太长时间来查找易受攻击的代码、调用跟踪和 To识别感兴趣的功能parse_cert_buf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
00000000000013d4 <parse_cert_buf>:
13d4: 55 push rbp
13d5: 48 89 e5 mov rbp,rsp
13d8: 53 push rbx
13d9: 48 83 ec 68 sub rsp,0x68
...
1473: 48 89 d6 mov rsi,rdx
1476: 48 89 c7 mov rdi,rax
1479: e8 92 fc ff ff call 1110 <strcpy@plt>
...
14d9: b8 00 00 00 00 mov eax,0x0
14de: 48 8b 5d f8 mov rbx,QWORD PTR [rbp-0x8]
14e2: c9 leave
14e3: c3 ret

注意: 你获得的地址可能会根据你的编译器、其版本、使用的选项等而改变。只要它们是一致的,就不用担心!

现在,最有趣的部分:模糊这个目标!为此,请关注本文的其余部分

语料

收集输入

在此之前,我们需要收集样本输入文件来构建语料库。事实上,这些AFL++ 文档指出:

要正确操作,Fuzzer 需要一个或多个启动文件
包含目标通常期望的输入数据的良好示例

在我们的例子中,由于目标解析证书,我们只需使用 OpenSSL 生成一个证书:

1
$ openssl req -nodes -new -x509 -keyout key.pem -out cert.pem

为了使事情更简单,我们已经在corpus文件夹中提供了一个。

预处理语料库

在使用这个语料库之前,我们可以:

  1. 仅保留导致不同执行路径的输入样本(使用 afl-cmin);
  2. 最小化每个输入样本以保留其独特的执行路径,同时使其大小尽可能小(使用 afl-tmin)。这将使未来的突变更加有效。

我们将这两个步骤合并到一个build_corpus.sh脚本中。

现在,假设你按照中 README.md 的步骤构建了 AFL++,你可以继续从 step0 目录中运行 build_corpus.sh。这将完成语料库最小化步骤,并为下一步做好准备。

我们现在应该具备真实的运行 AFL++ 的所有先决条件。我们将在下一步深入,所以继续跟上!

仪器仪表

AFL++ 是一个“覆盖率引导”的模糊测试工具,这意味着变异策略要分析先前执行的代码覆盖,以生成新的输入。为了构建覆盖信息,AFL++-QEMU 需要知道已经到达了哪些基本块。这是通过检测每个基本块以在其被击中时进行跟踪来实现的。

默认设置(step0

默认情况下,通过启动 AFL++-QEMU(如中所示step0),目标的所有基本块可以对其进行插装,并且插装中包含共享库。

注意这个 exec speed 指标:你可以关注它是如何随着我们帖子的每一步发展的。

插桩调整(step1

对于研究目标来说,改变这种默认的插桩行为是很有趣的。原因可能包括:

  • 你对覆盖由主二进制文件导入的库中所有可能的路径感兴趣
  • 你想要排除已测试安全性的库的特定部分
  • 检测完整的大型二进制文件会降低执行速度

要查看指令插入的范围,可使用以下选项:

  • AFL_INST_LIBS;
  • AFL_QEMU_INST_RANGES;
  • AFL_CODE_START;
  • AFL_CODE_END

在我们的示例中,虽然它对仪器 parse_cert_buf 至关重要,但它与仪器 main和共享库(例如 libssl.so)的关系不大。为了对此进行配置,我们将工具仅限于感兴趣的函数。这是通过设置 AFL_QEMU_INST_RANGES(请参阅step1)来完成的:

  • parse_cert_buf 第一条指令的地址开始
  • parse_cert_buf 最后一条指令的地址结束

注意:,在我们的例子中也可以使用 AFL_CODE_STARTAFL_CODE_END 来完成。但是, AFL_QEMU_INST_RANGES 更灵活,因为它允许指定多个要检测的范围,因此我们更喜欢使用此环境变量。

这些地址可以手动确定,也可以从 objdump 输出中推断出来:

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
# The base address at which QEMU loads our binary depends on the target
# See https://github.com/AFLplusplus/AFLplusplus/blob/stable/qemu_mode/README.persistent.md#21-the-start-address
case $(file "$target_path") in
*"statically linked"*)
QEMU_BASE_ADDRESS=0
;;
*"32-bit"*)
QEMU_BASE_ADDRESS=0x40000000
;;
*"64-bit"*)
QEMU_BASE_ADDRESS=0x4000000000
;;
*) echo "Failed to guess QEMU_BASE_ADDRESS"; exit 1
esac

# We use objdump to parse our target binary and obtain the address and size of a given function
function find_func() {
objdump -t "$target_path" | awk -n /"$1"'$/{print "0x"$1, "0x"$5}'
}

# Some environment variables for AFL++ must be hex encoded
function hex_encode() {
printf "0x%x" "$1"
}

read fuzz_func_addr fuzz_func_size < <(find_func "parse_cert_buf")
inst_start=$(hex_encode $(("$QEMU_BASE_ADDRESS" + "$fuzz_func_addr")))
inst_end=$(hex_encode $(("$inst_start" + "$fuzz_func_size")))
export AFL_QEMU_INST_RANGES="$inst_start-$inst_end"
1
2
$ find_func "parse_cert_buf"
0x00000000000013d4 0x0000000000000110

通过启用 AFL++-QEMU 的调试模式(AFL_DEBUG),我们可以检查插桩范围符合我们的要求

1
Instrument range: 0x40000013d4-0x40000014e4 (<noname>)

从现在开始我们的目标是仅对感兴趣的部分进行检测,并准备进行Fuzz

入口点

概念和默认行为

当模糊化时,AFL++ 运行目标,直到到达特定地址(AFL 入口点),然后从那里为每个迭代分叉。默认情况下,AFL 入口点设置为目标的入口点(在我们的示例 target 中为 _start 函数)。

实际上,在默认配置中,AFL++ 会打印以下消息:

1
2
3
# from the step0 directory
$ AFL_DEBUG=1 ./fuzz.sh | grep entrypoint
AFL forkserver entrypoint: 0x40000011a0

反汇编 target objdump 确认入口点设置为 _start 函数的地址:

1
2
3
4
5
6
7
8
9
# from the step0 directory
$ objdump -d --start-address=0x11a0 ../src/target | head -n20
00000000000011a0 <_start>:
11a0: 31 ed xor ebp,ebp
11a2: 49 89 d1 mov r9,rdx
...
11b4: 48 8d 3d fa 03 00 00 lea rdi,[rip+0x3fa] # 15b5 <main>
11bb: ff 15 1f 2e 00 00 call QWORD PTR [rip+0x2e1f] # 3fe0 <__libc_start_main@GLIBC_2.34>
...

使用此配置,每次迭代都会运行整个目标。

定位选择(step2

在某些情况下(如我们的示例),程序的初始化阶段可能需要一些时间。因为每次迭代都要执行初始化,所以对fuzz速度有直接影响。这正是 AFL_ENTRYPOINT 变量所要处理的情况。

实际上, AFL_ENTRYPOINT 可以设置为相关的自定义值,该值将:

  • 只运行一次初始化阶段,直到到达该 AFL_ENTRYPOINT 地址。
  • 将目标停止在, AFL_ENTRYPOINT 并与fuzzer同步;
  • 让 fuzzer 对目标的状态进行快照,然后在 AFL_ENTRYPOINT 地址之后继续执行。

这样,在 fork 服务器运行所有迭代之前,初始化阶段只运行一次,并且 fuzzing 被加速。

在我们的示例中,的 AFL_ENTRYPOINT 定位选择非常简单,因为:

  • init 代码不需要模糊化;
  • init 阶段每次都是执行同样内容;
  • 与模糊相关的函数已经确定(parse_cert)。

因此,我们可以将 AFL_ENTRYPOINT 设置为 parse_cert 函数的开头(请参阅step2):

1
2
3
4
# Define a custom AFL++ entrypoint executed later than the default (the binary's
# entrypoint)
read fuzz_func_addr fuzz_func_size < <(find_func "parse_cert")
export AFL_ENTRYPOINT=$(hex_encode $(("$QEMU_BASE_ADDRESS" + "$fuzz_func_addr")))

在此配置中,AFL++ 打印以下消息:

1
2
$ AFL_DEBUG=1 ./fuzz.sh | grep entrypoint
AFL forkserver entrypoint: 0x40000014e4

我们可以通过反汇编 target 来确认这是的地址 parse_cert

1
2
3
4
5
$ objdump -d --start-address=0x14e4 ../src/target | head -n10
00000000000014e4 <parse_cert>:
14e4: 55 push rbp
14e5: 48 89 e5 mov rbp,rsp
14e8: 48 83 ec 20 sub rsp,0x20

对性能的影响

运行 Fuzzer 让我们看到调整 AFL_ENTRYPOINT 的优势:

  • 使用默认设置 AFL_ENTRYPOINT

    1
    exec speed : 18.24/sec (zzzz...)
  • 调整 AFL_ENTRYPOINT 后(为了跳过 init 相位):

    1
    exec speed : 1038/sec

这说明AFL_ENTRYPOINT 选择对于模糊器可以执行的每秒测试数量的最大化至关重要

在下一节中,我们将看到性能仍然可以通过利用另一个 AFL++ 特性来进一步提高:持续模式。

持续

持续模式(step3

环境变量

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

注意: 除了 AFL_QEMU_PERSISTENT_RETAFL_QEMU_PERSISTENT_RETADDR_OFFSET 也可以使用。如果没有设置这些值,AFL++ 将在到达第一个 ret 指令时停止(仅当 AFL_QEMU_PERSISTENT_ADDR 指向函数的开始时,否则你将必须手动设置该值)。

“恢复”状态可以指“恢复寄存器”( AFL_QEMU_PERSISTENT_GPR)和/或“恢复内存”( AFL_QEMU_PERSISTENT_MEM)。由于恢复内存状态的开销很高,因此只应在必要时进行。在进行模糊处理时,请注意稳定性,以查看是否有必要启用此功能。

即使在使用持续模式时,AFL++ 仍将不时调用 fork(每AFL_QEMU_PERSISTENT_CNT 次迭代,或默认情况下为 1000 次)。如果稳定性足够高,增加此值可能会提高性能(最大值为 10000)。

应用于我们的示例

在我们的例子中,我们可以通过设置 AFL_QEMU_PERSISTENT_ADDRAFL_ENTRYPOINTparse_cert 函数的地址)相同的值来开始。这样,AFL++ 将把我们的进程恢复到它读取输入文件内容之前的状态。

以下是相关章节 afl_config.sh

1
2
3
4
read fuzz_func_addr fuzz_func_size < <(find_func "parse_cert")
export AFL_QEMU_PERSISTENT_ADDR=$(hex_encode $(("$QEMU_BASE_ADDRESS" + "$fuzz_func_addr")))
export AFL_QEMU_PERSISTENT_GPR=1
export AFL_QEMU_PERSISTENT_CNT=10000

在我们的示例中,稳定性保持在 100%,而不必恢复内存状态,因此我们只设置 AFL_QEMU_PERSISTENT_GPR。我们也增加 AFL_QEMU_PERSISTENT_CNT 到它的最大值,因为这不会对我们的稳定性产生负面影响。

step3 文件夹提供的文件直接对此进行测试。你还可以自己确认,AFL++ 文档中描述的性能提升确实存在:根据我们的测试,我们每秒的迭代次数增加了 10 倍以上!

内存中模糊处理(step4

尽管使用了持续模式,但在到达测试函数之前,我们的目标仍会执行一些不必要的操作,特别是打开和读取由 fuzzer 生成的文件的内容。相反,我们可以使用“内存中模糊处理”来跳过这一步,直接从模糊器的内存中读取输入。

钩子

要做到这一点,我们必须实施“挂钩”。它实际上非常简单,其源代码提供在这个文件

  • 我们定义了一个 afl_persistent_hook_init 函数,它声明我们是否要使用内存中的模糊处理。
  • 更有趣的是,我们定义了一个 afl_persistent_hook 函数,它可以在每次迭代时覆盖寄存器值和内存,就在 AFL_QEMU_PERSISTENT_ADDR 到达地址之前。我们所要做的就是覆盖包含要解析的缓冲区的内存,并在正确的寄存器中设置其长度。

注意: 你可以通过在目标函数的开头运行 gdb 和中断,或者直接通过查看反汇编代码来确定要使用哪些寄存器。

这个钩子应该被编译为一个共享库,AFL++ 将在运行时加载。

环境变量

要指示 AFL++ 使用我们的钩子,我们只需设置 AFL_QEMU_PERSISTENT_HOOK 文件的 .so 路径:

1
export AFL_QEMU_PERSISTENT_HOOK="$BASEPATH/src/hook/libhook.so"

正如所讨论的,我们希望更改 AFL_QEMU_PERSISTENT_ADDR 为在迭代期间跳过对 read_file 的调用。这里有两个选项:

  • 要么我们把它设置在 base64_decode 起始地址。在这种情况下,我们也将模糊化 base64_decode 函数。

  • 或者我们把它设置在 parse_cert_buf。在这种情况下 base64_decode,将不会模糊化。

    因为 base64_decode 是由一个受信任的外部库实现的,我们不想测试它(在本例中是 OpenSSL),所以我们将选择第二个选项。

因此,我们可以将 AFL_QEMU_PERSISTENT_ADDR 移动到以下地址 parse_cert_buf

1
2
read fuzz_func_addr fuzz_func_size < <(find_func "parse_cert_buf")
export AFL_QEMU_PERSISTENT_ADDR=$(hex_encode $(("$QEMU_BASE_ADDRESS" + "$fuzz_func_addr")))

输入格式

移动 AFL_QEMU_PERSISTENT_ADDR 对我们的语料库有影响。实际上,由 fuzzer 生成的缓冲区现在直接在中 parse_cert_buf 使用(从未传递给 base64_decode)。这意味着我们必须重建我们的语料库。在我们的例子中,这非常简单:我们只需要从以前的语料库中解码 Base64 文件,并将它们保存为原始二进制文件。

应用于我们的示例

这种方法的一个奇怪之处在于,因为我们不再从文件中读取数据,所以 fuzzer 不再需要在磁盘上创建一个文件。但是,请记住,我们的目标程序期望从其中读取,否则它将立即退出。由于该文件的内容不再相关(因为 read_file 不再调用),我们可以在调用程序之前手动创建一个空的占位符。

你可以在中step4 文件夹找到此新设置。

对性能的影响

总的来说,在我们的测试中,启用持续模式可以将性能提高 10 倍(但在实际场景中不要总是期望有这样的提升!),而内存中的钩子可以产生额外的双重改进:

1
exec speed : 25.6k/sec

注意: 由于执行速度并不是唯一重要的指标,你当然应该关注其他指标,如稳定性、新发现的路径、覆盖率等。

语法感知器(step5

动机

回顾一下我们迄今所取得的成就:

  • 我们将 AFL++ 设置为使用 QEMU 来模糊仅二进制目标。
  • 我们将工具配置为仅覆盖相关地址;
  • 我们调整了 AFL++ 入口点,并启用了持续模式以减少初始化时间。

在许多情况下,这样的配置(当与我们稍后将简要介绍的多处理相结合时)足以运行成功的活动。但是,在本例中,我们决定模糊处理高度结构化数据格式的目标。在这种情况下,引入改变输入数据的新方法可能是有趣的。

事实上,AFL++ 的另一个可调方面是生成和突变逻辑。AFL++ 内置了对一组简单(但非常有效)的突变的支持:

  • 随机比特翻转
  • 随机字节翻转
  • 运算
  • 等等

在大多数情况下,这些突变足以探索模糊的代码。某些数据格式具有内部约束,这些约束将导致样本因不满足这些约束而被过早拒绝。例如,我们的示例中使用的格式 ASN.1 就是这种情况:在不考虑这些约束的情况下生成突变可能会导致大多数样本被目标立即视为无效而丢弃,而不会实现任何额外的覆盖。这意味着 Fuzzing Campaign 汇聚前需要时间生成相关案例。

为了解决这种情况,AFL++ 允许用户提供他们自己的自定义变异体,以引导 Fuzzer 生成更适合的输入。如官方文档中所述,AFL++可插入自定义 Mutator ,只要此Mutator实现了所需的 API 函数。

实施

有几个选项可以在 AFL++ 中实现语法感知的转变器,其中之一是AFL++ 项目的语法变异器部分。然而,由于它不提供对 ASN.1 的支持,我们转而依赖于libprotobuf处理 ASN.1

我们从官方文档中获得了灵感,并在 AFL++现有骨架 和我们的自定义 Mutator 之间建立了“粘合剂”。

结果存在于中custom_mutator.cpp,并实现来自 AFL++API 的以下函数:

  • afl_custom_init: 初始化我们自定义的 mutator
  • afl_custom_fuzz: 用protobuf mutator 对输入进行变异
  • afl_custom_post_process 对变异数据执行后处理,以确保我们的目标接收到正确格式化的输入
  • afl_custom_deinit 清理所有东西

输入格式

实际上,该 afl_custom_post_process 函数起着重要的作用:我们的自定义 mutator 基于 libprotobuf,因此需要 protobuf 数据作为输入。然而,我们的目标只能解析 ASN.1 数据,因此我们需要将数据从 Protobuf 转换为 ASN.1. 幸运的是,protobuf mutator 已经在中 x509_certificate::X509CertificateToDER 实现了此功能。

整个过程概述如下:

PROTOBUF 到 ASN1

和以前一样,我们需要调整语料库中文件的格式,以与我们的 Fuzzing 工具保持一致。这一次,我们需要将 ASN.1 DER 文件转换为 Protobuf.为此,我们实现了一个自定义脚本(protobuf.py 的 ASN1),该脚本在此步骤build_corpus.sh中运行一次。

环境变量

有了这个,剩下的就是指示 AFL++ 使用我们的自定义 Mutator.为此,我们只需设置 AFL_CUSTOM_MUTATOR_LIBRARY 文件的 .so 路径:

1
export AFL_CUSTOM_MUTATOR_LIBRARY="$BASEPATH/libmutator.so"

我们还禁用 AFL++ 执行的所有默认突变和修剪:

1
2
export AFL_DISABLE_TRIM=1
export AFL_CUSTOM_MUTATOR_ONLY=1

应用于我们的示例

你可以在step5 文件夹中找到此新设置。

影响

这一次,它不是为了提高性能,而是为了达到更深的路径。在我们的例子中,这是一个非常小的目标,很难衡量这种影响。但是,这通常是通过比较覆盖率并检查是否使用自定义赋值函数到达新分支来完成的。

但是,你不需要在使用自定义 Mutator 和使用默认的 AFL++ 突变之间进行选择:你可以通过运行 Fuzzer 的多个实例来实现两全其美,我们将在下一步中讨论这一点。

多处理(step6

这一步是我们把所有的东西放在一起来运行我们实际的模糊运动。事实上,在真正的活动中,你不会限制自己只在一个核心/线程/机器上进行模糊测试。幸运的是,AFL++ 处理并行运行多个实例。

但是,如文档中所述,一次运行太多实例并不总是有用的:

在同一台机器上 由于 AFL++ 的设计,有一个有用的 CPU 核心/线程的最大数量,使用更多和整体性能反而会下降。此值取决于目标和限制在每台机器 32 到 64 个核心之间

需要注意的是,即使在达到该限制之前,性能的提高也不是成比例的(内核数量加倍并不会使每秒执行次数加倍):同步进程需要额外的开销。

不同的配置、变量、时间表

当运行 Fuzzer 的多个实例时,可以通过并行使用各种策略和配置来优化覆盖率。因为我们的目的不是反映官方的 AFL++ 文档,我们将参考你的文档,这一节该文档描述了如何在 Fuzzing 时使用多个内核。

然而,由于该页面主要针对源代码可用的 Fuzzing 目标,因此需要对某些方面进行调整,以便只进行二进制 Fuzzing.

仅限二进制的特性

当对源代码可用的目标进行模糊处理时,许多功能(例如 ASAN、UBSAN、CFISAN、COMPCOV)需要使用特定选项重新编译目标。尽管在处理二进制目标时不能选择重新编译,但其中一些特性在 QEMU 中仍然可用(如文档所述here)。

例如, AFL_USE_QASAN 允许使用 LD_PRELOAD 自动注入库来使用使用 QEMU 的 ASAN。类似地, AFL_COMPCOV_LEVEL 允许使用带有 QEMU 的 CompCov,而无需重新编译目标。

多设备设置

对于较大的 Fuzzing 活动,你可以使用多个主机,每个主机运行多个 Fuzzer 进程。此设置实际上相当简单,并且在中官方文档有详细介绍。为了简单和可重复性,我们没有在这篇文章中使用多台机器。

应用于我们的示例

从这篇博文开始,输入格式和目标函数就发生了变化。你可以在下表中找到这些更改的摘要:

Configuration Targeted function 预期的输入格式
Default entrypoint main() base64(ASN.1)
Custom entrypoint main() base64(ASN.1)
In-memory fuzzing parse_cert_buf() ASN.1/ DER
Custom mutator parse_cert_buf() protobuf -> ASN.1

在这一步中,我们运行了一个带有自定义赋值函数的实例,以及几个没有该赋值函数的实例。因此,我们需要 ASN.1( corpus_unique)中的一个语料库和 protobuf( corpus_protobuf_unique)中的一个语料库,以及单独的输出目录。

step6 文件夹提供了这种多语料库设置的示例。请注意,与其他步骤相反,最有趣的更改是在 fuzz.sh 文件中,而不是在中 afl_config.sh

评估活动

在开始一项活动后,你可能需要对其进行监控,评估其效率,并调查其结果。这超出了这篇文章的范围,并正式文件给出了各种细节,我们不打算在这里重复。然而,为了给你一个概念,我们将快速浏览其中的一些问题。

监视活动

AFL++ 提供以下工具来监控运行实例的状态:

  • afl-whatsup,显示在后台运行的 Fuzzer 实例的状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ afl-whatsup -s output
Summary stats
=============
Fuzzers alive : 4
Total run time : 4 minutes, 0 seconds
Total execs : 1 millions, 849 thousands
Cumulative speed : 30829 execs/sec
Average speed : 7707 execs/sec
Pending items : 0 faves, 0 total
Pending per fuzzer : 0 faves, 0 total (on average)
Crashes saved : 3
Cycles without finds : 204/26/1066/264
Time without finds : 1 minutes, 26 seconds
  • afl-plot,它可以绘制特定实例的指标随时间的演变:
1
$ afl-plot output/afl-main /tmp/plot

AFL-PLOT 输出示例

测量覆盖率

检查覆盖范围是另一回事,对于只有二进制的目标有各种特殊性。这超出了这篇文章的范围,但你可以找到有趣的资源在官方文件中

OURStep6 文件夹上下文中的典型命令如下:

1
$ afl-showmap -Q -C -i "$output_path"/afl-main/queue -o afl-main.cov -- "$target_path" /tmp/.afl_fake_input

检查崩溃和超时

一旦 AFL++ 识别出崩溃或挂起,它将把触发它的输入保存在输出目录中的专用文件夹中,以便你可以重现它。

理解这些结果的有用工具可以是:

  • afl-tmin 以获得再现崩溃的最小测试用例
  • Lighthouse探索特定案例的覆盖范围
  • Valgrind调查内存问题
  • 可能还有更多!

此外,对于自定义 Mutator 发现的情况,输入将采用 Protobuf 格式,这不容易直接在目标上重放。为此,我们实现了一个简单的程序,它允许将 protobuf 转换回 ASN.1(请参阅protobuf_to_der.CPP

到目前为止我们所做的

在这篇文章中,我们的目标是强调我们的方法,解释 AFL++ 的概念,并为 Fuzz 二进制目标提供一个框架。这导致我们根据我们的目标和我们自己的经验做出选择,这在其他情况下可能并不相关。特别地,其他语法变异体可能更容易实现(例如语法变异器,如果它支持正确的语法)。

然而,通过配置内存中的持续性、调整定制的语法感知突变以及实现多处理,我们实现了以有趣的执行速度和覆盖范围运行 Fuzzing 活动。

显然,这只是故事的开始:运行 Fuzzing 活动本身和分析结果都有自己的一套新问题和乐趣!

参考书目

工具

文件

⬆︎TOP