AFL漏洞挖掘技术漫谈(一):用AFL开始你的第一次Fuzzing - FreeBuf网络安全行业门户

AFL漏洞挖掘技术漫谈(二):Fuzz结果分析和代码覆盖率 - FreeBuf网络安全行业门户

google/AFL: american fuzzy lop - a security-oriented fuzzer (github.com)

fuzz实战之afl - SecPulse.COM | 安全脉搏

AFL二三事——源码分析(上篇) - 先知社区 (aliyun.com)

AFL二三事——源码分析(下篇) - 先知社区 (aliyun.com)

AFL++学习日志(一)开始Fuzz与crashes分析 - Hanyin’s Space (mundi-xu.github.io)

我的AFL入门之路 - 知乎 (zhihu.com)

什么是Fuzz

“Fuzz” 即模糊测试,通常用于描述在计算机编程和软件测试中的一种技术或方法。Fuzzing 是一种自动化的软件测试技术,Fuzzing 的基本原理是通过输入大量的随机或半随机数据来测试程序,观察程序如何处理这些数据。通过观察程序对不同输入的反应,可以发现潜在的漏洞和错误。

可以先看一下(https://pan.baidu.com/s/1UdLaijUA9AH7GpHcRKhDGQ?pwd=ccc6)

这篇论文对fuzz做了一个详细的介绍,从定义与分类到算法与实现,读完以后应该就对fuzz有一个基础的认识了

并且指出一个完整的fuzzer应该包含以下几个部分

  1. Preprocess
  2. Schduling
  3. Input Generation
  4. Input Evaluation
  5. Configuration Updating

现在已经有十分多的成熟Fuzz技术,例如AFLFuzz,libfuzzer,boofuzz等等等等

AFLFuzz

这里以较为经典的AFLFuzz为例进行讲解

american fuzzy lop(AFL)是由安全研究员Michał Zalewski(@lcamtuf)开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具,它通过记录输入样本的代码覆盖率,从而调整输入样本以提高覆盖率,增加发现漏洞的概率。其工作流程大致如下:

①从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage);

②选择一些输入文件,作为初始测试集加入输入队列(queue);

③将队列中的文件按一定的策略进行“突变”;

④如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;

⑤上述过程会一直循环进行,期间触发了crash的文件会被记录下来。

AFL既可以对源码进行编译时插桩,也可以使用AFL的QEMU mode对二进制文件进行插桩,但是前者的效率相对来说要高很多,在Github上很容易就能找到很多合适的项目。

AFL主要用于C/C++程序的测试,所以这是我们寻找软件的最优先规则。

AFL变异策略

  • 确定性变异

    • 比特翻转(bitflip):按位翻转,1变为0,0变为1.这一阶段还会按照不同的长度和步长进行多种不同的翻转,每次翻转1/2/4/8/16/32 bit,依次进行。
    • 算术运算(arithmetic):整数加/减算术运算。跟bitflip类似,arithmetic根据目标大小的不同,也分为了多个子阶段,依次对8/16/32 bit进行加减运算。
    • 特殊值替换(interest):把一些特殊内容替换到原文件中。同样每次对8/16/32 bit进行替换。所谓的特殊内容是AFL预设的一些比较特殊的数,比如可能造成溢出的数。
    • 字典值(dictionary):把自动生成或用户提供的字典值替换或插入到原测试用例中。
  • 随机变异

    • havoc大破坏:对文件进行大量破坏,此阶段会对原文件进行大量随机变异。包括随机翻转、加减、替换和删除等操作。
    • 文件拼接splice:此阶段会将两个文件拼接起来得到一个新的文件,并对这个新文件继续执行havoc变异。

如何使用AFL

安装

白盒模式

1
2
git clone https://github.com/google/AFL.git
make &&sudo make install

黑盒模式

1
cd qemu-mode

修改build_qemu_support.sh两处

1
2
3
4
5
QEMU_URL="https://download.qemu.org/qemu-${VERSION}.tar.xz"
...
CFLAGS="-O3 -ggdb" ./configure --disable-system \
--enable-linux-user --disable-gtk --disable-sdl --disable-vnc --python=/usr/bin/python2.7 \
--target-list="${CPU_TARGET}-linux-user" --enable-pie --enable-kvm || exit 1

修改./patchs/syscall.diff为

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
--- qemu-2.10.0-clean/linux-user/syscall.c	2020-03-12 18:47:47.898592169 +0100
+++ qemu-2.10.0/linux-user/syscall.c 2020-03-12 19:16:41.563074307 +0100
@@ -34,6 +34,7 @@
#include <sys/resource.h>
#include <sys/swap.h>
#include <linux/capability.h>
+#include <linux/sockios.h> // https://lkml.org/lkml/2019/6/3/988
#include <sched.h>
#include <sys/timex.h>
#ifdef __ia64__
@@ -116,6 +117,8 @@ int __clone2(int (*fn)(void *), void *ch
#include "qemu.h"

+extern unsigned int afl_forksrv_pid;
+
#ifndef CLONE_IO
#define CLONE_IO 0x80000000 /* Clone io context */
#endif

@@ -256,7 +259,9 @@ static type name (type1 arg1,type2 arg2,
#endif

#ifdef __NR_gettid
-_syscall0(int, gettid)
+// taken from https://patchwork.kernel.org/patch/10862231/
+#define __NR_sys_gettid __NR_gettid
+_syscall0(int, sys_gettid)
#else
/* This is a replacement for the host gettid() and must return a host
errno. */
@@ -6219,7 +6224,8 @@ static void *clone_func(void *arg)
cpu = ENV_GET_CPU(env);
thread_cpu = cpu;
ts = (TaskState *)cpu->opaque;
- info->tid = gettid();
+ // taken from https://patchwork.kernel.org/patch/10862231/
+ info->tid = sys_gettid();
task_settid(ts);
if (info->child_tidptr)
put_user_u32(info->tid, info->child_tidptr);
@@ -6363,9 +6369,11 @@ static int do_fork(CPUArchState *env, un
mapping. We can't repeat the spinlock hack used above because
the child process gets its own copy of the lock. */
if (flags & CLONE_CHILD_SETTID)
- put_user_u32(gettid(), child_tidptr);
+ // taken from https://patchwork.kernel.org/patch/10862231/
+ put_user_u32(sys_gettid(), child_tidptr);
if (flags & CLONE_PARENT_SETTID)
- put_user_u32(gettid(), parent_tidptr);
+ // taken from https://patchwork.kernel.org/patch/10862231/
+ put_user_u32(sys_gettid(), parent_tidptr);
ts = (TaskState *)cpu->opaque;
if (flags & CLONE_SETTLS)
cpu_set_tls (env, newtls);
@@ -11402,7 +11410,8 @@ abi_long do_syscall(void *cpu_env, int n
break;
#endif
case TARGET_NR_gettid:
- ret = get_errno(gettid());
+ // taken from https://patchwork.kernel.org/patch/10862231/
+ ret = get_errno(sys_gettid());
break;
#ifdef TARGET_NR_readahead
case TARGET_NR_readahead:

执行./build_qemu_support.sh

如果要安装其他架构,需要在执行构建之前声明变量

export CPU_TARGET=arm

构建语料库

AFL需要一些初始输入数据(也叫种子文件)作为Fuzzing的起点,这些输入甚至可以是毫无意义的数据,AFL可以通过启发式算法自动确定文件格式结构

如果要获得更快的Fuzzing速度,那么就有必要生成一个高质量的语料库,这一节就解决如何选择输入文件、从哪里寻找这些文件、如何精简找到的文件三个问题。

选择

(1) 有效的输入

尽管有时候无效输入会产生bug和崩溃,但有效输入可以更快的找到更多执行路径。

(2) 尽量小的体积

较小的文件会不仅可以减少测试和处理的时间,也能节约更多的内存,AFL给出的建议是最好小于1 KB,但其实可以根据自己测试的程序权衡,这在AFL文档的perf_tips.txt中有具体说明。

寻找

  1. 使用项目自身提供的测试用例
  2. 目标程序bug提交页面
  3. 使用格式转换器,用从现有的文件格式生成一些不容易找到的文件格式:
  4. afl源码的testcases目录下提供了一些测试用例
  5. 其他开源的语料库
  6. afl generated image test sets
  7. fuzzer-test-suite
  8. libav samples
  9. ffmpeg samples
  10. fuzzdata
  11. moonshine

修剪

网上找到的一些大型语料库中往往包含大量的文件,这时就需要对其精简,这个工作有个术语叫做——语料库蒸馏(Corpus Distillation)。AFL提供了两个工具来帮助我们完成这部工作——afl-cminafl-tmin

移除执行相同代码的输入文件——afl-cmin

afl-cmin的核心思想是:尝试找到与语料库全集具有相同覆盖范围的最小子集。举个例子:假设有多个文件,都覆盖了相同的代码,那么就丢掉多余的文件。其使用方法如下:

1
$ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params]

更多的时候,我们需要从文件中获取输入,这时可以使用“@@”代替被测试程序命令行中输入文件名的位置。Fuzzer会将其替换为实际执行的文件:

1
$ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] @@

减小单个输入文件的大小——afl-tmin

整体的大小得到了改善,接下来还要对每个文件进行更细化的处理

afl-tmin有两种工作模式,instrumented modecrash mode。默认的工作方式是instrumented mode,如下所示:

1
$ afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@

如果指定了参数-x,即crash mode,会把导致程序非正常退出的文件直接剔除。

1
$ afl-tmin -x -i input_file -o output_file -- /path/to/tested/program [params] @@

afl-tmin接受单个文件输入,所以可以用一条简单的shell脚本批量处理。如果语料库中文件数量特别多,且体积特别大的情况下,这个过程可能花费几天甚至更长的时间!

1
for i in *; do afl-tmin -i $i -o tmin-$i -- ~/path/to/tested/program [params] @@; done;

构建被测试程序

AFL从源码编译程序时进行插桩,以记录代码覆盖率。这个工作需要使用其提供的两种编译器的wrapper编译目标程序

afl-gcc模式

afl-gcc/afl-g++作为gcc/g++的wrapper,它们的用法完全一样,前者会将接收到的参数传递给后者,我们编译程序时只需要将编译器设置为afl-gcc/afl-g++就行,如下面演示的那样。如果程序不是用autoconf构建,直接修改Makefile文件中的编译器为afl-gcc/g++也行。

1
$ ./configure CC="afl-gcc" CXX="afl-g++"

在Fuzzing共享库时,可能需要编写一个简单demo,将输入传递给要Fuzzing的库(其实大多数项目中都自带了类似的demo)。这种情况下,可以通过设置LD_LIBRARY_PATH让程序加载经过AFL插桩的.so文件,不过最简单的方法是静态构建,通过以下方式实现:

1
$ ./configure --disable-shared CC="afl-gcc" CXX="afl-g++"

LLVM模式

LLVM Mode模式编译程序可以获得更快的Fuzzing速度,进入llvm_mode目录进行编译,之后使用afl-clang-fast构建序程序即可,如下所示:

1
2
3
$ cd llvm_mode
$ apt-get install clang
$ export LLVM_CONFIG=`which llvm-config` && make && cd ..$ ./configure --disable-shared CC="afl-clang-fast" CXX="afl-clang-fast++"

开始Fuzz测试插桩程序

白盒测试

编译好程序后,可以选择使用afl-showmap跟踪单个输入的执行路径,并打印程序执行的输出、捕获的元组(tuples),tuple用于获取分支信息,从而衡量衡量程序覆盖情况,下一篇文章中会详细的解释,这里可以先不用管。

1
2
3
$ afl-showmap -m none -o /dev/null -- ./build/bin/imagew 23.bmp out.png

[*] Executing './build/bin/imagew'...-- Program output begins --23.bmp -> out.pngProcessing: 13x32-- Program output ends --[+] Captured 1012 tuples in '/dev/null'.

使用不同的输入,正常情况下afl-showmap会捕获到不同的tuples,这就说明我们的的插桩是有效的,还有前面提到的afl-cmin就是通过这个工具来去掉重复的输入文件。

1
2
3
$ $ afl-showmap -m none -o /dev/null -- ./build/bin/imagew 111.pgm out.png

[*] Executing './build/bin/imagew'...-- Program output begins --111.pgm -> out.pngProcessing: 7x7-- Program output ends --[+] Captured 970 tuples in '/dev/null'.

执行fuzzer

在执行afl-fuzz前,如果系统配置为将核心转储文件(core)通知发送到外部程序。 将导致将崩溃信息发送到Fuzzer之间的延迟增大,进而可能将崩溃被误报为超时,所以我们得临时修改core_pattern文件,如下所示:

1
echo core >/proc/sys/kernel/core_pattern

之后就可以执行afl-fuzz了,通常的格式是:

1
$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program [params]

或者使用“@@”替换输入文件,Fuzzer会将其替换为实际执行的文件:

1
$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

如果没有什么错误,Fuzzer就正式开始工作了。首先,对输入队列中的文件进行预处理;然后给出对使用的语料库可警告信息,且输入文件过多;最后,开始Fuzz主循环,显示状态窗口。

使用screen

一次Fuzzing过程通常会持续很长时间,如果这期间运行afl-fuzz实例的终端终端被意外关闭了,那么Fuzzing也会被中断。而通过在screen session中启动每个实例,可以方便的连接和断开。关于screen的用法这里就不再多讲,大家可以自行查询。

1
$ screen afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

也可以为每个session命名,方便重新连接。

1
2
$ screen -S fuzzer1$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program [params] @@[detached from 6999.fuzzer1]
$ screen -r fuzzer1 ...

黑盒测试

所谓黑盒测试,通俗地讲就是对没有源代码的程序进行测试,这时就要用到AFL的QEMU模式了。启用方式和LLVM模式类似,也要先编译。但注意,因为AFL使用的QEMU版本太旧,util/memfd.c中定义的函数memfd_create()会和glibc中的同名函数冲突,在这里可以找到针对QEMU的patch,之后运行脚本build_qemu_support.sh就可以自动下载编译。

1
$ apt-get install libini-config-dev libtool-bin automake bison libglib2.0-dev -y$ cd qemu_mode$ build_qemu_support.sh$ cd .. && make install

现在起,只需添加-Q选项即可使用QEMU模式进行Fuzzing。

1
$ afl-fuzz -Q -i testcase_dir -o findings_dir /path/to/program [params] @@

并行测试

单系统并行测试

如果你有一台多核心的机器,可以将一个afl-fuzz实例绑定到一个对应的核心上,也就是说,机器上有几个核心就可以运行多少afl-fuzz 实例:

1
$ cat /proc/cpuinfo| grep "cpu cores"| uniq

afl-fuzz并行Fuzzing,一般的做法是通过-M参数指定一个主Fuzzer(Master Fuzzer)、通过-S参数指定多个从Fuzzer(Slave Fuzzer)。

1
2
3
4
$ screen afl-fuzz -i testcases/ -o sync_dir/ -M fuzzer1 -- ./program
$ screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer2 -- ./program
$ screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer3 -- ./program
...

这两种类型的Fuzzer执行不同的Fuzzing策略,前者进行确定性测试(deterministic ),即对输入文件进行一些特殊而非随机的的变异;后者进行完全随机的变异。

可以看到这里的-o指定的是一个同步目录,并行测试中,所有的Fuzzer将相互协作,在找到新的代码路径时,相互传递新的测试用例,如下图中以Fuzzer0的角度来看,它查看其它fuzzer的语料库,并通过比较id来同步感兴趣的测试用例。

afl-whatsup工具可以查看每个fuzzer的运行状态和总体运行概况,加上-s选项只显示概况,其中的数据都是所有fuzzer的总和。

afl-gotcpu工具可以查看每个核心使用状态。

多系统并行测试

多系统并行的基本工作原理类似于单系统并行中描述的机制,你需要一个简单的脚本来完成两件事。在本地系统上,压缩每个fuzzer实例目录中queue下的文件,通过SSH分发到其他机器上解压。

来看一个例子,假设现在有两台机器,基本信息如下:

fuzzer1 fuzzerr2
172.21.5.101 172.21.5.102
运行2个实例 运行4个实例

为了能够自动同步数据,需要使用authorized_keys的方式进行身份验证。现要将fuzzer2中每个实例的输入队列同步到fuzzer1中,可以下面的方式:

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
#!/bin/sh
# 所有要同步的主机
FUZZ_HOSTS='172.21.5.101 172.21.5.102'
# SSH 用户
FUZZ_USER=root
# 同步目录
SYNC_DIR='/root/syncdir'
# 同步间隔时间
SYNC_INTERVAL=$((30 * 60))

if [ "$AFL_ALLOW_TMP" = "" ]; then
if [ "$PWD" = "/tmp" -o "$PWD" = "/var/tmp" ]; then
echo "[-] Error: do not use shared /tmp or /var/tmp directories with this script." 1>&2
exit 1
fi
fi

rm -rf .sync_tmp 2>/dev/null
mkdir .sync_tmp || exit 1

while :; do
# 打包所有机器上的数据
for host in $FUZZ_HOSTS; do
echo "[*] Retrieving data from ${host}..."
ssh -o 'passwordauthentication no' ${FUZZ_USER}@${host} \
"cd '$SYNC_DIR' && tar -czf - SESSION*" > ".sync_tmp/${host}.tgz"
done
# 分发数据
for dst_host in $FUZZ_HOSTS; do
echo "[*] Distributing data to ${dst_host}..."
for src_host in $FUZZ_HOSTS; do
test "$src_host" = "$dst_host" && continue
echo " Sending fuzzer data from ${src_host}..."
ssh -o 'passwordauthentication no' ${FUZZ_USER}@$dst_host \
"cd '$SYNC_DIR' && tar -xkzf - &>/dev/null" < ".sync_tmp/${src_host}.tgz"
done
done
echo "[+] Done. Sleeping for $SYNC_INTERVAL seconds (Ctrl-C to quit)."
sleep $SYNC_INTERVAL
done

成功执行上述shell脚本后,不仅SESSION000 SESSION002中的内容更新了,还将SESSION003 SESSION004也同步了过来。

状态窗口

① Process timing:Fuzzer运行时长、以及距离最近发现的路径、崩溃和挂起经过了多长时间。

② Overall results:Fuzzer当前状态的概述。其中,总周期数可以用来作为何时停止fuzzing的参考。随着不断地fuzzing,周期数会不断增大,其颜色也会由洋红色,逐步变为黄色、蓝色、绿色,当其变为绿色时代表可执行的内容已经很少了此时便可以通过Ctrl-C,中止当前的fuzzing

③ Cycle progress:我们输入队列的距离。

④ Map coverage:目标二进制文件中的插桩代码所观察到覆盖范围的细节。

⑤ Stage progress:Fuzzer现在正在执行的文件变异策略、执行次数和执行速度。执行速度可以直观地反映当前跑的快不快,如果速度过慢,比如低于500次每秒,那么测试时间会变得非常漫长。如果发生了这种情况,我们需要进一步优化我们的Fuzzing

⑥ Findings in depth:有关我们找到的执行路径,异常和挂起数量的信息。

⑦ Fuzzing strategy yields:关于突变策略产生的最新行为和结果的详细信息。

⑧ Path geometry:有关Fuzzer找到的执行路径的信息。

⑨ CPU load:CPU利用率
更多可以参考官方文档status_screen (coredump.cx)

crashes处理

AFL++学习日志(一)开始Fuzz与crashes分析 - Hanyin’s Space (mundi-xu.github.io)

crash exploration mode

这是afl-fuzz的一种运行模式,也称为peruvian rabbit mode,用于确定bug的可利用性

1
afl-fuzz -C -i out/default/crashes/ -o crash_exploration/ ./vulnerable

将一个导致crash测试用例作为afl-fuzz的输入,使用-C选项开启crash exploration模式后,可以快速地产生很多和输入crash相关、但稍有些不同的crashes,从而判断能够控制某块内存地址的长度

可以参考这篇博客peruvian were-rabbit | Count Upon Security

triage_crashes

AFL源码的experimental目录中有一个名为triage_crashes.sh的脚本,可以帮助我们触发收集到的crashes

例如

1
2
3
4
5
6
7
8
9
$ ~/afl-2.52b/experimental/crash_triage/triage_crashes.sh fuzz_out ~/src/LuPng/a.out @@ out.png 2>&1 | grep SIGNAL
+++ ID 000000, SIGNAL 11 +++
+++ ID 000001, SIGNAL 06 +++
+++ ID 000002, SIGNAL 06 +++
+++ ID 000003, SIGNAL 06 +++
+++ ID 000004, SIGNAL 11 +++
+++ ID 000005, SIGNAL 11 +++
+++ ID 000006, SIGNAL 11 +++
...

crashwalk

这个工具基于gdb的exploitable插件,安装也相对简单,在ubuntu上,只需要如下几步即可:

1
2
3
4
5
6
7
8
$ apt-get install gdb golang
$ mkdir tools
$ cd tools
$ git clone https://github.com/jfoote/exploitable.git
$ mkdir go
$ export GOPATH=~/tools/go
$ export CW_EXPLOITABLE=~/tools/exploitable/exploitable/exploitable.py
$ go get -u github.com/bnagy/crashwalk/cmd/...

crashwalk支持AFL/Manual两种模式。前者通过读取crashes/README.txt文件获得目标的执行命令(前面第三节中提到的),后者则可以手动指定一些参数。两种使用方式如下:

1
2
3
4
#Manual Mode
$ ~/tools/go/bin/cwtriage -root syncdir/fuzzer1/crashes/ -match id -- ~/parse @@
#AFL Mode
$ ~/tools/go/bin/cwtriage -root syncdir -afl

afl-collect

它也是afl-utils套件中的一个工具,同样也是基于exploitable来检查crashes的可利用性。它可以自动删除无效的crash样本、删除重复样本以及自动化样本分类。使用起来命令稍微长一点,如下所示:

1
$ afl-collect -j 8 -d crashes.db -e gdb_script ./afl_sync_dir ./collection_dir --  /path/to/target --target-opts

实例demo

我们以这个简单的c语言程序为demo尝试一下AFLFuzz的使用

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
#include <stdio.h> 
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
int vuln(char *str)
{
int len = strlen(str);
if(str[0] == 'A' && len == 66)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为A并且长度为66,则异常退出
}
else if(str[0] == 'F' && len == 6)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为F并且长度为6,则异常退出
}
else
{
printf("it is good!\n");
}
return 0;
}
int main(int argc, char *argv[])
{
char buf[100]={0};
gets(buf);//存在栈溢出漏洞
printf(buf);//存在格式化字符串漏洞
vuln(buf);
return 0;
}

白盒

使用afl-gcc -g -o demo ./demo.c编译

创建两个文件夹fuzz_infuzz_out,然后在fuzz_in随便放个文本文件,写入任意字符串,这里写入的是hello

之后启动afl-fuzz -i fuzz_in -o fuzz_out demo

在跑了十分钟左右之后结果如下

发现了3条路径6个crash,进入fuzz_out,文件作用大致如下

  1. queue:存放所有具有独特执行路径的测试用例。
  2. crashes:导致目标接收致命signal而崩溃的独特测试用例。
  3. crashes/README.txt:保存了目标执行这些crash文件的命令行参数。
  4. hangs:导致目标超时的独特测试用例。
  5. fuzzer_stats:afl-fuzz的运行状态。
  6. plot_data:用于afl-plot绘图。

分析一下crash样本

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
find ./fuzz_out/crashes/ -type f -name 'id*' -exec sh -c 'for file do echo "Hex dump of $file:"; xxd "$file"; echo; done' sh {} +
Hex dump of ./fuzz_out/crashes/id:000001,sig:11,src:000001,op:arith8,pos:0,val:-34:
00000000: 4665 d374 6c6c Fe.tll

Hex dump of ./fuzz_out/crashes/id:000002,sig:06,src:000000,op:havoc,rep:128:
00000000: 1f1f 1f1f ece8 0000 0000 18e6 0400 000d ................
00000010: 001f 00fa 0000 18e6 0000 0000 211f 5dff ............!.].
00000020: 2f01 03ff fff5 0000 0000 0000 0000 fa00 /...............
00000030: 0018 e600 0000 0021 1f5d ffff 0000 1f1f .......!.]......
00000040: 00ff ffff 8000 3813 0000 0000 0000 18e6 ......8.........
00000050: 0400 000d 001f 00fa 0000 18e6 0000 0000 ................
00000060: 211f 5dff ff00 001f 1f00 ffff ff80 0038 !.]............8
00000070: 1300 0000 0000 0000 0001 0038 0000 fdff ...........8....
00000080: ff21 0018 ffff 7fff 005d 1f1f 1f00 245f .!.......]....$_

Hex dump of ./fuzz_out/crashes/id:000004,sig:06,src:000000,op:havoc,rep:2:
00000000: 6825 6e6c h%nl

Hex dump of ./fuzz_out/crashes/id:000005,sig:11,src:000002,op:havoc,rep:4:
00000000: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA
00000010: 4141 4141 4141 4141 4141 4c41 4141 4141 AAAAAAAAAALAAAAA
00000020: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA
00000030: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA
00000040: 4141 AA

Hex dump of ./fuzz_out/crashes/id:000003,sig:06,src:000001+000000,op:splice,rep:128:
00000000: 4141 5141 5741 3755 2841 4141 51ff 7f41 AAQAWA7U(AAAQ..A
00000010: 454f 6041 41a3 335c 5c5c 5c41 3741 4141 EO`AA.3\\\\A7AAA
00000020: 4141 4141 4151 ff7f 4145 4f60 8a41 5141 AAAAAQ..AEO`.AQA
00000030: 5741 3755 4141 5141 5741 3755 4141 5141 WA7UAAQAWA7UAAQA
00000040: 5741 3755 4141 4141 51ff 7f41 454f 608a WA7UAAAAQ..AEO`.
00000050: 41a3 335c 5c41 4157 4137 5541 4141 4151 A.3\\AAWA7UAAAAQ
00000060: ff7f 4145 4f60 4141 a341 4110 4141 51ff ..AEO`AA.AA.AAQ.
00000070: 7f41 454f 6041 41a3 4141 1000 003a 4100 .AEO`AA.AA...:A.
00000080: 0140 0000 0100 9f20 20 .@.....

Hex dump of ./fuzz_out/crashes/id:000000,sig:06,src:000000,op:havoc,rep:64:
00000000: c3b7 c3c3 c3c3 c3c3 c3c3 c3c3 c3af c3c3 ................
00000010: c320 c3c3 c3c3 c3c3 c3c3 c3c3 c3c3 c3c3 . ..............
00000020: c3c3 c3c3 0000 00ff c3c3 c3c3 c3c3 c3c3 ................
00000030: c3c3 c3c3 c3c3 c3c3 c3c3 e1c3 afad c3c3 ................
00000040: 79ff c3c3 c3c3 c3c3 c340 c3c3 c3c3 c3c3 y........@......
00000050: c3c3 c3c3 c3c3 c3c3 c3c3 c3c3 c3c3 c3c3 ................
00000060: c3c3 0000 00ff c3c3 c3c3 c3c3 c3c3 c3c3 ................
00000070: c3c3 c3c3 c3c3 c3c3 c3c3 c3c3 c3c3 c3c3 ................
00000080: c3c3 c3c3 c3c3 c3c3 c3c3 c3c3 cfc3 c3c3 ................
00000090: c3c3 c3c3 c3c3 c3c3 c3c3 b100 0003 e8c3 ................
000000a0: 43e2

可以看到所有漏洞都被Fuzz到了

黑盒

现在我们改为使用gcc -g -o demo_gcc demo.c进行编译

进行无源码fuzz,命令与之前差不多,只不过多了个-Q选项,记得先export AFL_PATH=~/fuzz/AFL-2.57b/不然找不到qemu

afl-fuzz -i fuzz_in -o fuzz_out2 -Q ./demo_gcc

在一分钟多时就已经发现了5个crash,不过同样到10分钟左右才完整的跑出六个

可以看到虽然时间差异不大,但通过对比执行速度,可以看出黑盒模式下速度只有白盒的三分之一左右

Fuzz网络程序

网络程序往往是从一个socket读取输入,那么如何进行fuzz,其实也可以将其转化为从标准输入读取

AFL文档阅读

基本概念

代码覆盖率(Code Coverage)

代码覆盖率是模糊测试中一个极其重要的概念,使用代码覆盖率可以评估和改进测试过程,执行到的代码越多,找到bug的可能性就越大,毕竟,在覆盖的代码中并不能100%发现bug,在未覆盖的代码中却是100%找不到任何bug的

代码覆盖率是一种度量代码的覆盖程度的方式,也就是指源代码中的某行代码是否已执行;对二进制程序,还可将此概念理解为汇编代码中的某条指令是否已执行。其计量方式很多,但无论是GCC的GCOV还是LLVM的SanitizerCoverage,都提供函数(function)、基本块(basic-block)、边界(edge)三种级别的覆盖率检测,更具体的细节可以参考LLVM的官方文档

基本块(Basic Block)

这个在之前的angr使用学习中已经有一些了解了

缩写为BB,指一组顺序执行的指令,BB中第一条指令被执行后,后续的指令也会被全部执行,每个BB中所有指令的执行次数是相同的,也就是说一个BB必须满足以下特征:

  • 只有一个入口点,BB中的指令不是任何跳转指令的目标。
  • 只有一个退出点,只有最后一条指令使执行流程转移到另一个BB

边(edge)

fuzzer通过插桩代码捕获边(edge)覆盖率

我们可以将程序看成一个控制流图(CFG),图的每个节点表示一个基本块,而edge就被用来表示在基本块之间的转跳。知道了每个基本块和跳转的执行次数,就可以知道程序中的每个语句和分支的执行次数,从而获得比记录BB更细粒度的覆盖率信息。

元组(tuple)

AFL的实现中,使用二元组(branch_src, branch_dst)来记录当前基本块 + 前一基本块 的信息,从而获取目标的执行流程和代码覆盖情况,伪代码如下:

1
2
3
cur_location = <COMPILE_TIME_RANDOM>;//用一个随机数标记当前基本块
shared_mem[cur_location ^ prev_location]++;//将当前块和前一块异或保存到shared_mem[]
prev_location = cur_location >> 1;//cur_location右移1位区分从当前块到当前块的转跳

实际插入的汇编代码,如下图所示,首先保存各种寄存器的值并设置ecx/rcx,然后调用__afl_maybe_log,这个方法的内容相当复杂,这里就不展开讲了,但其主要功能就和上面的伪代码相似,用于记录覆盖率,放入一块共享内存中。

AFL源码导读

AFLplusplus

The fuzzer afl++ is afl with community patches, qemu 5.1 upgrade, collision-free coverage, enhanced laf-intel & redqueen, AFLfast++ power schedules, MOpt mutators, unicorn_mode, and a lot more!

相比原版AFL++整合完善了许多功能,例如unicorn模式,frida模式等等

安装方法可见Building | AFLplusplus

默认全部安装可如下操作

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

注意安装过程中会覆盖普通afl版本的二进制文件

Fuzz一个pdf解析器

进阶使用