文章目录
- 前言
- 1、概述
- 2、安装与使用
-
- 2.1、源码安装
-
- 2.1.1、部署系统依赖组件
-
- [2.1.1.1、下载安装Git 2.7.4](#2.1.1.1、下载安装Git 2.7.4)
- [2.1.1.2、下载安装Vim 7.4.1689](#2.1.1.2、下载安装Vim 7.4.1689)
- 2.1.2、使用源码安装系统
- 2.2、使用方法
- 3、测试用例
-
- [3.1、对Linux 4.4.0-87-generic内核进行Fuzz测试](#3.1、对Linux 4.4.0-87-generic内核进行Fuzz测试)
- 4、总结
- 5、参考文献
- 总结
前言
本博客的主要内容为kAFL的部署、使用与原理分析。本博文内容较长,因为涵盖了kAFL的几乎全部内容,从部署的详细过程到如何使用kAFL对目标程序进行Fuzz测试,以及对kAFL进行漏洞检测的原理分析,相信认真读完本博文,各位读者一定会对kAFL有更深的了解。以下就是本篇博客的全部内容了。
1、概述
几十年来,许多类型的内存安全漏洞一直在危害软件系统。在其它方法中,Fuzz测试是一种很有前途的技术,可以揭示各种软件故障。近些年来,反馈引导的Fuzz测试展示了它的能力,发现了源源不断的安全关键软件漏洞。大多数Fuzz测试处理,尤其是反馈Fuzz测试处理,仅限于操作系统(OS)的用户空间组件,尽管内核组件中的错误更严重,因为它们允许攻击者以完全权限访问系统。不幸的是,内核组件很难进行Fuzz测试,因为反馈机制(即引导代码覆盖)无法轻易应用。此外,由于中断、内核线程、状态性和类似机制造成的不确定性也会带来问题。此外,如果一个进程Fuzz了它自己的内核,内核崩溃会严重影响Fuzz测试的性能,因为操作系统需要重新启动。
基于以上问题,德国波鸿鲁尔大学的研究团队提出以一种独立于操作系统和硬件辅助的方式来解决覆盖引导的内核Fuzz测试问题:他们使用了一个系统管理程序和Intel的处理器跟踪(PT)技术。该技术允许将反馈Fuzz测试应用于任意(甚至是闭源)基于x86-64的内核,而不需要任何自定义的ring 0目标代码,甚至根本不需要特定于操作系统的代码。并基于此研究思路,实现了kernel-AFL(kAFL)的设计和实现,这是作者提出的技术的原型实现。由于新的CPU功能,反馈生成的开销非常小(不到5%):Intel的处理器跟踪(PT)技术提供了有关运行代码的控制流信息。作者使用这些信息来构建类似于AFL的instrumentation的反馈机制。这使kAFL能够在现成的笔记本电脑(Thinkpad T460p、i7-6700HQ和32 GB RAM)上为简单的目标驱动程序每秒获得多达17000次执行。此外,作者还描述了一种有效的方法来处理内核Fuzz测试过程中出现的非确定性。由于采用了模块化设计,kAFL可扩展到Fuzz测试任何x86/x86-64操作系统。现在已经将kAFL应用于Linux、macOS和Windows,并在这些操作系统的内核驱动程序中发现了多个以前未知的错误。
kAFL工具原理较为复杂,而且部署的过程也十分麻烦,但是最终的性能和检测结果要比之前部署过的TriforceAFL效果要好。总之,kAFL是一个值得学习的Fuzz测试工具。此外,kAFL工具基于Python语言和C语言开发。
1.1、工作原理
kAFL系统包括三个组件:
- Fuzz逻辑
- 虚拟机基础设施(QEMU和KVM的修改版本,用QEMU-PT和KVM-PT表示)
- 用户代理模式
这些组件相互配合协作,组成了kAFL的Fuzz测试系统。kAFL体系结构示意图如下所示:
下面对这三个组件进行简单介绍:
-
虚拟机基础设施
虚拟机基础设施由一个ring 3组件(QEMU-PT)和一个ring 0组件(KVM-PT)组成。这有助于其它两个组件之间的通信,并使Intel PT跟踪数据以用于Fuzz逻辑。通常,GuestOS仅通过超调用与主机进行通信。然后,主机可以读取和写入GuestOS内存,并在处理完请求后继续执行虚拟机。
-
Fuzz逻辑
Fuzz逻辑是kAFL的命令和控制组件。它管理感兴趣的输入队列,创建变异输入,并安排它们进行评估。在大多数方面,它是基于AFL使用的算法。与AFL类似,使用位图来存储基本块转换。通过QEMU-PT的接口从虚拟机收集AFL位图,并决定哪些输入触发了有趣的行为。Fuzz逻辑还协调并行生成的虚拟机的数量。与AFL更大的设计差异之一是,kAFL广泛使用了多处理和并行性,其中AFL只是产生多个独立的Fuzzer,这些Fuzzer偶尔同步它们的输入队列。相反,kAFL并行执行确定性阶段,所有线程都处理最有趣的输入。大量时间花在不受CPU限制的任务上(例如延迟执行的GuestOS)。因此,由于每个内核的CPU负载更高,使用许多并行进程(每个CPU内核最多5-6个)大大提高了Fuzz处理的性能。最后,Fuzz逻辑与用户界面通信,以定期显示当前统计信息。
-
用户代理模式
期望用户模式代理在虚拟化的目标操作系统中运行。原则上,这个组件只需要通过超调用接口通过Fuzz逻辑同步和收集新的输入,并使用它与主机的内核进行交互。示例代理是试图将输入装载为文件系统映像的程序,将证书等特定文件传递给内核解析器,甚至执行各种系统调用链。
理论上,只需要一个这样的组件。在实践中,作者使用两个不同的组件:第一个程序是加载器组件。它的工作是通过超调用接口接受任意二进制文件。此二进制文件表示用户模式代理,并由加载器组件执行。此外,加载器组件将检查代理是否已崩溃(在系统调用Fuzz测试的情况下经常发生这种情况),并在必要时重新启动它。这种设置的优点是,可以将任何二进制文件传递给虚拟机,并为不同的Fuzz组件重用虚拟机快照。
另外,还需要注意的是,Fuzz逻辑使用QEMU-PT与KVM-PT交互以生成目标虚拟机。KVM-PT允许跟踪单个vCPU,而不是逻辑CPU。在CPU切换到GuestOS执行之前,此组件在相应的逻辑CPU上配置并启用Intel PT,并在虚拟机退出转换期间禁用跟踪。这样,关联的CPU将只提供虚拟化内核本身的跟踪数据。QEMU-PT用于与KVM-PT接口交互,从用户空间配置和切换Intel PT,并访问输出缓冲区以解码跟踪数据。解码的跟踪数据被直接转换为执行的条件分支指令的地址流。此外,QEMU-PT还基于非确定性基本块的先前知识对执行的地址流进行过滤,以防止假阳性Fuzz测试结果,并使这些Fuzz测试结果作为AFL兼容位图可用于Fuzz逻辑,此将Intel-PT跟踪转换为kAFL位图的流水线示意如下图所示。作者使用自己的自定义Intel PT解码器来缓存反汇编结果,与Intel提供的现成解决方案相比,这带来了显著的性能提升。
我们还可以将上面的过程细分,这就得到了下图。下面这张图表示在Fuzz测试期间kAFL发生的事件和通信,即这是kAFL的具体工作流程:
当虚拟机启动时,用户模式代理的第一部分(加载程序)使用超调用HC_SUBMIT_PANIC
将内核死机处理程序的地址(或Windows中的BugCheck内核地址)提交给QEMU-PT①。然后,QEMU-PT在死机处理程序的地址处修补一个超调用例程。这使我们能够得到通知,并对虚拟机中的crash做出快速反应(而不是等待超时/重新启动)。
然后,加载程序使用超调用HC_GET_PROGRAM
来请求实际的用户模式代理,并启动它②。现在,加载程序设置完成,Fuzzer开始初始化。代理触发将由KVM-PT处理的HC_SUBMIT_CR3
超调用。系统管理程序提取当前运行的进程的CR3值,并将其交给QEMU-PT进行过滤③。最后,代理使用超调用HC_SUBMIT_BUFFER
来通知主机它期望其输入的地址。Fuzzer设置现已完成,主Fuzz循环开始。
在主循环期间,代理使用HC_GET_INPUT
超调用④请求新的输入。Fuzz逻辑产生一个新的输入,并将其发送到QEMU-PT。由于QEMU-PT可以完全访问GuestOS的内存空间,因此它可以简单地将输入复制到代理指定的缓冲区中。然后,它执行VM-Entry以继续执行虚拟机⑤。同时,此VM-Entry事件启用PT跟踪机制。代理现在消耗输入并与内核交互(例如,它将输入解释为文件系统映像,并尝试挂载它⑥)。当内核被Fuzz时,QEMU-PT对跟踪数据进行解码,并根据需要更新位图。一旦交互完成,内核将控制权交还给代理,代理就会通过HC_FINISHED
超调用通知系统管理程序。所得到的VM-Exit停止跟踪,并且QEMU-PT对剩余的跟踪数据⑦进行解码。所得到的位图被传递到用于进一步处理的逻辑⑧。之后,在发出另一个HC_GET_INPUT
以开始下一个循环迭代之前,代理可继续运行任何未跟踪的清理例程。
1.2、工作流程
kAFL的工作流程如下图所示,只有搞清楚kAFL的工作原理后,对于本章节的分析才能游刃有余(具体可参见"1.1、工作原理"章节)。值得注意的是,kAFL的工作流程非常复杂,故将其工作流程分为不同的阶段,在分析时可对照本图。以下就将对kAFL的整个工作流程进行详细分析。
1.2.1、部署kAFL
在该阶段,我们只做了一件事情。即下载kAFL源代码,并使用sudo ./install.sh
命令部署kAFL。很明显,"install.sh"脚本才是部署kAFL的核心。故下面我们就从"/kAFL/install.sh"脚本开始对kAFL工作流程的分析。"install.sh"脚本的全部代码如下所示。
powershell
LINUX_VERSION="4.6.2"
LINUX_URL="https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-$LINUX_VERSION.tar.xz"
LINUX_MD5="70c4571bfb7ce7ccb14ff43b50165d43"
QEMU_VERSION="2.9.0"
QEMU_URL="http://download.qemu-project.org/qemu-2.9.0.tar.xz"
QEMU_MD5="86c95eb3b24ffea3a84a4e3a856b4e26"
echo "================================================="
echo " kAFL setup script "
echo "================================================="
echo
echo "[*] Performing basic sanity checks..."
if [ ! "$(uname -s)" = "Linux" ]; then
echo "[-] Error: KVM-PT is supported only on Linux ..."
exit 1
fi
if ! [ -f /etc/lsb-release ]; then
echo "[-] Error: Please use Ubuntu (16.04) ..."
exit 1
fi
for i in dpkg; do
T=$(which "$i" 2>/dev/null)
if [ "$T" = "" ]; then
echo "[-] Error: '$i' not found, please install first."
exit 1
fi
done
echo "[*] Installing essentials tools ..."
sudo -Eu root apt-get install make gcc libcapstone-dev bc libssl-dev python-pip python-pygraphviz -y gnuplot ruby python libgtk2.0-dev libc6-dev flex -y >/dev/null
echo "[*] Installing build dependencies for QEMU $QEMU_VERSION ..."
sudo -Eu root apt-get build-dep qemu-system-x86 -y >/dev/null
echo "[*] Installing python essentials ..."
sudo -Eu root pip2.7 install mmh3 lz4 psutil >/dev/null 2>/dev/null
echo
echo "[*] Downloading QEMU $QEMU_VERSION ..."
wget -O qemu.tar.gz $QEMU_URL 2>/dev/null
echo "[*] Checking signature of QEMU $QEMU_VERSION ..."
CHKSUM=$(md5sum qemu.tar.gz | cut -d' ' -f1)
if [ "$CHKSUM" != "$QEMU_MD5" ]; then
echo "[-] Error: signature mismatch..."
exit 1
fi
echo "[*] Unpacking QEMU $QEMU_VERSION ..."
tar xf qemu.tar.gz
echo "[*] Patching QEMU $QEMU_VERSION ..."
patch qemu-$QEMU_VERSION/hmp-commands.hx <QEMU-PT/hmp-commands.hx.patch >/dev/null
patch qemu-$QEMU_VERSION/monitor.c <QEMU-PT/monitor.c.patch >/dev/null
patch qemu-$QEMU_VERSION/hmp.c <QEMU-PT/hmp.c.patch >/dev/null
patch qemu-$QEMU_VERSION/hmp.h <QEMU-PT/hmp.h.patch >/dev/null
patch qemu-$QEMU_VERSION/Makefile.target <QEMU-PT/Makefile.target.patch >/dev/null
patch qemu-$QEMU_VERSION/kvm-all.c <QEMU-PT/kvm-all.c.patch >/dev/null
patch qemu-$QEMU_VERSION/vl.c <QEMU-PT/vl.c.patch >/dev/null
patch qemu-$QEMU_VERSION/configure <QEMU-PT/configure.patch >/dev/null
patch qemu-$QEMU_VERSION/linux-headers/linux/kvm.h <QEMU-PT/linux-headers/linux/kvm.h.patch >/dev/null
patch qemu-$QEMU_VERSION/include/qom/cpu.h <QEMU-PT/include/qom/cpu.h.patch >/dev/null
mkdir qemu-$QEMU_VERSION/pt/ 2>/dev/null
cp QEMU-PT/compile.sh qemu-$QEMU_VERSION/
cp QEMU-PT/hmp-commands-pt.hx qemu-$QEMU_VERSION/
cp QEMU-PT/pt.c qemu-$QEMU_VERSION/
cp QEMU-PT/pt.h qemu-$QEMU_VERSION/
cp QEMU-PT/pt/tmp.objs qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/decoder.h qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/hypercall.c qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/logger.h qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/khash.h qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/memory_access.h qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/tnt_cache.c qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/interface.h qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/interface.c qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/memory_access.c qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/logger.c qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/decoder.c qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/filter.h qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/hypercall.h qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/tnt_cache.h qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/filter.c qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/disassembler.c qemu-$QEMU_VERSION/pt/
cp QEMU-PT/pt/disassembler.h qemu-$QEMU_VERSION/pt/
patch -p1 qemu-$QEMU_VERSION/hw/misc/applesmc.c <QEMU-PT/applesmc_patches/v1-1-3-applesmc-cosmetic-whitespace-and-indentation-cleanup.patch
patch -p1 qemu-$QEMU_VERSION/hw/misc/applesmc.c <QEMU-PT/applesmc_patches/v1-2-3-applesmc-consolidate-port-i-o-into-single-contiguous-region.patch
patch -p1 qemu-$QEMU_VERSION/hw/misc/applesmc.c <QEMU-PT/applesmc_patches/v1-3-3-applesmc-implement-error-status-port.patch
echo "[*] Compiling QEMU $QEMU_VERSION ..."
cd qemu-$QEMU_VERSION
echo "-------------------------------------------------"
sh compile.sh
echo "-------------------------------------------------"
cd ..
echo
echo "[*] Downloading Kernel $LINUX_VERSION ..."
wget -O kernel.tar.gz $LINUX_URL 2>/dev/null
echo "[*] Checking signature of Kernel $LINUX_VERSION ..."
CHKSUM=$(md5sum kernel.tar.gz | cut -d' ' -f1)
if [ "$CHKSUM" != "$LINUX_MD5" ]; then
echo "[-] Error: signature mismatch..."
echo "$CHKSUM"
echo "$LINUX_MD5"
exit 1
fi
echo "[*] Unpacking Kernel $LINUX_VERSION ..."
tar xf kernel.tar.gz
echo "[*] Patching Kernel $LINUX_VERSION ..."
patch linux-$LINUX_VERSION/arch/x86/kvm/Makefile <KVM-PT/arch/x86/kvm/Makefile.patch >/dev/null
patch linux-$LINUX_VERSION/arch/x86/kvm/Kconfig <KVM-PT/arch/x86/kvm/Kconfig.patch >/dev/null
patch linux-$LINUX_VERSION/arch/x86/kvm/vmx.c <KVM-PT/arch/x86/kvm/vmx.c.patch >/dev/null
patch linux-$LINUX_VERSION/arch/x86/kvm/svm.c <KVM-PT/arch/x86/kvm/svm.c.patch >/dev/null
patch linux-$LINUX_VERSION/arch/x86/kvm/x86.c <KVM-PT/arch/x86/kvm/x86.c.patch >/dev/null
patch linux-$LINUX_VERSION/arch/x86/include/asm/kvm_host.h <KVM-PT/arch/x86/include/asm/kvm_host.h.patch >/dev/null
patch linux-$LINUX_VERSION/arch/x86/include/uapi/asm/kvm.h <KVM-PT/arch/x86/include/uapi/asm/kvm.h.patch >/dev/null
patch linux-$LINUX_VERSION/include/uapi/linux/kvm.h <KVM-PT/include/uapi/linux/kvm.h.patch >/dev/null
cp KVM-PT/arch/x86/kvm/vmx.h linux-$LINUX_VERSION/arch/x86/kvm/
cp KVM-PT/arch/x86/kvm/vmx_pt.h linux-$LINUX_VERSION/arch/x86/kvm/
cp KVM-PT/arch/x86/kvm/vmx_pt.c linux-$LINUX_VERSION/arch/x86/kvm/
mkdir linux-$LINUX_VERSION/usermode_test/ 2>/dev/null
cp KVM-PT/usermode_test/support_test.c linux-$LINUX_VERSION/usermode_test/
cp KVM-PT/usermode_test/test.c linux-$LINUX_VERSION/usermode_test/
echo "[*] Compiling Kernel $LINUX_VERSION ..."
cd linux-$LINUX_VERSION/
yes "" | make oldconfig >oldconfig.log
if [ ! "$(grep \"CONFIG_KVM_VMX_PT=y\" .config | wc -l)" = "1" ]; then
echo "CONFIG_KVM_VMX_PT=y" >>.config
fi
echo "-------------------------------------------------"
make -j 8
echo "-------------------------------------------------"
echo "KERNEL==\"kvm\", GROUP=\"kvm\"" | sudo -Eu root tee /etc/udev/rules.d/40-permissions.rules >/dev/null
sudo -Eu root groupadd kvm
sudo -Eu root usermod -a -G kvm $USER
sudo -Eu root service udev restart
sudo -Eu root make modules_install
sudo -Eu root make install
cd ../
echo
echo "[*] Done! Please reboot your system now!"
这段脚本有些长,其中进行了很多操作用于构建kAFL,下面我们将该脚本分为几个阶段和关键点来进行分析。
- 阶段一:基本检查和准备工作
- 系统检查
- 使用
uname -s
命令检查系统是否为Linux系统 - 使用"/etc/lsb-release"文件检查系统是否为Ubuntu 16.04
- 使用
- 工具检查
- 检查是否安装了dpkg等基本工具
- 系统检查
- 阶段二:安装必要的软件和依赖项
- 安装基本工具
- 使用
apt-get
命令安装各种基本工具和依赖项,例如make
、gcc
、libcapstone-dev
等
- 使用
- 安装QEMU的构建依赖
- 使用
apt-get build-dep
命令安装QEMU构建所需的依赖项
- 使用
- 安装Python依赖
- 使用
pip2.7
命令安装Python依赖,例如mmh3、lz4、psutil等
- 使用
- 安装基本工具
- 阶段三:下载和配置QEMU源码
- 下载和解压QEMU源码
- 使用wget下载指定版本(2.9.0)的QEMU源码,并解压。
- 检查QEMU源码的签名
- 使用MD5校验和检查下载的QEMU源码的完整性
- 补丁QEMU源码
- 使用
patch
命令应用一系列patch文件,修改QEMU源码以支持特定功能
- 使用
- 复制相关文件到QEMU源码目录
- 复制特定文件到QEMU源码目录中,以便编译和使用。
- 编译QEMU
- 进入QEMU源码目录,执行"compile.sh"脚本编译QEMU。
- 下载和解压QEMU源码
- 阶段四:下载和配置Linux内核源码
- 下载和解压Linux内核源码
- 使用wget下载指定版本(4.6.2)的Linux内核源码,并解压。
- 检查Linux内核源码的签名
- 使用MD5校验和检查下载的Linux内核源码的完整性。
- 补丁Linux内核源码
- 使用
patch
命令应用一系列patch文件,修改Linux内核源码以支持特定功能。
- 使用
- 编译Linux内核
- 进入Linux内核源码目录,执行编译。
- 下载和解压Linux内核源码
- 阶段五:设置udev规则和权限
- 设置udev规则和权限
- 创建udev规则,设置kvm设备的组和权限。
- 设置udev规则和权限
- 规则六:完成提示
- 输出完成提示信息,提示用户重新启动系统
可以发现,在该阶段做的最重要的事情实际只有两个,即阶段三和阶段四,这两个阶段分别给QEMU和Linux打补丁,并对其进行编译,至于具体打了什么补丁,在该阶段我们并不介绍,后续用到的时候我们再做介绍。关于其它部分都是比较常见且简单的命令,我们在此就不做过多介绍了。
1.2.2、准备工作
1.2.2.1、准备主机代理内核
在该阶段,我们只做了一件事情,即将当前操作系统的内核更换为上一阶段打好补丁并编译好的Linux 4.6.2内核,因为只有该Linux内核才能与待Fuzz目标进行通信,从而实现Fuzz测试。故本阶段要在我们的主机上配置好代理Linux内核,等待后续使用。
1.2.2.2、准备待Fuzz目标
在该阶段,我们需要准备一个虚拟机(即待Fuzz目标),注意要使用打好补丁并编译好的QEMU来启动该虚拟机,并对其进行一些操作。具体来说,该阶段工作了如下几件事情。
- 下载Ubuntu 16.04.3的ISO文件
- 使用打好补丁并编译好的QEMU安装Ubuntu 16.04.3系统(其Linux内核为4.4.0-87-generic,这就是我们准备进行Fuzz测试的目标)
- 在安装好的虚拟机中下载kAFL源代码
- 在kAFL源代码的对应目录中执行编译操作
- 关闭该虚拟机
该阶段的前三步骤以及最后一步骤并没有什么需要解释的,在这里要说明的是第四步骤(上面标红的部分),即在虚拟机中的kAFL源代码的对应目录中执行编译操作。
这里的"对应目录"是指"/kAFL/kAFL-Fuzzer/agents/"目录,我们编译的脚本为"/kAFL/kAFL-Fuzzer/agents/compile.sh"。该脚本的源代码如下图所示。
在这里我们主要关注上图中红框和红箭头处的代码,因为我们Fuzz的目标为Linux。这部分代码也很简单,其目的是进入到当前目录的"linux_x86_64"目录中,并执行"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/compile.sh"脚本。该脚本的具体内容如下所示。
该脚本比较简单,其目的主要是在虚拟机系统中编译一些二进制文件。具体来说,该脚本共编译如下几个二进制文件。
- "/kAFL/kAFL-Fuzzer/agents/linux_x86_64/loader/loader"
- "/kAFL/kAFL-Fuzzer/agents/linux_x86_64/info/info"
- "/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/kafl_vuln_test"
- "/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/ext4"
- "/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/ntfs"
- "/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/fat"
1.2.2.3、配置待Fuzz目标
在该阶段,我们主要来配置我们上面刚刚部署好的虚拟机(即待Fuzz目标),并且执行一些刚刚编译好的脚本以及进行对应的操作。具体来说,本阶段做的事情包括如下几点。
- 基于创建好的虚拟机创建一个快照(即"overlay_0.qcow2")
- 创建一个新的虚拟磁盘(即"ram.qcow2")
- 使用新创建的快照和新创建的虚拟磁盘再次启动虚拟机系统
- 执行"/kAFL/kAFL-Fuzzer/vuln_drivers/simple/linux_x86-64/load.sh"脚本
- 执行"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/loader/loader"二进制文件
- 使用QEMU管理控制台执行
savevm kafl
命令来保存快照 - 使用QEMU管理控制台执行
q
命令来退出虚拟机系统
该阶段的前三步骤和最后步骤都是很常规且很简单的操作,我们不再赘述。我们要强调的是上面标红的三个步骤。
- 执行"/kAFL/kAFL-Fuzzer/vuln_drivers/simple/linux_x86-64/load.sh"脚本
该脚本的具体内容如下所示:
这个脚本是一个Bash脚本,用于编译并加载一个内核模块。以下是该脚本的主要功能:
- 脚本首先检查当前用户是否为root用户,如果不是则打印提示信息并退出脚本。
- 如果当前用户是root用户,则执行make命令来编译项目中的代码。
- 接着使用insmod命令来加载编译好的内核模块kafl_vuln_test.ko。
- 最后打印
done
信息,并退出脚本。
这个脚本确保了在加载内核模块之前先编译它,并且只允许root用户执行这些操作。这里的核心逻辑为上面标红的部分,即执行了make
命令,该命令会去执行"/kAFL/kAFL-Fuzzer/vuln_drivers/simple/linux_x86-64/Makefile"文件。
当我们执行make
命令后,会自动构建该文件中的all
目标。具体来说,执行make
命令后,会根据Makefile中的规则,编译当前目录下的"kafl_vuln_test.o"内核模块,并将其输出到指定的内核构建目录(即当前目录)中。最终会在当前目录中生成"kafl_vuln_test.ko"内核模块文件
- 执行"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/loader/loader"二进制文件
要分析执行该二进制文件都做了什么,我们就要分析其源代码,而且还要从主函数开始分析。故我们来看实现在"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/loader/loader.c"的第74行的main()
函数。
该函数的作用是检查特权、获取内核符号地址、为Fuzzer分配虚拟内存空间、执行超级调用以生成虚拟机快照并终止QEMU的执行,然后执行Fuzzer的入口点部分,包括进行初始握手、提交内核panic函数地址和可能的KASAN函数地址,以及等待获取Fuzzer数据并执行Fuzzer程序。以下是其主要逻辑。
- 获取特权检查:
geteuid()
函数用于检查当前用户是否具有特权,即是否是root用户。如果不是root用户,则输出提示信息,要求以root身份运行程序,并返回1,表示程序执行失败。
- 获取内核符号地址:
panic_handler = get_address("T panic\n");
:调用get_address()
函数获取内核的panic
函数的地址,并将地址存储在panic_handler
变量中。kasan_handler = get_address("t kasan_report_error\n");
:同样,调用get_address()
函数获取内核的kasan_report_error
函数的地址,并将地址存储在kasan_handler
变量中。
- 输出内核符号地址:
- 如果成功获取了
panic
和kasan_report_error
函数的地址,则分别打印出来。
- 如果成功获取了
- 为Fuzzer分配虚拟内存:
- 使用
mmap()
函数为Fuzzer分配4MB的连续虚拟内存空间,并将其起始地址设为0xabcd0000
。分配的内存具有读写权限,并且是私有的、匿名的。 - 使用
memset()
函数将该虚拟内存区域填充为0xff
,以确保该虚拟内存在物理内存中真实存在。
- 使用
- 执行超级调用:
- 调用
kAFL_hypercall()
函数执行超级调用HYPERCALL_KAFL_LOCK
,这将生成一个虚拟机快照供Fuzzer使用,并冻结QEMU的执行。
- 调用
- Fuzzer入口点:
- 首先进行Fuzzer的初始握手,调用
kAFL_hypercall()
函数执行超级调用HYPERCALL_KAFL_ACQUIRE
和HYPERCALL_KAFL_RELEASE
。 - 提交内核
panic
函数地址,调用kAFL_hypercall()
函数执行超级调用HYPERCALL_KAFL_SUBMIT_PANIC
,将panic_handler
地址传递给超级调用。 - 如果成功获取了
kasan_report_error
函数的地址,则提交该地址,调用kAFL_hypercall()
函数执行超级调用HYPERCALL_KAFL_SUBMIT_KASAN
。 - 提交虚拟内存区域的地址,并等待获取Fuzzer数据,这是一个阻塞操作,调用
kAFL_hypercall()
函数执行超级调用HYPERCALL_KAFL_GET_PROGRAM
,并将虚拟内存区域的地址传递给超级调用。 - 最后,执行Fuzzer程序,调用
load_programm()
函数。
- 首先进行Fuzzer的初始握手,调用
- 结束程序:
- 返回 0,表示程序成功执行完毕。
该函数的核心逻辑是上面标红和标绿的两部分,我们在这里暂时先只分析标红的部分,因为标绿的逻辑是后续才要执行的(需要通过标红的逻辑冻结虚拟机后,再进行其它操作,最后才能执行标绿的逻辑)。
在该处标红的逻辑,是由kAFL_hypercall(HYPERCALL_KAFL_LOCK, 0);
函数调用实现的,在这里我们需要注意传入的HYPERCALL_KAFL_LOCK
参数,因为这是后面分析的重点。kAFL_hypercall()
函数实现在"/kAFL/kAFL-Fuzzer/agents/kafl_user.h"的第44行。
这个函数的作用是允许用户在用户空间执行一个hypercall,通过将参数加载到寄存器中,并使用vmcall
指令触发hypercall的执行。那这里究竟执行的哪个hypercall呢?此时刚刚传入的HYPERCALL_KAFL_LOCK
参数就排上用场了,最后我们执行的hypercall就是传入的HYPERCALL_KAFL_LOCK
参数对应的hypercall,不过在执行对应的hypercall之前,还有一层封装逻辑,即作为QEMU-2.9.0的补丁存在,其原始实现在"/kAFL/QEMU-PT/kvm-all.c.patch"的第89行(后续若在遇到类似的函数调用,不再赘述)。
很明显,在这里其最终调用了handle_hypercall_kafl_lock()
函数,故handle_hypercall_kafl_lock()
函数就是HYPERCALL_KAFL_LOCK
参数对应的hypercall。而handle_hypercall_kafl_lock()
函数实现在"/kAFL/QEMU-PT/pt/hypercall.c"的第345行。
该函数是用来处理超级调用HYPERCALL_KAFL_LOCK
的。在这个函数中,首先会输出一条信息提示用户虚拟机已经暂停,并且立即调用vm_stop
函数将虚拟机状态设置为RUN_STATE_PAUSED
(表示当前虚拟机被冻结),以便创建快照。
- 使用QEMU管理控制台执行
savevm kafl
命令来保存快照
当我们在上一步冻结虚拟机后,就需要在QEMU的管理控制台执行savevm kafl
命令来将上一步冻结的虚拟机保存为快照,那么这个快照被保存到哪里了呢?还记得我们之前创建的新的虚拟磁盘(即"ram.qcow2")吗?该虚拟磁盘是空的,专门用来存储快照,故在这步创建的"kafl"快照就被保存到"ram.qcow2"虚拟磁盘中,等待后续使用。
故本阶段最终得到了一个名为"ram.qcow2"的虚拟磁盘,该虚拟磁盘保存了待Fuzz目标的快照,且保存时的状态就是执行完上面标红部分的逻辑,等待绿色部分逻辑的执行。最终我们就要对该状态快照(即待Fuzz目标)进行Fuzz测试,即执行上面绿色部分的逻辑。不过这都是后话,我们后面再介绍。
1.2.2.4、配置kAFL组件
在该阶段,我们的主要任务是对主机中的kAFL做最后的配置,因为即将就要开始对目标进行Fuzz测试了。具体来说,本阶段主要做了如下几件事情。
- 设置打好补丁的QEMU的目录变量(位于"/opt/code/kAFL/kAFL-Fuzzer/kafl.ini"配置文件中)
- 在kAFL源代码的对应目录中执行编译操作
- 检查待Fuzz目标是否可以成功初始化
在该阶段中,比较重要的是最后两个步骤。我们依次介绍。
- 在kAFL源代码的对应目录中执行编译操作
这里的"对应目录"是指"/kAFL/kAFL-Fuzzer/agents/"目录,我们编译的脚本为"/kAFL/kAFL-Fuzzer/agents/compile.sh"。该脚本的源代码如下图所示。
在这里我们主要关注上图中红框和红箭头处的代码,因为我们Fuzz的目标为Linux。这部分代码也很简单,其目的是进入到当前目录的"linux_x86_64"目录中,并执行"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/compile.sh"脚本。该脚本的具体内容如下所示。
该脚本比较简单,其目的主要是在虚拟机系统中编译一些二进制文件。具体来说,该脚本共编译如下几个二进制文件。
-
"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/loader/loader"
-
"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/info/info"
-
"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/kafl_vuln_test"
-
"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/ext4"
-
"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/ntfs"
-
"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/fat"
-
检查待Fuzz目标是否可以成功初始化
在该步骤,我们在"/kAFL/kAFL-Fuzzer"执行了
sudo python kafl_info.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/info/info 512 -v
命令。这条命令的作用是使用"kafl_info.py"脚本获取指定快照的信息。故我们来看实现在/kAFL/kAFL-Fuzzer/kafl_info.py
的第42行的主函数。
在该函数中,只做了一件事情,即调用实现在/kAFL/kAFL-Fuzzer/kafl_info.py
的第27行的main()
函数。
该函数主要用于打印帮助信息、自检、以及启动核心功能模块。其具体功能包括如下几点。
- 打开文件"help.txt"并逐行读取其中的内容,然后将内容打印输出到控制台。
- 打印带有颜色标记的程序名称和说明信息到控制台。
- 调用
self_check()
函数进行自检,如果自检失败,则返回错误码1
。 - 导入"info.core"模块,并调用其中的
start()
函数。
很明显,在该函数中最重要的逻辑就是上面标红的部分,即调用了实现在"/kAFL/kAFL-Fuzzer/info/core.py"的第29行的start()
函数。
python
def start():
config = InfoConfiguration()
if not post_self_check(config):
return -1
if config.argument_values['v']:
enable_logging()
log_info("Dumping kernel addresses...")
if os.path.exists("/tmp/kAFL_info.txt"):
os.remove("/tmp/kAFL_info.txt")
q = qemu(0, config)
q.start()
q.__del__()
try:
for line in open("/tmp/kAFL_info.txt"):
print line,
os.remove("/tmp/kAFL_info.txt")
except:
pass
return 0
该函数通过创建配置对象和进行自检查,准备执行信息收集任务。如果需要,启用日志记录。然后,记录消息并清理临时文件。接着,启动虚拟机,读取和打印内核地址信息。最后,清理资源并返回成功标志。
- 创建配置对象和自检查:
- 创建配置对象
config
。 - 进行自检查,确保配置的有效性。如果自检查失败,则返回
-1
。
- 创建配置对象
- 启用日志记录:
- 如果配置中指定了
v
参数,则启用日志记录。
- 如果配置中指定了
- 记录日志信息和清理临时文件:
- 记录消息
"Dumping kernel addresses..."
。 - 删除临时文件
"/tmp/kAFL_info.txt"
。
- 记录消息
- 启动虚拟机:
- 创建虚拟机对象
q
,并使用配置对象config
初始化。 - 启动虚拟机。
- 关闭虚拟机。
- 创建虚拟机对象
- 读取和打印内核地址信息:
- 尝试打开
"/tmp/kAFL_info.txt"
文件,逐行读取其中的内容,并打印到标准输出中。 - 删除临时文件
"/tmp/kAFL_info.txt"
。
- 尝试打开
- 清理资源:
- 清理虚拟机对象。
- 返回值:
- 返回
0
表示成功结束函数执行。
- 返回
该函数算是一个比较重要的函数,在这里做了三个核心操作,即上面标红的三处逻辑,下面我们将对其进行分析。
- 创建配置对象
config
该逻辑由InfoConfiguration()
函数调用来完成。其中InfoConfiguration()
是一个类,其实现在"/kAFL/kAFL-Fuzzer/common/config.py"的第133行。对于类的分析,要着眼于其__init__()
函数。因为__init__()
函数才是调用该类时执行并返回对象的逻辑。
该函数主要做一些初始化工作,核心为self.__load_arguments()
函数调用和self.__load_config()
函数调用。
self.__load_arguments()
函数调用
在这里调用了实现在"/kAFL/kAFL-Fuzzer/common/config.py"的第158行的__load_arguments()
函数。
该函数很简单,其目的就是为了解析在该阶段最开始我们执行的sudo python kafl_info.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/info/info 512 -v
命令中的参数。并将其最终保存到self.argument_values
变量中。
self.__load_config()
函数调用
在这里调用了实现在"/kAFL/kAFL-Fuzzer/common/config.py"的第161行的__load_config()
函数。
这个函数也很简单,只是用于加载"/opt/code/kAFL/kAFL-Fuzzer/kafl.ini"配置文件,并将其保存到self.config_values
变量中。
所以在该阶段,我们获取到了配置文件信息以及参数信息,并将其封装为InfoConfiguration
对象,最终赋值给config
变量。故config
变量中就保存了启动虚拟机的信息。
- 创建虚拟机对象
q
,并使用配置对象config
初始化
该逻辑由qemu(0, config)
函数调用来完成。其中qemu()
是一个类,其实现在"/kAFL/kAFL-Fuzzer/common/qemu.py"的第56行。对于类的分析,要着眼于其__init__()
函数。因为__init__()
函数才是调用该类时执行并返回对象的逻辑。
python
class qemu:
......
def __init__(self, qid, config):
self.global_bitmap = None
self.lookup = QemuLookupSet()
self.bitmap_size = config.config_values['BITMAP_SHM_SIZE']
self.config = config
self.qemu_id = str(qid)
self.process = None
self.intervm_tty_write = None
self.control = None
self.control_fileno = None
self.payload_filename = "/dev/shm/kafl_qemu_payload_" + self.qemu_id
self.binary_filename = "/dev/shm/kafl_qemu_binary_" + self.qemu_id
self.argv_filename = "/dev/shm/kafl_argv_" + self.qemu_id
self.bitmap_filename = "/dev/shm/kafl_bitmap_" + self.qemu_id
if self.config.argument_values.has_key('work_dir'):
self.control_filename = self.config.argument_values['work_dir'] + "/kafl_qemu_control_" + self.qemu_id
else:
self.control_filename = "/tmp/kafl_qemu_control_" + self.qemu_id
self.start_ticks = 0
self.end_ticks = 0
self.tick_timeout_treshold = self.config.config_values["TIMEOUT_TICK_FACTOR"]
self.cmd = self.config.config_values['QEMU_KAFL_LOCATION'] + " " \
"-hdb " + self.config.argument_values['ram_file'] + " " \
"-hda " + self.config.argument_values['overlay_dir'] + "/overlay_" + self.qemu_id + ".qcow2 " \
"-serial mon:stdio " \
"-enable-kvm " \
"-k de " \
"-m " + str(config.argument_values['mem']) + " " \
"-nographic " \
"-net user " \
"-net nic " \
"-chardev socket,server,nowait,path=" + self.control_filename + \
",id=kafl_interface " \
"-device kafl,chardev=kafl_interface,bitmap_size=" + str(self.bitmap_size) + ",shm0=" + self.binary_filename + \
",shm1=" + self.payload_filename + \
",bitmap=" + self.bitmap_filename
for i in range(1):
key = "ip" + str(i)
if self.config.argument_values.has_key(key) and self.config.argument_values[key]:
range_a = hex(self.config.argument_values[key][0]).replace("L", "")
range_b = hex(self.config.argument_values[key][1]).replace("L", "")
self.cmd += ",ip" + str(i) + "_a=" + range_a + ",ip" + str(i) + "_b=" + range_b
self.cmd += ",filter" + str(i) + "=/dev/shm/kafl_filter" + str(i)
self.cmd += " -loadvm " + self.config.argument_values["S"] + " "
if self.config.argument_values["macOS"]:
self.cmd = self.cmd.replace("-net user -net nic", "-netdev user,id=hub0port0 -device e1000-82545em,netdev=hub0port0,id=mac_vnet0 -cpu Penryn,kvm=off,vendor=GenuineIntel -device isa-applesmc,osk=\"" + self.config.config_values["APPLE-SMC-OSK"].replace("\"", "") + "\" -machine pc-q35-2.4")
if qid == 0:
self.cmd = self.cmd.replace("-machine pc-q35-2.4", "-machine pc-q35-2.4 -redir tcp:5901:0.0.0.0:5900 -redir tcp:10022:0.0.0.0:22")
else:
self.cmd += " -machine pc-i440fx-2.6 "
self.kafl_shm_f = None
self.kafl_shm = None
self.fs_shm_f = None
self.fs_shm = None
self.payload_shm_f = None
self.payload_shm = None
self.bitmap_shm_f = None
self.bitmap_shm = None
self.e = select.epoll()
self.crashed = False
self.timeout = False
self.kasan = False
self.shm_problem = False
self.initial_mem_usage = 0
self.stat_fd = None
if qid == 0:
log_qemu("Launching Virtual Maschine...CMD:\n" + self.cmd, self.qemu_id)
else:
log_qemu("Launching Virtual Maschine...", self.qemu_id)
self.virgin_bitmap = ''.join(chr(0xff) for x in range(self.bitmap_size))
91. self.__set_binary(self.binary_filename, self.config.argument_values['executable'], (16 << 20))
这个__init__
函数的最终目的是初始化一个qemu
对象,并将指定的二进制文件映射到共享内存中。这个对象代表了一个QEMU虚拟机实例,通过该对象可以控制和管理该虚拟机的运行。在初始化过程中,它完成了以下几个主要任务:
- 初始化常量和变量:
- 初始化
SC_CLK_TCK
常量,用于获取系统时钟滴答数。 - 初始化
global_bitmap
为None
,用于存储全局位图。 - 初始化
lookup
为QemuLookupSet()
实例,用于查找QEMU相关信息。 - 初始化
bitmap_size
、config
、qemu_id
等变量,其中bitmap_size
从配置中获取,qemu_id
为虚拟机的标识。
- 初始化
- 设置文件路径:
- 初始化
payload_filename
、binary_filename
、argv_filename
、bitmap_filename
和control_filename
等文件路径,用于存储虚拟机的payload、二进制文件、命令行参数、位图和控制文件。
- 初始化
- 构建QEMU启动命令:
- 使用配置中的QEMU路径和参数构建QEMU的启动命令。
- 设置
-loadvm
参数为配置中指定的快照标题。 - 如果配置中启用了
macOS
支持,修改QEMU
命令以适应macOS环境,包括更改网络配置和添加macOS相关参数。 - 初始化共享内存和文件描述符:
- 初始化
kafl_shm_f
、fs_shm_f
、payload_shm_f
和bitmap_shm_f
等文件描述符。 - 初始化
kafl_shm
、fs_shm
、payload_shm
和bitmap_shm
等共享内存对象。
- 初始化事件轮询器:
- 使用
select.epoll()
初始化事件轮询器e
,用于监视文件描述符的事件。
- 使用
- 初始化状态变量:
- 初始化
crashed
、timeout
、kasan
和shm_problem
等状态变量,用于标记虚拟机的异常状态。 - 初始化
initial_mem_usage
为0
,用于记录虚拟机的初始内存使用情况。
- 初始化
- 打印启动信息:
- 如果是第一个虚拟机,打印启动QEMU的命令行参数。
- 初始化全局位图:
- 使用全
1
的字节初始化virgin_bitmap
,用于初始化全局位图。
- 设置二进制文件:
- 调用
__set_binary()
方法设置虚拟机的二进制文件,包括文件路径、二进制文件内容和大小。
- 调用
在该函数中,初始化了很多信息,不过做的最重要的两件事就是上面标红的两处逻辑,即。
-
构建QEMU启动命令
在这里通过我们传入的参数和配置文件中的信息,构建了QEMU启动命令,并将其赋值给
self.cmd
变量。后续就可以直接使用该命令来启动对应的QEMU虚拟机。在这里注意一个很重要的参数,即"-chardev socket,server,nowait,path=" + self.control_filename
,通过创建一个基于套接字的字符设备,为QEMU虚拟机进程提供了一个与主机进程通信的通道。该套接字文件后面我们会用到。 -
设置二进制文件
在这里将之前编译好的"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/info/info"二进制文件映射到共享内存中,等待后续使用。
总之,在该阶段我们进行了一些关于我们设置的QEMU虚拟机的初始化工作,并将该对象赋值给
q
变量。当这些初始化工作完成后,后面就可以直接启动这个我们已经配置好的虚拟机(即待Fuzz目标)。 -
启动虚拟机
因为在上一步我们已经配置好了对应的QEMU虚拟机(即待Fuzz目标),故在该步骤我们只需要直接启动这个已经配置好的QEMU虚拟机即可。所以最终该逻辑由
q.start()
函数调用来完成。在这里调用了上一步得到的q
变量(对象)中的start()
函数,而该函数实现在"/kAFL/kAFL-Fuzzer/common/qemu.py"的第237行。
该函数用于启动虚拟机进程,并初始化相关资源和状态,最终返回一个布尔值表示启动是否成功。具体来说,其逻辑为。
- 参数解析和处理:
- 在方法开始时,根据是否传入
verbose
参数决定是否启用详细模式。 - 使用
subprocess.Popen
启动虚拟机进程,根据verbose
参数设置标准输入、输出和错误的重定向。 - 打开与虚拟机进程关联的"/proc"文件系统下的"stat"文件,用于获取进程统计信息。
- 在方法开始时,根据是否传入
- 资源初始化:
- 调用
init()
方法初始化用于建立与控制通道的连接,并打开共享内存文件以进行通信和数据共享。
- 调用
- 初始状态设置:
- 调用
set_init_state()
方法设置虚拟机的初始状态并进行握手以确保通信的顺利进行。
- 调用
- 内存使用量记录:
- 获取当前进程的最大资源使用量,并将其记录为初始内存使用量。
- 覆盖率位图写入:
- 将初始的覆盖率位图写入共享内存,并刷新缓冲区。
- 启动状态返回:
- 返回一个布尔值,指示虚拟机启动是否成功。
该函数是当前阶段的核心,而其中的核心逻辑为上面标红的三部分。
-
使用
subprocess.Popen
启动虚拟机进程这没什么好说的,只是根据之前我们构建好的启动QEMU虚拟机的命令来启动对应的QEMU虚拟机进程。重要的是后面两步对该虚拟机的配置。
-
资源初始化
该逻辑由"/kAFL/kAFL-Fuzzer/common/qemu.py"的第282行的
init()
函数实现。
-
这段代码是初始化函数,用于建立与控制通道的连接并打开共享内存文件以进行通信和数据共享。具体分析如下:
- 建立控制通道连接:
- 使用Unix域套接字(socket.AF_UNIX)创建套接字对象,用于与控制文件进行通信。
- 使用循环不断尝试连接控制文件,直到连接成功。
- 打开共享内存文件:
- 使用
os.open()
函数打开位图文件和负载文件,获取文件描述符。 - 使用
os.ftruncate()
函数设置共享内存文件的大小。 - 使用
mmap.mmap()
函数将共享内存文件映射到内存中,以便读写共享数据。
- 使用
- 返回初始化结果:
- 返回
True
表示初始化成功。
- 返回
该函数做的最重要的操作就是利用之前创建的套接字(即之前介绍的QEMU启动命令中的"-chardev socket,server,nowait,path=" + self.control_filename
参数)建立主机与虚拟机之间的通信。
- 初始状态设置
该逻辑由"/kAFL/kAFL-Fuzzer/common/qemu.py"的第262行的set_init_state()
函数实现。
这段代码用于设置虚拟机的初始状态并进行握手以确保通信的顺利进行。具体分析如下:
- 重置虚拟机状态:
- 将虚拟机的崩溃状态、超时状态和KASAN状态都设置为
False
,表示虚拟机处于正常状态。 - 将起始时钟值(
start_ticks
)和结束时钟值(end_ticks
)都设置为0
。
- 将虚拟机的崩溃状态、超时状态和KASAN状态都设置为
- 设置控制通道超时时间:
- 使用
settimeout()
方法设置控制通道的超时时间为10
秒,以便在通信发生故障时能够及时检测到并处理。
- 使用
- 执行握手:
- 通过控制通道接收一个字节的数据,用于第一阶段握手,并打印日志表示握手成功。
- 调用
__set_binary()
方法重新设置虚拟机的二进制文件。 - 如果收到的字节不是
'D'
,则表示需要进行第二阶段握手,此时发送'R'
表示已准备好,并接收确认信息。 - 发送
'R'
表示第三阶段握手,以确认初始化完成,并接收确认信息。
- 恢复控制通道超时时间:
- 将控制通道的超时时间设置为
5
秒,以便后续的通信能够及时响应。
- 将控制通道的超时时间设置为
因为上一步骤我们已经建立了主机和虚拟机的通信,故这段代码的主要目的是确保虚拟机处于正常状态,并通过握手确认通信的顺利进行,以等待后续的其它操作。
看起来该阶段到这里就结束了,其实不然。经过分析我们发现,我们目前只是将之前的虚拟机快照启动,并且完成了在主机上的操作。在"1.2.2.3、配置待Fuzz目标"章节我们分析过,在创建虚拟机快照时,执行了虚拟机中"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/loader/loader.c"的第74行的main()
函数。
在这里我们是通过第100行的kAFL_hypercall(HYPERCALL_KAFL_LOCK, 0);
函数调用来冻结虚拟机系统,并创建快照的(已经在"1.2.2.3、配置待Fuzz目标"章节分析过了)。而第100行后面的代码还没来得及执行(即"1.2.2.3、配置待Fuzz目标"章节最后标绿的部分逻辑),所以,当我们每次启动这个虚拟机快照后,会继续执行该程序,即从第107行开始执行后面的代码(后续若再遇到启动虚拟机的情况,也是按照此分析去执行整个逻辑,故不再赘述)。
所以,现在只是在主机上的操作完成了,在虚拟机系统中,还要继续执行上述代码。在上面的代码中,只需要关注两处逻辑,即:
kAFL_hypercall(HYPERCALL_KAFL_GET_PROGRAM, (uint64_t)program_buffer);
该函数调用是通过调用一个超调用(即HYPERCALL_KAFL_GET_PROGRAM
)来完成对应的操作的。而在"1.2.2.3、配置待Fuzz目标"章节我们已经分析过,这里的超调用还有一层封装,其位于"/kAFL/QEMU-PT/pt/hypercall.c"的第61行。
在这里最终通过handle_hypercall_get_program(run, cpu);
函数调用来完成该逻辑,而handle_hypercall_get_program()
函数实现在"/kAFL/QEMU-PT/pt/hypercall.c"的第280行。
在这段代码中,run->hypercall.args[0]
表示从kvm_run
结构体中提取出的hypercall
参数数组中的第一个参数。它被转换为uint64_t类型,并用%lx格式符打印出来,表示一个64位的十六进制数值。这个参数是一个内存地址,用于指定程序的位置,以便在虚拟机中写入相应的程序数据到program_buffer
中。
那么这里获取的指定程序究竟是什么呢?我们回忆一下,我们是通过执行sudo python kafl_info.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/info/info 512 -v
进入的该阶段,在这里我们指定了一个名为"info"的二进制程序,故在这里加载到program_buffer
中的程序就是"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/info/info"。
- load_programm(program_buffer);
该函数调用是通过调用load_programm()
函数来进行对应的操作的,值得注意的是,在这里传入的参数就是上一步我们获取到的程序。该函数实现在"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/loader/loader.c"的第62行。
在该函数中,一切都不重要,只需要关注第71行的操作,在这里使用fexecve()
系统调用执行指定的程序,即上一步获取到的程序。故下面我们来看"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/info/info"这个程序都做了什么操作。对于该二进制程序,我们需要从其源代码的main()
开始进行分析。其位于"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/info/info.c"的第65行。
c
int main(int argc, char** argv){
char key[] = "(OE)";
char key2[] = "(O)";
char data[TMP_SIZE];
char module_name[MOD_NAME_SIZE];
uint64_t start;
uint64_t offset;
char * pch;
int counter;
int pos = 0;
void* info_buffer = mmap((void*)NULL, INFO_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memset(info_buffer, 0xff, INFO_SIZE);
FILE* f = fopen("/proc/modules", "r");
fread(data, 1, TMP_SIZE, f);
fclose(f);
counter = 0;
int i;
for(i = 0; i < strlen(data); i++){
if (data[i] == '\n'){
counter++;
}
}
pos += sprintf(info_buffer + pos, "kAFL Linux x86-64 Kernel Addresses (%d Modules)\n\n", counter);
printf("kAFL Linux x86-64 Kernel Addresses (%d Modules)\n\n", counter);
pos += sprintf(info_buffer + pos, "START-ADDRESS\t\tEND-ADDRESS\t\tDRIVER\n");
printf("START-ADDRESS\t\tEND-ADDRESS\t\tDRIVER\n");
pch = strtok(data, " \n");
counter = 0;
while (pch != NULL)
{
if(strcmp(key, pch) && strcmp(key2, pch)){
switch((counter++) % 6){
case 0:
strncpy(module_name, pch, MOD_NAME_SIZE);
break;
case 1:
offset = strtoull(pch, NULL, 10);
break;
case 5:
start = strtoull(pch, NULL, 16);
pos += sprintf(info_buffer + pos, "0x%016lx\t0x%016lx\t%s\n", start, start+offset, module_name);
printf("0x%016lx\t0x%016lx\t%s\n", start, start+offset, module_name);
break;
}
}
pch = strtok (NULL, " \n");
}
pos += sprintf(info_buffer + pos, "0x%016lx\t0x%016lx\t%s\n\n", get_address("T startup_64\n"), get_address("r __param_str_debug\n"), "Kernel Core");
kAFL_hypercall(HYPERCALL_KAFL_INFO, (uint64_t)info_buffer);
return 0;
}
该函数的主要功能是读取"/proc/modules"中的内核模块信息,并将其格式化输出到标准输出和一个内存缓冲区中。然后,通过kAFL_hypercall
函数调用,将内核模块信息传递给kAFL,以便进行进一步处理。整个程序的流程可以详细分析如下:
- 打开并读取文件:
- 使用
fopen
打开"/proc/modules"文件,读取其中的内容到data
缓冲区中。 - 使用
fread
函数将文件内容读取到data
缓冲区。
- 使用
- 统计模块数量:
- 使用
strlen
函数获取data
缓冲区的长度,然后遍历缓冲区,计算换行符的数量,从而得到模块的数量。
- 使用
- 格式化输出模块信息:
- 使用
strtok
函数以空格和换行符作为分隔符,逐行解析data
缓冲区中的模块信息。 - 对每个模块信息,根据其在每行中的位置,提取模块名、起始地址和偏移量等信息。
- 使用
sprintf
函数将提取到的信息格式化输出到标准输出和info_buffer
缓冲区中。
- 使用
- 获取其它符号的地址:
- 使用
get_address
函数获取其它符号(如startup_64
和__param_str_debug
)的地址,然后将其格式化输出到info_buffer
缓冲区中。
- 使用
- 传递信息给kAFL:
- 最后,调用
kAFL_hypercall
函数,将包含模块信息的info_buffer
缓冲区传递给kAFL进行进一步处理。
- 最后,调用
可以发现,这两个阶段的操作比较通用。即先获取指定程序,然后再执行该程序。后续若在遇到这两个阶段的操作,不再对其进行详细分析,直接说明其如何使用。
到此,该阶段的全部操作就完成了,总之最重要的操作就是启动了虚拟机,并与虚拟机建立了通信,然后执行指定程序。当然,最后关闭该QEMU虚拟机,关闭QEMU虚拟机的过程就不再赘述了,因为这比较简单,而且上面的分析中也有所提及。
1.2.3、Fuzz测试
当我们做完了以上准备工作后,终于来到使用kAFL对待Fuzz目标进行Fuzz测试的核心步骤。在该阶段,我们主要做了以下操作。
-
设置输入目录和输出目录
-
设置输入目录
设置输入目录为"/path/to/input/"。并将初始种子文件复制到该输入目录中。这些种子文件如下图所示。
-
查看种子文件
我们可以进入一个种子文件目录查看其中的具体种子文件内容。比如我们查看名为"fat"的种子文件目录中的种子文件细节。
-
设置输出目录
设置输出目录为"/path/to/working/output/"。
-
-
在指定目录对待Fuzz目标执行Fuzz测试
该阶段的核心步骤为最后一个步骤。在该步骤,我们来到"/kAFL/kAFL-Fuzzer/"目录中执行
sudo python kafl_fuzz.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/fuzzer/ext4 512 /path/to/input/seed/ /path/to/working/output/ -ip0 0xffffffffc0287000-0xffffffffc028b000 -v --Purge
命令以对待Fuzz目标进行Fuzz测试。这是一个用于运行"/kAFL/kAFL-Fuzzer/kafl_fuzz.py"文件的命令,具体解释如下:sudo
:使用管理员权限运行命令。python
:启动Python解释器。kafl_fuzz.py
:要运行的Python脚本文件。/path/to/snapshot/ram.qcow2
:虚拟机的内存快照文件路径。/path/to/snapshot/
:快照文件所在的路径。agents/linux_x86_64/fuzzer/ext4
:可执行文件的路径,这个文件就是要执行Fuzz测试的程序。512
:虚拟机的内存大小。/path/to/input/seed/
:输入种子文件的路径,用于初始化Fuzz测试输入。/path/to/working/output/
:工作目录的输出路径,Fuzz测试的结果将会输出到这里。-ip0 0xffffffffc0287000-0xffffffffc028b000
:定义了内存地址范围。-v
:启用详细模式,输出更多信息。--Purge
:清除旧的工作目录,以确保每次运行都是干净的。
很明显,在这里我们执行了"/kAFL/kAFL-Fuzzer/kafl-fuzz.py"文件,并传入了一些参数,且执行的权限为root用户权限。故我们来到该文件的第43行实现的主函数来看该文件都做了什么。
该主函数只调用了在"/kAFL/kAFL-Fuzzer/kafl-fuzz.py"的第28行实现的main()
函数。
该函数首先打印帮助文件内容,然后输出程序名称,并执行自检后调用start()
函数开始执行主要功能。故我们接下来看实现在"/kAFL/kAFL-Fuzzer/fuzzer/core.py"的第40行的start()
函数。
python
def start():
config = FuzzerConfiguration()
if not post_self_check(config):
return -1
if config.argument_values['v']:
enable_logging()
num_processes = config.argument_values['p']
if config.argument_values['Purge'] and check_if_old_state_exits(config.argument_values['work_dir']):
print_warning("Old workspace found!")
if ask_for_purge("PURGE"):
print_warning("Wiping old workspace...")
prepare_working_dir(config.argument_values['work_dir'], purge=config.argument_values['Purge'])
time.sleep(2)
else:
print_fail("Aborting...")
return 0
if not check_if_old_state_exits(config.argument_values['work_dir']):
if not prepare_working_dir(config.argument_values['work_dir'], purge=config.argument_values['Purge']):
print_fail("Working directory is weired or corrupted...")
return 1
if not copy_seed_files(config.argument_values['work_dir'], config.argument_values['seed_dir']):
print_fail("Seed directory is empty...")
return 1
config.save_data()
else:
log_core("Old state exist -> loading...")
config.load_data()
comm = Communicator(num_processes=num_processes, tasks_per_requests=config.argument_values['t'], bitmap_size=config.config_values["BITMAP_SHM_SIZE"])
comm.create_shm()
master = MasterProcess(comm)
update_process = multiprocessing.Process(name='UPDATE', target=update_loader, args=(comm, ))
mapserver_process = multiprocessing.Process(name='MAPSERVER', target=mapserver_loader, args=(comm, ))
slaves = []
for i in range(num_processes):
slaves.append(multiprocessing.Process(name='SLAVE' + str(i), target=slave_loader, args=(comm, i, )))
slaves[i].start()
update_process.start()
mapserver_process.start()
try:
master.loop()
except KeyboardInterrupt:
master.save_data()
log_core("Date saved!")
signal.signal( signal.SIGINT, signal.SIG_IGN)
counter = 0
print_pre_exit_msg(counter, clrscr=True)
for slave in slaves:
while True:
counter += 1
print_pre_exit_msg(counter)
slave.join(timeout=0.25)
if not slave.is_alive():
break
print_exit_msg()
return 0
该函数的作用是启动Fuzz测试的主要流程,包括配置初始化、自检、日志记录、工作目录准备、进程和通信器的创建、主进程循环处理、异常处理和清理工作。其主要逻辑如下
- 获取配置信息:
- 使用
FuzzerConfiguration
类创建config
对象,该对象包含了程序运行所需的配置信息。
- 使用
- 自检操作:
- 调用
post_self_check(config)
函数进行自检,确保程序所需的条件满足,如文件路径、权限等。如果自检失败,则返回错误码-1
。
- 调用
- 日志记录设置:
- 如果配置中指定了启用日志记录(
config.argument_values['v']
为True
),则调用enable_logging()
函数启用日志记录功能。
- 如果配置中指定了启用日志记录(
- 确定进程数量:
- 从配置中获取进程数量参数(
config.argument_values['p']
),用于确定需要启动的进程数量。
- 从配置中获取进程数量参数(
- 清除旧工作空间:
- 如果配置中指定了清除旧工作空间(
config.argument_values['Purge']
为True
),且检测到旧工作空间存在,则执行清除操作。如果用户选择不清除,则输出提示信息并返回0
。
- 如果配置中指定了清除旧工作空间(
- 准备工作目录和复制种子文件:
- 如果检测到旧状态不存在,则执行准备工作目录的操作,包括创建工作目录和复制种子文件到工作目录下。如果操作失败,则输出相应的错误信息并返回错误码
1
。
- 如果检测到旧状态不存在,则执行准备工作目录的操作,包括创建工作目录和复制种子文件到工作目录下。如果操作失败,则输出相应的错误信息并返回错误码
- 加载或保存状态:
- 如果存在旧状态,则加载旧状态,否则保存新状态。
- 创建通信器对象:
- 使用
Communicator
类创建comm
对象,传入进程数量、任务数量和位图大小等参数 - 调用
create_shm()
方法创建共享内存区域。
- 使用
- 创建主进程对象:
- 使用
MasterProcess
类创建master
对象,传入通信器对象comm
。
- 使用
- 创建更新进程和映射服务器进程:
- 使用
multiprocessing.Process
创建更新进程update_process
和映射服务器进程mapserver_process
,并分别传入通信器对象comm
作为参数。
- 使用
- 创建并启动从属进程:
- 使用循环创建多个从属进程对象
slaves
,传入通信器对象comm
和从属进程的索引作为参数,并启动各个从属进程。
- 使用循环创建多个从属进程对象
- 启动更新进程和映射服务器进程:
- 使用各自进程的
start()
函数来启动更新进程和映射服务器进程
- 使用各自进程的
- 主进程循环处理:
- 主进程调用
loop()
方法循环处理通信器的消息,直到捕获到键盘中断异常。
- 主进程调用
- 保存数据:
- 在捕获到键盘中断异常时,主进程调用
save_data()
方法保存数据,并输出相应的状态信息。
- 在捕获到键盘中断异常时,主进程调用
- 忽略
SIGINT
信号:- 使用
signal.signal
函数设置SIGINT
信号的处理方式为忽略。
- 使用
- 等待从属进程结束:
- 主进程使用循环等待所有从属进程结束,每次循环输出一条退出消息,直到所有从属进程均结束。
- 返回状态:
- 返回
0
表示程序成功执行完毕。
- 返回
该段代码比较长,包括很多步骤,不过我们只关心上面标红的部分,因为这才是该函数的核心逻辑。下面我们就将分章节对这些逻辑进行分析。
1.2.3.1、获取配置信息
该逻辑由FuzzerConfiguration()
函数调用实现,而FuzzerConfiguration()
实际是一个类,其实现在"/kAFL/kAFL-Fuzzer/common/config.py"的第179行。对于类的分析,要着眼于其__init__()
函数。因为__init__()
函数才是调用该类时执行并返回对象的逻辑。
该函数主要做一些初始化工作,核心为self.__load_arguments()
函数调用和self.__load_config()
函数调用。
self.__load_arguments()
函数调用
在这里调用了实现在"/kAFL/kAFL-Fuzzer/common/config.py"的第213行的__load_arguments()
函数。
python
def __load_arguments(self):
parser = ArgsParser(formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('ram_file', metavar='<RAM File>', action=FullPaths, type=parse_is_file,
help='Path to the RAM file.')
parser.add_argument('overlay_dir', metavar='<Overlay Directory>', action=FullPaths, type=parse_is_dir,
help='Path to the overlay directory.')
parser.add_argument('executable', metavar='<Fuzzer Executable>', action=FullPaths, type=parse_is_file,
help='Path to the fuzzer executable.')
parser.add_argument('mem', metavar='<RAM Size>', help='Size of virtual RAM (default: 300).', default=300, type=int)
parser.add_argument('seed_dir', metavar='<Seed Directory>', action=FullPaths, type=parse_is_dir,
help='Path to the seed directory.')
parser.add_argument('work_dir', metavar='<Working Directory>', action=FullPaths, type=create_dir,
help='Path to the working directory.')
#parser.add_argument('ip_filter', metavar='<IP-Filter>', type=parse_range_ip_filter,
# help='Instruction pointer filter range.')
parser.add_argument('-ip0', required=True, metavar='<IP-Filter 0>', type=parse_range_ip_filter, help='instruction pointer filter range 0')
parser.add_argument('-ip1', required=False, metavar='<IP-Filter 1>', type=parse_range_ip_filter, help='instruction pointer filter range 1 (not supported in this version)')
parser.add_argument('-ip2', required=False, metavar='<IP-Filter 2>', type=parse_range_ip_filter, help='instruction pointer filter range 2 (not supported in this version)')
parser.add_argument('-ip3', required=False, metavar='<IP-Filter 3>', type=parse_range_ip_filter, help='instruction pointer filter range 3 (not supported in this version)')
parser.add_argument('-p', required=False, metavar='<Process Number>', help='number of worker processes to start.', default=1, type=int)
parser.add_argument('-t', required=False, metavar='<Task Number>', help='tasks per worker request to provide.', default=1, type=int)
parser.add_argument('-v', required=False, help='enable verbose mode (./debug.log).', action='store_true',
default=False)
parser.add_argument('-g', required=False, help='disable GraphViz drawing.', action='store_false', default=True)
parser.add_argument('-s', required=False, help='skip zero bytes during deterministic fuzzing stages.',
action='store_true', default=False)
parser.add_argument('-b', required=False, help='enable usage of ringbuffer for findings.',
action='store_true', default=False)
parser.add_argument('-d', required=False, help='disable usage of AFL-like effector maps.',
action='store_false', default=True)
parser.add_argument('--Purge', required=False, help='purge the working directory.', action='store_true',
default=False)
parser.add_argument('-i', required=False, type=parse_ignore_range, metavar="[0-131072]",
help='range of bytes to skip during deterministic fuzzing stages (0-128KB).',
action='append')
parser.add_argument('-e', required=False, help='disable evaluation mode.', action='store_false', default=True)
parser.add_argument('-S', required=False, metavar='Snapshot', help='specifiy snapshot title (default: kafl).', default="kafl", type=str)
parser.add_argument('-D', required=False, help='skip deterministic stages (dumb mode).',action='store_false', default=True)
parser.add_argument('-I', required=False, metavar='<Dict-File>', help='import dictionary to fuzz.', default=None, type=parse_is_file)
parser.add_argument('-macOS', required=False, help='enable macOS Support (requires Apple OSK)', action='store_true', default=False)
parser.add_argument('-f', required=False, help='disable fancy UI', action='store_false', default=True)
parser.add_argument('-n', required=False, help='disable filter sampling', action='store_false', default=True)
parser.add_argument('-l', required=False, help='enable UI log output', action='store_true', default=False)
self.argument_values = vars(parser.parse_args())
该函数很简单,其目的就是为了解析在该阶段最开始我们执行的sudo python kafl_fuzz.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/fuzzer/ext4 512 /path/to/input/seed/ /path/to/working/output/ -ip0 0xffffffffc0287000-0xffffffffc028b000 -v --Purge
命令中的参数。并将其最终保存到self.argument_values
变量中。
self.__load_config()
函数调用
在这里调用了实现在"/kAFL/kAFL-Fuzzer/common/config.py"的第161行的__load_arguments()
函数。
这个函数也很简单,只是用于加载"/opt/code/kAFL/kAFL-Fuzzer/kafl.ini"配置文件,并将其保存到self.config_values
变量中。
故该阶段,我们完成了对执行命令中的参数的解析,以及对配置文件的读取。该阶段的操作很重要,因为这是后续操作的基础,有了基本数据之后,才能进行后续的Fuzz测试。
1.2.3.2、准备工作目录
该逻辑是由prepare_working_dir(config.argument_values['work_dir'], purge=config.argument_values['Purge'])
函数调用实现的。而prepare_working_dir()
函数实现在"/kAFL/kAFL-Fuzzer/common/util.py"的第67行。
该函数的目的是在工作目录创建所需要的文件夹,等待后续使用。具体来说,其逻辑如下。
- 定义需要创建的文件夹列表:
folders
包含了需要在工作目录下创建的各个文件夹的路径。 - 根据是否需要清理工作目录(
purge
参数),执行相应的清理操作:- 如果
purge
为True
,表示需要清理工作目录,函数会首先检查目录是否存在,然后递归地删除所有文件和子目录。接着,它会检查是否存在特定的共享内存文件,如果存在则删除。
- 如果
- 检查工作目录是否为空:
- 如果工作目录为空(即没有任何文件或文件夹),则根据
folders
列表创建相应的文件夹结构。 - 如果工作目录不为空,检查是否存在必需的文件夹结构:
- 如果存在所有必需的文件夹结构,则表示工作目录已准备就绪,函数返回
True
。 - 如果缺少必需的文件夹结构,则返回
False
,表示准备工作目录失败。
- 如果工作目录为空(即没有任何文件或文件夹),则根据
- 返回
True
或False
,表示准备工作目录的成功或失败状态。
1.2.3.3、复制种子文件
该逻辑由copy_seed_files(config.argument_values['work_dir'], config.argument_values['seed_dir'])
函数调用来完成。而copy_seed_files()
函数实现在"/kAFL/kAFL-Fuzzer/common/util.py"的第121行。
该函数的目的是将初始种子文件复制到目标目录中,等待后续使用。具体来说,其逻辑如下。
- 检查种子目录是否为空:
- 如果种子目录为空(即没有任何文件),函数会立即返回
False
,表示无法复制种子文件。 - 如果种子目录不为空,则继续执行后续操作。
- 如果种子目录为空(即没有任何文件),函数会立即返回
- 检查工作目录是否为空:
- 如果工作目录为空(即没有任何文件),函数会立即返回
True
,表示无需复制种子文件。 - 如果工作目录不为空,则需要复制种子文件到工作目录中。
- 如果工作目录为空(即没有任何文件),函数会立即返回
- 遍历种子目录下的文件:
- 使用
os.walk
函数遍历种子目录中的所有文件。 - 对于每个文件,获取其路径,并检查该路径是否存在。
- 使用
- 复制文件到工作目录:
- 使用
copyfile
函数将种子文件复制到工作目录的"corpus"子目录中,并按顺序重命名为"payload_i",其中"i"表示文件的序号。
- 使用
- 返回复制结果:
- 如果成功复制了至少一个文件,则函数返回
True
,表示成功复制种子文件到工作目录。 - 如果没有成功复制任何文件,则函数返回
False
,表示无法复制种子文件到工作目录。
- 如果成功复制了至少一个文件,则函数返回
总之,最后会在"/path/to/working/output/corpus/"目录中得到如下内容,这些内容就是从种子目录中拷贝过来的种子文件,等待后续的使用。
1.2.3.4、创建通信器对象
该逻辑由Communicator(num_processes=num_processes, tasks_per_requests=config.argument_values['t'], bitmap_size=config.config_values["BITMAP_SHM_SIZE"])
函数调用来完成。而Communicator()
实际是一个类,其实现在"/kAFL/kAFL-Fuzzer/fuzzer/communicator.py"的第26行。对于类的分析,要着眼于其__init__()
函数。因为__init__()
函数才是调用该类时执行并返回对象的逻辑。
这段代码的作用是初始化通信器的各种属性和参数,为多进程环境下的数据交换和同步提供基础支持。以下是其主要逻辑。
- 初始化队列:
- 创建了多个进程间通信的队列,用于不同组件之间的数据传递。
to_update_queue
: 用于从其它组件接收更新。to_master_queue
: 用于将数据发送给主进程。to_master_from_mapserver_queue
: 用于从映射服务器发送数据给主进程。to_master_from_slave_queue
: 用于从从属进程发送数据给主进程。to_mapserver_queue
: 用于将数据发送给映射服务器。to_slave_queues
: 用于将数据发送给从属进程,为每个从属进程创建一个队列。
- 初始化锁和信号量:
- 创建了多个进程锁,用于在多进程环境下对共享资源进行加锁操作。
- 创建了一个信号量,用于控制并发访问的数量,这里设置为CPU核心数的一半。
- 初始化参数:
- 设置了进程数和每个请求的任务数。
- 初始化共享变量:
- 创建了多个共享变量,用于在多进程环境下共享状态信息。
stage_abortion_notifier
: 用于通知其它进程是否终止当前阶段。slave_termination
: 用于通知其它进程从属进程是否终止。sampling_failed_notifier
: 用于通知其它进程采样是否失败。effector_mode
: 用于通知其它进程是否处于效果器模式。
- 初始化共享内存文件路径和大小:
- 设置了共享内存文件的路径和大小。
- 共享内存文件用于在多进程之间共享数据。
当创建好通信器对象后,该处逻辑还做了一件事情,就是利用创建好的通信器对象来创建共享内存区域,即comm.create_shm()
函数调用,而create_shm()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/communicator.py"的第76行。
这段代码是在Communicator
类中的create_shm
方法中。它的作用是创建共享内存段(Shared Memory)以用于进程间的通信。
具体来说,该方法会按照预定义的文件路径列表self.files
中的路径,为每个进程创建一段共享内存,并设置大小为self.sizes[j]*self.tasks_per_requests
,其中self.sizes[j]
表示文件大小,self.tasks_per_requests
表示每个请求中的任务数。在每个进程的循环中,会创建一个共享内存文件描述符shm_f
,并调用os.ftruncate
函数来设置文件大小。最后关闭文件描述符。
这样做的目的是为每个进程创建一块独立的共享内存,用于存储需要在不同进程之间共享的数据,以实现进程间的通信。
1.2.3.5、创建主进程对象
该逻辑由MasterProcess(comm)
函数调用来实现,而MasterProcess()
实际是一个类,其实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/master.py"的第42行。对于类的分析,要着眼于其__init__()
函数。因为__init__()
函数才是调用该类时执行并返回对象的逻辑。
这是MasterProcess
类的构造函数,初始化了一系列属性,并打印了一个日志信息。具体来说:
self.comm
:保存了一个Communicator
对象,用于与其它进程进行通信。self.kafl_state
:初始化了一个State
对象,用于跟踪Fuzz测试的状态。self.payload
:初始化为空字符串,准备存储payload
。self.counter
、self.round_counter
、self.start
、``self.benchmark_time、self.counter_offset
:用于记录计数和时间。self.payload_buffer
和self.byte_map
:用于缓存payload
和相关的字节映射。self.stage_abortion
和self.abortion_counter
:用于记录Fuzz测试中的异常终止情况。self.mapserver_status_pending
:标志位,表示映射服务器状态是否处于等待状态。self.config
:初始化了一个FuzzerConfiguration
对象,用于获取配置信息。self.skip_zero
、self.refresh_rate
、self.use_effector_map
、self.arith_max
:从配置中获取了一些参数值。- 根据配置决定是否使用效应图。
- 如果配置中指定了加载旧状态,则调用
load_data()
方法加载数据。 - 最后打印了一个日志信息,指示是否使用了效应图。
该函数的核心应该是上面标红的三处逻辑,其余的代码都是一些初始化变量的内容,并没什么好分析的。而标红的三处逻辑,我们之前已经分析过了,在此处不再赘述。
1.2.3.6、创建更新进程
该逻辑由multiprocessing.Process(name='UPDATE', target=update_loader, args=(comm, ))
函数调用实现。
这行代码创建了一个名为"update_process"的多进程对象,该进程将执行update_loader
函数,并传入参数comm
。这意味着在启动该进程后,将会执行update_loader(comm)
这个函数。同时,进程的名字被设置为'UPDATE'
。
1.2.3.7、创建映射服务器进程
该逻辑由multiprocessing.Process(name='MAPSERVER', target=mapserver_loader, args=(comm, ))
函数调用实现。
这行代码创建了一个名为"mapserver_process"的多进程对象,该进程将执行mapserver_loader
函数,并传入参数comm
。这意味着在启动该进程后,将会执行mapserver_loader(comm)
这个函数。同时,进程的名字被设置为'MAPSERVER'
。
1.2.3.8、创建从属进程
该逻辑由multiprocessing.Process(name='SLAVE' + str(i), target=slave_loader, args=(comm, i, ))
函数调用实现。
这行代码在一个循环中创建了多个(num_processes
个)多进程对象,这些进程将执行slave_loader
函数,并传入参数comm
和i
(即每个从属进程的编号)。这意味着在启动该进程后,将会执行slave_loader(comm, i)
这个函数。同时,进程的名字被设置为'SLAVEi'
。
最终创建的这些多进程对象将被添加到子进程列表slaves
中,后续就可以通过该子进程列表来调用创建的这些多进程对象。
1.2.3.9、启动从属进程
该逻辑由slaves[i].start()
实现,在这里调用的是从属进程对应的目标函数,即"1.2.3.8、创建从属进程"介绍的slave_loader(comm, i)
函数调用。而slave_loader()
函数实现在"/Kafl/kAFL-Fuzzer/fuzzer/process/slave.py"的第31行。
这段代码实现了一个用于管理子进程的加载器函数slave_loader()
,其主要功能如下:
- 获取进程ID并记录日志:
- 使用
os.getpid()
获取当前子进程的进程ID。 - 将子进程的进程ID记录在日志中,以便进行跟踪和调试。
- 使用
- 创建
SlaveProcess
对象:- 创建一个名为
slave_process
的SlaveProcess
对象,用于管理和执行子进程的主要逻辑。
- 创建一个名为
- 执行子进程主循环:
- 在
try-except
块中,调用slave_process
对象的loop()
方法,开始执行子进程的主循环逻辑。 - 主循环中将持续执行子进程的任务,直到收到终止信号。
- 在
- 处理终止信号:
- 如果捕获到
KeyboardInterrupt
异常,即用户按下了"Ctrl+C",将通信器对象的slave_termination
属性值设置为True
,以通知其它进程该子进程已终止。
- 如果捕获到
- 记录子进程终止消息:
- 在日志中记录子进程被终止的消息,以便跟踪子进程的状态。
在该函数中,其核心逻辑为上面标红的两处,下面我们将对其进行分析。
- 创建
SlaveProcess
对象
该逻辑由SlaveProcess(comm, slave_id)
函数调用来实现,而SlaveProcess()
实际是一个类,其实现在"/Kafl/kAFL-Fuzzer/fuzzer/process/slave.py"的第42行。对于类的分析,要着眼于其__init__()
函数。因为__init__()
函数才是调用该类时执行并返回对象的逻辑。
在这段代码中,SlaveProcess
类的构造函数完成了子进程的初始化工作,为其后续的执行提供了必要的属性和配置信息。具体来说,其逻辑如下。
- 初始化配置和通信器:
- 创建一个
FuzzerConfiguration
的配置对象,用于获取配置信息。 - 将通信器对象和子进程ID传递给
SlaveProcess
对象,以便子进程与主进程和其它子进程进行通信。
- 创建一个
- 设置子进程ID和计数器:
- 将传入的
slave_id
参数保存为子进程的ID。 - 初始化一个计数器,用于记录子进程执行的任务数量。
- 将传入的
- 创建QEMU实例:
- 调用
qemu
类的构造函数,创建一个与当前子进程相关联的QEMU实例对象。该对象用于管理和控制当前子进程模拟的虚拟机。
- 调用
- 初始化其它属性:
- 初始化一个用于存储误报信息的集合
false_positiv_map
。 - 设置阶段超时门槛
stage_tick_treshold
,用于确定阶段任务的超时阈值。 - 设置自动重载标志
auto_reload
,用于控制是否自动重新加载虚拟机镜像。 - 初始化软重载计数器
soft_reload_counter
,用于跟踪虚拟机的软重载次数。
- 初始化一个用于存储误报信息的集合
该函数的核心是上面标红的逻辑,其由qemu(self.slave_id, self.config)
函数调用实现。而关于qemu()
函数,我们在"1.2.2.4、配置kAFL组件"章节已经详细分析过了,故在此不再赘述。不过这里的细节和之前还是有一些区别,比如。
- 会根据传入的从属进程的编号创建对应编号的QEMU虚拟机实例
- 映射到共享内存中的二进制文件为"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/ext4"
- 执行子进程主循环
该逻辑由slave_process.loop()
函数调用实现。而loop()
函数实现在"/Kafl/kAFL-Fuzzer/fuzzer/process/slave.py"的第311行。
这段代码是SlaveProcess
类的loop
方法,负责子进程的主循环执行。以下是其主要步骤:
- 获取重载信号量:
- 调用
comm.reload_semaphore.acquire()
,获取重载信号量,确保只有一个子进程在任意时刻重载虚拟机。这样可以避免多个子进程同时重载虚拟机导致的冲突和不一致性。
- 调用
- 启动虚拟机:
- 调用
q.start()
方法,启动当前子进程关联的虚拟机。
- 调用
- 释放重载信号量:
- 调用
comm.reload_semaphore.release()
,释放重载信号量,允许其他子进程重载虚拟机。
- 调用
- 发送启动和请求消息:
- 调用
send_msg()
函数,向主进程发送启动消息(KAFL_TAG_START)
和请求消息(KAFL_TAG_REQ)
,通知主进程当前子进程已经启动并准备好执行任务。
- 调用
- 主循环执行:
- 进入主循环,通过
interprocess_proto_handler()
方法处理与主进程和其它子进程之间的通信和协议交互。
- 进入主循环,通过
- 异常处理(可选):
- 可以根据需要进行异常处理,当前代码中注释掉了相关代码段。
该函数的核心逻辑为上面标红的三部分,即:
- 启动虚拟机
该逻辑由self.q.start()
函数调用实现,而start()
函数实现在"/kAFL/kAFL-Fuzzer/common/qemu.py"的第237行。
关于该函数已经在"1.2.2.4、配置kAFL组件"章节详细分析过了,在此不再赘述。总之该函数启动了待Fuzz目标,不过需要注意的是,此处启动的虚拟机是之前处于冻结状态的虚拟机,该虚拟机正处于等待一些代码执行的状态中(具体可参见"1.2.2.3、配置待Fuzz目标"章节)
- 发送启动和请求消息
该处逻辑由下述代码完成。
这两行代码的作用是向MasterProcess发送两个不同类型的消息,用于通知MasterProcess有关SlaveProcess启动和请求操作。具体来说发送了KAFL_TAG_START
消息和KAFL_TAG_REQ
消息,后续就可以根据在此处发送的这两条消息进行对应的处理。
- 主循环执行
该逻辑由self.interprocess_proto_handler()
函数调用实现。而interprocess_proto_handler()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/slave.py"的第292行。
很明显,该函数根据接收到的消息进行相应的操作,具体来说包括四种操作方法,即:
KAFL_TAG_JOB
该操作由self.__respond_job_req(response)
函数调用实现。而__respond_job_req()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/slave.py"的第83行。
python
def __respond_job_req(self, response):
results = []
performance = 0.0
counter = 0
self.comm.slave_locks_A[self.slave_id].acquire()
for i in range(len(response.data)):
if not self.comm.stage_abortion_notifier.value:
new_bits = True
vm_reloaded = False
self.reloaded = False
bitmap = ""
payload = ""
payload_size = 0
if self.comm.slave_termination.value:
self.comm.slave_locks_B[self.slave_id].release()
send_msg(KAFL_TAG_RESULT, results, self.comm.to_mapserver_queue, source=self.slave_id)
return
while True:
while True:
try:
payload, payload_size = self.q.copy_master_payload(self.comm.get_master_payload_shm(self.slave_id), i,
self.comm.get_master_payload_shm_size())
start_time = time.time()
bitmap = self.q.send_payload()
performance = time.time() - start_time
break
except:
if not self.__restart_vm():
return
self.reloaded = True
if not bitmap:
log_slave("SHM ERROR....", self.slave_id)
if not self.__restart_vm():
self.comm.slave_locks_B[self.slave_id].release()
send_msg(KAFL_TAG_RESULT, results, self.comm.to_mapserver_queue, source=self.slave_id)
return
else:
break
self.q.finalize_iteration()
new_bits = self.q.copy_bitmap(self.comm.get_bitmap_shm(self.slave_id), i, self.comm.get_bitmap_shm_size(), bitmap, payload, payload_size, effector_mode=self.comm.effector_mode.value)
if new_bits:
self.q.copy_mapserver_payload(self.comm.get_mapserver_payload_shm(self.slave_id), i, self.comm.get_mapserver_payload_shm_size())
if self.comm.slave_termination.value:
self.comm.slave_locks_B[self.slave_id].release()
send_msg(KAFL_TAG_RESULT, results, self.comm.to_mapserver_queue, source=self.slave_id)
return
if self.q.timeout and not (self.q.crashed or self.q.kasan):
vm_reloaded = True
if mmh3.hash64(bitmap) not in self.false_positiv_map:
while True:
try:
if not self.__restart_vm():
return
start_time = time.time()
bitmap = self.q.send_payload()
performance = time.time() - start_time
break
except:
pass
if not self.q.timeout:
#false positiv timeout
self.false_positiv_map.add(mmh3.hash64(bitmap))
self.reloaded = False
else:
#false positiv timeout (already seen)
self.reloaded = False
counter += 1
if self.q.crashed or self.q.timeout or self.q.kasan or self.reloaded:
vm_reloaded = True
results.append(FuzzingResult(i, self.q.crashed, (self.q.timeout or self.reloaded), self.q.kasan, response.data[i],
self.slave_id, performance, reloaded=vm_reloaded, qid=self.slave_id))
if not self.__restart_vm():
self.comm.slave_locks_B[self.slave_id].release()
send_msg(KAFL_TAG_RESULT, results, self.comm.to_mapserver_queue, source=self.slave_id)
return
self.reloaded = True
else:
results.append(FuzzingResult(i, self.q.crashed, (self.q.timeout or self.reloaded), self.q.kasan, response.data[i],
self.slave_id, performance, reloaded=vm_reloaded, new_bits=new_bits, qid=self.slave_id))
if new_bits and self.auto_reload:
self.__restart_vm()
self.reloaded = False
else:
results.append(FuzzingResult(i, False, False, False, response.data[i], self.slave_id, 0.0, reloaded=False, new_bits=False, qid=self.slave_id))
if self.comm.slave_termination.value:
self.comm.slave_locks_B[self.slave_id].release()
send_msg(KAFL_TAG_RESULT, results, self.comm.to_mapserver_queue, source=self.slave_id)
return
self.comm.slave_locks_B[self.slave_id].release()
send_msg(KAFL_TAG_RESULT, results, self.comm.to_mapserver_queue, source=self.slave_id)
这个函数实现了对主进程发送的任务请求的响应,包括向虚拟机发送测试用例、获取测试结果、处理虚拟机的重启和超时情况、生成测试结果,并将结果发送给MapServer进程。
- 循环遍历任务请求:
- 函数进入一个循环,迭代处理接收到的任务请求中的每个任务。
- 获取测试用例数据:
- 每次迭代开始时,通过调用
self.q.copy_master_payload()
方法从主进程获取当前任务的测试用例数据,并同时获取数据的大小。
- 每次迭代开始时,通过调用
- 向虚拟机发送测试用例:
- 使用获取到的测试用例数据,调用
self.q.send_payload()
方法将数据发送给虚拟机进行测试。 - 在发送数据的过程中,记录发送数据的性能。
- 使用获取到的测试用例数据,调用
- 处理异常情况:
- 如果在发送测试用例数据时出现异常(例如虚拟机崩溃),则执行虚拟机的重启操作,以确保下一轮测试能够进行。
- 处理超时情况:
- 在发送测试用例数据后,如果发生了超时,会考虑是否需要进行虚拟机的重启操作,以获取更多的覆盖信息。
- 生成FuzzingResult对象:
- 根据虚拟机的测试结果(如崩溃、超时等情况),生成相应的FuzzingResult对象,并将其添加到结果列表中。
- 在生成对象时记录测试的性能指标,如处理时间等。
- 处理新位图:
- 如果测试结果中发现了新的覆盖位,并且自动重启标志被设置,会尝试重启虚拟机,以便获取更多的覆盖信息。
- 发送结果给MapServer进程:
- 在处理完所有任务后,将生成的所有FuzzingResult对象发送给MapServer进程进行进一步处理。
- 将结果发送给MapServer进程的过程通过调用
send_msg()
函数实现。
在该函数中,核心逻辑是上面标红的四处,下面对其进行分析。
- 获取测试用例数据
该逻辑由self.q.copy_master_payload()
函数调用实现。而该函数实现在"/kAFL/kAFL-Fuzzer/common/qemu.py"的第406行。
这个函数用于将文件系统中的有效负载数据复制到共享内存中的特定位置,以便在后续的操作中使用。通过逐步移动文件系统和共享内存中的指针,并将数据从文件系统读取并写入到共享内存中的目标位置来完成复制过程。
- 向虚拟机发送测试用例
该逻辑由self.q.send_payload()
函数调用实现。而send_payload()
函数实现在"/kAFL/kAFL-Fuzzer/common/qemu.py"的第349行。
该函数是用于向虚拟机发送Payload数据,并检测虚拟机执行过程中是否发生崩溃、超时或KASAN报告的方法。具体包括以下功能:
- 发送Payload:
- 调用底层的控制方法向虚拟机发送Payload数据,表示开始执行Fuzz测试。
- 标记崩溃、超时和KASAN状态为False,以便在后续的检查中更新这些状态。
- 超时检测:
- 如果启用了超时检测(默认情况下是启用的),则进入超时检测的逻辑。
- 通过循环检查接收到的数据,判断是否发生了超时。
- 如果接收到的数据表明存在超时情况,则设置超时标志,并结束当前测试迭代。
- 如果超过了预设的超时阈值或循环计数达到一定次数,也认定为发生了超时情况。
- 处理崩溃和KASAN:
- 如果接收到的数据表明发生了崩溃或KASAN报告,则设置相应的标志,并结束当前测试迭代。
- 如果接收到的数据表明没有发生崩溃、超时或KASAN报告,则不做任何操作。
- 返回结果:
- 读取共享内存中的位图数据,并返回给调用者,以供后续的处理和分析使用。
- 处理异常情况
该逻辑由self.__restart_vm()
函数调用完成。而__restart_vm()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/slave.py"的第56行。
这段代码是一个私有方法,用于重新启动虚拟机以进行下一轮的Fuzz测试。下面是该方法的详细分析:
- 检查终止条件:
- 在函数开始时,首先检查是否需要终止虚拟机的运行。这是通过检查
self.comm.slave_termination.value
的值来实现的。如果需要终止,则直接返回False
,表示无法重新启动虚拟机。
- 在函数开始时,首先检查是否需要终止虚拟机的运行。这是通过检查
- 获取重启信号量:
- 调用
self.comm.reload_semaphore.acquire()
获取重启信号量,以确保在重新启动过程中不会发生冲突。
- 调用
- 尝试软重启:
- 尝试使用软重启方式重新启动虚拟机,这是通过调用
self.q.soft_reload()
实现的。如果软重启失败(例如出现内存泄漏等问题),则会转入下一步的彻底重启方式。
- 尝试使用软重启方式重新启动虚拟机,这是通过调用
- 彻底重启:
- 在彻底重启过程中,首先销毁当前的虚拟机对象
(self.q.__del__())
,然后重新创建一个新的虚拟机对象(self.q = qemu(self.slave_id, self.config))
。这一过程会一直尝试,直到成功创建一个新的虚拟机对象并成功启动。 - 如果重启失败,会记录日志并等待一段时间(
0.5
秒),然后再次尝试重新启动虚拟机。
- 在彻底重启过程中,首先销毁当前的虚拟机对象
- 释放重启信号量:
- 最后,调用
self.comm.reload_semaphore.release()
释放重启信号量。
- 最后,调用
- 更新超时阈值:
- 在成功重新启动虚拟机后,会根据当前阶段的超时阈值更新虚拟机的超时阈值,以确保在新的测试阶段内正确处理超时情况。
- 检查终止条件(再次):
- 在函数的末尾再次检查是否需要终止虚拟机的运行。如果需要终止,则返回
False
;否则返回True
,表示成功重新启动虚拟机。
- 在函数的末尾再次检查是否需要终止虚拟机的运行。如果需要终止,则返回
- 生成FuzzingResult对象
该逻辑由FuzzingResult()
函数调用实现。而FuzzingResult()
实际是一个类,其实现在"/kAFL/kAFL-Fuzzer/fuzzer/protocol.py"的第20行。对于类的分析,要着眼于其__init__()
函数。因为__init__()
函数才是调用该类时执行并返回对象的逻辑。
这个类用于表示一次Fuzz测试的结果。它包含了以下初始化内容。
- pos: 测试用例的位置。
- crash: 表示是否导致了崩溃。
- timeout: 表示是否发生了超时。
- kasan: 表示是否触发了KASAN。
- affected_bytes: 受影响的字节。
- slave_id: 执行测试的从属进程的ID。
- performance: 测试的性能。
- reloaded: 表示是否进行了重启。
- new_bits: 表示是否发现了新的覆盖位。
- qid: 关联的队列 ID。
KAFL_TAG_REQ_BITMAP
该操作由self.__respond_bitmap_req(response)
函数调用实现。而__respond_bitmap_req()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/slave.py"的第270行。
这段代码是SlaveProcess类中的一个方法,用于响应来自MasterProcess的位图请求。以下是其详细分析:
- 方法签名和参数:
- 方法名为"__respond_bitmap_req",表明其功能是响应位图请求。
- 接受一个参数
response
,这是从MasterProcess接收到的消息对象,其中包含了请求的数据。
- 设置虚拟机负载:
- 使用
self.q.set_payload(response.data)
将从MasterProcess收到的负载数据设置到虚拟机中。
- 使用
- 发送负载并接收位图:
- 使用虚拟机对象
self.q
的send_payload()
方法发送负载,并将返回的位图赋值给变量bitmap
。 - 由于这里可能存在发送失败的情况,使用了一个
while True
的循环来反复尝试发送负载,直到成功为止。
- 使用虚拟机对象
- 处理异常情况:
- 如果发送负载的过程中出现异常,记录日志并调用
__restart_vm()
方法重新启动虚拟机。 - 然后再次尝试发送负载,直到成功或达到最大尝试次数为止。
- 如果发送负载的过程中出现异常,记录日志并调用
- 发送位图回复:
- 使用
send_msg()
方法将收到的位图作为回复发送回MasterProcess,以便后续处理和分析。
- 使用
这个方法的核心作用是确保SlaveProcess能够正确响应MasterProcess的位图请求,通过与虚拟机的交互,获取位图信息并将其发送给MasterProcess。其核心逻辑为上面标红的三部分。
- 设置虚拟机负载
该逻辑由self.q.set_payload(response.data)
函数调用完成。而set_payload()
函数实现在/kAFL/kAFL-Fuzzer/common/qemu.py
的第502行。
这个函数用于设置虚拟机的测试用例(payload)。它将测试用例写入到虚拟机的共享内存中,以便在后续的测试过程中使用。具体分析如下:
- 定位到共享内存开头:
self.fs_shm.seek(0)
将文件指针移动到共享内存的开头位置,以便开始写入测试用例数据。 - 写入测试用例长度:将测试用例的长度转换为32位的字节表示,并按照小端序写入到共享内存中。这里使用
to_string_32()
函数将长度转换为4字节的字符串表示,然后逐字节写入到共享内存中。 - 写入测试用例数据:将测试用例的实际数据(
payload
)写入到共享内存中,紧接着长度信息的后面。- 发送负载并接收位图
该逻辑由self.q.send_payload()
函数调用实现,而send_payload()
函数在之前已经详细分析过了,在此不再赘述 - 处理异常情况
该逻辑由self.__restart_vm()
函数调用实现,而__restart_vm()
函数在之前已经详细分析过了,在此不再赘述 KAFL_TAG_REQ_SAMPLING
该操作由__respond_sampling_req(response)
函数调用实现。而__respond_sampling_req()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/slave.py"的第184行。
- 发送负载并接收位图
python
def __respond_sampling_req(self, response):
payload = response.data[0]
sampling_rate = response.data[1]
self.stage_tick_treshold = 0
sampling_counter = 0
sampling_ticks = 0
error_counter = 0
round_checker = 0
self.__restart_vm()
self.q.set_payload(payload)
filter_hash = self.__check_filter_bitmaps()
while True:
error = False
while True:
try:
self.q.enable_sampling_mode()
bitmap = self.q.send_payload()
break
except:
log_slave("Sampling fail...", self.slave_id)
if not self.__restart_vm():
return
for i in range(5):
try:
if error_counter >= 2:
log_slave("Abort sampling...", self.slave_id)
error = False
break
new_bitmap = self.q.send_payload()
if self.q.crashed or self.q.timeout or self.q.kasan:
log_slave("Sampling timeout...", self.slave_id)
error_counter += 1
if not self.__restart_vm():
error = False
break
else:
self.q.submit_sampling_run()
sampling_counter += 1
sampling_ticks = self.q.end_ticks - self.q.start_ticks
except:
log_slave("Sampling wtf??!", self.slave_id)
if not self.__restart_vm():
return
while True:
try:
self.q.disable_sampling_mode()
break
except:
if not self.__restart_vm():
return
tmp_hash = self.__check_filter_bitmaps()
if tmp_hash == filter_hash:
round_checker += 1
else:
round_checker = 0
filter_hash = tmp_hash
if round_checker == 5:
break
log_slave("Sampling findished!", self.slave_id)
if sampling_counter == 0:
sampling_counter = 1
self.stage_tick_treshold = sampling_ticks / sampling_counter
log_slave("sampling_ticks: " + str(sampling_ticks), self.slave_id)
log_slave("sampling_counter: " + str(sampling_counter), self.slave_id)
log_slave("STAGE_TICK_TRESHOLD: " + str(self.stage_tick_treshold), self.slave_id)
if self.stage_tick_treshold == 0.0:
self.stage_tick_treshold = 1.0
self.q.set_tick_timeout_treshold(3 * self.stage_tick_treshold * self.timeout_tick_factor)
send_msg(KAFL_TAG_REQ_SAMPLING, bitmap, self.comm.to_master_from_slave_queue, source=self.slave_id)
该函数实现了对采样请求的响应,并在虚拟机中进行相应的操作和采样,最终将采样结果发送给主进程进行进一步处理。以下是其完成的具体操作。
- 初始化参数:
- 从响应中获取采样请求的
payload
和采样率。 - 初始化采样相关的参数,如阶段tick阈值、采样计数器、采样ticks、错误计数器等。
- 从响应中获取采样请求的
- 重置虚拟机:
- 检查是否需要中止当前虚拟机操作,若是,则返回
False
,结束操作。 - 获取重置信号量,确保单次只有一个进程可以重置虚拟机。
- 重置虚拟机,如果连续软重启次数达到32次,则抛出异常,重新创建虚拟机实例。
- 设置虚拟机的阶段tick超时阈值。
- 检查是否需要中止当前虚拟机操作,若是,则返回
- 设置测试用例:
- 将从采样请求中获取的
payload
设置到虚拟机中,为采样操作做准备。
- 将从采样请求中获取的
- 开始采样:
- 启用采样模式,向虚拟机发送测试用例进行采样。
- 若发送失败,则记录错误信息并重新启动虚拟机。
- 采样过程:
- 循环进行多次采样操作,每次发送测试用例进行采样。
- 检查虚拟机的状态,如崩溃、超时或KASAN错误,并做相应处理。
- 每次采样后,记录采样计数器和采样ticks。
- 停止采样:
- 禁用采样模式,确保采样结束。
- 检查过滤器位图是否发生变化,若连续5轮未发生变化,则停止采样。
- 设置阶段超时阈值:
- 根据采样结果设置阶段tick的超时阈值,以确保后续测试用例在合理的时间内完成。
- 发送采样结果:
- 将采样得到的位图发送给主进程进行后续处理。
该函数的核心逻辑为上面标红的五处,下面对其进行分析。
- 重置虚拟机
该逻辑由self.__restart_vm()
函数调用实现,而__restart_vm()
函数在之前已经详细分析过了,在此不再赘述 - 设置测试用例
该逻辑由self.q.set_payload(payload)
函数调用实现,而set_payload()
函数在之前已经详细分析过了,在此不再赘述 - 开始采样
该逻辑由self.q.enable_sampling_mode()
函数调用和self.q.send_payload()
函数调用实现,而send_payload()
函数在之前已经详细分析过了,在此不再赘述。而enable_sampling_mode()
函数实现在"/Kafl/Kafl-Fuzzer/common/qemu.py"的第389行。
这个方法通过向控制通道发送一个指定的字符来启用采样模式。具体来说,它发送了一个字符"S"
,这是约定好的指令,告诉虚拟机开始记录执行时间的采样信息。虚拟机接收到指令后会根据约定的处理逻辑开启采样模式。
- 采样过程
该逻辑由self.q.send_payload()
函数调用实现,而send_payload()
函数在之前已经详细分析过了,在此不再赘述 - 停止采样
该逻辑由self.q.disable_sampling_mode()
函数调用实现。而self.q.disable_sampling_mode()
函数实现在"/Kafl/Kafl-Fuzzer/common/qemu.py"的第392行。
这个方法通过向控制通道发送一个指定的字符来停止采样模式。具体来说,它发送了一个字符"O"
,这是约定好的指令,告诉虚拟机停止记录执行时间的采样信息。虚拟机接收到指令后会根据约定的处理逻辑停止采样模式。
KAFL_TAG_REQ_BENCHMARK
该操作由self.__respond_benchmark_req(response)
函数调用实现。而__respond_benchmark_req()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/slave.py"的第282行。
这段代码是SlaveProcess类中的一个方法,用于响应来自MasterProcess的基准测试请求。下面是该方法的详细分析:
- 方法签名和参数:
- 方法名为"__respond_benchmark_req",表明其功能是响应基准测试请求。
- 接受一个参数
response
,这是从MasterProcess接收到的消息对象,其中包含了基准测试所需的数据。
- 提取基准测试数据:
- 从
response.data
中提取基准测试所需的负载数据和基准测试速率。 payload
变量存储了负载数据,benchmark_rate
变量存储了基准测试速率。
- 从
- 执行基准测试:
- 使用
for
循环执行基准测试,循环次数为benchmark_rate
,即执行基准测试的次数。 - 在每次循环中,将负载数据设置到虚拟机中,并发送负载。
- 如果在发送负载的过程中虚拟机发生了崩溃、超时或KASAN报告,就调用
__restart_vm()
方法重新启动虚拟机。
- 使用
- 发送基准测试完成消息:
- 在基准测试完成后,向MasterProcess发送一个空消息,表示基准测试已经完成。
这个方法的目的是确保SlaveProcess能够响应MasterProcess的基准测试请求,并在指定的速率下执行基准测试。在执行基准测试过程中,会监测虚拟机的状态,以确保测试的准确性和可靠性。该函数的核心逻辑为上面标红的部分,而该逻辑包含三个重要的函数调用,即:
self.q.set_payload(payload)
self.q.send_payload()
self.__restart_vm()
而以上三个函数在前面已经分析过了,故在此不再赘述。
现在我们已经清楚了这四种操作的具体方法,但是在主循环中究竟调用了哪个函数呢?在我们之前的分析中,在进入主循环之前,传入KAFL_TAG_START
消息和KAFL_TAG_REQ
消息,很明显这两个消息在这里并没有对应的处理函数,所以在该阶段并不做任何事情。那这四个函数的用途是什么呢?在该阶段用不到,不代表在后续用不到,因为Fuzz测试是一个循环的过程,所以后续会用到这里介绍的四种函数,而后续若遇到对该四个函数的调用,不会做过多分析,因为在这里已经介绍的很详细了。此外,如果后续遇到类似的结构,也不会做过多赘述,因为这里是第一次遇到这种结构,所以分析了一下,后续若在遇到这种结构,等使用到相应的处理函数时再做详细分析。
故在该阶段,最终启动了各个从属进程,同时启动了各个从属进程对应的虚拟机实例,并且对其进行初始化,然后就可以等待主进程对其发送对应的消息而开始进行Fuzz测试了。
让我们思考,难道该阶段到此就结束了么?其实并没有,在"1.2.2.4、配置待kAFL组件"章节我们分析过,虚拟机启动后,会继续执行其冻结后的操作,即"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/loader/loader.c"的第107及其后面的代码,并且最重要的是将本章节最开始执行的命令中的参数对应的二进制加载到内存中(即kAFL_hypercall(HYPERCALL_KAFL_GET_PROGRAM, (uint64_t)program_buffer);
函数调用),最后将其执行(即load_programm(program_buffer);
函数调用)。
那么在这里执行的是什么二进制程序呢?在我们本章最开始执行的是sudo python kafl_fuzz.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/fuzzer/ext4 512 /path/to/input/seed/ /path/to/working/output/ -ip0 0xffffffffc0287000-0xffffffffc028b000 -v --Purge
命令,故在这里执行的是"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/ext4"二进制程序(关于所使用的二进制程序可参见"1.2.2.2、准备待Fuzz目标"章节)。若要分析该二进制程序,就要来看其对应的"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/fs_fuzzer.c"源代码的第54行实现的main()
函数。
c
int main(int argc, char** argv)
{
struct stat st = {0};
int fd, ret;
char loopname[4096];
int loopctlfd, loopfd, backingfile;
long devnr;
kAFL_payload* payload_buffer = mmap((void*)NULL, PAYLOAD_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memset(payload_buffer, 0xff, PAYLOAD_SIZE);
kill_systemd();
system("mkdir /tmp/a/");
loopctlfd = open("/dev/loop-control", O_RDWR);
devnr = ioctl(loopctlfd, LOOP_CTL_GET_FREE);
sprintf(loopname, "/dev/loop%ld", devnr);
close(loopctlfd);
kAFL_hypercall(HYPERCALL_KAFL_SUBMIT_CR3, 0);
kAFL_hypercall(HYPERCALL_KAFL_GET_PAYLOAD, (uint64_t)payload_buffer);
loopfd = open(loopname, O_RDWR);
backingfile = open(KAFL_TMP_FILE, O_RDWR | O_CREAT | O_SYNC, 0777);
ioctl(loopfd, LOOP_SET_FD, backingfile);
while(1){
lseek(backingfile, 0, SEEK_SET);
kAFL_hypercall(HYPERCALL_KAFL_NEXT_PAYLOAD, 0);
write(backingfile, payload_buffer->data, payload_buffer->size-4);
ioctl(loopfd, LOOP_SET_CAPACITY, 0);
kAFL_hypercall(HYPERCALL_KAFL_ACQUIRE, 0);
#ifdef EXT4
ret = mount(loopname, "/tmp/a/", "ext4", payload_buffer->data[payload_buffer->size-4], NULL);
#elif NTFS
ret = mount(loopname, "/tmp/a/", "ntfs", payload_buffer->data[payload_buffer->size-4], NULL);
#elif FAT32
ret = mount(loopname, "/tmp/a/", "msdos", payload_buffer->data[payload_buffer->size-4], NULL);
#endif
if(!ret){
mkdir("/tmp/a/trash", 0700);
stat("/tmp/a/trash", &st);
umount2("/tmp/a", MNT_FORCE);
}
kAFL_hypercall(HYPERCALL_KAFL_RELEASE, 0);
}
close(backingfile);
return 0;
}
这段代码的主要作用是模拟对文件系统的挂载操作,并在其中嵌入了kAFL的超级调用来收集Payload和执行Payload。具体来说,其逻辑如下。
- 初始化阶段:
- 初始化变量:
struct stat st = {0};
:用于存储文件状态信息的结构体,初始化为零。int fd, ret;
:文件描述符和返回值的整型变量。char loopname[4096];
:存储loop设备名称的字符数组。int loopctlfd, loopfd, backingfile;
:loop控制设备文件描述符、loop设备文件描述符和backing file文件描述符。long devnr;
:存储loop设备编号的长整型变量。
- 创建内存映射:
- 使用
mmap()
函数创建一个大小为PAYLOAD_SIZE
的匿名内存映射,权限为读写,初始化为0xff
。 PAYLOAD_SIZE
是一个预先定义的常量,表示Payload数据的大小。
- 使用
- 停止systemd相关服务:
- 调用
kill_systemd()
函数停止systemd相关的服务,这是为了确保在挂载文件系统时不受其影响。
- 调用
- 创建目录和获取loop设备:
- 使用
system()
函数创建"/tmp/a/"目录。 - 打开"/dev/loop-control"设备,并使用
ioctl()
函数获取一个空闲的loop设备编号。 - 将获取的loop设备编号拼接成loop设备的名称,并存储在
loopname
变量中。
- 使用
- 执行kAFL超级调用、打开loop设备和创建备份文件:
- 执行kAFL的超级调用,以提交CR3寄存器的值,并且获取Payload到
payload_buffer
变量中。 - 打开上一小阶段获取到的loop设备(即
loopname
变量)。 - 调用
ioctl()
函数,将loop设备和backing file文件关联起来。它将backing file文件关联到loopfd
所代表的loop设备上,使得后续对loop设备的操作实际上是对backing file文件的操作。
- 执行kAFL的超级调用,以提交CR3寄存器的值,并且获取Payload到
- 初始化变量:
- 主循环阶段:
- 在一个无限循环中执行以下操作:
- 设置backing file文件的偏移量为
0
。 - 使用
kAFL_hypercall()
函数获取下一个Payload,并将之前获取到的Payload(即payload_buffer
变量)写入backing file文件。 - 设置loop设备的容量。
- 调用
kAFL_hypercall()
函数获取控制权。 - 根据预定义的宏选择文件系统类型,并进行挂载操作。
- 如果挂载成功,则创建"/tmp/a/trash"目录,并获取其状态,然后强制卸载"/tmp/a"。
- 调用
kAFL_hypercall()
函数进行Payload释放。
- 设置backing file文件的偏移量为
- 在一个无限循环中执行以下操作:
- 资源释放阶段:
- 关闭backing file文件:
- 使用
close()
函数关闭backing file文件,并释放资源。
- 使用
- 关闭backing file文件:
以上函数就是kAFL进行Fuzz测试的最终执行逻辑,而其中最重要的部分就是上面红色部分的逻辑,即下面所示的代码。
这段代码是一个条件编译块,根据预处理器中定义的不同宏来选择性地编译不同的代码块。在这种情况下,根据宏的定义选择要挂载的文件系统类型。具体来说:
- 如果定义了
EXT4
宏,则将挂载loopname
所指向的设备到"/tmp/a/"目录,文件系统类型为"ext4"
,并用payload_buffer->data[payload_buffer->size-4]
指定挂载选项。 - 如果定义了
NTFS
宏,则将挂载loopname
所指向的设备到"/tmp/a/"目录,文件系统类型为 "ntfs",并用payload_buffer->data[payload_buffer->size-4]
指定挂载选项。 - 如果定义了 FAT32 宏,则将挂载
loopname
所指向的设备到"/tmp/a/"目录,文件系统类型为"msdos"
,并用payload_buffer->data[payload_buffer->size-4]
指定挂载选项。
故这部分代码会根据宏的不同定义,选择对应的文件系统类型进行挂载。由于我们测试的都是Linux系统,故最终挂载的就是"ext4"
类型的文件系统,其实就是挂载的Payload到目标目录中,从而实现了Fuzz测试。当然,后续章节中也对Payload的变异过程进行了分析,通过不停的变异Payload而进行覆盖率更高的Fuzz测试。
1.2.3.10、启动更新进程
该逻辑由update_process.start()
实现,在这里调用的是更新进程对应的目标函数,即"1.2.3.6、创建更新进程"介绍的update_loader(comm)
函数调用。而update_loader(comm)
函数实现在"/Kafl/kAFL-Fuzzer/fuzzer/process/update.py"的第33行。
该函数的目的是创建一个用于更新的子进程,并执行该子进程的主循环。以下是其详细操作。
- 记录进程PID:
- 使用
os.getpid()
获取当前进程的PID,并记录在日志中。
- 使用
- 创建
UpdateProcess
对象:- 实例化一个名为"slave_process"的
UpdateProcess
对象,传入通信对象comm
。
- 实例化一个名为"slave_process"的
- 执行主循环:
- 尝试调用
slave_process
对象的loop
方法执行主循环。
- 尝试调用
- 处理中断异常:
- 如果捕获到
KeyboardInterrupt
异常,记录一条退出消息,然后结束函数的执行。
- 如果捕获到
很明显,该函数的核心是上面标红的两部分,下面将对其进行分析。
- 创建
UpdateProcess
对象
该逻辑由UpdateProcess(comm)
函数调用实现,而UpdateProcess()
实际是一个类,其实现在"/Kafl/kAFL-Fuzzer/fuzzer/process/update.py"的第41行。对于类的分析,要着眼于其__init__()
函数。因为__init__()
函数才是调用该类时执行并返回对象的逻辑。
这个UpdateProcess
类用于处理更新相关的操作,在该构造函数中,主要初始化了以下三个属性。
名称 | 作用 | 备注 |
---|---|---|
self.comm |
保存与其它进程进行通信的通道 | 在"1.2.3.4、创建通信器对象"章节中已经介绍过了 |
self.config |
保存Fuzzer的配置信息 | 在"1.2.3.9、启动从属进程"章节中已经介绍过了 |
self.timeout |
保存UI刷新率的超时时间 | 从配置中获取 |
- 执行主循环
该逻辑由slave_process.loop()
函数调用实现,而loop()
函数实现在"/Kafl/kAFL-Fuzzer/fuzzer/process/update.py"的第87行。
该函数是更新进程的主循环,负责接收更新消息并更新Fuzzer的用户界面和评估状态。具体来说,包含如下操作。
- 初始化UI和评估对象:
- 创建了
FuzzerUI
对象ui
,用于管理Fuzzer的用户界面,初始化时传入了与Fuzzer相关的参数。 - 创建了
Evaluation
对象ev
,用于评估Fuzzer的状态和性能。
- 创建了
- 安装信号处理器:
- 调用
ui.install_sighandler()
方法,安装信号处理器,用于处理用户界面的信号事件。
- 调用
- 启动黑名单更新线程:
- 使用
Thread
创建了一个新线程,目标函数为blacklist_updater
,传入参数为ui
对象。
- 使用
- 接收并处理更新消息:
- 进入主循环,通过
recv_msg
从更新队列中接收消息,设置超时时间为self.timeout
。 - 调用
__update_ui
方法处理接收到的更新消息,并更新UI和评估对象。 - 在处理完当前消息后,检查更新队列中是否还有未处理的消息,如果有,立即处理。
- 进入主循环,通过
- 循环继续:
- 循环持续进行,不断接收和处理更新消息,保持更新进程处于活动状态。
总而言之,该阶段的目的是维护Fuzzer的UI界面的相关内容。由于我们分析的重点并不是Fuzzer的UI界面,而是Fuzzer进行Fuzz测试的逻辑,故对该阶段我们了解这些足矣,不做过多分析。
1.2.3.11、启动映射服务器进程
该逻辑由mapserver_process.start()
实现,在这里调用的是映射服务器进程对应的目标函数,即"1.2.3.7、创建映射服务器进程"介绍的mapserver_loader(comm)
函数调用。而mapserver_loader(comm)
函数实现在"/Kafl/kAFL-Fuzzer/fuzzer/process/mapserver.py"的第43行。
该函数名为"mapserver_loader",其作用是加载映射服务器进程。该函数接受一个参数comm
,用于与其它进程进行通信。具体来说,该函数的具体操作包括以下内容。
- 日志记录:
- 在函数开始时,记录了进程的PID,以便跟踪和调试。
- 使用
log_mapserver
函数记录PID。
- 创建映射服务器进程:
- 通过
MapserverProcess(comm)
创建了一个映射服务器进程的实例mapserver_process
。
- 通过
- 进程循环:
- 使用
try
和except
捕获KeyboardInterrupt
异常,以确保在收到键盘中断信号时能够优雅地退出进程。 - 在循环中调用了
mapserver_process
的loop()
方法,开始执行映射服务器的主要逻辑。
- 使用
- 异常处理:
- 在捕获
KeyboardInterrupt
异常后,设置了mapserver_process
的slave_termination
值为True
,以通知其它进程终止。 - 保存了映射服务器的数据,包括
treemap
数据和其它数据。 - 记录了数据保存成功的日志。
- 在捕获
很明显,该函数的核心逻辑为上面标红的两处逻辑,下面我们将对其进行纤细分析。
- 创建映射服务器进程
该逻辑由MapserverProcess(comm)
函数调用完成,而MapserverProcess()
实际是一个类,其实现在"/Kafl/kAFL-Fuzzer/fuzzer/process/mapserver.py"的第56行。对于类的分析,要着眼于其__init__()
函数。因为__init__()
函数才是调用该类时执行并返回对象的逻辑。
python
class MapserverProcess:
def __init__(self, comm, initial=True):
self.comm = comm
self.mapserver_state_obj = MapserverState()
self.hash_list = set()
self.crash_list = []
self.shadow_map = set()
self.last_hash = ""
self.post_sync_master_tag = None
self.effector_map = []
self.abortion_counter = 0
self.abortion_alredy_sent = False
self.comm.stage_abortion_notifier.value = False
self.new_findings = 0
self.effector_initial_bitmap = None
self.effector_sync = False
self.performance = 0
self.post_sync = False
self.pre_sync = False
self.round_counter = 0
self.round_counter_effector_sync = 0
self.round_counter_master_post = 0
self.round_counter_master_pre = 0
self.config = FuzzerConfiguration()
self.enable_graphviz = self.config.argument_values['g']
self.abortion_threshold = self.config.config_values['ABORTION_TRESHOLD']
#self.q = qemu(1337, self.config)
#self.q.start()
self.ring_buffers = []
for e in range(self.config.argument_values['p']):
self.ring_buffers.append(collections.deque(maxlen=30))
if self.config.load_old_state:
self.load_data()
self.treemap = KaflTree.load_data(enable_graphviz=self.enable_graphviz)
else:
msg = recv_msg(self.comm.to_mapserver_queue)
self.mapserver_state_obj.pending = len(msg.data)
self.treemap = KaflTree(msg.data, enable_graphviz=self.enable_graphviz)
这是一个映射服务器进程类的初始化方法,这个初始化方法主要用于设置映射服务器进程的初始状态和属性。下面是对其的详细分析。
- 初始化属性:
comm
:用于进程间通信的通信对象。mapserver_state_obj
:映射服务器的状态对象,用于跟踪服务器状态。hash_list
:存储已发现的覆盖信息的哈希值集合。crash_list
:存储已发现的崩溃信息列表。shadow_map
:存储覆盖信息的阴影映射。last_hash
:上一个处理的哈希值。post_sync_master_tag
:同步后的主标签。effector_map
:效应图。abortion_counter
:中止计数器,记录已发生的中止次数。abortion_alredy_sent
:标记是否已发送中止信号。comm.stage_abortion_notifier.value
:阶段中止通知器的值。new_findings
:新发现的覆盖信息数量。effector_initial_bitmap
:效应图初始位图。effector_sync
:标记是否进行了效应图同步。performance
:性能值。post_sync
:标记是否进行了同步后的处理。pre_sync
:标记是否进行了同步前的处理。round_counter
:循环计数器。round_counter_effector_sync
:效应图同步的循环计数器。round_counter_master_post
:主同步后处理的循环计数器。round_counter_master_pre
:主同步前处理的循环计数器。config
:保存着Fuzzer的配置信息。enable_graphviz
:是否启用Graphviz的标志。
- 初始化环形缓冲区:
- 创建了多个环形缓冲区,每个缓冲区长度为30,用于存储历史数据。
- 加载旧状态:
- 如果配置了加载旧状态,则尝试加载数据并创建
KaflTree
对象。 - 否则,从映射服务器队列中接收数据,并将其作为初始化数据创建
KaflTree
对象。
- 如果配置了加载旧状态,则尝试加载数据并创建
在该阶段除了进行各种变量的初始化外,还进行了一个比较重要的操作,即创建KaflTree对象,用于表示树形结构。目前并不了解创建的KaflTree对象对于我们的主线分析有什么影响(看起来好像并不是很重要),所以暂时先不对其进行分析,仍是对主线Fuzz测试进行后续分析。
- 进程循环
该逻辑mapserver_process.loop()
函数调用完成。而loop()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/mapserver.py"的第379行。
这段代码是一个循环,不断地接收来自映射服务器队列的消息,并根据消息的标签来调用相应的处理函数。具体分析如下:
- 同步处理器(
__sync_handler
):在每次循环开始时,首先调用__sync_handler
处理同步操作。 - 接收消息:使用
recv_msg
函数从映射服务器队列接收消息,并将其存储在request
变量中。 - 处理消息:根据接收到的消息的标签(
tag
),选择相应的处理函数进行处理。- 如果标签为
KAFL_TAG_RESULT
,调用__result_tag_handler
处理结果。 - 如果标签为
KAFL_TAG_MAP_INFO
,调用__map_info_tag_handler
处理映射信息。 - 如果标签为
KAFL_TAG_NXT_FIN
或KAFL_TAG_NXT_UNFIN
,调用__next_tag_handler
处理下一个标签。 - 如果标签为
KAFL_TAG_UNTOUCHED_NODES
,调用__untouched_tag_handler
处理未触及的节点。 - 如果标签为
KAFL_TAG_REQ_EFFECTOR
,调用__req_effector_tag_handler
处理请求执行器。 - 如果标签为
KAFL_TAG_GET_EFFECTOR
,调用__get_effector_tag_handler
处理获取执行器。
- 如果标签为
- 循环继续:处理完消息后,继续下次循环,接收并处理下条消息。
这里的逻辑很明显和"1.2.3.9、启动从属进程"章节最后所介绍的代码的处理逻辑相同,故在此并不赘述各个处理函数的具体内容,后续若使用到相应函数,再做具体分析。
1.2.3.12、主进程循环处理
该逻辑由master.loop()
函数调用完成,而loop()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/master.py"的第500行。
这段代码是一个循环,用于在主进程中执行Fuzz测试的主要逻辑。以下是其详细分析:
- 初始化循环:调用
__init_fuzzing_loop()
函数和__perform_benchmark()
函数来初始化Fuzz测试环境。 - 主循环:无限循环执行以下操作:
- 获取当前阶段的Fuzz测试次数限制(
limiter_map
)。 - 如果当前阶段未完成:
- 执行采样操作,用于收集执行路径信息。
- 执行确定性变异操作,如字节翻转、字节插入等。
- 标记未完成状态为
False
。
- 获取已发现的漏洞数量,如果数量为
0
或已完成状态为True
,则执行以下操作:- 在Fuzz测试阶段内循环执行混沌变异操作。
- 标记已完成状态为
True
。 - 如果循环结束时发现新的漏洞,重置计数器并记录重复的Fuzz测试阶段。
- 执行后同步操作,并更新完成状态。
- 获取当前阶段的Fuzz测试次数限制(
以上是整个函数的执行逻辑,在这里我们不挑重点逻辑进行分析,因为该函数中的每部分逻辑都十分重要。故下面我们将对该函数中的核心几个子函数进行详细分析。
__init_fuzzing_loop()
该函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/master.py"的第304行。
这段代码的主要目的是初始化Fuzz测试循环,并准备好开始执行Fuzz测试。以下是其主要执行逻辑。
- 初始化阶段:
- 初始化Fuzz测试循环的循环计数器,将其设为零。
- 启动Fuzz测试所需的进程或线程,包括Fuzz测试目标程序以及其它必要的服务进程。
- 环境准备阶段:
- 检查是否需要加载之前保存的状态,如果需要,则加载状态
- 否则,执行以下操作:
- 打印消息,表示状态不存在。
- 获取种子文件列表,通常从指定目录(如"work_dir/corpus")中获取。
- 对于每个种子文件,获取其对应的位图信息,并将种子文件和位图信息组合成元组,添加到数据列表中。
- 发送消息将数据列表发送到映射服务器进程的消息队列中。
- 将第一个种子文件作为当前测试的载荷文件,以备后续Fuzz测试循环使用。
__perform_bechmark()
该函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/master.py"的第355行。
这个方法用于执行基准测试操作,基准测试的目的是评估Fuzz测试环境的性能,并为后续的Fuzz测试提供参考。以下是其核心逻辑。
- 检查基准测试配置:
- 检查是否启用了基准测试模式,根据命令行参数中的
-n
参数决定是否执行基准测试。
- 检查是否启用了基准测试模式,根据命令行参数中的
- 设置技术类型:
- 将模糊测试状态对象中的技术类型设置为
"BENCHMARKING"
(基准测试),用于标识当前执行的是基准测试。
- 将模糊测试状态对象中的技术类型设置为
- 发送输出消息:
- 向更新队列发送消息,将当前Fuzz测试状态对象发送给更新进程,以便更新UI界面显示基准测试状态。
- 执行基准测试:
- 调用
__benchmarking()
方法执行基准测试操作,传递当前载荷文件作为参数。
- 调用
__calc_stage_iterations()
该函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/master.py"的第320行。
这个方法的主要目的是根据Fuzz测试环境的配置和性能,计算出当前阶段的迭代次数和可变异字节的limiter_map
。具体分析如下:
- 初始化进度状态:
- 将Fuzz测试状态对象中的各种进度指标(如比特翻转、算术变异、有趣变异等)置为零。
- 设置载荷大小为当前载荷的长度。
- 将当前载荷内容保存到Fuzz测试状态对象中。
- 初始化
limiter_map
:- 创建一个长度与当前载荷相同的列表
limiter_map
,用于表示哪些字节是被忽略的。 - 如果指定了忽略范围参数
-i
,则将相应范围内的字节设置为不可变。
- 创建一个长度与当前载荷相同的列表
- 计算变异数量:
- 如果启用了特定变异模式
-D
,则调用相应的函数计算比特翻转、算术变异、有趣变异的数量,并保存到Fuzz测试状态对象中。 - 如果未启用特定变异模式,则将这些变异的数量设置为零。
- 如果启用了特定变异模式
- 设置混沌变异数量:
- 根据Fuzz测试环境的性能和混沌变异系数(
HAVOC_MULTIPLIER
),计算出混沌变异的数量,并保存到Fuzz测试状态对象中。
- 根据Fuzz测试环境的性能和混沌变异系数(
- 开始基准测试:
- 调用
__start_benchmark()
方法开始基准测试(其实并没有进行真正的基准测试,只是进行了一些配置,不过并不重要,就不在此展示了),传入当前迭代次数作为参数。
- 调用
- 返回
limiter_map
:- 返回
limiter_map
,用于在后续的Fuzz测试阶段中确定哪些字节是可变异的。
- 返回
__perform_sampling()
该函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/master.py"的第361行。
该方法用于执行预采样操作。在这个方法中,首先检查是否启用了预采样,并将当前技术设置为"PRE-SAMPLING"
。然后,它通过向通信管道发送消息来更新UI,以显示当前状态。如果状态中的总数为零,则调用__sampling
方法执行预采样操作,并将initial_run
参数设置为True
。这样做的目的是在开始Fuzz测试时执行预采样操作。
__perform_deterministic()
该函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/master.py"的第370行。
python
def __perform_deterministic(self, payload_array, limiter_map):
if self.config.argument_values['D']:
if not self.comm.sampling_failed_notifier.value:
if self.use_effector_map:
self.comm.effector_mode.value = True
log_master("Request effector map")
bitmap = self.__request_bitmap(self.payload)
self.__commission_effector_map(bitmap)
if self.comm.sampling_failed_notifier.value:
self.stage_abortion = True
self.comm.sampling_failed_notifier.value = False
log_master("Bit Flip...")
mutate_seq_walking_bits_array(payload_array, self.__bitflip_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=limiter_map)
mutate_seq_two_walking_bits_array(payload_array, self.__bitflip_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=limiter_map)
mutate_seq_four_walking_bits_array(payload_array, self.__bitflip_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=limiter_map)
mutate_seq_walking_byte_array(payload_array, self.__bitflip_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=limiter_map)
mutate_seq_two_walking_bytes_array(payload_array, self.__bitflip_handler, kafl_state=self.kafl_state, effector_map=limiter_map)
mutate_seq_four_walking_bytes_array(payload_array, self.__bitflip_handler, kafl_state=self.kafl_state, effector_map=limiter_map)
self.__buffered_handler(None, last_payload=True)
log_master("progress_bitflip: " + str(self.kafl_state.progress_bitflip))
log_master("progress_bitflip_amount: " + str(self.kafl_state.progress_bitflip_amount))
#self.kafl_state.progress_bitflip = self.kafl_state.progress_bitflip_amount
self.kafl_state.progress_bitflip_amount = self.kafl_state.progress_bitflip
log_master("Bit Flip done...")
#self.use_effector_map = False
if not self.stage_abortion:
if self.use_effector_map:
log_master("tUse Effector Map...")
effector_map = self.__get_effector_map(self.kafl_state.progress_bitflip)
self.comm.effector_mode.value = False
self.byte_map = []
self.kafl_state.progress_arithmetic_amount = arithmetic_range(self.payload, skip_null=self.skip_zero, effector_map=effector_map)
self.kafl_state.progress_interesting_amount = interesting_range(self.payload, skip_null=self.skip_zero, effector_map=effector_map)
self.kafl_state.technique = "EFF-SYNC"
send_msg(KAFL_TAG_OUTPUT, self.kafl_state, self.comm.to_update_queue)
log_master("Effectormap size is " + str(sum(x is True for x in effector_map)))
log_master("Effector arihmetic size is " + str(arithmetic_range(self.payload, skip_null=self.skip_zero, effector_map=effector_map)))
log_master("Effector intersting size is " + str(interesting_range(self.payload, skip_null=self.skip_zero, effector_map=effector_map)))
new_effector_map = []
for i in range(len(effector_map)):
if effector_map[i] and limiter_map[i]:
new_effector_map.append(True)
else:
new_effector_map.append(False)
effector_map = new_effector_map
else:
log_master("No effector map!")
effector_map = limiter_map
self.kafl_state.progress_arithmetic_amount = arithmetic_range(self.payload, skip_null=self.skip_zero, effector_map=effector_map)
self.kafl_state.progress_interesting_amount = interesting_range(self.payload, skip_null=self.skip_zero, effector_map=effector_map)
send_msg(KAFL_TAG_OUTPUT, self.kafl_state, self.comm.to_update_queue)
self.comm.effector_mode.value = False
log_master("Arithmetic...")
mutate_seq_8_bit_arithmetic_array(payload_array, self.__arithmetic_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=effector_map, set_arith_max=self.arith_max)
mutate_seq_16_bit_arithmetic_array(payload_array, self.__arithmetic_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=effector_map, set_arith_max=self.arith_max)
mutate_seq_32_bit_arithmetic_array(payload_array, self.__arithmetic_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=effector_map, set_arith_max=self.arith_max)
self.__buffered_handler(None, last_payload=True)
self.kafl_state.progress_arithmetic = self.kafl_state.progress_arithmetic_amount
log_master("Intesting...")
mutate_seq_8_bit_interesting_array(payload_array, self.__interesting_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=effector_map)
mutate_seq_16_bit_interesting_array(payload_array, self.__interesting_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=effector_map, set_arith_max=self.arith_max)
mutate_seq_32_bit_interesting_array(payload_array, self.__interesting_handler, skip_null=self.skip_zero, kafl_state=self.kafl_state, effector_map=effector_map, set_arith_max=self.arith_max)
self.__buffered_handler(None, last_payload=True)
self.kafl_state.progress_interesting = self.kafl_state.progress_interesting_amount
self.kafl_state.technique = "PRE-SYNC"
send_msg(KAFL_TAG_OUTPUT, self.kafl_state, self.comm.to_update_queue)
else:
effector_map = self.__get_effector_map(self.abortion_counter)
这个方法的关键点在于根据配置参数和通信状态的不同情况,执行不同的变异操作,并及时更新进度和状态。以下是其代码逻辑。
- 初始化阶段:
- 首先检查配置参数中是否设置了
D
,如果设置了,则表示要进行确定性Fuzz测试。 - 检查采样失败通知是否为假(
False
),如果使用了效果器映射,则设置效果器模式为True
,并请求效果器映射。 - 如果采样失败通知为真(
True
),则设置阶段中止标志为True
,重置采样失败通知为假(False
)。
- 首先检查配置参数中是否设置了
- 位翻转变异阶段:
- 进行一系列不同粒度的位翻转变异操作,包括单个位、两个位、四个位的翻转,以及单个字节和两个字节的翻转。
- 更新
bitflip
进度为已处理的bit
数量,并更新bitflip
进度量为bitflip
进度。
- 算术和有趣变异阶段:
- 如果没有阶段中止,并且使用了效果器映射,则获取效果器映射,并执行一系列算术变异操作和有趣变异操作。
- 更新算术变异进度为已处理的算术操作数量,并将有趣变异进度更新为已处理的有趣操作数量。
- 通信和状态更新阶段:
- 在每个阶段结束时,将Fuzz测试进度和相关信息发送给通信模块,以便进一步处理和调整。
- 如果没有阶段中止,并且使用了效果器映射,则获取效果器映射,并相应地更新进度。
在以上代码中,标红的部分是核心逻辑,看似是两个部分,实际是三个部分。下面我们对其进行分析。
- 位翻转变异阶段
该逻辑由以下部分代码实现。
在这里看似有很多函数,不过这是根据不同粒度进行相应的变异,故都是一样的逻辑,只是变异的位数不同而已。故我们对其中一个函数进行分析即可。比如我们来实现在"/kAFL/kAFL-Fuzzer/fuzzer/technique/bitflip.py"的第54行的mutate_seq_walking_bits_array()
函数。
该函数用于执行比特翻转,即逐个遍历数据中的比特,并在每个比特位置执行翻转操作。具体步骤如下:
- 准备阶段:
- 确定是否要跳过空值字节。
- 检查是否提供了效果器映射。
- 遍历比特阶段:
- 遍历输入数据中的每个比特。
- 应用比特翻转阶段:
- 根据条件确定是否对当前比特执行翻转操作。
- 如果满足条件,将当前比特所在字节中的对应比特位置取反。
- 处理阶段:
- 调用指定的处理函数(即
func
参数对应的函数),对修改后的数据进行进一步处理。 - 记录哪些字节被修改了。
- 调用指定的处理函数(即
- 恢复原始数据阶段:
- 恢复原始数据,确保原始数据不受影响。
- 算术变异阶段
- 算术变异阶段
该逻辑由以下部分代码实现。
在这里看似有很多函数,不过这是根据不同粒度进行相应的变异,故都是一样的逻辑,只是变异的位数不同而已。故我们对其中一个函数进行分析即可。比如我们来在"/kAFL/kAFL-Fuzzer/fuzzer/technique/arithmetic.py"的第41行的mutate_seq_8_bit_arithmetic_array()
函数。
这段代码实现了对输入数据进行8位算术运算的操作。具体分析如下:
- 首先,如果传入了
kafl_state
对象,则设置kafl_state
对象的技术属性为"ARITH 8"
,表示当前使用的是8位算术运算技术。 - 然后,根据
set_arith_max
参数确定了每个字节执行算术运算的次数,通常为`AFL_ARITH_MAX``,即默认的最大算术运算次数。 - 接着,通过一个循环遍历输入数据的每个字节,每个字节执行
set_arith_max
次算术运算。在每次循环中,如果输入数据的相邻字节在效果器映射中,则执行以下步骤:- 如果
skip_null
为True
并且当前字节的值为0
,则调用func
两次,将两次调用的no_data
参数设置为True
,表示没有数据修改。 - 否则,判断是否执行了比特翻转操作,如果没有执行比特翻转,则进行以下步骤:
- 记录当前字节的值为
was
。 - 将当前字节的值按照
(was + (i % set_arith_max)) & 0xff
的方式进行算术运算,并将结果存储回原始数据中。 - 如果``i % set_arith_max
不等于
0,则调用
func,将修改后的数据传递给
func`函数。 - 如果
i % set_arith_max
等于0
,则调用func
两次,将两次调用的no_data
参数设置为True
,表示没有数据修改。
- 记录当前字节的值为
- 如果执行了比特翻转,则调用
func
,将no_data
参数设置为True
,表示没有数据修改。
- 如果
通过这样的操作,可以对输入数据进行8位算术运算,并根据效果器映射的情况决定是否执行这些操作。
- 有趣变异阶段
该逻辑由以下部分代码实现。
在这里看似有很多函数,不过这是根据不同粒度进行相应的变异,故都是一样的逻辑,只是变异的位数不同而已。故我们对其中一个函数进行分析即可。比如我们来看实现在"/kAFL/kAFL-Fuzzer/fuzzer/technique/interesting_values.py"的第34行的mutate_seq_8_bit_interesting_array()
函数。
这段代码实现了对输入数据进行8位感兴趣值变异操作的功能。具体分析如下:
- 首先,如果传入了
kafl_state
对象,则设置kafl_state
对象的技术属性为"INTERST 8"
,表示当前使用的是8位感兴趣值变异技术。 - 然后,通过一个循环遍历输入数据的每个字节,对每个字节执行感兴趣值变异操作。在每次循环中,如果输入数据的相应字节在效果器映射中,则执行以下步骤:
- 如果
skip_null
为True
并且当前字节的值为0
,则继续下一次循环。 - 否则,对当前字节的所有感兴趣值进行遍历,并将感兴趣值应用到当前字节上。
- 如果应用感兴趣值后没有执行比特翻转操作,则调用
func
函数,将修改后的数据传递给func
函数。 - 如果执行了比特翻转,则调用
func
函数,将no_data
参数设置为True
,表示没有数据修改。
- 如果
通过这样的操作,可以对输入数据进行8位感兴趣值变异操作,并根据效果器映射的情况决定是否执行这些操作。
__get_num_of_finds()
该函数实现在"/kAFL/Kafl-Fuzzer/fuzzer/process/master.py"的第198行。
这个函数用于获取当前的发现数量。它会向映射服务器发送请求,询问当前发现的数量。如果处于阶段性中止状态,则发送的是中止计数器(abortion_counter
),否则发送的是轮数计数器(round_counter
)。然后,函数接收来自映射服务器的响应,并返回发现的数量。
__perform_havoc()
该函数实现在"/kAFL/Kafl-Fuzzer/fuzzer/process/master.py"的第448行。
这个方法执行"混沌"变异操作,通过改变有效载荷的各个部分来引入变化。以下是其主要执行逻辑。
- 重置进度状态:在开始混沌变异操作之前,方法首先重置了"位翻转"、"算术运算"和"感兴趣值变异"的进度状态。这是为了确保新的一轮变异操作能够从初始状态开始,并正确跟踪变异操作的进度。
- 计算混沌变异迭代次数:根据当前的性能评分和预定义的倍增器,计算并设置了混沌变异操作的迭代次数。这个迭代次数决定了混沌变异操作的频率和强度,它会随着性能评分的变化而动态调整。
- 执行混沌变异操作:调用
mutate_seq_havoc_array()
函数来执行混沌变异操作。在这个过程中,有效载荷的各个部分会被随机改变,引入一些变化,如位翻转、算术运算等。这些变化有助于增加测试用例的多样性和覆盖率。 - 更新进度状态:混沌变异操作执行完毕后,方法更新了混沌变异操作的进度状态,以反映已执行的变异操作数量。这个步骤是为了确保在后续的变异操作中能够正确跟踪进度。
- 执行切片变异操作:调用
mutate_seq_splice_array()
函数来执行切片操作。在这个步骤中,有效载荷会被切割和重新组合,以生成新的变异版本,从而进一步增加测试用例的多样性。 - 最后的处理:调用
__buffered_handler()
函数来确保最后一个有效载荷被正确处理和记录。这个步骤是为了完成本轮变异操作,并准备开始下一轮的变异。
很明显,在该函数中,上面标红的两处是核心逻辑,即:
- 执行混沌变异操作
该逻辑由实现在"/kAFL/kAFL-Fuzzer/fuzzer/technique/havoc.py"的第39行的mutate_seq_havoc_array()
函数完成。
这段代码用于执行Fuzz测试的混沌变异阶段,通过随机选择不同的混沌变异操作函数多次对输入数据进行修改,以生成新的测试用例。以下是其主要执行逻辑。
- 种子化随机数生成器:
- 在执行混沌操作之前,首先通过
reseed()
函数重新种子化随机数生成器,以确保每次运行得到的随机序列不同。
- 在执行混沌操作之前,首先通过
- 检查是否需要调整大小:
- 如果
resize
参数为True
,则将输入数据扩展为原来的两倍,生成输入数据的副本;否则,直接复制输入数据。
- 如果
- 循环执行混沌变异操作:
- 随机生成一个值
value
,范围在0
到AFL_HAVOC_STACK_POW2
之间,用于确定混沌变异操作的数量。 - 循环执行混沌变异操作,循环次数为
2^(1+value)
。 - 在每次循环中,随机选择一个混沌变异操作处理函数,并将输入数据的副本传递给该函数进行处理。
- 随机生成一个值
- 记录混沌变异操作次数:
- 每执行一次混沌变异操作,就记录一次操作次数。
- 结束条件:
- 检查已执行的混沌变异操作次数是否达到了最大迭代次数
max_iterations
,如果达到,则结束函数的执行。
- 检查已执行的混沌变异操作次数是否达到了最大迭代次数
很明显,在这里最重要的逻辑就是上面标红的逻辑,即随机选择混沌变异操作处理函数对输入数据进行变异操作。该处随机选择的混沌变异操作处理函数来自于其中的havoc_handler
变量。而该变量定义在"/kAFL/kAFL-Fuzzer/technique/fuzzer/technique/havoc_handler.py"的第217行。
可以发现,这里有非常多的变异处理函数,由于其变异原理非常相似,只是变异的位数有变化,所以我们只分析其中的havoc_perform_bit_flip()
函数即可,对于其它函数,并不赘述。havoc_perform_bit_flip()
函数实现在"/kAFL/kAFL-Fuzzer/technique/fuzzer/technique/havoc_handler.py"的第10行。
该函数的功能是随机选择数据中的一个比特位,并将其翻转(从0
变为1
或从1
变为0
),并将修改后的数据传递给指定的处理函数进行后续处理。
可以发现,该函数的变异策略基于随机,故在混沌变异阶段的所有变异函数都是基于随机的策略对数据进行变异的。
- 执行切片变异操作
该逻辑由实现在"/kAFL/kAFL-Fuzzer/fuzzer/technique/havoc.py"的第74行的mutate_seq_splice_array()
函数完成。
这段代码实现了对数据序列的变异操作,利用外部文件内容作为参考,以增加Fuzz测试的覆盖率和效果。通过随机选择并打乱文件列表,实现了多样化的变异操作,提高了测试的质量和效率。
- 文件列表构建
- 通过遍历
kafl_state
中记录的文件路径,构建了一个包含多种文件的列表files
,包括崩溃文件、KASAN报告等。 - 将文件列表进行了随机打乱,以确保每次选择的文件顺序都是随机的,增加变异的多样性。
- 通过遍历
- 变异操作调用
- 调用了
mutate_seq_havoc_array
函数,传入参数files_to_splice=files
,即使用files
列表中的文件内容作为变异操作的参考。 mutate_seq_havoc_array
函数负责根据文件内容进行变异操作,具体的变异过程和次数由max_iterations
控制。
- 调用了
很明显,以上代码的核心逻辑为上面标红的部分,该逻辑主要由实现在"/kAFL/kAFL-Fuzzer/fuzzer/technique/havoc.py"的第39行的mutate_seq_havoc_array()
。
该函数通过随机选择处理函数和控制迭代次数来实现变异操作,可以在给定的迭代次数内对数据进行多次变异,以增加测试覆盖率和发现潜在的漏洞。以下是其主要执行逻辑。
- 初始化和参数设置:
- 在开始时,通过
reseed()
函数重新设置随机数生成器的种子。 - 根据参数
resize
确定是否需要调整数据的大小,并将数据复制到copy
变量中。
- 在开始时,通过
- 主要循环:
- 通过
max_iterations
定义的次数进行循环,执行变异操作。 - 如果需要调整数据大小,则在每次迭代开始前重新复制数据到
copy
中。
- 通过
- 切片操作:
- 如果指定了外部文件列表
files_to_splice
,则使用havoc_splicing()
函数进行文件切片操作,并根据AFL_HAVOC_STACK_POW2
的值来调整操作次数。
- 如果指定了外部文件列表
- 内部循环:
- 在每次迭代中,根据
AFL_HAVOC_STACK_POW2
的值确定内部循环次数。 - 在每次内部循环中,从
havoc_handler
列表中随机选择处理函数,并对数据执行具体的突变操作。 - 计数器
cnt
用于控制变异操作的总次数,当达到最大迭代次数时,退出循环。
- 在每次迭代中,根据
- 结束:
- 当主循环中的迭代次数达到
max_iterations
时,函数执行结束。
- 当主循环中的迭代次数达到
该函数有两个核心逻辑,即上面标红的两处,下面我们将对其进行详细分析。
- 切片操作
该逻辑由实现在"/kAFL/kAFL-Fuzzer/fuzzer/technique/havoc_handler.py"的第178行的havoc_splicing()
函数。
该函数的主要功能是通过将外部文件的内容插入到输入数据中的随机位置,对输入数据进行修改。这有助于引入更多的变化和多样性,从而提高测试覆盖率和发现潜在漏洞的能力。具体来说,该函数的主要逻辑如下。
- 读取文件:对于提供的文件列表中的每个文件,它读取文件的内容。
- 比较差异:将文件的内容与输入数据进行比较,找到它们之间的第一个和最后一个不同的字节。
- 选择分割点:在找到的不同点之间随机选择一个位置作为分割点。
- 切片:将输入数据从分割点处截断,并将文件的内容插入到分割点后。
- 返回结果:返回修改后的数据。
- 内部循环
该逻辑的核心是随机选择切片变异操作处理函数对输入数据进行变异操作。该处随机选择的切片变异操作处理函数来自于其中的havoc_handler
变量。而该变量定义在"/kAFL/kAFL-Fuzzer/technique/fuzzer/technique/havoc_handler.py"的第217行。
可以发现,这里有非常多的变异处理函数,由于其变异原理非常相似,只是变异的位数有变化,所以我们只分析其中的havoc_perform_bit_flip()
函数即可,对于其它函数,并不赘述。havoc_perform_bit_flip()
函数实现在"/kAFL/kAFL-Fuzzer/technique/fuzzer/technique/havoc_handler.py"的第10行。
该函数的功能是随机选择数据中的一个比特位,并将其翻转(从0
变为1
或从1
变为0
),并将修改后的数据传递给指定的处理函数进行后续处理。
可以发现,该函数的变异策略基于随机,故在切片变异阶段的所有变异函数都是基于随机的策略对数据进行变异的。
havoc_range()
该函数实现在"/kAFL/Kafl-Fuzzer/fuzzer/technique/havoc.py"的第29行。
这段代码定义了一个名为"havoc_range"的函数,用于确定性能分数对应的最大迭代次数。它根据性能分数计算出最大迭代次数,并确保最大迭代次数不低于预先定义的最小值AFL_HAVOC_MIN
。
get_performance()
该函数实现在"/kAFL/Kafl-Fuzzer/fuzzer/state.py"的第122行。
这个方法用于计算性能评分的平均值。它首先检查性能评分环形缓冲区的长度,如果长度为0
,则返回0
。否则,它将性能评分环形缓冲区中所有值的总和除以长度,以计算平均值,并将结果返回。
__perform_post_sync()
该函数实现在"/kAFL/Kafl-Fuzzer/fuzzer/process/master.py"的第470行。
这个函数的主要作用是在同步测试的后阶段执行一些操作:
- 设置技术阶段:将当前测试阶段设置为
"POST-SYNC"
。 - 发送消息:发送一个消息,其中包含当前的kAFL状态信息,以便更新用户界面。
- 接收下一个负载:调用
__recv_next()
函数接收下一个负载,并根据传入的finished
参数和停止基准条件进行相应的处理。 - 记录状态:记录完成状态的信息,通常用于调试和日志记录。
- 重置状态:将负载、轮数计数器、阶段中止标志和中止计数器重置为初始状态。
- 返回结果:返回接收到的负载和完成状态信息,以供后续处理使用。
1.2.3.13、保存数据
该逻辑由master.save_data()
函数调用完成,而save_data()
函数实现在"/kAFL/kAFL-Fuzzer/fuzzer/process/master.py"的第540行。
这个函数用于保存主进程的状态到一个JSON文件中,具体分析如下:
- 遍历主进程对象的属性:
- 使用
self.__dict__.iteritems()
方法遍历主进程对象的所有属性。
- 使用
- 保存
kafl_state
属性的数据:- 对于每个属性,检查是否为
kafl_state
。 - 若是
kafl_state
属性,则调用save_data()
方法保存其数据,并将结果存储在dump
字典中。
- 对于每个属性,检查是否为
- 写入JSON文件:
- 使用
json.dump()
方法将dump
字典的内容写入到指定的JSON文件中。 - 文件名为
master.json
,保存位置由work_dir
参数指定。
- 使用
- 复制
kafl_filter0
文件:- 将
/dev/shm/kafl_filter0
文件复制到工作目录下。
- 将
这个函数的核心是将主进程的状态数据保存到一个JSON文件中,以便稍后可以根据需要重新加载和恢复到相同的状态,并且还可以通过分析这些数据来推断是否发生了Crash。
2、安装与使用
软件环境 | 硬件环境 | 约束条件 |
---|---|---|
Ubuntu 16.04.3 LTS(内核版本为Linux 4.10.0-28-generic) | 使用2个处理器,每个处理器4个内核,共分配8个内核 | kAFL部署在Ubuntu 16.04.3(物理机)上 |
具体的软件环境可见"2.1、源码安装"章节所示的软件环境 | 内存28GB | 本文所讲解的kAFL源代码于2024.03.22下载 |
暂无 | 硬盘500GB | 本文所安装的kAFL源代码于2024.03.22下载 |
暂无 | 暂无 | 本文的所有命令操作。若是以文字形式呈现,则是在主机中操作;若是以图片形式呈现,则是在虚拟机(即待Fuzz目标)中操作 |
暂无 | 暂无 | 具体的约束条件可见"2.1、源码安装"章节所示的软件版本约束 |
2.1、源码安装
2.1.1、部署系统依赖组件
主要任务:① 下载安装部署kAFL时所需要的组件。需要注意的是,这些操作均在主机上进行。
2.1.1.1、下载安装Git 2.7.4
- 首先执行如下命令安装来更新软件源:
powershell
$ sudo apt update
- 然后执行如下命令重启系统:
python
$ sudo reboot
- 然后执行如下命令安装Git 2.7.4:
powershell
$ sudo apt install git
- 然后执行如下命令来查看Git 2.7.4是否安装成功:
powershell
$ git --version
- 可以发现Git 2.7.4已经成功安装了:
2.1.1.2、下载安装Vim 7.4.1689
- 首先执行如下命令安装Vim 7.4.1689:
powershell
$ sudo apt install vim
- 然后输入如下命令来查看Vim 7.4.1689是否安装成功:
powershell
$ vim
- 出现如下内容即代表安装成功:
2.1.2、使用源码安装系统
主要任务:① 安装基础组件;② 安装kAFL;③ 编译QEMU 2.9.0(先对其打补丁);④ 编译Linux 4.6.2(先对其打补丁)。此外,这些所有操作均在主机上操作。
- 首先执行如下命令来到"/opt/"目录:
powershell
$ cd /opt/
- 然后使用如下命令创建"/code/"文件夹,用来保存kAFL的源码:
powershell
$ sudo mkdir code
- 然后使用如下命令进入"/code/"文件夹:
powershell
$ cd code/
- 然后在"/code/"文件夹中下载kAFL的源码:
powershell
$ sudo git clone https://github.com/RUB-SysSec/kAFL.git
- 查看是否下载成功:
powershell
$ ls
-
已经下载成功:
-
进入到kAFL的源代码目录:
powershell
$ cd kAFL/
- 查看一下源代码目录中的文件:
powershell
$ ls -l
-
整个代码目录还是比较简洁的,需要注意此时的"install.sh"还是白色的,这表明此shell安装脚本文件还不能够直接安装kAFL,因为其权限不够:
-
修改shell安装脚本文件的权限:
powershell
$ sudo chmod u+x install.sh
- 我们再来查看一下shell安装脚本文件的权限:
powershell
$ ls -l
-
可以发现"install.sh"变绿了,而且已经拥有了足够的权限,然后我们就可以使用"install.sh"进行kAFL的安装了:
-
然后执行如下命令来进行安装:
powershell
$ sudo ./install.sh
-
可以看到,kAFL已经安装成功了,最后提示我们需要重启系统:
-
使用如下命令来重启系统:
powershell
$ sudo reboot
- 到此我们就已经完成了kAFL的安装,我们来到kAFL的安装目录,使用如下命令查看一下此目录的内容:
powershell
$ cd /opt/code/kAFL/
$ ls -l
-
kAFL安装目录的内容如下所示,下面对这些内容做简单介绍:
- install.sh:kAFL的安装脚本
- kAFL-Fuzzer:kAFL进行内核Fuzzer的核心代码
- kernel.tar.gz:Linux 4.6.2的内核压缩包
- KVM-PT:作者开发的KVM的扩展
- LICENSE:kAFL的软件许可证
- linux-4.6.2:编译后的Linux 4.6.2内核
- qemu-2.9.0:QEMU虚拟机
- QEMU-PT:作者开发的QEMU的扩展
- qemu.tar.gz:QEMU虚拟机压缩包
- README.md:kAFL的帮助文档
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在步骤13安装kAFL的时候,出现如下问题:
-
这个错误提示我们没有在"sources.list"中指定"deb-src",那么我们就执行如下命令来修改"sources.list":
powershell
$ sudo vim /etc/apt/sources.list
-
将"deb-src"前面的注释符号(#)全部删除即可(下图是删除后的样子):
-
保存修改后更新软件源:
powershell
$ sudo apt-get update
- 至此我们就解决了这个问题,我们只需要回到步骤13重新继续向下操作即可
B 问题2:
-
在步骤13安装kAFL的时候,出现如下问题:
-
错误提示并没给我们太多有关于出错的信息,那么我们只能使用如下命令来打开"install.sh"来看一下究竟是哪里出了问题:
powershell
$ sudo vim install.sh
-
报错的是红框处:
-
红框处的报错提示我们MD5值不匹配,首先的思路就是查看下载位置的url是否更新了,所以向上查看,果然,"QEMU_URL"保存的url已经不再使用了,目前使用的url中不包括"-project",所以只需要将蓝框处的"-project"删除即可,最后保存修改后退出:
-
至此我们就解决了这个问题,我们只需要回到步骤13重新继续向下操作即可
C 问题3:
-
在步骤13安装kAFL的时候,出现如下问题:
-
这个错误和上一个错误一样,我们还是打开"install.sh"文件查看一下:
powershell
$ sudo vim install.sh
-
经过分析,出现该问题是因为目标网址的证书过期了,故需要在目标网站下载的时候取消对证书的验证。我们只需要按照如下图蓝框和蓝箭头处所示进行修改,即可取消对证书的验证。最后保存修改后退出:
-
至此我们就解决了这个问题,我们只需要回到步骤13重新继续向下操作即可
2.2、使用方法
需要注意的是,在本章节Fuzz的目标为Linux 4.4.0-87-generic内核,其所在的操作系统为Ubuntu 16.04.3(虚拟机)。整体的使用方法是通用的,关于测试细节,或者说对其它版本的内核进行测试,可以参考"3、测试用例"章节中的相关内容。
2.2.1、准备主机代理内核
主要任务:① 更换主机的内核为打补丁并编译后的Linux 4.6.2。需要注意的是,这些操作均在主机上进行。
- 我们下面的工作重点是将操作系统的Linux内核切换为之前安装kAFL时编译好的Linux 4.6.2,该内核目的是与待Fuzz目标进行通信,故可被称为主机代理内核。首先执行如下几条命令:
powershell
$ cd /opt/code/kAFL/
$ sudo systemctl disable unattended-upgrades.service
$ sudo systemctl stop unattended-upgrades.service
$ sudo systemctl disable apt-daily.service
$ sudo systemctl stop apt-daily.service
$ sudo systemctl disable apt-daily-upgrade.service
$ sudo systemctl stop apt-daily-upgrade.service
- 然后执行如下命令:
powershell
$ sudo echo 'GRUB_TIMEOUT_STYLE=menu' | sudo tee -a /etc/default/grub
- 然后执行如下命令更新内核:
powershell
$ sudo update-grub
-
可以发现内核已经成功更新了,其中Linux 4.6.2的内核是我们需要的:
-
然后执行如下命令重启Ubuntu:
powershell
$ sudo reboot
-
重启后进入如下界面,选择红框部分,然后按一下回车键:
-
然后会来到如下界面,选择红框部分,这就是我们需要的Linux 4.6.2版本的内核,然后按一下回车键:
-
可以正常进入系统,没有任何问题,说明内核切换成功:
-
然后输入如下命令来查看当前Ubuntu的内核版本:
powershell
$ uname -a
- 可以看到,目前操作系统的内核版本为Linux 4.6.2:
2.2.2、准备待Fuzz目标
主要任务:① 下载安装GuestOS(在主机中操作);② 下载kAFL源码(在在GuestOS中操作);③ 编译kAFL组件(在GuestOS中操作)。
- 首先进入kAFL的源代码目录:
powershell
$ cd /opt/code/kAFL/
- 在正式开始之前,使用如下命令安装qemu-system-x86,这样我们就能使用qemu的相关命令了:
powershell
$ sudo apt-get install qemu-system-x86 -y
- 然后使用如下命令创建QEMU硬盘驱动器映像:
powershell
$ sudo qemu-img create -f qcow2 linux.qcow2 20G
-
可以发现已经创建成功了:
-
然后执行如下命令创建相应目录存放相关信息:
powershell
$ sudo mkdir -p /path/to/where/to/store
- 创建成功后,使用如下命令下载虚拟机系统的ISO文件,以Ubuntu 16.04.3为例(这就是待Fuzz目标所在的操作系统):
powershell
$ sudo wget -O /path/to/where/to/store/ubuntu.iso https://old-releases.ubuntu.com/releases/16.04.3/ubuntu-16.04.3-server-amd64.iso --no-check-certificate
- 准备工作已经完成了,下面就要向虚拟机中安装准备好的ISO文件了,执行如下命令进行安装即可:
powershell
$ sudo qemu-system-x86_64 -cpu host -enable-kvm -m 512 -hda linux.qcow2 -cdrom /path/to/where/to/store/ubuntu.iso -usbdevice tablet
-
执行上面的代码后,回进入QEMU虚拟机安装界面:
-
选择"English"后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
等待安装中:
-
选择红框处后按回车:
-
随便输入一个名字后,选择"<Continue>"后按回车:
-
这里需要连续输入两次相同的密码后,选择"<Continue>"后按回车:
-
选择红框处后按回车:
-
继续等待安装中:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
等待一会安装后,会出现如下界面,我们只需要选择红框处后按回车:
-
继续等待安装中:
-
选择红框处后按回车:
-
直接按回车,不需要安装其他软件:
-
继续等待安装中:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
到这里就安装完成了,输入我们之前保存的用户名和密码后按回车即可:
-
已经成功进入到刚刚安装的虚拟机中了:
-
然后使用如下命令查看目标系统的Linux内核版本,可以目标系统的Linux内核版本为4.4.0-87-generic(这就是待Fuzz目标):
-
由于需要在虚拟机中使用kAFL,所以在虚拟机中进入如下目录:
-
创建文件夹,用来保存kAFL源代码:
-
进入新创建的文件夹中:
-
下载kAFL的源码:
-
进入到如下目录:
-
修改编译脚本的权限:
-
执行编译脚本:
-
只要出现下图中两个红框内容即为编译成功,其余的错误不用在意,因为我们并不进行MacOS和Windows的漏洞检测:
-
然后我们可以暂时关闭虚拟机:
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在步骤7安装待Fuzz目标系统的时候,出现如下问题:
-
经过查阅,此处报错是因为当前系统没有开启虚拟化引擎,首先使用如下命令来查看是否开启虚拟化引擎:
powershell
$ sudo kvm-ok
-
果然没有开启,所以我们下面就要开启当前系统的虚拟化引擎:
-
此时我们重启系统进入BIOS,按照如下图所示进行选择:
-
然后按照如下图所示进行选择,开启当前系统的虚拟化引擎。最后保存修改后重启当前系统:
-
重新启动系统后,使用如下命令来查看是否成功开启虚拟化引擎:
powershell
$ sudo kvm-ok
-
可以发现,已经成功开启虚拟化引擎了:
-
然后进入kAFL的安装目录:
powershell
$ cd /opt/code/kAFL/
- 此时我们就解决了这个问题,现在我们只需要回到步骤7重新继续向下操作即可
B 问题2:
-
在步骤7安装待Fuzz目标系统的时候,出现如下问题:
-
出现该问题是因为我们使用了类似XShell的远端命令行连接工具,而这种工具不支持图形化界面的使用(安装待Fuzz目标系统的时候需要使用图形界面)。想要解决该问题很简单,我们只需要点击"否",然后回到主机中打开一个新的命令行终端,后面的所有操作都要在此命令行终端中进行。最后来到kAFL源代码目录中即可:
powershell
$ cd /opt/code/kAFL/
- 此时我们就解决了这个问题,我们现在只需要回到步骤7重新继续向下操作即可
C 问题3:
-
在步骤43执行编译脚本的时候,出现如下问题:
-
出现该问题是因为我们没有安装GCC,我们只需要使用下面的命令安装GCC即可:
-
此时我们就解决了这个问题,我们只需要回到步骤43重新继续向下操作即可
2.2.3、配置待Fuzz目标
主要任务:① 创建GuestOS快照(在主机中操作)并将其启动;② 编译并安装内核组件(在GuestOS中操作);③ 冻结GuestOS(在GuestOS中操作)。
- 首先进入如下目录:
powershell
$ cd /path/to/
- 然后在此目录下创建快照目录并进入:
powershell
$ sudo mkdir snapshot && cd snapshot
- 然后在此目录下创建快照:
powershell
$ sudo qemu-img create -b /opt/code/kAFL/linux.qcow2 -f qcow2 overlay_0.qcow2
-
可以发现,创建成功了:
-
然后使用如下命令来创建虚拟磁盘:
powershell
$ sudo qemu-img create -f qcow2 ram.qcow2 512
-
同样创建成功了:
-
然后来到kAFL的原始源代码目录:
powershell
$ cd /opt/code/kAFL/
- 然后使用如下命令来使用QEMU-PT启动虚拟机:
powershell
$ sudo ./qemu-2.9.0/x86_64-softmmu/qemu-system-x86_64 -hdb /path/to/snapshot/ram.qcow2 -hda /path/to/snapshot/overlay_0.qcow2 -machine pc-i440fx-2.6 -monitor stdio -enable-kvm -k de -m 512
-
当然,以上命令要在可视化界面中完成,只要涉及到启动虚拟机,就在可视化界面中完成。可以发现,已经成功启动虚拟机系统,并且启动了QEMU的管理控制台:
-
然后进入到如下目录:
-
修改权限:
-
执行脚本:
-
执行成功:
-
然后进入如下目录:
-
执行此目录下的二进制脚本:
-
执行成功,此时QEMU虚拟机将会被冻结:
-
然后在QEMU管理控制台使用
savevm kafl
命令来保存快照:
-
然后使用
q
命令退出QEMU管理控制台:
-
当我们进行完上面的操作后,QEMU虚拟机系统也会关闭:
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在步骤12执行脚本的时候,出现如下问题:
-
出现该问题是因为我们没有安装make,故我们只需要执行下面的命令安装make即可:
-
此时我们就解决了该问题,我们只需要回到步骤12重新继续向下操作即可
2.2.4、配置kAFL组件
主要任务:① 修改kAFL的配置文件(在主机中操作);② 编译kAFL组件(在主机中操作);③ 测试kAFL是否配置成功(在主机中操作)。
- 首先修改kAFL的配置文件:
powershell
$ sudo vim /opt/code/kAFL/kAFL-Fuzzer/kafl.ini
-
将"QEMU_KAFL_LOCATION"的值修改为下图右边红框处的值:
-
保存修改后,执行如下命令进入kAFL-Fuzzer的agents文件夹:
powershell
$ cd /opt/code/kAFL/kAFL-Fuzzer/agents/
- 修改编译脚本的权限:
powershell
$ sudo chmod u+x compile.sh
- 执行编译脚本:
powershell
$ sudo ./compile.sh
-
执行成功,只需要关注下图中两个红框即可,因为我们并不检测MaxOS和Windows的漏洞:
-
然后来到如下目录:
powershell
$ cd /opt/code/kAFL/kAFL-Fuzzer/
- 然后执行如下命令来检查待Fuzz目标是否可以成功初始化:
powershell
$ sudo python kafl_info.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/info/info 512 -v
- 当上面的命令正常执行完毕后,会在命令行终端中打印如下图所示内容:
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在步骤8检索加载的驱动程序的地址范围的时候,出现如下问题:
-
上面的错误提示我们没有安装mmh3这个包,那我们只需执行如下命令安装即可:
powershell
$ sudo pip install mmh3
- 此时我们就解决了该问题,我们只需要回到步骤8重新继续向下操作即可
B 问题2:
-
在问题1的第2步安装mmh3的时候,出现如下问题:
-
经过查阅资料,是因为Python 2.7已经停止维护了(当前主机系统的Python版本为2.7),所以Python 2.7的pip存在很多问题,所以目前能想到的办法就是使用如下命令重新下载pip:
powershell
$ sudo wget https://bootstrap.pypa.io/pip/2.7/get-pip.py
- 然后使用如下命令重新安装pip:
powershell
$ python get-pip.py
- 然后就要重新安装mmh3了,但是这里还有一个坑,就是Python 2.7的版本太老了,不支持最新版的mmh3,Python 2.7支持的mmh3版本为2.0,所以输入如下命令安装2.0版本的mmh3:
powershell
$ sudo pip install mmh3==2.0
-
终于安装成功了:
-
上面这个包终于安装成功了,但是后面还有很多包需要安装,为了避免多次重复的工作,我将剩下所有需要安装的包的安装命令总结到下面了,依次执行即可:
powershell
$ sudo pip install lz4
$ sudo pip install psutil
- 此时我们就解决了该问题,我们只需要回到步骤8重新继续向下操作即可
2.2.5、开始Fuzz测试
主要任务:① 设置输入目录并将种子文件放置其中(在主机中操作);② 设置输出目录(在主机中操作);③ 开始进行Fuzz测试(在主机中操作)。
- 首先执行如下命令设置输入目录,并将种子文件复制到输入目录中:
python
$ sudo mkdir /path/to/input
$ sudo cp -r /opt/code/kAFL/kAFL-Fuzzer/seed/ /path/to/input/
- 然后设置输出目录:
powershell
$ sudo mkdir -p /path/to/working/output
- 最后执行如下命令开始进行Fuzz测试:
powershell
$ cd /opt/code/kAFL/kAFL-Fuzzer/
$ sudo python kafl_fuzz.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/fuzzer/ext4 512 /path/to/input/seed/ /path/to/working/output/ -ip0 0xffffffffc0287000-0xffffffffc028b000 -v --Purge
注:这里不仅仅可以使用"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/ext4"二进制文件作为Fuzzer使用,也可以使用"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/"目录中的其它二进制文件作为Fuzzer使用,只不过我们目前测试的是Linux系统,所以在这里使用"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/ext4"二进制文件作为Fuzzer。若以后需要测试其它系统,使用"/kAFL/kAFL-Fuzzer/agents/linux_x86_64/fuzzer/"目录中对应的Fuzzer即可。
-
可以发现,已经开始对目标进行Fuzz测试了:
-
若想退出Fuzz测试,按一下"Ctrl+Z"即可。至此,我们就完成了使用kAFL对目标Linux内核进行Fuzz测试的操作
3、测试用例
3.1、对Linux 4.4.0-87-generic内核进行Fuzz测试
本章节将会使用Ubuntu 16.04.3操作系统作为基准平台(主机),对Linux 4.4.0-87-generic内核(在Ubuntu 16.04.3虚拟机中)进行Fuzz测试。关于一些准备工作以及操作细节,请参考"2、安装与使用"章节中的对应内容。因为我们默认已经编译并安装好kAFL,并且将其配置好了才能进行下面的操作。
3.1.1、准备主机代理内核
- 我们下面的工作重点是将操作系统的Linux内核切换为之前安装kAFL时编译好的Linux 4.6.2,该内核目的是与待Fuzz目标进行通信,故可被称为主机代理内核。首先执行如下几条命令:
powershell
$ cd /opt/code/kAFL/
$ sudo systemctl disable unattended-upgrades.service
$ sudo systemctl stop unattended-upgrades.service
$ sudo systemctl disable apt-daily.service
$ sudo systemctl stop apt-daily.service
$ sudo systemctl disable apt-daily-upgrade.service
$ sudo systemctl stop apt-daily-upgrade.service
- 然后执行如下命令:
powershell
$ sudo echo 'GRUB_TIMEOUT_STYLE=menu' | sudo tee -a /etc/default/grub
- 然后执行如下命令更新内核:
powershell
$ sudo update-grub
-
可以发现内核已经成功更新了,其中Linux 4.6.2的内核是我们需要的:
-
然后执行如下命令重启Ubuntu:
powershell
$ sudo reboot
-
重启后进入如下界面,选择红框部分,然后按一下回车键:
-
然后会来到如下界面,选择红框部分,这就是我们需要的Linux 4.6.2版本的内核,然后按一下回车键:
-
可以正常进入系统,没有任何问题,说明内核切换成功:
-
然后输入如下命令来查看当前Ubuntu的内核版本:
powershell
$ uname -a
- 可以看到,目前操作系统的内核版本为Linux 4.6.2:
3.1.2、准备待Fuzz目标
- 首先进入kAFL的源代码目录:
powershell
$ cd /opt/code/kAFL/
- 在正式开始之前,使用如下命令安装qemu-system-x86,这样我们就能使用qemu的相关命令了:
powershell
$ sudo apt-get install qemu-system-x86 -y
- 然后使用如下命令创建QEMU硬盘驱动器映像:
powershell
$ sudo qemu-img create -f qcow2 linux.qcow2 20G
-
可以发现已经创建成功了:
-
然后执行如下命令创建相应目录存放相关信息:
powershell
$ sudo mkdir -p /path/to/where/to/store
- 创建成功后,使用如下命令下载虚拟机系统的ISO文件,以Ubuntu 16.04.3为例(这就是待Fuzz目标所在的操作系统):
powershell
$ sudo wget -O /path/to/where/to/store/ubuntu.iso https://old-releases.ubuntu.com/releases/16.04.3/ubuntu-16.04.3-server-amd64.iso --no-check-certificate
-
然后此时我们重启系统进入BIOS,按照如下图所示进行选择以开启虚拟化功能:
-
然后按照如下图所示进行选择,开启当前系统的虚拟化引擎。最后保存修改后重启当前系统:
-
重启系统后,再次进入kAFL的源代码目录中(注意,从现在开始的命令,都要在可视化的命令行终端中运行,不要使用类似XShell的远程连接工具):
powershell
$ cd /opt/code/kAFL/
- 准备工作已经完成了,下面就要向虚拟机中安装准备好的ISO文件了,执行如下命令进行安装即可:
powershell
$ sudo qemu-system-x86_64 -cpu host -enable-kvm -m 512 -hda linux.qcow2 -cdrom /path/to/where/to/store/ubuntu.iso -usbdevice tablet
-
执行上面的代码后,回进入QEMU虚拟机安装界面:
-
选择"English"后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
等待安装中:
-
选择红框处后按回车:
-
随便输入一个名字后,选择"<Continue>"后按回车:
-
这里需要连续输入两次相同的密码后,选择"<Continue>"后按回车:
-
选择红框处后按回车:
-
继续等待安装中:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
等待一会安装后,会出现如下界面,我们只需要选择红框处后按回车:
-
继续等待安装中:
-
选择红框处后按回车:
-
直接按回车,不需要安装其他软件:
-
继续等待安装中:
-
选择红框处后按回车:
-
选择红框处后按回车:
-
到这里就安装完成了,输入我们之前保存的用户名和密码后按回车即可:
-
已经成功进入到刚刚安装的虚拟机中了:
-
然后使用如下命令查看目标系统的Linux内核版本,可以目标系统的Linux内核版本为4.4.0-87-generic(这就是待Fuzz目标):
-
由于需要在虚拟机中使用kAFL,所以在虚拟机中进入如下目录:
-
创建文件夹,用来保存kAFL源代码:
-
进入新创建的文件夹中:
-
下载kAFL的源码:
-
进入到如下目录:
-
在该目录中执行如下命令来安装GCC,目的是为了后续的编译操作:
powershell
$ sudo apt install gcc
-
然后修改编译脚本的权限:
-
最后执行编译脚本:
-
只要出现下图中两个红框内容即为编译成功,其余的错误不用在意,因为我们并不进行MacOS和Windows的漏洞检测:
-
然后我们可以暂时关闭虚拟机:
3.1.3、配置待Fuzz目标
- 首先进入如下目录:
powershell
$ cd /path/to/
- 然后在此目录下创建快照目录并进入:
powershell
$ sudo mkdir snapshot && cd snapshot
- 然后在此目录下创建快照:
powershell
$ sudo qemu-img create -b /opt/code/kAFL/linux.qcow2 -f qcow2 overlay_0.qcow2
-
可以发现,创建成功了:
-
然后使用如下命令来创建虚拟磁盘:
powershell
$ sudo qemu-img create -f qcow2 ram.qcow2 512
-
同样创建成功了:
-
然后来到kAFL的原始源代码目录:
powershell
$ cd /opt/code/kAFL/
- 然后使用如下命令来使用QEMU-PT启动虚拟机:
powershell
$ sudo ./qemu-2.9.0/x86_64-softmmu/qemu-system-x86_64 -hdb /path/to/snapshot/ram.qcow2 -hda /path/to/snapshot/overlay_0.qcow2 -machine pc-i440fx-2.6 -monitor stdio -enable-kvm -k de -m 512
-
当然,以上命令要在可视化界面中完成,只要涉及到启动虚拟机,就在可视化界面中完成。可以发现,已经成功启动虚拟机系统,并且启动了QEMU的管理控制台:
-
然后进入到如下目录:
-
修改权限:
-
然后在该目录下执行如下命令来安装Make,目的是为了后面的编译操作:
powershell
$ sudo apt-get install make
-
执行脚本:
-
执行成功:
-
然后进入如下目录:
-
执行此目录下的二进制脚本:
-
执行成功,此时QEMU虚拟机将会被冻结:
-
然后在QEMU管理控制台使用
savevm kafl
命令来保存快照:
-
然后使用
q
命令退出QEMU管理控制台:
-
当我们进行完上面的操作后,QEMU虚拟机系统也会关闭:
3.1.4、配置kAFL组件
- 首先修改kAFL的配置文件:
powershell
$ sudo vim /opt/code/kAFL/kAFL-Fuzzer/kafl.ini
-
将"QEMU_KAFL_LOCATION"的值修改为下图右边红框处的值:
-
保存修改后,执行如下命令进入kAFL-Fuzzer的agents文件夹:
powershell
$ cd /opt/code/kAFL/kAFL-Fuzzer/agents/
- 修改编译脚本的权限:
powershell
$ sudo chmod u+x compile.sh
- 执行编译脚本:
powershell
$ sudo ./compile.sh
-
执行成功,只需要关注下图中两个红框即可,因为我们并不检测MaxOS和Windows的漏洞:
-
然后来到如下目录:
powershell
$ cd /opt/code/kAFL/kAFL-Fuzzer/
- 然后使用如下命令重新下载pip:
powershell
$ sudo wget https://bootstrap.pypa.io/pip/2.7/get-pip.py
- 然后使用如下命令重新安装pip:
powershell
$ python get-pip.py
- 然后执行如下命令来下载安装所需要的依赖:
powershell
$ sudo pip install mmh3==2.0
$ sudo pip install lz4
$ sudo pip install psutil
- 最后执行如下命令来检查待Fuzz目标是否可以成功初始化:
powershell
$ sudo python kafl_info.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/info/info 512 -v
- 当上面的命令正常执行完毕后,会在命令行终端中打印如下图所示内容:
3.1.5、开始Fuzz测试
- 首先执行如下命令设置输入目录,并将种子文件复制到输入目录中:
powershell
$ sudo mkdir /path/to/input
$ sudo cp -r /opt/code/kAFL/kAFL-Fuzzer/seed/ /path/to/input/
- 然后设置输出目录:
powershell
$ sudo mkdir -p /path/to/working/output
- 最后执行如下命令开始进行Fuzz测试:
powershell
$ cd /opt/code/kAFL/kAFL-Fuzzer/
$ sudo python kafl_fuzz.py /path/to/snapshot/ram.qcow2 /path/to/snapshot/ agents/linux_x86_64/fuzzer/ext4 512 /path/to/input/seed/ /path/to/working/output/ -ip0 0xffffffffc0287000-0xffffffffc028b000 -v --Purge
-
可以发现,已经开始对目标进行Fuzz测试了:
-
若想退出Fuzz测试,按一下"Ctrl+Z"即可。至此,我们就完成了使用kAFL对Linux 4.4.0-87-generic内核进行Fuzz测试的操作
4、总结
4.1、部署架构
关于kAFL部署的架构图,如下所示。
对于以上架构图,我们具体来看kAFL是否对其中的组件进行了修改。详情可参见下方的表格。
是否有修改 | 具体修改内容 | 备注 | |
---|---|---|---|
主机内核 | 有 | /kAFL/KVM-PT/arch/x86/kvm/Makefile.patch | 目的是启动程序追踪功能 |
/kAFL/KVM-PT/arch/x86/kvm/Kconfig.patch | |||
/kAFL/KVM-PT/arch/x86/kvm/vmx.c.patch | |||
/kAFL/KVM-PT/arch/x86/kvm/svm.c.patch | |||
/kAFL/KVM-PT/arch/x86/kvm/x86.c.patch | |||
/kAFL/KVM-PT/arch/x86/include/asm/kvm_host.h.patch | |||
/kAFL/KVM-PT/arch/x86/include/uapi/asm/kvm.h.patch | |||
/kAFL/KVM-PT/include/uapi/linux/kvm.h.patch | |||
cp /kAFL/KVM-PT/arch/x86/kvm/vmx.h linux-4.6.2/arch/x86/kvm/ | |||
cp /kAFL/KVM-PT/arch/x86/kvm/vmx_pt.h linux-4.6.2/arch/x86/kvm/ | |||
cp /kAFL/KVM-PT/arch/x86/kvm/vmx_pt.c linux-4.6.2/arch/x86/kvm/ | |||
mkdir linux-4.6.2/usermode_test/ 2> /dev/null | |||
cp /kAFL/KVM-PT/usermode_test/support_test.c linux-4.6.2/usermode_test/ | |||
cp /kAFL/KVM-PT/usermode_test/test.c linux-4.6.2/usermode_test/ | |||
设置linux-4.6.2内核源码中的".config"文件中的CONFIG_KVM_VMX_PT=y | |||
主机操作系统 | 有 | 虚拟化Intel VT-x/EPT或AMD-V/RVI(V) | 目的是启动程序追踪功能 |
创建规则,该规则指定了当出现名为"kvm"的内核模块时,该模块应该属于"kvm"用户组。 | |||
创建一个名为"kvm"的用户组。 | |||
将当前用户添加到"kvm"用户组中。 | |||
Guest内核 | 无 | 无 | 无 |
Guest操作系统 | 有 | 与主机相同版本的kAFL源代码放在"/opt/code/"目录中 | 作者要求,目前并不清楚这样做的目的 |
虚拟机监视器QEMU | 有 | /kAFL/QEMU-PT/hmp-commands.hx.patch | 目的是启动程序追踪功能 |
/kAFL/QEMU-PT/monitor.c.patch | |||
/kAFL/QEMU-PT/hmp.c.patch | |||
/kAFL/QEMU-PT/hmp.h.patch | |||
/kAFL/QEMU-PT/Makefile.target.patch | |||
/kAFL/QEMU-PT/kvm-all.c.patch | |||
/kAFL/QEMU-PT/vl.c.patch | |||
/kAFL/QEMU-PT/configure.patch | |||
/kAFL/QEMU-PT/linux-headers/linux/kvm.h.patch | |||
/kAFL/QEMU-PT/include/qom/cpu.h.patch | |||
/kAFL/QEMU-PT/applesmc_patches/v1-1-3-applesmc-cosmetic-whitespace-and-indentation-cleanup.patch | |||
/kAFL/QEMU-PT/applesmc_patches/v1-2-3-applesmc-consolidate-port-i-o-into-single-contiguous-region.patch | |||
/kAFL/QEMU-PT/applesmc_patches/v1-3-3-applesmc-implement-error-status-port.patch | |||
mkdir qemu-2.9.0/pt/ 2> /dev/null | |||
cp /kAFL/QEMU-PT/compile.sh qemu-2.9.0/ | |||
cp /kAFL/QEMU-PT/hmp-commands-pt.hx qemu-2.9.0/ | |||
cp /kAFL/QEMU-PT/pt.c qemu-2.9.0/ | |||
cp /kAFL/QEMU-PT/pt.h qemu-2.9.0/ | |||
cp /kAFL/QEMU-PT/pt/tmp.objs qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/decoder.h qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/hypercall.c qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/logger.h qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/khash.h qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/memory_access.h qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/tnt_cache.c qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/interface.h qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/interface.c qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/memory_access.c qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/logger.c qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/decoder.c qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/filter.h qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/hypercall.h qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/tnt_cache.h qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/filter.c qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/disassembler.c qemu-2.9.0/pt/ | |||
cp /kAFL/QEMU-PT/pt/disassembler.h qemu-2.9.0/pt/ |
4.2、漏洞检测对象
- 检测的对象为Guest内核
- 针对的内核版本为Linux 4.4.0-87-generic
- 针对的漏洞类型为崩溃性错误
4.3、漏洞检测方法
- 使用
mount()
函数(其函数原型为int mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *data);
)将测试用例挂载到Guest操作系统中 - 将测试用例挂载结果保存到主机中
- 目前可以进行测试的文件系统有三类,包括:
- MacOS操作系统中的Fat文件系统
- Linux操作系统中的Ext4文件系统
- Windows操作系统中的Ntfs文件系统
4.4、种子生成/变异技术
- 初始种子(二进制文件)由作者提供在kAFL的源代码中
- 基于是否发现漏洞,对种子进行变异
- 变异的策略基于随机,即随机对种子的二进制位进行翻转、插入和删除等操作
5、参考文献
- 跟着白泽读论文丨kAFL: Hardware-Assisted Feedback Fuzzing*
- 内核漏洞挖掘技术系列(6)------使用AFL进行内核漏洞挖掘(1)
- RUB-SysSec/kAFL
- 使用 monitor command 监控 QEMU 运行状态
- QEMU monitor控制台使用详解
- QEMU控制台
- kAFL
- Linux内核通信之---proc文件系统(详解)
- Linux内核开发:创建proc文件并与用户空间接口
总结
以上就是本篇博文的全部内容,可以发现,kAFL的部署与使用比较复杂,踩了很多坑,不过我都在博客中一一列出来了,避免各位读者再次遇到同样的问题。
对于各类漏洞检测工具的深入研究,我都已经整理成博客供大家学习了,因为知识是共享的。若对该方向感兴趣的读者一定可以从我的博客中收获满满。
总而言之,kAFL是一个不错的Fuzz测试的工具,值得大家学习。相信读完本篇博客,各位读者一定对kAFL有了更深的了解。