前言
本博客的主要内容为TriforceAFL的部署、使用与原理分析。本博文内容较长,因为涵盖了TriforceAFL的几乎全部内容,从部署的详细过程到如何使用TriforceAFL对操作系统的系统调用进行Fuzz测试,以及对TriforceAFL进行漏洞检测的原理分析,相信认真读完本博文,各位读者一定会对TriforceAFL有更深的了解。以下就是本篇博客的全部内容了。
1、概述
TriforceAFL是AFL的补丁版本,支持使用QEMU进行全系统Fuzz测试。TriforceAFL所包含的QEMU已经更新,以便在运行x86_64的系统仿真器时能够跟踪分支。添加了额外的指令来启动AFL的fork server,进行Fuzz设置,并标记测试用例的开始和停止。TriforceAFL相对于AFL具体的修改包括:
- 增加了默认内存限制
- 增加了AFL等待fork server的时间
- 将所有非零退出状态视为崩溃
- 实用程序支持fork server特性
但是TriforceAFL还不能直接对Linux内核进行Fuzz,因为TriforceAFL只是对AFL和QEMU进行了patch而已,为了可以使用TriforceAFL对Linux内核进行Fuzz,作者又提出了TriforceLinuxSyscallFuzzer,此工具利用TriforceAFL和一个待Fuzz的Linux内核进行动态分析。本文的目的就是介绍如何对TriforceAFL和TriforceLinuxSyscallFuzzer进行部署,以便对Linux内核进行Fuzz。此外,TriforceAFL工具和TriforceLinuxSyscallFuzzer工具都是基于Python语言和C语言开发。
1.1、工作原理
AFL系列的Fuzz工具的核心都是利用QEMU去进行硬件模拟,具体而言,QEMU可以:
- 将一个架构(被模拟的架构)的BB(Basic Blocks,基本块)翻译到另一个架构(QEMU正在之上运行的架构)
- 将TB(translated blocks,翻译块)存储在TBC(Translated Block Cache,翻译块缓存)中,一次翻译并多次使用
- 在基本块中添加prologue和epilogue,以处理基本块之间的跳转以及恢复控制等等操作
以上QEMU功能可用下图来表示:
而在AFL系列的Fuzz工具中,关于使用QEMU进行Fuzz测试的具体执行流程如下图所示:
- 启动预生成的代码prologue,初始化进程并跳转到二进制文件的_start
- 查找缓存中包含_start PC(program counter,程序计数器)的已翻译块,如果没有生成翻译并缓存它
- 跳转到已翻译的块并执行它
此外,在AFL系列的Fuzz工具中,都是利用fork server模型来Fuzz程序,运行目标的QEMU实列将用作一个fork server,它将通过fds 198(控制队列)和199(状态队列)与Fuzzer进行通信,这个fork server实例的克隆用于运行测试用例。目标的执行跟踪可以通过共享内存(shm)到达Fuzzer进程。
通常当使用AFL Fuzz时,每个测试用例都会启动一个驱动程序并运行直到完成或崩溃。当对操作系统进行Fuzz时,这并不总是可能的。为了解决该问题,TriforceAFL允许操作系统启动并加载一个驱动程序,该驱动程序控制Fuzz的生命周期以及托管测试用例。
1.2、工作流程
使用TriforceAFL对内核进行Fuzz的工作流程总结如下图所示,可以发现,整个过程还是比较复杂的,我们要从环境初始化开始将TriforceAFL工具的整个工作流程进行详细分析。不过对于非核心环节我们并不赘述,只关注TriforceAFL工具的核心工作流程:
1.2.1、编译TriforceAFL
当我们下载好TriforceAFL的源代码后,会在其源代码目录中执行make
命令,该命令会默认执行“Makefile”文件中的all
目标,而all
目标实现在“/TriforceAFL/Makefile”的第47行。
在这里,all
目标依赖于其它多个目标,这些目标会被按顺序执行。具体来说,all
目标依赖于以下目标:
test_x86
:用于检查是否能够编译x86
代码的目标。$(PROGS)
:一系列程序的编译目标,包括afl-gcc、afl-fuzz等。afl-as
:用于编译afl-as
程序的目标。test_build
:用于测试编译的目标,检查CC包装器和插桩输出是否正常。all_done
:表示所有操作完成的标记。afl-qemu-system-trace
和afl-qemu-trace
:构建QEMU支持的目标。
因此,执行make
命令时,会按照上述顺序依次执行这些目标。那么下面我们就将逐一分析构建这些目标时都做了什么。
- 构建
test_x86
目标
关于test_x86
目标的构建代码如下所示。其实现在“/TriforceAFL/Makefile”的第51行和第60行。
这段代码主要用于检查是否能够编译x86代码。它使用了一个条件编译语句ifndef AFL_NO_X86
,如果AFL_NO_X86
宏没有定义,则执行test_x86
目标;否则,输出一个提示信息,跳过检查。具体来说:
ifndef AFL_NO_X86
:检查AFL_NO_X86
宏是否未定义。如果未定义,则执行后续操作。test_x86
目标:用于检查能否编译x86代码。它执行以下操作:- 输出一条提示消息,表示正在检查是否能够编译x86代码。
- 使用
echo
命令生成一个包含x86汇编代码的C程序,并通过$(CC)编译器进行编译。如果编译成功,则说明编译器能够生成x86代码;否则,输出错误消息,并终止编译过程。 - 删除临时生成的“.test”文件。
- 输出一条成功消息,表示一切正常,准备好进行编译。
- 如果
AFL_NO_X86
宏已定义,则执行else
分支,输出一条提示信息,表示跳过x86编译检查。
这样的设计允许用户通过设置AFL_NO_X86=1
环境变量,从而跳过x86的编译检查。
-
构建
$(PROGS)
目标
$(PROGS)
目标定义在“/TriforceAFL/Makefile”的第27行。该目标最终的目的是构建出afl-gcc、afl-fuzz、afl-showmap、afl-tmin、afl-gotcpu和afl-analyze这几个二进制文件。
我们暂时先不对在该阶段构建出的二进制文件进行分析,后续用到的时候再对其进行分析。 -
构建
afl-as
目标
该目标实现在“/TriforceAFL/Makefile”的第68行,其最终会构建出afl-as这个二进制文件。
我们暂时先不对在该阶段构建出的二进制文件进行分析,后续用到的时候再对其进行分析。
-
构建
test_build
目标
该目标实现在“/TriforceAFL/Makefile”的第89行,其目的是用于测试TriforceAFL中的编译器包装器(CC wrapper)以及代码插桩(instrumentation)的正确性。
-
构建
all_done
目标
该目标实现在“/TriforceAFL/Makefile”的第105行,其目的是在构建完成后向用户输出一些提示消息。
-
构建
afl-qemu-system-trace
目标
该目标实现在“/TriforceAFL/Makefile”的第110行,其目的是构建QEMU模式支持所需的组件或配置。
具体来说,该部分代码调用了“/TriforceAFL/qemu_mode/build_qemu_support.sh”脚本。以下是该脚本的具体内容。
这段脚本用于构建带有Triforce修补程序的QEMU,以支持AFL(American Fuzzy Lop)Fuzz测试工具。以下是其具体逻辑:
cd qemu
: 进入名为“qemu”的目录,这是QEMU源代码所在的目录。CFLAGS="-O3" ./configure ...
: 这是运行QEMU的配置脚本,设置了一些选项:--disable-werror
: 禁用所有编译警告。--enable-system --enable-linux-user
: 启用系统模式和Linux用户模式。--enable-guest-base
: 启用客户机(虚拟机)基础设施。--disable-gtk --disable-sdl --disable-vnc
: 禁用GTK、SDL和VNC支持,这些是用于图形界面的库。--target-list="x86_64-linux-user x86_64-softmmu arm-softmmu aarch64-softmmu"
: 指定要构建的目标列表,包括x86_64、ARM和AArch64架构的软件模拟器。
make
: 运行make
命令来编译QEMU。cp -f ...
: 将编译后的QEMU可执行文件复制到指定位置:x86_64-linux-user/qemu-x86_64
复制为../../afl-qemu-trace
,用于用户模式跟踪。x86_64-softmmu/qemu-system-x86_64
复制为../../afl-qemu-system-trace
,用于系统模式跟踪。- 还有其它架构的可执行文件也复制到了相应的位置。
这个脚本的目的是编译带有TriforceAFL修补程序的QEMU,并将所需的可执行文件复制到指定位置,以便后续在AFL中使用。
- 构建
afl-qemu-trace
目标
同“6. 构建afl-qemu-system-trace
目标”所分析的内容,在此不再赘述。
1.2.2、编译TriforceLinuxSyscallFuzzer
当我们下载好TriforceLinuxSyscallFuzzer的源代码后,会在其源代码目录中执行make
命令,该命令会默认执行“Makefile”文件中的all
目标,而all
目标实现在“/TriforceLinuxSyscallFuzzer/Makefile”的第4行。
在这里,all
目标依赖于其它多个目标,这些目标会被按顺序执行。具体来说,all
目标依赖于driver
目标、testAfl
目标和heater
目标。而这三个目标的构建过程分别实现在“/TriforceLinuxSyscallFuzzer/Makefile”的第7行、第14行和第11行。
总而言之,最终构建出driver、heater和testAfl这三个二进制程序,不过我们暂时先不对在该阶段构建出的二进制文件进行分析,后续用到的时候再对其进行分析。
1.2.3、初始化资源
1.2.3.1、种子初始化
在使用TriforceAFL工具和TriforceLinuxSyscallFuzzer工具配合进行Fuzz测试之前,我们需要执行make inputs
命令来对当前Fuzz测试的环境进行初始化。该命令实际是TriforceLinuxSyscallFuzzer工具源代码目录中的“Makefile”文件中的一个规则,其实现在“/TriforceLinuxSyscallFuzzer/Makefile”文件的第25行。
这个规则用于生成输入文件。首先,它检查是否存在名为“inputs”的目录,如果不存在则创建该目录。然后,它执行“./gen.py”脚本来生成输入文件。很明显,在这里的核心是执行“./gen.py”脚本,而该脚本存在于“/TriforceLinuxSyscallFuzzer/”目录中。这个脚本是一个Python代码文件,所以首先来看该脚本的main()
函数,此main()
函数实现在“/TriforceLinuxSyscallFuzzer/gen.py”的第133行:
这部分代码是脚本的主程序,定义了一些系统调用的示例,并使用这些示例调用了之前定义的函数来生成输入文件。每个示例都生成了一个不同的输入文件,并将其写入到名为“inputs”的目录中。具体来说其逻辑为:
- 定义系统调用常量:
- 使用整数值为不同的系统调用定义了常量,如
read = 0
、write = 1
、open = 2
、writev = 20
、execve = 59
。这些常量用于表示不同系统调用的调用号。
- 使用整数值为不同的系统调用定义了常量,如
- 创建参数对象:
- 创建了几个对象来表示系统调用的参数,如
buf
、l
、fd
。这些对象将在后续的系统调用示例中被使用。
- 创建了几个对象来表示系统调用的参数,如
- 定义系统调用示例:
- 创建了几个系统调用示例,每个示例包含了一个或多个系统调用以及对应的参数。
- 示例中使用了之前定义的系统调用常量和参数对象。
- 调用生成函数生成输入文件:
- 对每个系统调用示例调用了
mkSyscalls
函数,将系统调用示例转换为字节流数据,并调用writeFn
函数将字节流写入文件中。 - 每个系统调用示例生成的文件路径以“inputs”目录为基准进行命名,依次命名为“ex1”、“ex2”、“ex3”等。
- 对每个系统调用示例调用了
以上代码逻辑中的核心是上面标红的两部分,下面我们对其进行分析。
- 创建参数对象
在这里创建了三个参数对象,即buf
、l
和fd
,而创建这三个参数对象分别使用了自定义的Alloc()
函数、Len()
函数和File()
函数:
Alloc()
函数
Alloc()
函数实现在“/TriforceLinuxSyscallFuzzer/gen.py”的第29行,这实际是一个类,故我们来看该类中的__init__()
函数(因为类都是由该函数初始化的)。
可以发现,__init__()
函数并没有做什么特别的操作,只是将传入的参数进行赋值。
Len()
函数
Len()
函数实现在“/TriforceLinuxSyscallFuzzer/gen.py”的第50行,这实际是一个类,故我们来看该类中的__init__()
函数(因为类都是由该函数初始化的)。
可以发现,该类中并没有__init__()
函数,所以只是初始化了该对象,并没有做任何操作。
File()
函数
File()
函数实现在“/TriforceLinuxSyscallFuzzer/gen.py”的第53行,这实际是一个类,故我们来看该类中的__init__()
函数(因为类都是由该函数初始化的)。
可以发现,该类中并没有__init__()
函数,所以只是初始化了该对象,并没有做任何操作。
- 调用生成函数生成输入文件
该逻辑由mkSyscalls()
函数实现,而mkSyscalls()
函数实现在“/TriforceLinuxSyscallFuzzer/gen.py”的第115行。
该函数接受多个参数,每个参数都是一个系统调用的元组。然后,它遍历这些系统调用,对每个系统调用调用另一个函数mkSyscall()
,将结果添加到一个列表中。最后,它使用CALLDELIM
将所有系统调用的结果连接起来,并返回这个连接后的字符串。下面我们来看一下mkSyscall()
函数都做了什么,mkSyscall()
函数实现在“/TriforceLinuxSyscallFuzzer/gen.py”的第102行。
这个函数的作用是生成一个系统调用的字节表示形式。它首先将参数列表args
转换为列表,然后使用零值填充列表,直到列表的长度为6
。接着,它创建了两个缓冲区buf
和xtra
,并将系统调用号nr
打包到buf
中。然后,对于参数列表中的每个参数,使用mkArg
函数将其打包到buf
和xtra
缓冲区中。最后,将buf
和xtra
缓冲区的内容转换为字符串,并将它们拼接在一起返回,以得到系统调用的完整字节表示形式。
总之,最后会生成包含系统调用编号和参数的二进制字符串文件,而这些初始化的文件就是后续我们进行Fuzz测试的种子文件。
1.2.3.2、GuestOS初始化
当做完上一章节的配置后,我们还需要执行./runFuzz -M M0
命令来进一步初始化环境,并开始准备进行后续的Fuzz测试。该命令实际执行了TriforceLinuxSyscallFuzzer工具源代码目录中名为“runFuzz”的Shell脚本,该Shell脚本的具体内容如下所示。
#!/bin/sh
#
# usage: ./runFuzz [-C] [-M n | -S n] xtraargs..
# -C continue existing fuzz run
# -M n and -S n for master/slave. n must be unique
# xtraargs are passed to qemu
#
# choose kernel with K=name env variable, ie K=linux34 for linux34/bzImage
#
AFL=${TAFL:-../TriforceAFL}
KERN=${K:-kern}
# hokey arg parsing, sorry!
if [ "x$1" = "x-C" ] ; then # continue
INP="-"
shift
else
INP=inputs
fi
if [ "x$1" = "x-M" -o "x$1" = "x-S" ] ; then # master/slave args
FARGS="$1 $2"
shift; shift
else
echo "specify -M n or -S n please"
exit 1
fi
# find our kernel and it's parameters
getSym() { (grep " $1\$" $KERN/kallsyms|| echo 0 0)|cut -d' ' -f1; }
PANIC=`getSym panic`
LOGSTORE=`getSym log_store`
# make a rootfs image
make inputs fuzzRoot.cpio.gz || exit 1
# run fuzzer and qemu-system
export AFL_SKIP_CRASHES=1
$AFL/afl-fuzz $FARGS -t 500+ -i $INP -o outputs -QQ -- \
$AFL/afl-qemu-system-trace \
-L $AFL/qemu_mode/qemu/pc-bios \
-kernel $KERN/bzImage -initrd ./fuzzRoot.cpio.gz \
-m 64M -nographic -append "console=ttyS0" \
-aflPanicAddr "$PANIC" \
-aflDmesgAddr "$LOGSTORE" \
-aflFile @@
这个脚本的主要目的是自动化执行Fuzz过程,包括准备Fuzz测试环境(如创建init ramdisk image)、运行Fuzzer、监视Fuzz测试进度等。具体来说,该段代码的执行逻辑如下。
- 变量定义:
AFL=${TAFL:-../TriforceAFL}
:定义了变量AFL
,其值取决于环境变量TAFL
,如果未设置,则默认为../TriforceAFL
。KERN=${K:-kern}
:定义了变量KERN
,其值也取决于环境变量K
,默认为kern
。
- 参数解析:
- 根据命令行参数设置
INP
和FARGS
:INP
用于指定输入目录,如果命令行参数包含-C
,则输入目录为-
,否则为inputs
。FARGS
用于存储主机/从机参数,如果命令行参数包含-M
或-S
,则将其赋给FARGS
,然后将这两个参数从命令行中移除。
- 根据命令行参数设置
- 符号检索:
- 定义了一个函数
getSym()
,用于从内核符号表中检索指定符号的地址。 - 使用
getSym()
函数分别获取了内核中panic符号和日志存储符号的地址,存储在变量PANIC
和LOGSTORE
中。
- 定义了一个函数
- 创建Init Ramdisk Image:
- 使用
make
命令创建了一个名为“fuzzRoot.cpio.gz”的init ramdisk image文件。这是通过调用“makeRoot”脚本实现的,该脚本会基于“rootTemplate/”目录中的文件创建一个root文件系统镜像,并将驱动程序复制到镜像中。
- 使用
- 开始Fuzz测试:
- 导出了环境变量
AFL_SKIP_CRASHES=1
,这会告诉AFL跳过处理Crashes的步骤。 - 使用afl-fuzz工具执行Fuzz测试过程,其中包含以下参数:
-t 500+
:设置时间限制为500毫秒以上。-i $INP
:指定输入文件夹。-o outputs
:指定输出文件夹。-QQ
:关闭QEMU的输出。$AFL/afl-qemu-system-trace
:运行QEMU,与AFL结合使用。-L $AFL/qemu_mode/qemu/pc-bios
:指定QEMU BIOS路径。-kernel $KERN/bzImage
:指定内核镜像路径。-initrd ./fuzzRoot.cpio.gz
:指定init ramdisk镜像路径。-m 64M
:设置QEMU内存大小为64MB。-nographic
:设置QEMU以无图形界面模式运行。-append "console=ttyS0"
:设置内核启动参数。-aflPanicAddr "$PANIC"和-aflDmesgAddr "$LOGSTORE"
:指定AFL用于跟踪崩溃的地址和日志存储的地址。-aflFile @@
:指定Fuzz测试目标的位置。
- 导出了环境变量
这段代码的核心逻辑为上面标红的部分,这两部分代码也是环境初始化最终的两步,下面我们对这两部分代码进行详细分析:
- 创建Init Ramdisk Image
该逻辑由make inputs fuzzRoot.cpio.gz || exit 1
命令实现,而该命令实现在“/TriforceLinuxSyscallFuzzer/Makefile”的第18行(inputs
规则已经分析过,在此不再赘述)。
该规则将fuzzRoot.cpio.gz
构建成一个包含driver
的根文件系统。它会调用“/TriforceLinuxSyscallFuzzer/makeRoot”脚本,将fuzzRoot
(文件系统的名称)和driver
(在“1.2.2、编译TriforceLinuxSyscallFuzzer”章节中编译好的二进制程序)合并成一个文件系统。而“/TriforceLinuxSyscallFuzzer/makeRoot”脚本中的具体内容如下所示。
这段脚本用于创建一个用于根文件系统的init内存盘镜像。以下是脚本的逐步分析:
- 解析命令行参数:
- 如果参数数量小于2,则显示脚本的使用方法,并退出。
- 否则,将第一个参数作为生成的文件名,第二个参数作为驱动程序文件名,并将其余参数作为传递给驱动程序的参数。
- 设置变量:
templ=rootTemplate
:设置模板目录的路径。name="$1"
:设置生成的文件名。driver="$2"
:设置驱动程序文件名。args="$@"
:设置传递给驱动程序的参数。
- 复制模板文件夹并进行修补:
- 删除已存在的生成文件夹。
- 复制模板文件夹到生成文件夹。
- 如果驱动程序文件不存在于模板文件夹中,则将其复制到生成文件夹中。
- 使用
sed
命令替换生成文件夹中init
文件中的DRIVER
字符串为驱动程序路径,并将结果保存为新的init文件。
- 打包:
- 进入生成文件夹。
- 使用
find
命令列出文件,并通过管道传递给cpio
命令以创建新的内存盘镜像。 - 使用
gzip
压缩新创建的内存盘镜像文件。
让我们注意上面标红的三处逻辑:
-
第一处
这里将传入的第一个参数,即fuzzRoot
作为生成的文件名。将传入的第二个参数,即driver
作为驱动程序。具体来说是如下所示的代码片段。
-
第二处
因为TriforceAFL使用自定义的模板文件生成用于引导内核的init内存盘镜像,所以在这里定义了模板文件的路径,即“/TriforceLinuxSyscallFuzzer/rootTemplate/”目录中的内容,如下图所示。
具体来说,实现该目的的代码片段如下图所示。
- 第三处
该处逻辑的代码片段如下图所示。
该处代码的作用是将驱动程序的路径和参数插入到初始化脚本中,并确保初始化脚本可执行。因此,初始化脚本(init)被修改为在系统启动时执行特定的驱动程序(即传入的名为“driver”的驱动程序,其源代码位于“/TriforceLinuxSyscallFuzzer/driver.c”)及它们的参数。
- 使用afl-fuzz工具执行Fuzz测试过程
该逻辑由下面这部分代码实现,此处的逻辑就是TriforceAFL工具开始进行Fuzz的入口。
在这段代码中看似是一整条命令,实则是两个命令同时执行(刚开始因为没看懂,所以分析错了,导致浪费了很多时间),具体来说这两条命令分别是:
$AFL/afl-fuzz $FARGS -t 500+ -i $INP -o outputs -QQ
这条命令调用了“/TriforceAFL/afl-fuzz”(该目录已由AFL=${TAFL:-../TriforceAFL}
命令设置完毕)这个二进制文件并传入一些参数。而该二进制文件,就是我们进行Fuzz测试的入口,也就意味着要开始使用afl-fuzz调度整个Fuzz的过程了。而这一过程比较复杂,我们在下一章节进行详细分析。$AFL/afl-qemu-system-trace -L $AFL/qemu_mode/qemu/pc-bios -kernel $KERN/bzImage -initrd ./fuzzRoot.cpio.gz -m 64M -nographic -append "console=ttyS0" -aflPanicAddr "$PANIC" -aflDmesgAddr "$LOGSTORE" -aflFile @@
这条命令启动了待测试目标(即编译好的待检测内核+构建好的文件系统),这是通过QEMU虚拟机启动的待测试目标,并传入了相应的参数。这一过程是TriforceAFL进行Fuzz测试的核心,我们将在下下章节进行详细分析。
1.2.4、afl-fuzz调度
经过上一章节的学习,我们清楚下面要开始分析名为“afl-fuzz”的二进制文件,所以要分析其对应的源代码。其源代码位于“/TriforceAFL/afl-fuzz.c”中,对于该C语言文件,我们首先从main()
函数(实现在“/TriforceAFL/afl-fuzz.c”的第7414行)开始分析。
/* Main entry point */
int main(int argc, char** argv) {
s32 opt;
u64 prev_queued = 0;
u32 sync_interval_cnt = 0, seek_to;
u8 *extras_dir = 0;
u8 mem_limit_given = 0;
u8 exit_1 = !!getenv("AFL_BENCH_JUST_ONE");
char** use_argv;
SAYF(cCYA "afl-fuzz " cBRI VERSION cRST " by <lcamtuf@google.com>\n");
doc_path = access(DOC_PATH, F_OK) ? "docs" : DOC_PATH;
while ((opt = getopt(argc, argv, "+i:o:f:m:t:T:dnCB:S:M:x:Q")) > 0)
switch (opt) {
case 'i':
if (in_dir) FATAL("Multiple -i options not supported");
in_dir = optarg;
if (!strcmp(in_dir, "-")) in_place_resume = 1;
break;
case 'o': /* output dir */
if (out_dir) FATAL("Multiple -o options not supported");
out_dir = optarg;
break;
case 'M':
force_deterministic = 1;
/* Fall through */
case 'S': /* sync ID */
if (sync_id) FATAL("Multiple -S or -M options not supported");
sync_id = optarg;
break;
case 'f': /* target file */
if (out_file) FATAL("Multiple -f options not supported");
out_file = optarg;
break;
case 'x':
if (extras_dir) FATAL("Multiple -x options not supported");
extras_dir = optarg;
break;
case 't': {
u8 suffix = 0;
if (timeout_given) FATAL("Multiple -t options not supported");
if (sscanf(optarg, "%u%c", &exec_tmout, &suffix) < 1 ||
optarg[0] == '-') FATAL("Bad syntax used for -t");
if (exec_tmout < 5) FATAL("Dangerously low value of -t");
if (suffix == '+') timeout_given = 2; else timeout_given = 1;
break;
}
case 'm': {
u8 suffix = 'M';
if (mem_limit_given) FATAL("Multiple -m options not supported");
mem_limit_given = 1;
if (!strcmp(optarg, "none")) {
mem_limit = 0;
break;
}
if (sscanf(optarg, "%llu%c", &mem_limit, &suffix) < 1 ||
optarg[0] == '-') FATAL("Bad syntax used for -m");
switch (suffix) {
case 'T': mem_limit *= 1024 * 1024; break;
case 'G': mem_limit *= 1024; break;
case 'k': mem_limit /= 1024; break;
case 'M': break;
default: FATAL("Unsupported suffix or bad syntax for -m");
}
if (mem_limit < 5) FATAL("Dangerously low value of -m");
if (sizeof(rlim_t) == 4 && mem_limit > 2000)
FATAL("Value of -m out of range on 32-bit systems");
}
break;
case 'd':
if (skip_deterministic) FATAL("Multiple -d options not supported");
skip_deterministic = 1;
use_splicing = 1;
break;
case 'B':
/* This is a secret undocumented option! It is useful if you find
an interesting test case during a normal fuzzing process, and want
to mutate it without rediscovering any of the test cases already
found during an earlier run.
To use this mode, you need to point -B to the fuzz_bitmap produced
by an earlier run for the exact same binary... and that's it.
I only used this once or twice to get variants of a particular
file, so I'm not making this an official setting. */
if (in_bitmap) FATAL("Multiple -B options not supported");
in_bitmap = optarg;
read_bitmap(in_bitmap);
break;
case 'C':
if (crash_mode) FATAL("Multiple -C options not supported");
crash_mode = FAULT_CRASH;
break;
case 'n':
if (dumb_mode) FATAL("Multiple -n options not supported");
if (getenv("AFL_DUMB_FORKSRV")) dumb_mode = 2; else dumb_mode = 1;
break;
case 'T':
if (use_banner) FATAL("Multiple -T options not supported");
use_banner = optarg;
break;
case 'Q':
//if (qemu_mode) FATAL("Multiple -Q options not supported");
qemu_mode += 1;
if (!mem_limit_given) mem_limit = MEM_LIMIT_QEMU;
break;
default:
usage(argv[0]);
}
if (optind == argc || !in_dir || !out_dir) usage(argv[0]);
setup_signal_handlers();
check_asan_opts();
if (sync_id) fix_up_sync();
if (!strcmp(in_dir, out_dir))
FATAL("Input and output directories can't be the same");
if (dumb_mode) {
if (crash_mode) FATAL("-C and -n are mutually exclusive");
if (qemu_mode) FATAL("-Q and -n are mutually exclusive");
}
if (getenv("AFL_NO_FORKSRV")) no_forkserver = 1;
if (getenv("AFL_NO_CPU_RED")) no_cpu_meter_red = 1;
if (getenv("AFL_NO_VAR_CHECK")) no_var_check = 1;
if (getenv("AFL_SHUFFLE_QUEUE")) shuffle_queue = 1;
if (dumb_mode == 2 && no_forkserver)
FATAL("AFL_DUMB_FORKSRV and AFL_NO_FORKSRV are mutually exclusive");
if (getenv("AFL_LD_PRELOAD"))
setenv("LD_PRELOAD", getenv("AFL_LD_PRELOAD"), 1);
save_cmdline(argc, argv);
fix_up_banner(argv[optind]);
check_if_tty();
get_core_count();
check_crash_handling();
check_cpu_governor();
setup_post();
setup_shm();
setup_dirs_fds();
read_testcases();
load_auto();
pivot_inputs();
if (extras_dir) load_extras(extras_dir);
if (!timeout_given) find_timeout();
detect_file_args(argv + optind + 1);
if (!out_file) setup_stdio_file();
check_binary(argv[optind]);
start_time = get_cur_time();
if (qemu_mode)
use_argv = get_qemu_argv(qemu_mode, argv[0], argv + optind, argc - optind);
else
use_argv = argv + optind;
perform_dry_run(use_argv);
cull_queue();
show_init_stats();
seek_to = find_start_position();
write_stats_file(0, 0);
save_auto();
if (stop_soon) goto stop_fuzzing;
/* Woop woop woop */
if (!not_on_tty) {
sleep(4);
start_time += 4000;
if (stop_soon) goto stop_fuzzing;
}
while (1) {
u8 skipped_fuzz;
cull_queue();
if (!queue_cur) {
queue_cycle++;
current_entry = 0;
cur_skipped_paths = 0;
queue_cur = queue;
while (seek_to) {
current_entry++;
seek_to--;
queue_cur = queue_cur->next;
}
show_stats();
if (not_on_tty) {
ACTF("Entering queue cycle %llu.", queue_cycle);
fflush(stdout);
}
/* If we had a full queue cycle with no new finds, try
recombination strategies next. */
if (queued_paths == prev_queued) {
if (use_splicing) cycles_wo_finds++; else use_splicing = 1;
} else cycles_wo_finds = 0;
prev_queued = queued_paths;
if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST"))
sync_fuzzers(use_argv);
}
skipped_fuzz = fuzz_one(use_argv);
if (!stop_soon && sync_id && !skipped_fuzz) {
if (!(sync_interval_cnt++ % SYNC_INTERVAL))
sync_fuzzers(use_argv);
}
if (!stop_soon && exit_1) stop_soon = 2;
if (stop_soon) break;
queue_cur = queue_cur->next;
current_entry++;
}
if (queue_cur) show_stats();
write_bitmap();
write_stats_file(0, 0);
save_auto();
stop_fuzzing:
SAYF(CURSOR_SHOW cLRD "\n\n+++ Testing aborted %s +++\n" cRST,
stop_soon == 2 ? "programatically" : "by user");
/* Running for more than 30 minutes but still doing first cycle? */
if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) {
SAYF("\n" cYEL "[!] " cRST
"Stopped during the first cycle, results may be incomplete.\n"
" (For info on resuming, see %s/README.)\n", doc_path);
}
fclose(plot_file);
destroy_queue();
destroy_extras();
ck_free(target_path);
alloc_report();
OKF("We're done here. Have a nice day!\n");
exit(0);
}
这段代码是afl-fuzz的主程序入口,负责解析命令行参数、设置环境、初始化各种变量,并在启动Fuzz测试之前进行一系列的准备工作。下面是对代码的分点分析。
- 设置环境和初始化变量:
- 设置基本的输出提示信息,包括AFL版本号。
- 初始化一些全局变量,如计数同步间隔(
sync_interval_cnt
)、内存限制(mem_limit_given
)和要使用的命令行参数(use_argv)等。
- 解析命令行参数:
- 使用
getopt
函数解析命令行参数,支持的选项包括-i
、-o
、-M
、-S
、-f
、-x
、-t
、-m
、-d
、-B
、-C
、-n
、-T
、-Q
等。 - 根据选项的不同,设置相应的全局变量,例如输入目录
in_dir
、输出目录out_dir
、同步IDsync_id
、超时时间exec_tmout
等。
- 使用
- 执行一系列准备工作:
- 设置信号处理器,处理程序中断信号。
- 检查ASAN(AddressSanitizer)选项是否开启。
- 修复同步ID。
- 检查输入输出目录是否相同。
- 检查是否为终端运行。
- 准备测试环境:
- 获取CPU核心数,用于计算并行执行时的线程数。
- 检查异常处理功能是否可用。
- 检查CPU调节器的状态,用于检测性能问题。
- 设置文件和目录,并加载测试用例:
- 设置输入输出目录。
- 读取测试用例文件。
- 加载自动生成的测试用例。
- 执行检查操作:
- 检测输入参数的有效性。
- 检测二进制程序的有效性。
- 进行干预测试和检查:
- 执行干预测试(dry run),测试是否能够正常运行。
- 对队列进行修剪操作。
- 开始Fuzz测试:
- 进入主循环,循环执行Fuzz测试的主要逻辑。
- 检查是否需要同步Fuzzer。
- 执行Fuzz测试,进行样本的Fuzz操作。
- 结束Fuzz测试:
- 显示测试统计信息。
- 保存Bitmap文件。
- 写入统计文件。
- 释放资源,关闭文件。
- 输出结果:
- 根据程序中断的原因输出相应的提示信息。
- 输出测试的总结信息。
这段代码实现了对Fuzz测试的控制流程、环境初始化、参数解析等功能,是afl-fuzz程序的核心部分。这部分代码确实非常长,不过我们并不全部关心,我们只关心上面标红的逻辑,因为这才是afl-fuzz调度的核心逻辑。
- 读取测试用例文件
该逻辑由read_testcases();
函数调用实现,而read_testcases()
函数实现在“/TriforceAFL/afl-fuzz.c”的第1275行。
/* Read all testcases from the input directory, then queue them for testing.
Called at startup. */
static void read_testcases(void) {
struct dirent **nl;
s32 nl_cnt;
u32 i;
u8* fn;
/* Auto-detect non-in-place resumption attempts. */
fn = alloc_printf("%s/queue", in_dir);
if (!access(fn, F_OK)) in_dir = fn; else ck_free(fn);
ACTF("Scanning '%s'...", in_dir);
/* We use scandir() + alphasort() rather than readdir() because otherwise,
the ordering of test cases would vary somewhat randomly and would be
difficult to control. */
nl_cnt = scandir(in_dir, &nl, NULL, alphasort);
if (nl_cnt < 0) {
if (errno == ENOENT || errno == ENOTDIR)
SAYF("\n" cLRD "[-] " cRST
"The input directory does not seem to be valid - try again. The fuzzer needs\n"
" one or more test case to start with - ideally, a small file under 1 kB\n"
" or so. The cases must be stored as regular files directly in the input\n"
" directory.\n");
PFATAL("Unable to open '%s'", in_dir);
}
if (shuffle_queue && nl_cnt > 1) {
ACTF("Shuffling queue...");
shuffle_ptrs((void**)nl, nl_cnt);
}
for (i = 0; i < nl_cnt; i++) {
struct stat st;
u8* fn = alloc_printf("%s/%s", in_dir, nl[i]->d_name);
u8* dfn = alloc_printf("%s/.state/deterministic_done/%s", in_dir, nl[i]->d_name);
u8 passed_det = 0;
free(nl[i]); /* not tracked */
if (lstat(fn, &st) || access(fn, R_OK))
PFATAL("Unable to access '%s'", fn);
/* This also takes care of . and .. */
if (!S_ISREG(st.st_mode) || !st.st_size || strstr(fn, "/README.txt")) {
ck_free(fn);
ck_free(dfn);
continue;
}
if (st.st_size > MAX_FILE)
FATAL("Test case '%s' is too big (%s, limit is %s)", fn,
DMS(st.st_size), DMS(MAX_FILE));
/* Check for metadata that indicates that deterministic fuzzing
is complete for this entry. We don't want to repeat deterministic
fuzzing when resuming aborted scans, because it would be pointless
and probably very time-consuming. */
if (!access(dfn, F_OK)) passed_det = 1;
ck_free(dfn);
add_to_queue(fn, st.st_size, passed_det);
}
free(nl); /* not tracked */
if (!queued_paths) {
SAYF("\n" cLRD "[-] " cRST
"Looks like there are no valid test cases in the input directory! The fuzzer\n"
" needs one or more test case to start with - ideally, a small file under\n"
" 1 kB or so. The cases must be stored as regular files directly in the\n"
" input directory.\n");
FATAL("No usable test cases in '%s'", in_dir);
}
last_path_time = 0;
queued_at_start = queued_paths;
}
这段代码定义了一个名为“read_testcases”的函数,其作用是从输入目录中读取所有测试用例,并将它们添加到测试队列中以供后续测试使用。这个函数通常在启动时调用。主要步骤如下:
- 路径检查和更新:
- 使用
alloc_printf
函数生成输入目录中队列文件的路径。 - 检查该路径是否存在,如果存在则更新
in_dir
变量,否则释放内存。
- 使用
- 扫描输入目录:
- 打印提示信息,指示正在扫描输入目录。
- 使用
scandir
函数和alphasort
排序规则获取输入目录中的所有文件和子目录的列表。
- 处理获取的文件列表:
- 检查获取文件列表的结果,如果小于
0
,则打印错误信息并退出程序。 - 如果输入目录不存在或者不是一个目录,则给出相应的错误提示。
- 如果开启了
shuffle_queue
选项并且文件列表中的文件数量大于1
,则对文件列表进行随机重排序,并打印相应的提示信息。 - 遍历文件列表,对每个文件进行进一步处理。
- 检查获取文件列表的结果,如果小于
- 处理每个文件:
- 使用
lstat
函数获取文件的属性,并检查文件是否可读,如果不可读则打印错误信息并退出程序。 - 检查文件是否为普通文件且大小不为
0
,并且不是README文件。 - 生成标志着该文件是否已完成确定性Fuzz测试的路径,如果存在则将
passed_det
标记为1
。 - 将文件路径、文件大小和
passed_det
参数传递给add_to_queue
函数,将文件添加到测试队列中。
- 使用
- 结束处理:
- 释放文件列表所占用的内存。
- 如果没有在输入目录中找到任何测试用例,则打印错误信息并退出程序。
- 将
last_path_time
重置为0
,表示还没有执行过路径操作。 - 更新
queued_at_start
变量为当前测试队列中的条目数量,用于后续的统计和比较。
该函数最重要的操作就是将“1.2.1、环境初始化”章节在“/TriforceLinuxSyscallFuzzer/inputs”目录中生成的测试用例加载到测试队列中。也就是上面红色部分的逻辑,即add_to_queue(fn, st.st_size, passed_det);
函数调用。而add_to_queue()
函数实现在“TriforceAFL/afl-fuzz.c”的第634行。
这段代码用于将在“1.2.1、环境初始化”章节中生成的测试用例添加到测试队列中。以下是它的主要步骤:
- 分配内存并初始化新的队列条目:
- 使用
ck_alloc
函数为新的队列条目分配内存。 - 将文件名、文件长度、深度和确定性Fuzz测试状态传递给新的队列条目。
- 使用
- 更新最大深度:
- 如果当前条目的深度大于当前最大深度,则将最大深度更新为当前条目的深度。
- 将新的队列条目链接到队列中:
- 如果队列不为空,则将新的队列条目链接到队列的末尾。
- 否则,将新的队列条目设置为队列的第一个元素,并更新
queue_top
和q_prev100
指针。
- 更新计数器和标志:
- 增加已排队的测试用例数量和待执行但尚未Fuzz测试的数量。
- 重置循环未找到新路径的次数(
cycles_wo_finds
)为0
。 - 如果排队的测试用例数量是
100
的倍数,则更新q_prev100
指针。
- 更新最后路径时间:
- 获取当前时间并将其设置为最后路径时间,用于后续的统计和比较。
总之,这段代码的作用是在Fuzz测试的过程中,将测试用例添加到测试队列中,以便后续的Fuzz测试过程能够逐个对这些测试用例进行执行。
- 执行Fuzz测试,进行样本的Fuzz操作
该逻辑由fuzz_one(use_argv);
函数调用实现,而fuzz_one()
函数实现在“/TriforceAFL/afl-fuzz.c”的第4691行。
该函数由于代码实在是太长了(几千行),所以就不全粘贴到本文档中。另外也不是所有代码都是我们分析的重点,这是因为该部分代码是AFL工具的Fuzz测试逻辑。AFL是另一个Fuzz测试工具,TriforceAFL利用了该工具的部分代码逻辑,那么问题就来了,TriforceAFL为什么要这么做呢?这是因为TriforceAFL利用了其加载种子和对种子进行变异的逻辑。关于加载种子的操作我们上面已经介绍过了。下面我们来看一下该函数对种子进行变异的逻辑。
值得强调的是,该Fuzz变异逻辑作者写的十分冗余,因为把它们全放在一个函数里面了(就是我们现在介绍的函数)。具体来说,该函数中所包含的种子变异逻辑有以下几种:
- 简单位翻转(+字典构建):这是指在输入数据中简单地翻转一些位,然后观察程序的行为。这种方法会构建一个字典,记录那些导致程序异常或不同行为的输入样本。
作者实现了单个位翻转、两个位翻转和四个位翻转等,这些实现的逻辑都差不多,只是翻转的位数有所变换,比如我们来看两个位翻转的具体实现(实现在fuzz_one()
函数的第4939行)。
该变异逻辑通过迭代每个输入位,并尝试翻转相邻的两个位来生成新的输入。在每次迭代中,程序执行常规的Fuzzing操作,例如执行目标程序并检查是否发现新的路径或崩溃。
- 算术增减:对输入数据进行算术操作,例如增加或减少数值,以查看程序的反应。
作者实现了8字节的算术增减、16字节的算术增减和32字节的算术增减等,这些实现的逻辑都差不多,只是进行算术增减的位数有所变换,比如我们来看8字节的算术增减的具体实现(实现在fuzz_one()
函数的第5168行)。
/* 8-bit arithmetics. */
stage_name = "arith 8/8";
stage_short = "arith8";
stage_cur = 0;
stage_max = 2 * len * ARITH_MAX;
stage_val_type = STAGE_VAL_LE;
orig_hit_cnt = new_hit_cnt;
for (i = 0; i < len; i++) {
u8 orig = out_buf[i];
/* Let's consult the effector map... */
if (!eff_map[EFF_APOS(i)]) {
stage_max -= 2 * ARITH_MAX;
continue;
}
stage_cur_byte = i;
for (j = 1; j <= ARITH_MAX; j++) {
u8 r = orig ^ (orig + j);
/* Do arithmetic operations only if the result couldn't be a product
of a bitflip. */
if (!could_be_bitflip(r)) {
stage_cur_val = j;
out_buf[i] = orig + j;
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;
} else stage_max--;
r = orig ^ (orig - j);
if (!could_be_bitflip(r)) {
stage_cur_val = -j;
out_buf[i] = orig - j;
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;
} else stage_max--;
out_buf[i] = orig;
}
}
new_hit_cnt = queued_paths + unique_crashes;
stage_finds[STAGE_ARITH8] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_ARITH8] += stage_max;
该变异逻辑通过对输入数据的每个字节执行一系列算术操作来生成新的输入。在每次迭代中,算法尝试将当前字节增加或减少一定值,并检查结果是否为位翻转的产物。如果结果不可能是位翻转的结果,就执行常规的Fuzzing操作,并跟踪发现的新路径或崩溃。
- 有趣的值:指使用一些特定的值作为输入,这些值可能会导致程序的异常行为或边缘情况。
作者实现了设置8字节值、设置16字节值和设置32字节值等,这些实现的逻辑都差不多,只是设置值的位数有所变换,比如我们来看设置8字节值的具体实现(实现在fuzz_one()
函数的第5433行)。
该变异逻辑尝试将每个字节设置为一系列有趣的8位整数值。在每次迭代中,算法会跳过可能是位翻转或算术操作的结果的值,并执行常规的Fuzzing操作。最后,它会跟踪新发现的路径或崩溃,并更新相关的统计数据。
- 字典处理:这部分涉及构建或使用字典,其中包含已知的有效输入或特定测试用例,以确保这些情况被测试覆盖。
作者实现了使用用户提供的额外测试用例覆盖和使用用户提供的额外测试用例插入等,这些实现的逻辑都差不多,比如我们来看使用用户提供的额外测试用例插入的具体实现(实现在fuzz_one()
函数的第5678行)。
该变异逻辑尝试将用户提供的额外数据插入到原始输入数据中的每个可能位置。在每次迭代中,算法将额外数据插入到当前位置,并执行常规的Fuzzing操作。最后,它会跟踪新发现的路径或崩溃,并更新相关的统计数据。
- 随机混乱:对输入数据进行随机修改或混乱,以观察程序的反应。
作者实现了多种随机混乱的逻辑,不过这些实现的逻辑都差不多,比如我们来看下面这个随机混乱的具体实现(实现在fuzz_one()
函数的第5886行)。
该变异逻辑随机从一个字节中减去一个随机数(范围为1
到ARITH_MAX
)。
- 拼接:将多个输入片段组合成一个新的输入,以创建更多的变化和测试情况。
关于拼接的变异逻辑,作者将其实现在fuzz_one()
函数的第6237行。
retry_splicing:
if (use_splicing && splice_cycle++ < SPLICE_CYCLES &&
queued_paths > 1 && queue_cur->len > 1) {
struct queue_entry* target;
u32 tid, split_at;
u8* new_buf;
s32 f_diff, l_diff;
/* First of all, if we've modified in_buf for havoc, let's clean that
up... */
if (in_buf != orig_in) {
ck_free(in_buf);
in_buf = orig_in;
len = queue_cur->len;
}
/* Pick a random queue entry and seek to it. Don't splice with yourself. */
do { tid = UR(queued_paths); } while (tid == current_entry);
splicing_with = tid;
target = queue;
while (tid >= 100) { target = target->next_100; tid -= 100; }
while (tid--) target = target->next;
/* Make sure that the target has a reasonable length. */
while (target && (target->len < 2 || target == queue_cur)) {
target = target->next;
splicing_with++;
}
if (!target) goto retry_splicing;
/* Read the testcase into a new buffer. */
fd = open(target->fname, O_RDONLY);
if (fd < 0) PFATAL("Unable to open '%s'", target->fname);
new_buf = ck_alloc_nozero(target->len);
ck_read(fd, new_buf, target->len, target->fname);
close(fd);
/* Find a suitable splicing location, somewhere between the first and
the last differing byte. Bail out if the difference is just a single
byte or so. */
locate_diffs(in_buf, new_buf, MIN(len, target->len), &f_diff, &l_diff);
if (f_diff < 0 || l_diff < 2 || f_diff == l_diff) {
ck_free(new_buf);
goto retry_splicing;
}
/* Split somewhere between the first and last differing byte. */
split_at = f_diff + UR(l_diff - f_diff);
/* Do the thing. */
len = target->len;
memcpy(new_buf, in_buf, split_at);
in_buf = new_buf;
ck_free(out_buf);
out_buf = ck_alloc_nozero(len);
memcpy(out_buf, in_buf, len);
goto havoc_stage;
}
该变异逻辑实现了重试和进行splice操作。它首先检查是否满足splice的条件,然后选择一个随机的队列条目,从中读取测试用例数据到一个新的缓冲区。接着,在第一个和最后一个不同的字节之间选择一个适当的位置进行splice操作。如果找不到合适的位置,则会重试。完成splice后,程序跳转到havoc_stage
标签,继续执行后续操作。
总而言之。TriforceAFL就用到了AFL工具的这两处逻辑,所以说TriforceAFL是AFL工具的增强版。不过需要注意的是,AFL工具仍会对目标进行Fuzz测试的操作,不过此时测试并不会成功,因为我们传入的测试用例是打包好的二进制系统调用信息,而AFL工具最终是通过common_fuzz_stuff()
函数来执行目标程序来进行Fuzz测试的,而我们的测试用例并不能被执行,所以AFL工具对于TriforceAFL的作用就到此为止了。那么TriforceAFL又是如何配合TriforceLinuxSyscallFuzzer对操作系统内核进行Fuzz测试的呢?这就是下一章节我们要介绍的内容。
在这里将common_fuzz_stuff()
函数的具体实现展示在下面,不过我们并不对其进行分析,因为与我们的主线任务没什么关系。该函数实现在“/TriforceAFL/afl-fuzz.c”的第4348行。
1.2.5、启动待Fuzz目标
经过前一个章节的学习,我们已经知道了TriforceAFL获取种子文件以及对其进行变异操作的具体实现了,本章我们就会介绍如何通过TriforceAFL和TriforceLinuxSyscallFuzzer的配合实现最终对操作系统内核进行Fuzz的操作。
首先,需要注意的是,在“1.2.3.2、GuestOS初始化”章节我们已经介绍过了,此时我们需要启动待Fuzz目标。当目标虚拟机系统启动后,会自动执行载入的驱动程序,即名为“driver”的驱动程序(见“1.2.3.2、GuestOS初始化”章节)。所以我们现在来看一下这个驱动程序的源代码,故来看实现在“/TriforceLinuxSyscallFuzzer/driver.c”的第80行的main()
函数。
int
main(int argc, char **argv)
{
struct sysRec recs[3];
struct slice slice;
unsigned short filtCalls[MAXFILTCALLS];
char *prog, *buf;
u_long sz;
long x;
int opt, nrecs, nFiltCalls, parseOk;
int noSyscall = 0;
int enableTimer = 0;
nFiltCalls = 0;
prog = argv[0];
while((opt = getopt(argc, argv, "f:tTvx")) != -1) {
switch(opt) {
case 'f':
if(nFiltCalls >= MAXFILTCALLS) {
printf("too many -f args!\n");
exit(1);
}
if(parseU16(optarg, &filtCalls[nFiltCalls]) == -1) {
printf("bad arg to -f: %s\n", optarg);
exit(1);
}
nFiltCalls++;
break;
case 't':
aflTestMode = 1;
break;
case 'T':
enableTimer = 1;
break;
case 'v':
verbose++;
break;
case 'x':
noSyscall = 1;
break;
case '?':
default:
usage(prog);
break;
}
}
argc -= optind;
argv += optind;
if(argc)
usage(prog);
if(!aflTestMode)
watcher();
startForkserver(enableTimer);
buf = getWork(&sz);
//printf("got work: %d - %.*s\n", sz, (int)sz, buf);
/* trace our driver code while parsing workbuf */
extern void _start(), __libc_start_main();
startWork((u_long)_start, (u_long)__libc_start_main);
mkSlice(&slice, buf, sz);
parseOk = parseSysRecArr(&slice, 3, recs, &nrecs);
if(verbose) {
printf("read %ld bytes, parse result %d nrecs %d\n", sz, parseOk, (int)nrecs);
if(parseOk == 0)
showSysRecArr(recs, nrecs);
}
if(parseOk == 0 && filterCalls(filtCalls, nFiltCalls, recs, nrecs)) {
/* trace kernel code while performing syscalls */
startWork(0xffffffff81000000L, 0xffffffffffffffffL);
if(noSyscall) {
x = 0;
} else {
/* note: if this crashes, watcher will do doneWork for us */
x = doSysRecArr(recs, nrecs);
}
if (verbose) printf("syscall returned %ld\n", x);
} else {
if (verbose) printf("Rejected by filter\n");
}
fflush(stdout);
doneWork(0);
return 0;
}
该函数是程序的主函数,负责解析命令行参数,启动监视器和Fork服务器,获取工作缓冲区,解析系统调用记录,过滤系统调用,执行系统调用,并输出执行结果。具体来说,其逻辑如下。
- 定义了所需的变量,包括用于存储系统调用记录的结构体数组
recs
,用于存储工作缓冲区片段的slice
结构体,用于存储过滤系统调用的数组filtCalls
,以及其它辅助变量。 - 处理命令行参数:
- 使用
getopt()
函数解析命令行选项,支持的选项包括-f
、-t
、-T
、-v
和-x
。 -f
选项用于指定要过滤的系统调用。-t
选项用于启用AFL测试模式。-T
选项用于启用计时器。-v
选项用于增加输出的详细程度。-x
选项用于禁用系统调用。
- 使用
- 启动监视器(watcher):
- 如果未处于AFL测试模式,则调用
watcher()
函数。
- 如果未处于AFL测试模式,则调用
- 启动Fork服务器:
- 调用
startForkserver()
函数,启动Fork服务器。 - 可以选择是否启用计时器。
- 调用
- 获取工作缓冲区:
- 调用
getWork()
函数,获取工作缓冲区及其大小。
- 调用
- 开始工作:
- 调用
_start()
和__libc_start_main()
函数,开始工作。
- 调用
- 解析工作缓冲区:
- 调用
mkSlice()
函数,将工作缓冲区分割成片段。 - 调用
parseSysRecArr()
函数,解析系统调用记录。
- 调用
- 过滤系统调用:
- 如果解析成功且通过了过滤器,则继续执行,否则退出。
- 执行系统调用:
- 如果未禁用系统调用,则调用
doSysRecArr()
函数执行系统调用。
- 如果未禁用系统调用,则调用
- 输出结果:
- 输出执行结果。
- 结束工作:
- 刷新输出缓冲区并调用
doneWork()
函数,结束工作并返回 0
- 刷新输出缓冲区并调用
很明显,该函数是TriforceAFL和TriforceLinuxSyscallFuzzer配合实现对操作系统内核进行Fuzz操作的核心函数,而该函数的核心内容为上面标红的三处。
- 获取工作缓冲区
该逻辑由getWork(&sz);
函数调用实现,而getWork()
函数实现在“/TriforceLinuxSyscallFuzzer/aflCall.c”的第65行。
这个函数是用来获取工作负载数据的。它首先调用aflInit()
来初始化AFL模式,然后根据aflTestMode
的设置决定从标准输入中读取数据还是调用aflCall()
函数来获取数据。如果aflTestMode
为真,则调用read()
从标准输入中读取数据到缓冲区buf
中,并将读取的数据大小存储在sizep
指向的变量中;否则,调用aflCall()
函数来获取数据,并将数据大小存储在sizep
指向的变量中。最后,函数返回缓冲区buf
的指针,其中包含了获取的工作负载数据。
这里其实就是通过调用aflCall()
函数来获取数据的,那么这些数据是从哪里获取的呢?在“1.2.4、afl-fuzz调度”章节我们分析过如何加载种子文件的,其实在这里获取到的负载数据就是在“1.2.4、afl-fuzz调度”章节获取到的种子文件。
那么又是如何获取到的这些种子文件的呢?那就要看实现在“/TriforceLinuxSyscallFuzzer/aflCall.c”的第45行的aflCall()
函数了。
这个函数是一个内联汇编函数,用于进行系统调用。它使用了汇编指令0f 24
(SYSCALL
指令),并通过输入和输出操作数约束(operand constraint)将参数传递给系统调用。在这里,参数a0
、a1
和a2
分别被传递到寄存器rdi
、rsi
和rdx
中,并且返回值被存储在寄存器rax
中。最后,返回系统调用的返回值。
这个函数看起来没有任何逻辑,其实这是向QEMU发送一个指令,因为我们使用该函数获取工作负载时的函数调用为aflCall(2, (u_long)buf, bufsz)
,注意这里的第一个参数2
,该参数就代表要获取工作负载,通过向QEMU发送该参数,QEMU就可以执行对应的函数来为我们获取工作负载了。
那么问题来了,QEMU又是如何获取到该参数,并执行对应函数的呢?我们来看实现在“/TriforceAFL/qemu_mode/qemu/target-arm/translate.c”的第11568行的helper_aflCall()
函数。
可以发现,在这里就会根据传入的不同参数来执行对应的函数来完成我们的操作,比如我们在这里传入的参数为2
,所以就会去执行实现在“/TriforceAFL/qemu_mode/qemu/target-arm/translate.c”的第11507行的getWork()
函数。
这是用于将数据从文件中读取到内存中的函数。它的功能是打开名为“aflFile”的文件并以二进制只读模式打开。然后,它从文件中逐字节读取数据,并将每个字节写入到指定的内存地址中(使用cpu_stb_data()
函数)。该过程将持续直到达到指定的大小(sz
),或者直到文件结束。最后,函数返回成功读取的字节数。
- 解析系统调用记录
该逻辑由parseSysRecArr(&slice, 3, recs, &nrecs);
函数调用实现,该函数实现在“/TriforceLinuxSyscallFuzzer/sysc.c”的第337行。
这个函数实现了从数据块中解析系统调用记录的过程,它首先将数据块切分成分片,然后对每个分片进行解析,并将解析结果存储在数组中。以下是其核心逻辑。
- 创建分片数组:
- 声明一个
struct slice
数组slices[10]
,用于存储从数据块中提取的分片。 - 声明
size_t
类型的变量i
和nslices
,用于迭代和存储分片数量。
- 声明一个
- 提取分片:
- 如果
maxRecs
大于10
,则将其限制为10
,防止数组越界。 - 调用
getDelimSlices()
函数,从数据块中提取分片,并存储在slices
数组中。 - 如果提取分片的过程中出现问题(返回值为
-1
),则函数返回-1
。
- 如果
- 解析分片:
- 遍历
slices
数组中的每个分片。 - 对于每个分片,调用
parseSysRec()
函数进行解析,并将解析结果存储在x
数组中的相应位置。 - 如果解析过程中出现问题,立即返回
-1
。
- 遍历
- 更新记录数:
- 将成功解析的系统调用记录数
nslices
存储到nRecs
指针指向的位置。
- 将成功解析的系统调用记录数
- 返回结果:
- 如果所有分片都成功解析,则返回
0
表示成功完成解析。
- 如果所有分片都成功解析,则返回
这里比较重要的两处逻辑为上面标红的两处函数调用,下面我们对其进行分析。
getDelimSlices()
函数
该函数实现在“/TriforceLinuxSyscallFuzzer/parse.c”的第72行。
这个函数实现了从数据块中提取分片的过程,它根据指定的分隔符将数据块切分成多个分片,并将分片存储在数组中。
- 循环提取分片:
- 使用
for
循环遍历每个分片的位置,最多遍历max
次,以限制分片数量。 - 在循环中,判断当前分片的起始位置
b->cur
是否已经达到数据块的末尾b->end
。 - 如果未达到末尾,则调用
memmem
函数在当前分片中查找分隔符delim
。 - 如果找到了分隔符,则将当前分片的起始位置
b->cur
设置为分隔符后的位置ep + delsz
。 - 如果未找到分隔符,则将当前分片的结束位置
ep
设置为数据块的末尾b->end
。
- 使用
- 更新分片数量:
- 如果循环结束后,当前分片的起始位置
b->cur
不等于数据块的末尾b->end
,表示未能提取全部分片,则返回-1
表示提取失败。 - 否则,将成功提取的分片数量
i
存储到nx
指针指向的位置。
- 如果循环结束后,当前分片的起始位置
- 返回结果:
- 如果成功提取了所有分片,则返回
0
表示提取成功。
- 如果成功提取了所有分片,则返回
parseSysRec()
函数
该函数实现在“/TriforceLinuxSyscallFuzzer/sysc.c”的第311行。
该函数负责解析输入的系统调用记录数据块,并将解析结果存储到指定的数据结构中。其具体逻辑如下。
- 解析过程:
- 定义了一个
parseState
结构体st
,用于存储解析过程中的状态信息。 - 调用
getDelimSlices()
函数将输入数据块b
切分成多个分片,存储在st.slices
数组中。 - 如果切分过程失败或者切分得到的分片数量少于
1
,则返回-1
表示解析失败。 - 将解析的起始分片
st.slices[0]
赋值给b
,并初始化解析状态的缓冲区位置st.bufpos
和栈位置st.stkpos
。 - 将系统调用记录数组指针
calls
和最大记录数量ncalls
存储到解析状态结构体中。 - 使用
getU16()
函数从当前分片中解析系统调用号x->nr
,如果解析失败则返回-1
。 - 如果启用了详细模式,则输出解析的系统调用号。
- 循环解析系统调用的参数,调用
parseArg()
函数解析每个参数,并将结果存储到x->args
数组中。 - 如果参数解析失败,则返回
-1
表示解析失败。
- 定义了一个
- 返回结果:
- 如果解析成功,则返回
0
表示解析完成。 - 如果解析失败,则返回
-1
表示解析失败。
- 如果解析成功,则返回
该函数比较重要的三处逻辑如上面标红的三处函数调用所示,下面我们对其进行详细分析。
-
getDelimSlices()
函数
该函数在上面已经分析过了,在此不再赘述。 -
getU16()
函数
该函数实现在“/TriforceLinuxSyscallFuzzer/parse.c”的第40行。
该函数从提供的数据块中解析出一个16位的无符号整数,通过读取两个连续的字节,并将它们合并为一个整数值。如果成功解析,函数返回解析得到的整数值;否则,返回-1
表示解析失败。该函数的核心是调用了getU8()
函数,而该函数实现在“/TriforceLinuxSyscallFuzzer/parse.c”的第32行。
该函数从提供的数据块中解析出一个8位的无符号整数,从当前位置读取一个字节,并将其存储在指定的变量中。如果成功解析,则返回0
,同时将读取的值存储在提供的变量中;如果到达数据块的末尾,返回-1
表示解析失败。
总而言之,该阶段从种子文件中解析出了系统调用的系统调用号,等待后续使用。
parseArg()
函数
该函数实现在“/TriforceLinuxSyscallFuzzer/sysc.c”的第289行。
该函数根据提供的参数类型解析数据,并调用相应的解析函数来处理。首先,它从数据块中读取一个字节,表示参数类型。然后根据类型值,选择性地调用不同的解析函数来处理参数,并将结果返回。如果解析成功,返回0
;如果解析失败或者遇到未知的参数类型,返回-1
。
总而言之,该阶段用于从种子文件中提取系统调用的系统调用函数的参数。比如我们查看parseArgFile()
解析函数,其实现在“/TriforceLinuxSyscallFuzzer/sysc.c”的第111行。
该函数用于创建临时文件,并将给定数据写入该文件中,并返回文件描述符。其具体逻辑如下。
- 生成临时文件名:
- 函数内部定义了一个静态变量
num
用于生成唯一的文件名编号。 - 使用
snprintf
函数将临时文件名存储在namebuf
数组中。
- 函数内部定义了一个静态变量
- 打开文件并写入数据:
- 使用
open
函数创建一个新文件,如果文件已存在则截断文件内容。 - 文件打开模式为读写,并设置文件权限为
0777
。 - 使用
write
函数将来自bslice
的数据写入文件。 - 若写入或创建文件失败,则打印错误信息并退出程序。
- 使用
- 将文件指针移到开头:
- 使用
lseek
函数将文件指针移动到文件开头,以便后续读取数据。
- 使用
- 赋值文件描述符:
- 将文件描述符赋值给参数
x
,以便后续使用该文件。
- 将文件描述符赋值给参数
- 打印调试信息:
- 若启用了调试模式,则打印文件描述符和文件名以及数据大小的调试信息。
- 数据展示:
- 调用
dumpContents
函数展示文件内容。
- 调用
- 错误处理:
- 如果创建文件或写入数据失败,则使用
perror
函数打印错误信息,并退出程序。
- 如果创建文件或写入数据失败,则使用
- 执行系统调用
该逻辑由doSysRecArr(recs, nrecs);
函数调用实现,而doSysRecArr()
函数实现在“/TriforceLinuxSyscallFuzzer/sysc.c”的第378行。
该函数接收一个系统调用记录数组和记录数量作为输入参数,依次执行每个系统调用,并返回所有调用的执行结果的累加值。而该函数的核心为doSysRec(x + i);
函数调用,因为这才是最终执行系统调用测试用例的核心逻辑,而doSysRec()
函数实现在“/TriforceLinuxSyscallFuzzer/sysc.c”的第371行。
该函数用于执行一个系统调用,使用系统调用号和参数数组作为输入,并返回系统调用的执行结果。所以这就是TriforceAFL和TriforceLinuxSyscallFuzzer配合对操作系统内核进行Fuzz测试的最终逻辑。
2、安装与使用
软件环境 | 硬件环境 | 约束条件 |
---|---|---|
Ubuntu 16.04.3 LTS(内核版本为4.10.0-28-generic) | 使用4个处理器,每个处理器4个内核,共分配16个内核 | 本文所讲解的TriforceAFL源代码于2024.03.14下载 |
具体的软件环境可见“2.1、安装方法”章节所示的软件环境 | 内存16GB | 本文所安装的TriforceAFL源代码于2023.06.05下载 |
硬盘40GB | 本文所讲解的TriforceLinuxSyscallFuzzer源代码于2024.03.14下载 | |
TriforceAFL部署在VMware Pro 17上的Ubuntu 16.04.3系统上(主机系统为Windows 11),硬件环境和软件环境也是对应的VMware Pro 17的硬件环境和软件环境 | 本文所安装的TriforceLinuxSyscallFuzzer源代码于2023.06.05下载 | |
具体的约束条件可见2.1、安装方法章节所示的软件版本约束 |
2.1、安装方法
2.1.1、部署系统依赖组件
2.1.1.1、下载安装Git 2.7.4
- 只需要执行如下命令就可以安装Git 2.7.4:
$ sudo apt-get install git -y
- 然后执行如下命令来查看Git 2.7.4是否安装成功:
$ git --version
- 出现如下图所示的内容即代表安装成功:
2.1.1.2、下载安装Vim 7.4.1689
- 首先执行如下命令安装Vim 7.4.1689:
$ sudo apt install vim
- 然后输入如下命令来查看Vim 7.4.1689是否安装成功:
$ vim
- 出现如下内容即代表安装成功:
2.1.1.3、下载安装Docker 20.10.7
- 首先执行如下命令来更新包管理器:
$ sudo apt update
- 然后执行如下命令来安装Docker 20.10.7的依赖包:
$ sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release
- 然后执行如下命令来添加阿里云Docker 20.10.7镜像源GPG密钥:
$ curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
- 然后执行如下命令来添加阿里云Docker 20.10.7镜像源:
$ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- 更新apt缓存:
$ sudo apt update
- 安装Docker 20.10.7:
$ sudo apt install docker-ce docker-ce-cli containerd.io
- 启动Docker 20.10.7服务:
$ sudo systemctl start docker
- 使用如下命令查看Docker 20.10.7是否安装成功:
$ docker -v
- 可以发现,已经安装成功了:
2.1.1.3、下载安装Make 4.1
- 执行如下命令安装make 4.1:
$ sudo apt install make
- 执行如下命令查看make 4.1是否安装成功:
$ make -v
- 可以发现,已经安装成功了:
2.1.2、具体安装方法
2.1.2.1、通过Docker安装
- 直接使用如下命令拉取作者配置好的TriforceAFL镜像:
$ sudo docker pull moflow/afl-triforce:latest
- 然后使用如下命令查看是否拉取成功:
$ sudo docker images
-
可以看到已经拉取成功了:
-
然后使用如下命令来查看一下拉取的具体内容都包括什么:
$ sudo docker run -it --entrypoint sh moflow/afl-triforce
- 此时我们就已经进入到了拉取的镜像中了,然后输入如下命令来查看一下:
$ ls
- 如下图所示,就是我们拉取下来,并执行的内容(注意,该目录为“TriforceLinuxSyscallFuzzer”):
为了方便后续学习,解释一下这些文件夹/文件的作用,后面不再赘述:
- crash_reports:保存发现的crash
- docs:TriforceAFL的相关文档
- rootTemplate和makeRoot:makeRoot根据rootTemplate中的文件为根文件系统生成ramdisk镜像,然后把driver复制进去,并安排init来执行它
- aflCall.c:发起hypercall,调用startForkserver/getWork/startWork/doneWork
- argfd.c:创建并返回系统调用参数使用的文件描述符
- driver.c:驱动程序负责接收来自AFL的输入,将它们解析为许多系统调用记录,然后执行每个系统调用。驱动程序首先fork出一个子进程,让子进程执行主要的工作,然后等待子进程死亡
- gen.py:生成驱动程序使用的格式的系统调用输入文件
- gen2.py:生成驱动程序使用的格式的系统调用输入文件
- gen2-shapes.txt:生成驱动程序使用的格式的系统调用输入文件
- getSyms:使用runCmd执行cat /proc/kallsyms然后将输出提取到kallsyms文件
- getvmlinux:从bzImage中提取vmlinux文件
- heater.c:调用测试的系统调用
- parse.c:一些解析函数
- runCmd:启动内核并运行命令,如果没有参数它将执行一个shell命令,否则它将运行指定的命令,若要指定命令,应该将此命令存于rootTemplate/bin目录中
- runFuzz:启动Fuzz
- runTest:复现crash时使用
- testAfl.c:复现crash时使用
- sysc.c:生成系统调用参数并发起系统调用
- 然后我们可以执行
cd ..
命令来到上一级,然后执行ls
命令来查看一下此目录的内容:
这个目录中,我们着眼于以下两个文件夹:
- TriforceAFL:对AFL和QEMU进行了patch的Fuzz工具
- TriforceLinuxSyscallFuzzer:可以使用TriforceAFL对操作系统内核进行Fuzz
2.1.2.2、通过源代码安装
- 首先执行如下命令来安装编译所需要的库:
$ sudo apt-get install libtool-bin -y
$ sudo apt-get install libglib2.0-dev -y
$ sudo apt-get install autoconf automake libtool -y
$ sudo apt-get install libffi-dev -y
- 然后执行如下命令来到当前用户的根目录目录:
$ cd ~
- 然后使用如下命令来下载TriforceAFL的源代码:
$ git clone https://github.com/nccgroup/TriforceAFL.git
- 然后进入TriforceAFL源代码目录:
$ cd TriforceAFL/
- 然后然后执行如下命令进行编译:
$ make
-
编译成功:
-
然后再次回到当前用户的根目录中,并使用如下命令来下载TriforceLinuxSyscallFuzzer的源代码:
$ cd ~
$ git clone https://github.com/nccgroup/TriforceLinuxSyscallFuzzer.git
- 然后执行如下命令进入TriforceLinuxSyscallFuzzer的源代码目录:
$ cd TriforceLinuxSyscallFuzzer/
- 然后执行如下命令对TriforceLinuxSyscallFuzzer的源代码进行编译:
$ make
- 编译成功:
2.2、使用方法
对Linux内核进行Fuzz,需要TriforceAFL和TriforceLinuxSyscallFuzzer的相互配合,本文提供了两种使用方法:
- 通过Docker使用
- 通过源代码使用
Docker使用的方法较简单,因为作者已经写好了,只需要把DockerFile拉取到本地,直接运行即可。而通过源代码使用的方法就比较麻烦了,作者的文档写的也不是非常详细,在部署的过程中走了很多弯路,不过最后部署成功。
以上内容都已记录在下方。此外,TriforceAFL部署的系统环境为Ubuntu 16.04.3,因为此版本的Ubuntu较稳定,而在Ubuntu 22.04.2上部署TriforceAFL失败,因为此工具较老,存在很多不兼容的情况。
2.2.1、通过Docker使用
- 使用Docker运行TriforceAFL就很简单,只需要输入如下命令来启动刚才拉取的镜像:
$ sudo docker run -it moflow/afl-triforce
-
可以发现,TriforceAFL已经成功运行,正在进行Fuzz测试(不过通过Docker进行Fuzz测试并不清楚Fuzz测试的目标是什么,也就是说自定义能力不强,故不推荐通过Docker进行Fuzz测试,主要精力还是要集中在如何通过源代码进行Fuzz测试):
-
检测一段时间后,可以按“CTRL+C”结束检测,然后输入如下命令来进入TriforceAFL镜像,查看检测结果:
$ docker run -it --entrypoint sh moflow/afl-triforce
- 然后进入此目录:
$ cd crash_reports/report_compatIpt
- 运行
ls
命令查看检测结果:
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在进行步骤1进行Fuzz测试的时候,出现如下问题:
-
为了解决这个问题,首先进入root用户权限:
$ su
- 然后执行如下命令:
# echo core >/proc/sys/kernel/core_pattern
- 然后退出root用户权限:
# exit
- 执行完以上命令后,回到步骤1重新继续向下操作即可
2.2.2、通过源代码使用
在本章节,演示如何通过源代码的方式使用TriforceAFL进行Fuzz测试,测试的目标是Linux 4.6.2内核。该种使用方法是我们要着重研究的,并且该使用方法是通用的。关于通过源代码的方式使用TriforceAFL对其它版本内核进行Fuzz测试,请参考“3、测试用例”章节的具体内容。
- 首先执行如下命令,安装编译工具和一些工具包:
$ sudo apt-get update
$ sudo apt-get install flex bison libncurses5-dev libssl-dev build-essential openssl -y
- 执行如下命令进入当前用户的根目录:
$ cd ~
- 然后执行如下命令创建文件夹,用来存放待检测的Linux内核并进入此目录:
$ mkdir kern && cd kern
- 然后我们下载一个准备要进行Fuzz的Linux内核:
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.6.2.tar.xz --no-check-certificate
- 然后执行如下命令,解压Linux 4.6.2内核并进入其目录:
$ tar xvf linux-4.6.2.tar.xz && cd linux-4.6.2/
- 然后执行如下命令,进入图形界面:
$ make menuconfig
-
直接退出即可:
-
选择“Yes”:
-
然后执行如下命令编译:
$ make bzImage
-
出现如下内容即代表编译成功:
-
然后使用如下命令,将编译好的bzImage移动到如下目录中:
$ mv ~/kern/linux-4.6.2/arch/x86/boot/bzImage ~/kern/
- 然后执行如下命令拷贝kallsyms:
$ cp /proc/kallsyms ~/kern/
- 然后执行如下命令进入TriforceLinuxSyscallFuzzer源代码目录:
$ cd ~/TriforceLinuxSyscallFuzzer/
- 然后执行如下命令,将刚刚设置好的内容复制到此目录中:
$ cp -r ~/kern/ ~/TriforceLinuxSyscallFuzzer/
- 然后执行如下命令设置环境变量:
$ export K='~/TriforceLinuxSyscallFuzzer/kern'
- 然后在此目录中执行如下命令,以设置输入文件夹:
$ make inputs
- 执行如下命令进行Fuzz:
$ ./runFuzz -M M0
-
可以发现,成功开始对Linux 4.6.2内核进行Fuzz测试了:
-
检测一段时间后,可以按“CTRL+C”结束检测。如果我们想重现测试用例(或者漏洞),可以使用如下三条命令:
$ mkdir -p outputs/crashes/id*
$ sudo ./runTest inputs/ex1
$ sudo ./runTest outputs/crashes/id*
-
可以发现,成功执行,这说明可以成功重现测试用例(或者漏洞):
-
此外,还可以使用
-t
选项在模拟环境中运行驱动程序,使用-vv
选项进行详细日志记录,而无需实际执行系统调用使用-x
选项。只需要执行如下两条命令:
$ ./driver -tvvx < inputs/ex1
$ strace ./driver -t < inputs/ex1
-
同样成功执行了:
-
最后,还可以引导内核并以交互方式运行Fuzz测试,只需要执行如下命令即可:
$ sudo ./runCmd
- 成功运行:
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在进行步骤17进行Fuzz测试的时候,出现如下问题:
-
为了解决这个问题,首先进入root用户权限:
$ su
- 然后执行如下命令:
# echo core >/proc/sys/kernel/core_pattern
- 然后退出root用户权限:
# exit
- 执行完以上命令后,回到步骤17重新继续向下操作即可
B 问题2:
-
在进行步骤17进行Fuzz测试的时候,出现如下问题:
-
出现这个问题,其实是因为我们执行步骤18的命令没有权限,只需要使用如下命令重新操作步骤17即可:
$ sudo ./runFuzz -M M0
- 执行完以上命令后,回到步骤17(不需要再操作步骤17)继续向下操作即可
3、测试用例
3.1、对Linux 4.6.2内核进行Fuzz测试
- 首先执行如下命令,安装编译工具和一些工具包:
$ sudo apt-get update
$ sudo apt-get install flex bison libncurses5-dev libssl-dev build-essential openssl -y
- 执行如下命令进入当前用户的根目录:
$ cd ~
- 然后执行如下命令创建文件夹,用来存放待检测的Linux内核并进入此目录:
$ mkdir kern && cd kern
- 然后我们下载一个准备要进行Fuzz的Linux内核:
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.6.2.tar.xz --no-check-certificate
- 然后执行如下命令,解压Linux 4.6.2内核并进入其目录:
$ tar xvf linux-4.6.2.tar.xz && cd linux-4.6.2/
- 然后执行如下命令,进入图形界面:
$ make menuconfig
-
直接退出即可:
-
选择“Yes”:
-
然后执行如下命令编译:
$ make bzImage
-
出现如下内容即代表编译成功:
-
然后使用如下命令,将编译好的bzImage移动到如下目录中:
$ mv ~/kern/linux-4.6.2/arch/x86/boot/bzImage ~/kern/
- 然后执行如下命令拷贝kallsyms:
$ cp /proc/kallsyms ~/kern/
- 然后执行如下命令进入TriforceLinuxSyscallFuzzer源代码目录:
$ cd ~/TriforceLinuxSyscallFuzzer/
- 然后执行如下命令,将刚刚设置好的内容复制到此目录中:
$ cp -r ~/kern/ ~/TriforceLinuxSyscallFuzzer/
- 然后执行如下命令设置环境变量:
$ export K='~/TriforceLinuxSyscallFuzzer/kern'
- 然后在此目录中执行如下命令,以设置输入文件夹:
$ make inputs
- 然后执行如下命令进入root用户权限:
$ su
- 然后设置核心转储文件的明明模式为core:
# echo core >/proc/sys/kernel/core_pattern
- 然后退出root用户权限:
# exit
- 然后执行如下命令进行Fuzz:
$ sudo ./runFuzz -M M0
- 可以发现,成功开始对Linux 4.6.2内核进行Fuzz测试:
3.2、对Linux 5.6.2内核进行Fuzz测试
- 首先执行如下命令,安装编译工具和一些工具包:
$ sudo apt-get update
$ sudo apt-get install flex bison libncurses5-dev libssl-dev build-essential openssl -y
$ sudo apt-get install libelf-dev -y
- 执行如下命令进入当前用户的根目录:
$ cd ~
- 然后执行如下命令创建文件夹,用来存放待检测的Linux内核并进入此目录:
$ mkdir kern && cd kern
- 然后我们下载一个准备要进行Fuzz的Linux内核:
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.6.2.tar.gz
- 然后执行如下命令,解压Linux 4.6.2内核并进入其目录:
$ tar -zxvf linux-5.6.2.tar.gz && cd linux-5.6.2/
- 然后执行如下命令,进入图形界面:
$ make menuconfig
-
直接退出即可:
-
选择“Yes”:
-
然后执行如下命令编译:
$ make bzImage
-
出现如下内容即代表编译成功:
-
然后使用如下命令,将编译好的bzImage移动到如下目录中:
$ mv ~/kern/linux-5.6.2/arch/x86/boot/bzImage ~/kern/
- 然后执行如下命令拷贝kallsyms:
$ cp /proc/kallsyms ~/kern/
- 然后执行如下命令进入TriforceLinuxSyscallFuzzer源代码目录:
$ cd ~/TriforceLinuxSyscallFuzzer/
- 然后执行如下命令,将刚刚设置好的内容复制到此目录中:
$ cp -r ~/kern/ ~/TriforceLinuxSyscallFuzzer/
- 然后执行如下命令设置环境变量:
$ export K='~/TriforceLinuxSyscallFuzzer/kern'
- 然后在此目录中执行如下命令,以设置输入文件夹:
$ make inputs
- 然后执行如下命令进入root用户权限:
$ su
- 然后设置核心转储文件的明明模式为core:
# echo core >/proc/sys/kernel/core_pattern
- 然后退出root用户权限:
# exit
- 然后执行如下命令进行Fuzz:
$ sudo ./runFuzz -M M0
- 可以发现,成功开始对Linux 5.6.2内核进行Fuzz测试:
4、总结
4.1、部署架构
关于TriforceAFL部署的架构图,如下所示。
对于以上架构图,我们具体来看TriforceAFL是否对其中的组件进行了修改。详情可参见下方的表格。
是否有修改 | 具体修改内容 | 备注 | |
---|---|---|---|
主机内核 | 无 | 无 | 无 |
主机操作系统 | 无 | 无 | 无 |
Guest内核 | 无 | 无 | 无 |
Guest操作系统 | 有 | 将名为“driver”的驱动程序加载到Guest操作系统中 | 目的是进行Fuzz测试 |
由于使用的是由作者直接提供的Guest操作系统,故无法得知其具体修改内容 | 无 | ||
虚拟机监视器QEMU | 有 | 由于使用的是由作者直接提供的虚拟机监视器QEMU,故无法得知其具体修改内容 | 无 |
4.2、漏洞检测对象
- 检测的对象为Guest内核
- 针对的内核版本为Linux 4.6.2和Linux 5.6.2
- 针对的漏洞类型为崩溃性错误
4.3、漏洞检测方法
- 将系统调用信息(测试用例)打包为二进制程序
- 将二进制程序重新解析为系统调用信息
- 使用
syscall()
函数(其函数原型为long syscall(long number, ...);
)执行系统调用,从而对内核进行Fuzz测试 - 将测试结果保存到主机中
- 目前可以进行测试的系统调用共5个,包括:
read
write
open
writev
execve
4.4、种子生成/变异技术
- 初始种子(二进制程序)由TriforceLinuxSyscallFuzzer生成
- 基于位图反馈,对种子进行变异
- 变异的策略基于随机,即随机对种子的二进制位进行翻转、算术增/减和拼接等
5、参考文献
- 内核漏洞挖掘技术系列(6)——使用AFL进行内核漏洞挖掘(1)
- GitHub - nccgroup/TriforceAFL
- nccgroup/TriforceLinuxSyscallFuzzer
- AFL二三事——源码分析
- AFL速通——流程及afl-fuzz.c源码简析
- [原创]漏洞挖掘技术之 AFL 项目分析
- 翻译afl-fuzz白皮书 - 简书
- AFL源码阅读(一):启程
- AFL源码阅读(二):Main Payload 汇编
- AFL源码阅读(三):afl-tmin
- AFL源码阅读(四):两个工具
- AFL源码阅读(五):fuzzer启动
- AFL源码阅读(六):队列、变异、同步
- AFL源码阅读(七):如何修改 AFL
- AFL 及其相关拓展项目总结
总结
以上就是本篇博文的全部内容,可以发现,TriforceAFL的部署与使用的过程并不复杂,不过其原理较为复杂,但是我对其进行了详细的源码分析,不得不感慨TriforceAFL的作者太强了,能写出这么好的工具。总而言之,TriforceAFL是一个不错的Fuzz测试的工具,值得大家学习。相信读完本篇博客,各位读者一定对TriforceAFL有了更深的了解。