【FUZZ.0x00】从零开始的 AFL 学习笔记 - I:基本用法

本文最后更新于:2024年3月12日 上午

兔兔这么可爱,不可以吃兔兔!

0x00. 一切开始之前

Fuzzing模糊测试, 是一种自动化的软件测试方法,其通过将大量的测试数据”喂“给测试程序并观察程序执行状态与结果的方式进行软件测试,因而命名为模糊测试

现代的 Fuzzer 在每轮变异样本的输入运行后,会评估该样本是否触发了更多的状态(例如,执行到了更多的代码块),从而决定是否保留它进而进行更深度的变异,以自动构建下一次的输入

在起初,人们通常以monkey testing来指代最原始的fuzz,就像著名的无限猴子定理一样:让一只猴子在打字机上随机地按键,当按键时间达到无穷时,几乎必然能够打出任何给定的文字,比如莎士比亚的全套著作,当然也有可能包含一套Nginx RCE。

Flanker:《Fuzzing战争: 从刀剑弓斧到星球大战》

当前 FUZZING 界比较有名的 fuzzer 有 AFL、libfuzzer 等,世界各地的安全研究员们利用这些现代化的漏洞挖掘工具已经在真实世界中发现了数以万计的漏洞

这里我们选择跟随着GitHub上的一个项目进行FUZZING工具AFL的学习:https://github.com/mykter/afl-training

0x01. 环境搭建

方案一:使用 docker 运行 AFL++(推荐)

AFL++ 提供了一个预先准备好近乎一切事物的 docker 环境,我们可以通过如下方式将镜像拉取到本地并运行:

注意 afl++ 的镜像没有保活进程,所以不太能 -d 在后台运行,建议用 tmux 挂着,或是自己写 DockerfileFROM aflplusplus/aflplusplus:latest 创建一个 wrapper

1
2
$ docker pull aflplusplus/aflplusplus:latest
$ docker run -ti -v /location/of/your/target:/src aflplusplus/aflplusplus

不过这个镜像并没有帮我们编译好 AFL,我们仍需要手动进行构建,启动一个容器后进入内部的 /AFLplusplus 目录进行编译安装:

注意容器内得用 root 权限

1
2
3
# cd /AFLplusplus
# make distrib
# make install

方案二:直接安装 AFL++ 到本地(不推荐)

笔者还是更推荐使用 docker 这样灵活的部署方式

首先是配置一些必须的环境,笔者的环境是 Ubuntu 20.0.4:

1
2
3
4
$ sudo apt-get install git build-essential curl libssl-dev sudo libtool libtool-bin libglib2.0-dev bison flex automake python3 python3-dev python3-setuptools libpixman-1-dev gcc-9-plugin-dev cgroup-tools \
clang-11 clang-tools-11 libc++1-11 libc++-11-dev libc++abi1-11 libc++abi-11-dev libclang1-11 libclang-11-dev libclang-common-11-dev libclang-cpp11 libclang-cpp11-dev liblld-11 liblld-11-dev liblldb-11 liblldb-11-dev libllvm11 libomp-11-dev libomp5-11 lld-11 lldb-11 python3-lldb-11 llvm-11 llvm-11-dev llvm-11-runtime llvm-11-tools
$ sudo apt-get install clang
$ sudo apt-get install llvm

接下来安装AFL(American Fuzzy Lop):

1
2
3
4
$ git clone https://github.com/AFLplusplus/AFLplusplus
$ cd AFLplusplus
$ make distrib
$ sudo make install

最后,在使用之前需要使用如下命令设置 core dump 的目录,因为 AFL 需要通过程序崩溃的记录(core dump)来判断是否触发了 crash 的路径:

1
2
$ sudo su root
$ echo core >/proc/sys/kernel/core_pattern

这里可能会出现提示 Permission denied 的报错,参见这里,解决方案是通过如下指令来写入 core_pattern

1
$ sudo bash -c 'echo core >/proc/sys/kernel/core_pattern'

0x02. AFL++ 基本架构

在 Fuzzing Theory 中有个基本理论:“越多的状态被发现,漏洞被发现的可能性越高” —— 代码覆盖率 (code coverage) 便是现在最常用来表示状态数量的指标,即执行到的代码越多,发现漏洞的可能性越高,因此现在的绝大多数主流 fuzzer 都以获得更高的代码覆盖率作为目标,称为覆盖率指引(coverage-guided)

AFL++ 同样是一个覆盖率引导的 Fuzzer,其 通常(edge,定义为控制流图中由一个基本块到另一基本块的控制流转移)作为代码覆盖率测试粒度,并使用位图(bitmap)存储代码覆盖率,其基本结构如下图所示:

  • afl-fuzz :AFL++ 本体,负责管控一切
  • input 文件夹原始输入语料库,AFL++ 将其中的文件作为初始输入喂给待测目标程序
  • harness待测目标,afl 执行待测目标获得覆盖率信息,再通过覆盖率信息对输入进行编译喂给待测目标,并持续循环该过程;harness 可以是待测目标本体,也可以是我们自行编写的 wrapper(对于测试共享库而言,这会很有用)
  • queue :输入队列,在获取到覆盖率信息后,afl 会将触发了新的状态的输入放到 queue 中,在下次执行时从 queue 中取出新的测试用例并进行变异
  • crashes:存放崩溃信息的文件夹

那么在程序执行过程当中如何获得覆盖率信息呢,答案是进行代码插桩(code instrumentation),即在不改变程序原有逻辑的情况下,通过向程序中插入额外的探针代码,从而获取程序执行信息

例如我们可以在每个基本块执行前插入一个函数调用,从而记录每个基本块的执行情况

代码插桩的方式主要分为两类:

  • 静态插桩:主要针对有目标代码源码的情况,在编译期间进行代码插桩,从而最大程度保留了程序的执行效率
  • 动态插桩:主要针对仅有二进制可执行程序的情况,在运行时动态识别指令并进行替换,这种方式会极大程度损耗程序执行效率

0x03. 使用方式

这里我们以 afl-training 项目中提供给我们的快速进行fuzz测试的样本来说明 afl++ 的基本使用方法

有源码程序:静态插桩

首先我们使用 afl-clang 编译该程序并进行静态代码插桩:

1
2
$ cd quickstart
$ CC=afl-clang-fast AFL_HARDEN=1 make

编译成功之后我们来试一下程序是否能正常运行:

1
$ ./vulnerable < inputs/u

一切无误后,使用如下命令开始进行fuzz测试:

  • -i :指示输入所在文件夹,你需要在该文件夹下放置二进制文件作为初始输入
  • -o :指示 crash 输出的文件夹,当产生 crash 后相应的信息便会被存放到该文件夹中
  • 待测试目标路径应当放在命令尾,其后的其余参数都将被作为该目标执行时的默认命令行参数
1
$ afl-fuzz -i inputs -o out ./vulnerable

当你见到如下界面时,则说明一切已经正常运行

面板基本说明如下:

  • process time:总运行时间、上次发现新路径时间、上次崩溃时间、上次挂起时间
  • overall result :所有输入循环次数、总路径数、独特崩溃数、独特挂起数
  • cycle progress :当前队列循环执行情况
  • map coverage :我们所命中的分支元组对位图可承载的比例(当前输入/整个输入语料库)、元组命中计数
  • stage progress :正在运行的输入 类型 、当前阶段执行进度、总执行数、每秒执行数
  • findings in depth :优权路径数(与最小化算法的路径模糊器相关)、发现的新的边数、总崩溃数量、总超时数
  • fuzzing strategy yields :翻转位(从输入文件移除数量、达成该目标所需执行数、无法删除但被认为无效果的位比例,后同)、翻转字节、一些其他参数
  • path geometry :路径深度(初始输入为 level 1,每次原地生成便多加一级)、等待执行的输入(从未执行)、优权等待执行的输入、该 fuzzer 所找到的路径数、其他 fuzzer 导入的路径数(afl++ 支持多路并行)、可靠性

有关该面板各部分的相关说明详见官方文档

其实很多参数文档里讲得都不明不白的:(

无源码程序:动态插桩

对于没有源码的程序,我们需要使用 afl++QEMU Mode 来在运行时进行动态代码插桩,首先在 AFL++ 源码目录下编译 qemuafl :

如果你想要测试其他架构的程序,则可以在运行 ./build_qemu_support.sh 脚本时通过参数 CPU_TARGET= 来进行指定

1
2
$ cd qemu_mode
$ ./build_qemu_support.sh

命令行参数和在静态插桩程序上进行 fuzz 基本相同,只是额外多了 -Q 参数指示 QEMU 模式,同时可以通过 -m 参数指示最大内存大小(单位 MB,默认 200 ),以及效率会大打折扣:

1
$ afl-fuzz -i inputs/ -o out/ -m 400 -Q ./vulnerable

crash 分析

造成 crash 的输入会被做成一个个文件放在输出目录的 default/crashes 目录下:

直接将任一文件作为输入即可复现 crash:

image.png

我们可以使用 afl-analyze 来分析这些造成 crash 的输入:

1
$ afl-analyze -i 输入文件 目标文件

可以得到造成该 crash 的原始输入,以及其中真正造成 crash 的部分:

需要注意的是,使用 QEMU mode 进行 fuzz 的程序所得到的 crash 似乎无法使用 afl-analyze 进行分析

0x04. AFL-training:harness 编写

这一节我们将学习如何编写一个基础的 harness 作为 wrapper 来进行测试

afl-traininig/harness 目录下提供给我们一个待测库 library.c 以及相应的头文件 library.h ,其中定义了两个待测函数:

1
2
3
4
5
6
#include <unistd.h>
// an 'nprintf' implementation - print the first len bytes of data
void lib_echo(char *data, ssize_t len);

// optimised multiply - returns x*y
int lib_mul(int x, int y);

怎么去测试这两个库函数呢?最简单的想法便是写一个 wrapper 程序 my_harness.c ,以 lib_echo 为例,这里我们接收用户输入作为该函数的输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

#include "library.h"

int main(int argc, char **argv, char **envp)
{
char buf[0x1000];
size_t len;

len = read(0, buf, 0x1000);
lib_echo(buf, len);

return 0;
}

按照常规的想法,我们应该首先把 library.c 编译为动态链接库:

1
2
$ gcc -c -fPIC library.c -o library.o 
$ gcc -shared library.o -o library.so

然后把 my_harness.clibrary.so 一起编译:

1
$ AFL_HARDEN=1 afl-clang-fast my_harness.c library.so  -o my_harness

最后我们要将 library.so 的路径临时添加到当前的环境变量 LD_LIBRARY_PATH 中,否则没办法运行:

1
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/afl-training/harness

完成之后我们可以开始进行测试了,随便写点什么东西放在输入文件夹中,然后启动 AFL++:

1
$ afl-fuzz -i input/ -o out/ ./my_harness

然后你会发现可以 fuzz 但没法有效 fuzz,还提示了 "odd, check syntax!" ,为什么呢?这是因为我们虽然能够通过这种方式来 fuzz 动态链接库,但是我们没办法获取动态链接库中的代码覆盖率,因为覆盖率是通过代码插桩获得的,而我们仅在 my_harness.c 中进行了插桩:

似乎使用 QEMU mode 也无法解决这个问题

所以实际上我们应当将 my_harness.c 与 library.c 一起使用 afl-clang 进行编译 ,从而对待测函数进行插桩,获取覆盖率信息:

1
$ AFL_HARDEN=1 afl-clang-fast my_harness.c library.c -o your_harness

测试运行,警告消失,且很快便获得了 crash:

如果我们想要把两个函数都通过同一个 harness 进行测试,则可以通过命令行参数来决定调用哪一函数:

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
#include <stdio.h>
#include <string.h>

#include "library.h"

int main(int argc, char **argv, char **envp)
{
char buf[0x1000];
size_t len;
int *ibuf;

if (argc < 2) {
puts("[x] No args provided.");
return 0;
}

if (!strcmp(argv[1], "echo")) {
len = read(0, buf, 0x1000);
lib_echo(buf, len);
} else if (!strcmp(argv[1], "mul")) {
ibuf = (int*) buf;
read(0, buf, 8);
lib_mul(ibuf[0], ibuf[1]);
} else {
puts("[x] Invalid args.");
}

return 0;
}

然后正常编译运行便能通过命令行参数指定想要 fuzz 的函数:

1
2
$ AFL_HARDEN=1 afl-clang-fast cmd_narness.c library.c -o cmd_narness
$ afl-fuzz -i input/ -o out/ ./cmd_harness echo

也是瞬间出 crash:

0x05. AFL-training:libxml2(🕊)

libxml2 是一个解析 XML 文件的库,其相关接口可以通过 wiki 进行了解,这次 afl-training 给了我们带有 CVE-2015-8317 的 libxml2 源码,我们需要自行编写 harness 来挖掘出该漏洞

首先从官方仓库下载对应版本的 libxml2:

1
2
3
4
5
$ git clone https://github.com/GNOME/libxml2.git
$ cd libxml2
$ git submodule init
$ git submodule update
$ git checkout v2.9.2

也可以通过 afl-traning 进行拉取:

1
2
3
$ cd afl-training/challenges/libxml2/
$ git submodule init && git submodule update
$ cd libxml2

接下来使用 afl-clang-fast 来插桩编译 libxml2,这里我们会使用 AddressSanitizer 来辅助漏洞寻找:

1
2
$ CC=afl-clang-fast ./autogen.sh 
$ AFL_USE_ASAN=1 make -j$(nproc)

编译失败了,估计是因为源码太老而编译器太新了,晚点再想办法看看(🕊🕊🕊)


【FUZZ.0x00】从零开始的 AFL 学习笔记 - I:基本用法
https://arttnba3.github.io/2021/02/01/FUZZ-0X00-AFL-I/
作者
arttnba3
发布于
2021年2月1日
许可协议