文章目录
- 前言
- 1、概述
- 2、安装与使用
-
- 2.1、源码安装
-
- 2.1.1、部署系统依赖组件
-
- [2.1.1.1、下载安装Python 3.5](#2.1.1.1、下载安装Python 3.5)
- [2.1.1.2、下载安装Visual Studio 2013](#2.1.1.2、下载安装Visual Studio 2013)
- 2.1.1.3、下载安装PTVS
- 2.1.2、使用源码安装系统
- 2.2、使用方法
- 3、测试用例
-
- [3.1、对Windows 10内核进行Fuzz测试](#3.1、对Windows 10内核进行Fuzz测试)
- [3.2、对Windows 7内核进行Fuzz测试](#3.2、对Windows 7内核进行Fuzz测试)
- 4、总结
- 5、参考文献
- 总结
前言
本博客的主要内容为KernelFuzzer的部署、使用与原理分析。本博文内容较长,因为涵盖了KernelFuzzer的几乎全部内容,从部署的详细过程到如何使用KernelFuzzer对操作系统的系统调用进行Fuzz测试,以及对KernelFuzzer进行漏洞检测的原理分析,相信认真读完本博文,各位读者一定会对KernelFuzzer有更深的了解。以下就是本篇博客的全部内容了。
1、概述
KernelFuzzer是mwrlab前几年在defcon24开源的内核Fuzz工具,通过示例库调用和系统调用来Fuzz Windows,作者强调,KernelFuzzer已经在Windows 7/10、OS X和QNX上进行了相关测试。但是根据查阅,作者并没有在其它系统进行测试,只在Windows系统上进行了测试,作者Fuzz Windows时的测试环境为:
- 宿主操作系统:Windows 10 专业版 64位
- 虚拟机软件:VMWare Workstation 12.1.0
- 来宾操作系统:Windows 7 家庭基础版 64位
- 虚拟机规格:
- RAM:2GB
- CPU:1个
测试共历时48小时,检测出65次崩溃,其中有13次崩溃产生的漏洞是以前没有发现过的,其中包括:
- Null Pointer Dereference:4个
- Use-After-Free:2个
- Pool Buffer Overflow:4个
- Miscellaneous:3个
最后,作者也总结了关于KernelFuzzer未来的工作,作者总结的相关内容,我们也可以在未来进行更深入的研究,因为对于闭源的Fuzz漏洞检测的相关工具我目前接触的并不是很多,个人认为对这方面深度探究很有必要,具体未来的工作总结如下,以便后续查阅:
- 提高覆盖率
- 对象标记
- 实现更多调用
- 实验性用户模式回调
- 更好的多线程支持
- 基于CPU功能的覆盖率反馈
- 其他
- 日志记录
- 处理虚拟机监控程序崩溃
- 测试用例减少器
- 监控虚拟机负载并从虚拟机监控系统重新启动
整体来说,KernelFuzzer的检测效果还是不错的。此外,KernelFuzzer工具基于Python语言和C语言开发。
1.1、整体架构
因为KernelFuzzer这个工具目前还没有相关论文可供阅读,所以我只能尽自己所能来分析一下KernelFuzzer的整体架构,这个Fuzzer的整体架构如下图所示:
可以看到KernelFuzzer工具主要包括六大部分,乍一看还是比较复杂的,下面我们一一介绍这几部分:
-
OS API Knowledge Base:这是与系统库交互的操纵系统API,其中许多都包含系统调用,可以将特定于操作系统的API调用列表"插入"到框架中。其具体由如下几部分构成:
- 文件和数据访问
- 用户界面
- 图形和多媒体
- 设备
- 网络
-
System Calls Knowledge Base:此部分的作用主要是进行用户空间到内核空间的通信(包括请求资源和动作)。此部分主要在内核态实施功能和处理数据,并与低级别操作系统进行特定交互(这需要特定于操作系统和体系结构的组装),另外,特定于操作系统的系统调用列表可以"插入"到框架中。其结构如下所示:
-
Fuzzed Values:此部分的函数功能是返回Fuzz的基本数据类型,包括Boolean、integer、floating等。具体可以返回的基本数据类型如下所示:
-
Object Store:此部分的主要功能如下:
- 在通信中保持状态
- 保留感兴趣的操作系统特定对象
- 由Fuzzer决定性地填充
- 检索、更新和插入对象
- 实现为对象的全局数组
-
Helpers:此部分的主要功能如下:
- 给库调用生成有效的特定于操作系统的结构
- 生成、填充和返回有效结构
- 填充了"大部分"有效数据
-
Framework Core:此部分为KernelFuzzer的核心内容,主要功能包括:
- 日志功能
- Crash检测
- Crash存储和分类
- 检测0-Day漏洞
1.2、工作流程
如果用文字来描述KernelFuzzer的工作流程我觉得叙述不清楚,所以我通过仔细阅读源码以及查阅相关资料,自己总结并画出了一份关于KernelFuzzer在Windows系统下的整个工作流程的示意图,如下所示。
可以看到,KernelFuzzer整个工作流程细节很多,具体到哪步该做什么不能错,在Windows系统下部署起来比较简单,因为作者已经帮我们写好了,不过未来如果在Linux等系统中进行部署测试,可能会修改上述工作流程以及相关代码,这也是未来需要研究的内容之一。
1.2.1、环境配置流程
最终是通过python worker_setup.py
命令来启动的KernelFuzzer,所以我们就要从"/kernelfuzzer/worker_setup/worker_setup.py"这个文件开始分析。我们首先分析该文件的main()
函数(其实现在"/kernelfuzzer/worker_setup/worker_setup.py"的第86行)。
该函数首先检查用户是否具有管理员权限,然后依次执行一系列操作,包括安装调试工具、修改系统设置、创建计划任务等,以准备系统环境进行后续的内核Fuzz工作,故将本章节的标题命名为"环境配置流程"。具体来说:
- 安装WinDbg调试工具(
install_windbg()
函数调用)。 - 安装CouchDB Python模块(
install_couch_module()
函数调用)。 - 修改注册表以禁用UAC(用户账户控制)、锁屏、Windows错误报告和Windows更新(
change_registry()
函数调用)。 - 启用内核转储,以便在系统崩溃时生成转储文件(
enable_kernel_dumps()
函数调用)。 - 创建一个计划任务,使得目标脚本在用户登录时自动执行(
schedule_task()
函数调用)。 - 启用特殊池,以便对win32k.sys系统文件启用特殊池(
enable_special_pool()
函数调用)。 - 重启系统(
reboot_system()
函数调用)。
在这里我们主要关注上面标红的部分,即schedule_task()
函数调用,schedule_task()
函数实现在"/kernelfuzzer/worker_setup/worker_setup.py"的第54行。
该函数的目的是创建一个名为"Bug Hunter"的计划任务,在用户登录时执行指定的程序"bughunt_loop.py"。具体来说。
- 打印提示消息"[!] Scheduling Task..."。
- 调用
subprocess.call
函数执行命令schtasks。 - 使用schtasks命令创建一个名为"Bug Hunter"的计划任务,该任务在用户登录时执行,执行的程序为"bughunt_loop.py",路径为当前工作目录的上级目录。
- 函数结束执行。
可以发现该函数的核心逻辑是上面的红色部分,即在每次系统启动时,都要执行"/kernelfuzzer/bughunt_loop.py"程序,故我们接下来就要分析该程序。
1.2.2、计划任务执行流程
通过上一章节的分析,我们清楚,KernelFuzzer启动后,首先创建一个名为"Bug Hunter"的计划任务,在每次系统启动后,都会执行这个计划任务。而该计划任务最终执行的就是"/kernelfuzzer/bughunt_loop.py"程序。该程序的全部代码如下所示。
python
from subprocess import call, TimeoutExpired
import fnmatch
import sys
import shutil
import os
import time
# Current timeout set to 25 minutes, i.e. 25*60 seconds.
#TIMEOUT=1500
TIMEOUT=600
THREADS=1
# Current number of executions is 1M.
#EXECUTIONS=1000000
EXECUTIONS=350000
SEED=1
COMMAND='bughunt.exe'
# Change working directory to where this wrapper is located.
wrapper_path = os.path.abspath(__file__)
wrapper_path_parent = os.path.dirname(wrapper_path)
os.chdir(wrapper_path_parent)
# Folder name where to preserve any logs and dumps.
#folder = "crashes/%s" % str(tstamp)
folder = "to-be-populated"
crash_found = False
#try:
# os.makedirs(folder, 777)
#except:
# print("Error, cannot create folder structure.")
# pass
# Look for memory dumps and move them to a new folder
for r, d, filenames in os.walk('C:/Dumps/'):
for filename in fnmatch.filter(filenames, '*.dmp'):
try:
print("Crash found!")
try:
print("Creating folder...")
tstamp = time.time()
folder = "crashes/%s" % str(tstamp)
os.makedirs(folder, 777)
except:
print("Error, cannot create folder structure.")
pass
# Currently copying the memory dump as opposed to just moving it.
#shutil.copyfile("C:/Dumps/%s" % filename, "%s/%s" % (folder, filename))
# Move instead of copying.
print("Moving memory dump to new folder...")
os.rename("C:/Dumps/%s" % filename, "%s/%s" % (folder, filename))
# Process the memory dump using kd_batch_commands.txt.
print("Analysing memory dump... %s" % filename)
#kd_log = open("%s/%s.log" % (folder, filename.split('.')[0]),"wb")
kd_log = open("%s/windbg.log" % folder, "wb")
call(["C:\\Program Files\\Debugging Tools for Windows (x64)\\kd.exe", "-z", "%s\%s" % (folder, filename), "-c", "$$<crash_processing\\kd_batch_commands.txt;Q"], stdout=kd_log)
kd_log.close()
crash_found = True
except:
print("Error, cannot process memory dump.")
pass
if crash_found:
# Preserve log file(s) left before BSODs and move to same folder.
for r, d, filenames in os.walk('.'):
for filename in fnmatch.filter(filenames, 'log.*'):
try:
os.rename("./%s" % filename, "%s/%s" % (folder, filename))
except:
print("Error, cannot move log file.")
pass
# Submit crash information to our CouchDB instance.
try:
# Invoke couchdb_submit.py with the correct arguments.
call(["C:\Python35\python.exe", "crash_processing\\couchdb_submit.py", "--server", "IPADDRESS", "--database", "DBNAME", "--username", "USER", "--password", "PW", "add-crash", "--crash-path", "%s" % folder])
except:
print("Error, cannot submit crash information to database.")
raise
try:
# Run the fuzzer with the specified timeout.
call(['%s' % COMMAND, '%s' % THREADS, '%s' % EXECUTIONS, '%s' % SEED], timeout=TIMEOUT)
# We need a way of querying the CPU load while doing a long, e.g. 25 minute run. If the load is below 90% for more than 2-3 minute, we assume our process is dead. Should that be the case, reboot. Sound like 'psutils' is the module we need for the performance check.
except TimeoutExpired:
print("Timeout of %d seconds expired.\n" % TIMEOUT)
except:
# Process other exceptions accordingly, to be implemented.
pass
'''''
for i in range(10):
try:
call(['%s' % COMMAND, '%s' % THREADS, '%s' % EXECUTIONS, '%s' % SEED], timeout=TIMEOUT)
except:
pass
call(['taskkill', '/f', '/im', 'notepad.exe'])
call(['del', '*.txt'], shell=True)
call(['del', 'log.*'], shell=True)
'''
# Kill all instances of Notepad.
call(['taskkill', '/f', '/im', 'notepad.exe'])
# Clean-up any temporary files logs left.
call(['del', '*.txt'], shell=True)
call(['del', 'log.*'], shell=True)
# Reboot and start fresh.
call(['shutdown', '-r'])
该脚本用于执行Fuzz测试工具,并在测试过程中监测系统崩溃并将崩溃信息提交到数据库。具体来说,该脚本的代码执行逻辑如下。
- 处理内存转储文件:
- 通过
os.walk()
函数遍历指定文件夹中的文件。 - 使用
fnmatch.filter()
函数过滤出扩展名为".dmp"的文件。 - 对每个找到的转储文件执行以下操作:
- 创建一个以当前时间戳命名的新文件夹,并将其路径保存在
folder
变量中。 - 将转储文件移动到新创建的文件夹中。
- 使用kd.exe分析转储文件,并将分析结果保存到名为"windbg.log"的日志文件中。
- 如果成功分析转储文件,则设置
crash_found
标志为True
,表示发现了崩溃。
- 创建一个以当前时间戳命名的新文件夹,并将其路径保存在
- 通过
- 提交崩溃信息到数据库:
- 尝试使用Python脚本"couchdb_submit.py"将崩溃信息提交到CouchDB数据库。
- 如果提交失败,则继续执行脚本而不中断。
- 运行Fuzz测试工具(即"bughunt.exe"):
- 尝试使用给定的超时时间运行Fuzz测试工具(即"bughunt.exe")。
- 如果超时,打印相应的消息。
- 捕获所有其他异常,以便脚本可以继续执行。
- 清理临时文件和进程:
- 终止所有Notepad进程。
- 删除临时文件和日志文件。
- 最后,重新启动计算机。
以上代码的核心逻辑就是上面标红的部分,即启动"bughunt.exe"可执行文件,那么问题就来了。
- "bughunt.exe"可执行文件是怎么得到的?
- "bughunt.exe"可执行文件都做了什么事情?
让我们首先解决第一个问题,还记得我们在安装KernelFuzzer时执行的一个脚本么?即"/kernelfuzzer/bughunt_build_x64_release.bat"脚本文件。
该脚本用于设置环境变量,并在当前目录下编译"bughunt.c"和汇编"bughunt_syscall_x64.asm"文件,生成可执行文件"bughunt.exe"。具体来说。
- 设置环境变量:
- 调用Visual Studio的vcvarsall.bat脚本来设置64位环境变量。
- 删除旧文件:
- 删除当前目录下的所有日志、目标文件和可执行文件。
- 汇编目标文件:
- 使用ml64.exe汇编"bughunt_syscall_x64.asm"文件,生成目标文件"bughunt_syscall_x64.obj"。
- 编译和链接:
- 使用cl.exe编译"bughunt.c"文件,并链接生成可执行文件"bughunt.exe"。
- 编译过程中使用了一系列系统库文件,包括gdi32.lib、kernel32.lib、User32.lib、Advapi32.lib、Shell32.lib、Msimg32.lib、Dxva2.lib和Mscms.lib。
现在我们就能回答第一个问题了,即可执行文件"bughunt.exe"是在安装KernelFuzzer时编译好的(上述代码逻辑的标红的部分),就是为了在本章节所介绍的流程中使用,该可执行文件也是KernelFuzzer工具对内核进行Fuzz测试的核心组件。
我们现在已经了解可执行文件"bughunt.exe"是怎么来的了,下面就要回答第二个问题,即可执行文件"bughunt.exe"都做了什么事情?这是我们下一章节要介绍的内容,也是KernelFuzzer工具执行的下一个流程。
1.2.3、Fuzz测试流程
1.2.3.1、整体资源调度
经过上一章节的分析,我们清楚,KernelFuzzer最终执行的是"bughunt.exe"可执行文件,而"bughunt.exe"可执行文件又是由"bughunt.c"及一些系统库文件编译得来的,既然我们要分析"bughunt.exe"可执行文件都做了什么,就要分析将其编译的源代码文件。但是对于系统库文件我们并不关心(因为这是由系统实现的,我们只需要直接使用即可),我们主要关心的是"bughunt.c"文件(这才是"bughunt.exe"可执行文件的核心源代码文件),该文件位于"/kernelfuzzer/bughunt.c"中,这是一个C语言的源代码文件,所以应该从main()
函数开始分析,该main()
函数实现在"/kernelfuzzer/bughunt.c"的第10行。
c
int main (int argc, char* argv[])
{
// Number of threads to create.
unsigned int subprocess_count = 0;
// Thread index.
unsigned int subprocess_idx = 0;
// Thread data structures.
unsigned int dwThreadIdArray[MAX_THREADS];
HANDLE hThreadArray[MAX_THREADS];
// PRNG Seed.
unsigned int seed = NULL;
unsigned int control_seed = NULL;
if (argc != 4)
{
printf ("USAGE : %s <subprocess_count> <syscall_count> <seed>\n", argv[0]);
printf ("WHERE : subprocess_count : is a base 10 DWORD\n");
printf (" syscall_count : is a base 10 DWORD\n");
printf (" seed : Seed to use. Use 1 if you want to create a new seed.\n");
return (0xDEADBEEF); // Error.
}
// Populate our object store, i.e. handles database in the Windows case.
make_HANDLES();
subprocess_count = strtol (argv[1], NULL, 10);
syscall_count = strtol (argv[2], NULL, 10);
seed = strtol (argv[3], NULL, 10);
// Also check if syscall_count is less then MAX_THREADS.
for (subprocess_idx = 0; subprocess_idx < subprocess_count; subprocess_idx += 1)
{
hThreadArray[subprocess_idx] = CreateThread(NULL, // default security attributes
0, // use default stack size
(LPTHREAD_START_ROUTINE) bughunt_thread, // thread function name
seed, // argument to thread function
0, // use default creation flags
&dwThreadIdArray[subprocess_idx]); // returns the thread identifier
// Check if thread was successfully created. Bail out should we fail to create a new thread.
if (hThreadArray[subprocess_idx] == NULL)
{
printf ("Error creating thread, exiting.");
return (0xDEADBEEF); // Error.
}
} // For loop.
// Comment out the lines above and uncomment the following one for no threads.
//bughunt_thread(syscall_count, seed);
// Wait until all threads have terminated.
WaitForMultipleObjects(subprocess_count, hThreadArray, TRUE, INFINITE);
// Close all thread handles and free memory allocations.
for(subprocess_idx = 0; subprocess_idx < subprocess_count; subprocess_idx += 1)
{
CloseHandle(hThreadArray[subprocess_idx]);
}
return (0);
}
整体来看,该程序是一个多线程程序,根据命令行参数创建一定数量的线程,并等待所有线程执行完毕后退出。具体来说,该段程序的逻辑为。
- 变量声明和初始化:
- 声明了一些变量,包括子进程数量、线程索引、线程句柄数组等。
- 声明了两个无符号整数变量
seed
和control_seed
,并初始化为NULL
。
- 命令行参数解析和错误检查:
- 通过命令行参数
argc
和argv[]
,获取传递给程序的参数。 - 如果参数数量不为
4
,打印使用说明并返回错误码0xDEADBEEF
。
- 通过命令行参数
- 初始化句柄:
- 调用
make_HANDLES()
函数,用于初始化一组不同类型的句柄(Handle)。
- 调用
- 参数转换:
- 使用
strtol()
函数将命令行参数转换为无符号整数,分别表示子进程数量、系统调用次数和种子值。
- 使用
- 线程创建:
- 使用
CreateThread()
函数创建多个线程,根据传入的参数动态调整子进程数量。 - 每个线程执行相同的函数
bughunt_thread()
,并传递种子值作为参数。 - 检查线程句柄是否创建成功,若失败则打印错误信息并返回错误码。
- 使用
- 等待线程结束:
- 使用
WaitForMultipleObjects()
函数等待所有线程执行完毕。 - 当所有线程都退出时,程序继续执行。
- 使用
- 清理资源:
- 使用
CloseHandle()
函数关闭每个线程的句柄,释放内存。
- 使用
- 返回退出码:
- 最后返回退出码
0
,表示程序成功结束。
- 最后返回退出码
以上代码虽然比较多,不过我们并不需要全部分析,我们只需要关注上面标红的两处重点逻辑即可。下面将会对其进行详细分析。
- 初始化句柄
此处的逻辑由make_HANDLES()
函数调用完成,该函数实现在"kernelfuzzer/handles_database.h"的第105行。
c
void make_HANDLES (void)
{
// Improve the code as we see certain functions failing every time, which means we're not calling them the right way.
// Diversify the handles!
unsigned int handle_idx = 0;
const POINT ptZero = { 0, 0 }; //to get a handle to the primary monitor
BITMAP bmp = { 0, 8, 8, 2, 1, 1 };
BYTE bits [8][2] = { 0xFF, 0, 0x0C, 0, 0x0C, 0, 0x0C, 0,
0xFF, 0, 0xC0, 0, 0xC0, 0, 0xC0, 0 };
HKEY keyCurrentUser;
HANDLE tempHandle;
unsigned int tempUINT1, tempUINT2;
INT NumberOfNotepadHandles = 0;
tempUINT1 = 0;
tempUINT2 = 0;
// Initialise the array of handles by setting every handle to 0.
for (handle_idx = 0; handle_idx < HANDLES_N; handle_idx += 1) {
HANDLES[handle_idx] = 0x0000000000000000;
}
// Populate each one of the handle slots sequentially.
//for (handle_idx = 0; handle_idx < HANDLES_N; handle_idx += 1) {
for (handle_idx = 0; handle_idx < 64; handle_idx += 1) {
while(HANDLES[handle_idx] == 0x0000000000000000) {
if (!tempUINT1) {
tempHandle = GetDesktopWindow();
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "GetDesktopWindow");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "GetDesktopWindow";
tempHandle = -1;
tempUINT1 = 1;
break;
}
}
if (!tempUINT2) {
tempHandle = MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY);
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "MonitorFromPoint");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "MonitorFromPoint";
tempHandle = -1;
tempUINT2 = 1;
break;
}
}
switch(rand() % 8) {
case 0:
tempHandle = CreateFile(TEXT("C:\\boot.ini"), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "CreateFile");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "CreateFile";
tempHandle = -1;
}
break;
case 1:
tempHandle = CreateSolidBrush(RGB(0, 255, 0));
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "CreateSolidBrush");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "CreateSolidBrush";
tempHandle = -1;
}
break;
case 2:
tempHandle = FindWindow(NULL, TEXT("Explorer"));
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "FindWindow");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "FindWindow";
tempHandle = -1;
}
break;
case 3:
tempHandle = CreateFont(46, 28, 215, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_ROMAN, "Times New Roman");
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "CreateFont");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "CreateFont";
tempHandle = -1;
}
break;
case 4:
tempHandle = CreateBitmapIndirect(&bmp);
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "CreateBitmapIndirect");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "CreateBitmapIndirect";
tempHandle = -1;
}
break;
case 5:
tempHandle = GlobalAlloc(GMEM_FIXED, 10);
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "GlobalAlloc");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "GlobalAlloc";
tempHandle = -1;
}
break;
case 6:
RegOpenCurrentUser(KEY_READ, &tempHandle);
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "RegOpenCurrentUser");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "RegOpenCurrentUser";
tempHandle = -1;
}
break;
case 7:
tempHandle = OpenNotepad();
if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) {
logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle.");
}
else {
logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "OpenNotepad");
HANDLES[handle_idx] = tempHandle;
HANDLE_CREATOR[handle_idx] = "OpenNotepad";
tempHandle = -1;
} // if
break;
} // switch
} // while
} // for
// Alternatively, increment every iteration of the for loop.
// Array has been populated from 0 to 64, i.e. first available slot is 64.
HANDLES_ARRAY_AVAILABLE_SLOT_INDEX = 64;
}
这段代码的目的是初始化一组不同类型的句柄,以便后续的程序可以使用这些句柄进行各种操作,如创建窗口、文件、位图等。其主要代码执行逻辑如下。
- 变量声明和初始化:
- 声明了一些变量,包括句柄索引、临时句柄、临时整数等。
- 定义了一个
BITMAP
结构体和一个字节型二维数组bits
,用于创建位图。
- 句柄数组初始化:
- 初始化了一个叫做
HANDLES
的句柄数组,长度为HANDLES_N
。 - 通过循环将句柄数组中的所有元素初始化为
0
。
- 初始化了一个叫做
- 句柄创建:
- 在循环中,根据随机选择的方式,尝试创建不同类型的句柄。
- 使用不同的WinAPI函数创建句柄,如
GetDesktopWindow()
、MonitorFromPoint()
、CreateFile()
等。此外还有一个名为OpenNotepad
的自定义函数来创建关于notepad.exe的句柄 - 检查句柄是否有效,若有效则记录到
HANDLES
数组中,并记录句柄创建方式到HANDLE_CREATOR
数组中。
- 可用句柄索引初始化:
- 将
HANDLES_ARRAY_AVAILABLE_SLOT_INDEX
设置为64
,表示数组中第一个可用句柄的索引。
- 将
该函数最终会得到一个HANDLES
数组(最重要的代码逻辑就是上面标红的部分),该数组记录了最终获得的所有有效句柄,这些句柄后面进行Fuzz测试的时候会用到,因为有些系统调用的参数就是句柄。
- 线程创建
该逻辑是由如下图所示的函数调用(在我们现在介绍的函数内部)实现的,即CreateThread()
函数,并向该函数传入一些参数。
不过我们并不对CreateThread()
函数进行分析(因为CreateThread()
函数是由Windows系统提供的已经实现好的函数,我们只需要直接使用其来创建新的线程,并不需要关心其具体实现),而是着眼于该CreateThread()
函数的参数。该CreateThread()
函数的参数包括:
NULL
:默认的安全属性。0
:默认的线程堆栈大小。(LPTHREAD_START_ROUTINE) bughunt_thread
:指向线程函数的指针,即bughunt_thread()
函数。seed
:传递给线程函数的参数。0
:默认的线程创建标志。&dwThreadIdArray[subprocess_idx]
:用于接收线程标识符的变量。
这么多的参数,我们只关心上面标红的部分,也就是指向线程函数bughunt_thread()
的指针,该线程函数就是新线程要执行的函数。而bughunt_thread()
函数实现在"/kernelfuzzer/bughunt_thread.h"的第143行。
c
DWORD bughunt_thread(unsigned int seed)
{
unsigned int syscall_idx = 0;
SYSCALL* syscall = NULL;
unsigned int syscall_argument_datatype_idx = 0;
unsigned int syscall_arguments[SYSCALL_ARGUMENT_N - 1]; // DWORD syscall_arguments[32];
FILE* stream; // For logging.
BH_Handle syscall_handle_argument;
// The syscall_log_string will hold the string to be logged before the syscall invocation.
char syscall_log_string[512];
memset(syscall_log_string, '\0', 512);
// It turns out rand() is thread-safe after all as its state is kept in a thread-local storage (TLS). This means we have to seed every single state on its own. In this case we choose to use a comination of time(), current process ID, and current thread ID.
if (seed == 1)
{
seed = time(NULL) + GetCurrentProcessId() + GetCurrentThreadId();
logger("//[PRNG Seed] (0x%08X, 0x%08X, %u)", GetCurrentProcessId(), GetCurrentThreadId(), seed);
srand(seed);
}
else //we have been given a seed to use, so use that.
{
logger("//[PRNG Seed] (0x%08X, 0x%08X, %u)", GetCurrentProcessId(), GetCurrentThreadId(), seed);
srand(seed);
}
for (syscall_idx = 0; syscall_idx < syscall_count; syscall_idx += 1)
{
// Invoke one or more library calls.
while (TRUE) {
//fflush(NULL);
// To hook or not to hook? Hook functions at random.
if (rand() % 5 == 1) {
// Uncomment below for hooking.
// 1. Okay, we'll hook. Proceed with installing hook.
//BH_SetWindowsHookEx();
// 2. Make a library call.
(*random_LIBRARY_CALL())();
// 3. Uninstall the hook.
//BH_UnhookWindowsHookEx();
}
else {
(*random_LIBRARY_CALL())();
}
if (rand() % 2) {
break;
}
}
// Start cionstructing the syscall invocation log string little by little, i.e. argument by argument.
syscall = random_SYSCALL ();
syscall_argument_datatype_idx = 0;
sprintf(syscall_log_string, "bughunt_syscall(0x%08x,", syscall->uid);
while ((syscall_argument_datatype_idx < (SYSCALL_ARGUMENT_N - 1))
&& (syscall->argument_datatypes[syscall_argument_datatype_idx] != NIL))
{
//logger("//syscall_argument_datatype_idx = %d\n", syscall_argument_datatype_idx);
switch (syscall->argument_datatypes[syscall_argument_datatype_idx])
{
// Something to check is whether the 0x%08x format string specifier is okay in all cases, e.g. 64-bit.
case _BOOL:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_bool());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _CHAR8:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_char8());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _CHAR16:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_char16());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _INT8:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_int8());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _INT16:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_int16());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _INT32:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_int32());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _INT64:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_int64());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _UINT8:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_uint8());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _UINT16:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_uint16());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _UINT32:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_uint32());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _UINT64:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_uint64());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _REAL32:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_real32());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _REAL64:
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_real64());
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08x,", syscall_arguments[syscall_argument_datatype_idx]);
break;
case _HANDLE:
syscall_handle_argument = get_random_HANDLE();
syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)syscall_handle_argument.value);
sprintf(syscall_log_string + strlen(syscall_log_string), "get_specific_HANDLE(%d),", syscall_handle_argument.index);
break;
} // Switch statement.
syscall_argument_datatype_idx += 1;
} // While loop.
for (; syscall_argument_datatype_idx < 32; ) {
//logger("//syscall_argument_datatype_idx = %d\n", syscall_argument_datatype_idx);
sprintf(syscall_log_string + strlen(syscall_log_string), "0x%08X,", 0x4142434445464748);
syscall_argument_datatype_idx += 1;
}
sprintf(syscall_log_string + strlen(syscall_log_string) - 1, ");");
logger(syscall_log_string);
/* INVOKE THE SYSCALL... */
bughunt_syscall (
syscall->uid,
syscall_arguments[0],
syscall_arguments[1],
syscall_arguments[2],
syscall_arguments[3],
syscall_arguments[4],
syscall_arguments[5],
syscall_arguments[6],
syscall_arguments[7],
syscall_arguments[8],
syscall_arguments[9],
syscall_arguments[10],
syscall_arguments[11],
syscall_arguments[12],
syscall_arguments[13],
syscall_arguments[14],
syscall_arguments[15],
syscall_arguments[16],
syscall_arguments[17],
syscall_arguments[18],
syscall_arguments[19],
syscall_arguments[20],
syscall_arguments[21],
syscall_arguments[22],
syscall_arguments[23],
syscall_arguments[24],
syscall_arguments[25],
syscall_arguments[26],
syscall_arguments[27],
syscall_arguments[28],
syscall_arguments[29],
syscall_arguments[30],
syscall_arguments[31]
);
} // For loop.
return (0);
}
这段代码是用于模拟多线程环境下的系统调用执行过程,通过随机选择系统调用和参数,以及记录执行过程中的详细信息,来进行系统调用的测试和分析。具体来说,该函数的代码执行逻辑如下:
- 变量声明和初始化:
syscall_idx
:用于迭代系统调用数组的索引。syscall
:指向SYSCALL
结构体的指针,用于存储随机选择的系统调用。syscall_argument_datatype_idx
:用于迭代系统调用参数数据类型的索引。syscall_arguments[]
:存储系统调用参数值的数组。stream
:文件指针,用于日志记录。syscall_handle_argument
:BH_Handle
结构体,用于存储随机选择的句柄。syscall_log_string
:用于构建系统调用日志字符串的字符数组,初始化为全零。
- 随机数种子初始化:
- 检查传入的种子参数
seed
是否为1
,如果是,则将种子重新设置为基于当前时间、当前进程ID和当前线程ID的组合值,并使用srand()
函数初始化随机数生成器。同时记录下种子值。 - 如果种子不为 1,则直接使用传入的种子值,并记录下种子值。
- 检查传入的种子参数
- 系统调用循环:
- 使用
for
循环遍历系统调用数组。 - 在每次循环中,执行以下操作:
- 随机选择一个库调用或者对其进行钩子(hook)操作(不过目前作者并没有实现该功能)。
- 随机选择一个系统调用,并将其赋值给
syscall
。 - 构建系统调用日志字符串,记录系统调用的UID以及参数。
- 生成随机选择的系统调用的对应用于Fuzz测试的参数。
- 使用
bughunt_syscall(*)
函数(其中*
表示该函数接收的参数)调用系统调用,并传入参数数组中的值。
- 使用
- 参数值获取:
- 在
while
循环中,通过调用get_fuzzed_*()
函数(其中*
表示各种参数类型)获取不同数据类型的模糊值,并将其存储在syscall_arguments[]
数组中。 - 在
while
循环中,对于句柄类型的参数,调用get_random_HANDLE()
函数获取随机句柄,并记录在日志中。
- 在
- 系统调用日志记录:
- 在
while
循环结束后,在for
循环中,使用sprintf()
函数将系统调用的UID和参数值格式化成字符串,并记录在syscall_log_string
中。 - 在
while
循环结束后,在for
循环中,调用logger()
函数记录系统调用日志字符串。
- 在
- 系统调用执行:
- 在
while
循环结束后,在for
循环中,调用bughunt_syscall()
函数(其中*
表示该函数接收的参数)执行系统调用,传入系统调用的UID和参数数组中的值。
- 在
- 返回值:
- 函数执行完毕后,返回一个值为
0
的DWORD类型。
- 函数执行完毕后,返回一个值为
在该函数中,核心逻辑为上面标红的部分,这几部分操作又可以归为三类,即分别为:
- 选取Fuzz测试目标
该逻辑由(*random_LIBRARY_CALL())();
函数调用和random_SYSCALL ();
函数调用来完成。 - 生成Fuzz测试参数
该逻辑由((DWORD)get_fuzzed_*());
函数(其中*
表示各种参数类型)调用来完成。 - 进行Fuzz测试
该逻辑由bughunt_syscall (*);
函数(其中*
表示该函数接收的参数,即Fuzz测试目标的ID和Fuzz测试参数)调用来完成。
可以发现,KernelFuzzer工具从这里开始才算是真正的开始进行Fuzz测试了,在这之前可以看作是准备工作。而KernelFuzzer工具进行Fuzz测试的核心逻辑就是上面这三点,那么下面我们将会对这三点核心逻辑进行详细分析。
1.2.3.2、选取Fuzz测试目标
在这部分,KernelFuzzer要选取Fuzz测试的目标。通过阅读代码,发现KernelFuzzer提供了两类目标供Fuzz,分别是:
- 库调用:即
(*random_LIBRARY_CALL())();
函数调用
random_LIBRARY_CALL()
函数实现在"/kernel/library_calls.h"的第22行。
这段代码定义了一个函数random_LIBRARY_CALL()
,其返回类型为指向函数的指针,该函数指针指向不接受任何参数并且返回void类型的函数。函数内部首先计算了库函数数组LIBRARY_CALLS
的大小,然后通过取余操作随机选择一个库函数的指针,并将其返回。这个函数的作用是从预定义的库函数数组中随机选择一个库函数,并返回其函数指针,用于模拟随机的库函数调用过程。
很明显,库调用存储在数组LIBRARY_CALLS
中,而数组LIBRARY_CALLS
定义在"/kernel/library_calls.h"的第18行。
这段代码定义了一个数组LIBRARY_CALLS
,数组中的元素是指向函数的指针。这个数组包含了一个函数指针,指向函数BH_GetSysColorBrush
。换句话说,LIBRARY_CALLS
是一个函数指针数组,其中包含了一个指向BH_GetSysColorBrush
函数的指针。
可以发现,KernelFuzzer目前只支持Fuzz这一个库调用,我们可以来看这个库调用内部做了什么事情。BH_GetSysColorBrush
函数实现在"/kernel/library_calls/brush.h"的第4行。
根据代码中的层层调用方式,BH_GetSysColorBrush
函数最终会被执行。而在此函数中,就完成了对库调用的参数生成和Fuzz的全过程。具体来说,该函数的代码逻辑为:
- 函数声明和变量定义:
- 函数
BH_GetSysColorBrush()
被定义为VOID
类型,没有返回值。 - 函数内部声明了两个变量
result_BH_GetSysColorBrush
和tempInt_BH_GetSysColorBrush
,分别用于存储系统颜色刷的句柄和Fuzz的整数参数。
- 函数
- 生成日志标识字符串:
- 使用
get_time_in_ms()
函数获取当前时间(毫秒级),结合rand() % 1024
的随机数,生成一个字符串vid
作为日志标识。
- 使用
- 获取Fuzz整数参数:
- 调用
get_fuzzed_int32()
函数,获取一个Fuzz的整数作为参数,用于调用GetSysColorBrush()
函数。
- 调用
- 调用系统函数:
- 使用
tempInt_BH_GetSysColorBrush
作为参数,调用GetSysColorBrush()
函数获取系统颜色刷,并将结果存储在result_BH_GetSysColorBrush
中。
- 使用
- 记录日志和存储句柄:
- 在日志中记录函数调用和参数信息,包括获取的系统颜色刷的句柄。
- 使用
put_random_HANDLE()
函数将获取的系统颜色刷的句柄存储起来,用于后续的处理。
该函数的核心逻辑为上面标红的两部分,即:
- 调用
get_fuzzed_int32()
函数来生成Fuzz参数。get_fuzzed_int32()
函数实现在"/kernelfuzzer/bughunt.h"的第258行。
c
int32_t get_fuzzed_int32 (void)
{
int32_t n;
switch (rand() % 10) {
case 0:
switch (rand() % 11)
{
case 0:
n = 0x80000000 >> (rand() & 0x1f); // 2^n (1 -> 0x10000)
break;
case 1:
n = rand(); // 0 -> RAND_MAX (likely 0x7fffffff)
break;
case 2:
n = (unsigned int)0xff << (4 * (rand() % 7));
break;
case 3:
n = 0xffff0000;
break;
case 4:
n = 0xffffe000;
break;
case 5:
n = 0xffffff00 | rand() & 0xff;
break;
case 6:
n = 0xffffffff - 0x1000;
break;
case 7:
n = 0x1000;
break;
case 8:
n = 0x1000 * ((rand() % (0xffffffff / 0x1000)) + 1);
break;
case 9:
n = 0xffffffff; // max
break;
case 10:
n = 0x7fffffff;
break;
}
break;
case 1:
n = 1;
break;
case 2:
n = 0;
break;
case 3:
n = -1;
break;
case 4:
n = 8;
break;
case 5:
n = 16;
break;
case 6:
n = 32;
break;
case 7:
n = 64;
break;
case 8:
n = 128;
break;
case 9:
n = 256;
}
logger("//[Get Fuzzed Value] get_fuzzed_int32 : n = %ld", n);
return n;
}
这段代码定义了一个函数get_fuzzed_int32
,用于生成用于Fuzz测试的32位整数值。该函数根据随机数的不同分支返回不同的整数值。具体来说,它会根据随机数的值选择不同的分支,并返回相应的整数值。这些分支包括:
- 生成接近32位整数范围的随机值。
- 返回特定的整数值,如0、1、-1、8、16、32、64、128、256。
- 生成特定位模式的整数值,如0xff、0xffff0000、0xffffffff。
- 生成随机位模式的整数值,如随机选择的位模式或随机范围内的值。
该函数还会记录生成的整数值,以便进行日志记录。总体而言,对于库调用的Fuzz测试的参数生成,还是利用随机生成的方式。当生成了用于Fuzz测试库调用的参数后,就可以对其进行Fuzz测试了。
- 调用
GetSysColorBrush()
函数来对其进行Fuzz。
当我们在上一步骤生成了关于该库调用的Fuzz测试参数后,就可以将这些Fuzz测试参数应用于该库调用了,即调用GetSysColorBrush()
函数。而GetSysColorBrush()
函数是由系统提供的函数,我们就不深入分析了,我们只需要知道,当我们使用生成的Fuzz测试参数后,调用GetSysColorBrush()
函数若出现漏洞/崩溃,就会将其记录下来。
以上就是KernelFuzzer对库调用进行Fuzz测试的全部过程,可以发现KernelFuzzer目前仅仅支持对这一个库调用进行Fuzz测试,而且整个流程也比较简单,无非就是生成对应的Fuzz测试参数,然后调用目标库调用进行Fuzz测试。如果后续我们想自行定义目标库调用,就可以按照这个流程进行自定义。
关于KernelFuzzer对库调用进行Fuzz测试的分析就到此为止了,因为阅读代码后发现,KernelFuzzer主要针对的是系统调用,对库调用的Fuzz测试说实话比较简陋,代码也不多。而对于系统调用的Fuzz测试过程才是我们要研究的重点,故下面我们继续分析KernelFuzzer对系统调用进行Fuzz测试的整个流程。
- 系统调用:即
random_SYSCALL ();
函数调用
random_SYSCALL ()
函数实现在"/kernelfuzzer/bughunt.h"的第441行。
这段代码定义了一个函数random_SYSCALL
,用于从系统调用数组中随机选择一个系统调用,并返回指向该系统调用结构体的指针。该函数首先计算系统调用数组的大小,并使用random_DWORD_0_to_N
函数生成一个0
到n
之间的随机索引。然后,函数返回指向所选系统调用的指针。
很明显,在这里最重要的就是SYSCALLS
这个系统调用数组,因为选取的过程是随机的,选取的目标就是这个SYSCALLS
系统调用数组。而这个SYSCALLS
系统调用数组又定义在"/kernelfuzzer/bughunt_syscalls.h"的第23行。
该数组包含了一系列系统调用的信息,具体来说:
- 定义
SYSCALLS
数组:- 代码开始处定义了一个名为
SYSCALLS
的数组,用于存储系统调用的信息。 - 此数组包含了一系列
SYSCALL
结构体元素,每个元素表示一个系统调用。
- 代码开始处定义了一个名为
SYSCALL
结构体:- 每个
SYSCALL
结构体包含三个主要字段:唯一标识符(uid)、参数类型数组(argument_datatypes)、返回值类型(return_type)。 - 唯一标识符(uid)用于标识每个系统调用,以便在代码中引用。
- 参数类型数组(argument_datatypes)列出了系统调用的参数类型,以便在调用时使用正确的参数。
- 返回值类型(return_type)指定了系统调用的返回值类型,以便调用者了解调用结果的类型。
- 每个
- 系统调用定义:
- 此处列出了两个Windows 7 x64用户界面(user32)库的系统调用,分别为
0x12F5
和0x12D4
。 - 第一个系统调用(即
0x12F5
)没有参数,返回一个布尔类型的值。 - 第二个系统调用(即
0x12D4
)包含四个参数类型(void指针、void指针、void指针、句柄),返回一个布尔类型的值。
- 此处列出了两个Windows 7 x64用户界面(user32)库的系统调用,分别为
最终,KernelFuzzer就通过上面介绍的整套逻辑返回一个随机的系统调用供后续使用。不过我们也发现,KernelFuzzer目前仅仅支持对这两个系统调用进行Fuzz测试。后续如果我们想添加新的系统调用并对其进行Fuzz测试,就可以按照本章节所介绍的流程进行自定义。
现在我们已经得到了Fuzz测试目标,即目标系统调用,那下面就可以对该系统调用生成对应的Fuzz测试参数了。
1.2.3.3、生成Fuzz测试参数
当KernelFuzzer获取到Fuzz测试目标后(即目标系统调用),就可以对其生成对应的Fuzz测试参数了。
在之前的章节中我们已经分析过了,在while
循环中,通过get_fuzzed_*()
函数(其中*
表示各种参数类型)来生成Fuzz测试目标的对应Fuzz测试参数,最终将生成的Fuzz测试参数保存到syscall_arguments
数组中。具体来说,KernelFuzzer可以生成以下几种类型的Fuzz测试参数:
- bool类型
get_fuzzed_bool()
- char类型
get_fuzzed_char8()
get_fuzzed_char16()
- int类型
get_fuzzed_int8()
get_fuzzed_int16()
get_fuzzed_int32()
get_fuzzed_int64()
- uint类型
get_fuzzed_uint8()
get_fuzzed_uint16()
get_fuzzed_uint32()
get_fuzzed_uint64()
- real类型
get_fuzzed_real32()
get_fuzzed_real64()
- 句柄类型
get_random_HANDLE()
可以发现,Kernel提供了丰富的Fuzz测试参数生成函数,为了搞清楚其究竟是如何生成对应的Fuzz测试参数的,我们可以进入这些函数内部进行进一步的分析。比如,get_fuzzed_bool()
函数就实现在"/kernelfuzzer/bughunt.h"的第133行。
这段代码定义了一个函数get_fuzzed_bool()
,其作用是从一个包含两个bool值的数组中随机选择一个bool值,并返回选中的bool值。具体分析如下:
- 定义了一个bool类型的数组
bool_BH[]
,包含两个bool值0
和1
。 - 声明了一个变量
n
,用于存储随机选择的bool值。 - 使用
rand() % sizeof(bool_BH) / sizeof(bool_BH[0])
生成一个随机索引,取余操作确保索引在数组范围内,除以数组中元素的大小可以得到数组的长度。 - 根据随机生成的索引,从数组中选择一个bool值并将其赋值给变量
n
。 - 记录选中的bool值到日志中。
- 返回选中的bool值
n
。
上述代码是关于bool类型参数的生成函数,下面我们还可以来分析关于char类型参数的生成函数。比如,get_fuzzed_char8()
实现在"/kernelfuzzer/bughunt.h"的第143行。
这段代码定义了一个函数get_fuzzed_char8()
,其作用是从一个包含各种字符的数组中随机选择一个字符,并返回选中的字符。具体分析如下:
- 定义了一个
char8_t
类型的数组char8_BH[]
,包含了一系列字符,如空格、制表符、换行符、特殊符号等。 - 声明了一个变量
n
,用于存储随机选择的字符。 - 使用
rand() % sizeof(char8_BH) / sizeof(char8_BH[0])
生成一个随机索引,取余操作确保索引在数组范围内,除以数组中元素的大小可以得到数组的长度。 - 根据随机生成的索引,从数组中选择一个字符并将其赋值给变量
n
。 - 记录选中的字符到日志中。
- 返回选中的字符
n
。
其余的Fuzz测试参数生成函数也都是基本一样的逻辑,不过在这里有一个Fuzz测试生成函数需要强调一下。即get_random_HANDLE()
函数,该函数实现在"/kernelfuzzer/handles_database.h"的第36行。
这段代码定义了一个函数get_random_HANDLE()
,其作用是从存储句柄的数组中随机选择一个句柄,并返回选中的句柄及其索引。具体分析如下:
- 声明了一个BH_Handle类型的结构体变量
temp_handle
,用于存储随机选择的句柄及其索引。 - 声明了一个无符号整数变量
n
,用于存储随机生成的索引值。 - 如果句柄数组已经完全填满(即
HANDLE_ARRAY_FULLY_POPULATED
为真),则计算句柄数组的长度,随机生成一个索引值n
。 - 如果句柄数组未完全填满,则随机生成一个不超过可用句柄索引的随机数
n
。 - 将选中的句柄及其索引赋值给
temp_handle
结构体变量的对应成员。 - 记录选中的句柄及其相关信息到日志中。
- 返回包含选中句柄及其索引的
temp_handle
结构体变量。
因为有些系统调用的参数是句柄,所以需要根据该函数返回所需要的Fuzz测试的句柄参数。而这些句柄是由之前的操作保存到HANDLES
数组中的,其它Fuzz测试生成参数函数并不都是提前获取好的,而是实时生成的。
对于其它Fuzz测试参数生成函数就不一一分析了,因为它们基本都是一样的逻辑,即都遵循"随机"这个概念,换句话说,Fuzz测试参数都是随机生成的。故不再赘述。
当我们获取到Fuzz测试目标和Fuzz测试参数后,就可以对其进行Fuzz测试了,这就是下一章节我们要分析的内容。
1.2.3.4、进行Fuzz测试
经过上面的分析后,我们现在已经得到了Fuzz测试目标和Fuzz测试参数,下面就要通过bughunt_syscall (*);
函数(其中*
表示该函数接收的参数,即Fuzz测试目标的ID和Fuzz测试参数)对其进行Fuzz测试了。bughunt_syscall()
函数实现在"/kernelfuzzer/bughunt_thread.h"的第101行(实际在"/kernelfuzzer/bughunt_thread.h"的第17行也有bughunt_syscall()
函数的实现,不过该实现是关于X86架构的,而我们都是在X64架构下进行测试和分析的,故就不对第17行的对应函数实现进行分析了)。
这段代码声明了一个名为"bughunt_syscall"的函数,其返回类型为DWORD,参数列表包括_syscall_uid
和32个QWORD类型的参数_dw0x01
到_dw0x20
。该函数声明使用__stdcall
调用约定,这意味着参数通过堆栈传递,调用方负责清理堆栈。这里只是对bughunt_syscall()
函数的声明,关于其具体实现在"/kernelfuzzer/bughunt_syscall_x64.asm"的第6行。
c
bughunt_syscall PROC
; RCX -> arg1
; RDX -> arg2
; R8 -> arg3
; R9 -> arg4
push rbp ; prologue
mov rbp, rsp
sub rsp, 118h
mov rax, rcx ;
mov r10, rdx
mov rdx, r8
mov r8, r9
; mov rcx, [rbp + XXh] ; main (argv[X + 4])
; push rcx
mov rcx, [rbp + 110h] ; main (argv[28 + 4]) = dw0x1B
push rcx
mov rcx, [rbp + 108h] ; main (argv[28 + 4]) = dw0x1B
push rcx
mov rcx, [rbp + 100h] ; main (argv[27 + 4]) = dw0x1A
push rcx
mov rcx, [rbp + 0F8h] ; main (argv[26 + 4]) = dw0x19
push rcx
mov rcx, [rbp + 0F0h] ; main (argv[25 + 4]) = dw0x18
push rcx
mov rcx, [rbp + 0E8h] ; main (argv[24 + 4]) = dw0x17
push rcx
mov rcx, [rbp + 0E0h] ; main (argv[23 + 4]) = dw0x16
push rcx
mov rcx, [rbp + 0D8h] ; main (argv[22 + 4]) = dw0x15
push rcx
mov rcx, [rbp + 0D0h] ; main (argv[21 + 4]) = dw0x14
push rcx
mov rcx, [rbp + 0C8h] ; main (argv[20 + 4]) = dw0x13
push rcx
mov rcx, [rbp + 0C0h] ; main (argv[19 + 4]) = dw0x12
push rcx
mov rcx, [rbp + 0B8h] ; main (argv[18 + 4]) = dw0x11
push rcx
mov rcx, [rbp + 0B0h] ; main (argv[17 + 4]) = dw0x10
push rcx
mov rcx, [rbp + 0A8h] ; main (argv[16 + 4])
push rcx
mov rcx, [rbp + 0A0h] ; main (argv[15 + 4])
push rcx
mov rcx, [rbp + 98h] ; main (argv[14 + 4])
push rcx
mov rcx, [rbp + 90h] ; main (argv[13 + 4])
push rcx
mov rcx, [rbp + 88h] ; main (argv[12 + 4])
push rcx
mov rcx, [rbp + 80h] ; main (argv[11 + 4])
push rcx
mov rcx, [rbp + 78h] ; main (argv[10 + 4])
push rcx
mov rcx, [rbp + 70h] ; main (argv[9 + 4])
push rcx
mov rcx, [rbp + 68h] ; main (argv[8 + 4])
push rcx
mov rcx, [rbp + 60h] ; main (argv[7 + 4])
push rcx
mov rcx, [rbp + 58h] ; main (argv[6 + 4])
push rcx
mov rcx, [rbp + 50h] ; main (argv[5 + 4])
push rcx
mov rcx, [rbp + 48h] ; main (argv[4 + 4])
push rcx
mov rcx, [rbp + 40h] ; main (argv[3 + 4])
push rcx
mov rcx, [rbp + 38h] ; main (argv[2 + 4])
push rcx
mov r9, [rbp + 30h]
mov rcx, r10
; R9 <- main (argv[4])
; R8 <- main (argv[3])
; RDX <- main (argv[2])
; RCX <- main (argv[1])
syscall ; invoke syscall
mov rsp, rbp ; epilogue, either that or `leave'
pop rbp
ret
bughunt_syscall ENDP
该处的代码是bughunt_syscall()
函数的具体实现,这些具体实现是一个汇编语言的过程(Procedure),它定义了一个名为"bughunt_syscall"的过程。在这个过程中,它接收一系列参数,然后通过syscall
指令调用系统调用服务例程。具体解释如下:
- 参数传递和栈帧设置:
- RCX, RDX, R8, R9寄存器用于传递函数的前四个参数。
push rbp
和mov rbp, rsp
用于保存当前函数调用之前的栈帧,以便后续恢复。sub rsp, 118h
分配局部变量或栈空间的大小。
- 参数备份:
- 一系列的
mov
和push
指令将函数参数传递到栈上,以便在syscall
调用中使用。
- 一系列的
- 系统调用指令:
mov r9, [rbp + 30h]
和mov rcx, r10
将栈中保存的参数重新加载到寄存器中,以备syscall
调用。syscall
是x64架构下的系统调用指令,触发相应的系统调用服务例程。
- 栈帧恢复和返回:
mov rsp, rbp
和pop rbp
用于恢复栈帧。ret
指令返回到调用者。
syscall
是x86-64架构中的指令,用于执行系统调用。当CPU执行到这条指令时,会触发操作系统的系统调用处理程序,从而执行操作系统提供的服务。在这段代码中,syscall
指令被用来执行之前传递好的函数参数所代表的系统调用。因为在前面的代码中,参数已经按照调用约定备份到了适当的寄存器/栈中,所以执行syscall
指令时,操作系统会根据寄存器中的参数信息来执行相应的系统调用服务例程。
这样,KernelFuzzer就可以利用之前生成好的Fuzz测试参数来对闭源操作系统内核(比如Windows操作系统)进行Fuzz测试了。
2、安装与使用
软件环境 | 硬件环境 | 约束条件 |
---|---|---|
Windows 10企业版 | 使用4个处理器,每个处理器4个内核,共分配16个内核 | 具体的约束条件可见"2.1、源码安装"章节所示的软件版本约束 |
具体的软件环境可见"2.1、源码安装"章节所示的软件环境 | 内存16GB | 本文所讲解的KernelFuzzer源代码于2024.03.11下载 |
暂无 | 硬盘60GB | 本文所安装的KernelFuzzer源代码于2023.06.02下载 |
暂无 | KernelFuzzer部署在VMware Pro 17上的Windows 10系统上(主机系统为Windows 11),硬件环境和软件环境也是对应的VMware Pro 17的硬件环境和软件环境 | 暂无 |
2.1、源码安装
2.1.1、部署系统依赖组件
若要完美地运行KernelFuzzer工具,需要安装如下组件,为了方便后续使用,记录于此,若有需要,及时查阅即可:
- Python 3.5
- Visual Studio 2013
下面就逐一安装这些相关组件。不过要注意的一点是,以下部署系统依赖组件的过程都是在Windows 10系统上进行的,但是不管在哪个系统上进行系统依赖组件的部署,都是这个过程,就不赘述了。
2.1.1.1、下载安装Python 3.5
-
首先来到Python官网下载Python 3.5:
-
选择红框的版本下载:
-
下载好后双击打开,按照下图选择:
-
点击"Next":
-
按照下图选择,最后点击"Install":
-
点击"Close":
-
然后在命令提示符(cmd)窗口输入"python"后出现下图内容,说明Python 3.5已经安装成功了:
2.1.1.2、下载安装Visual Studio 2013
-
首先来到Visual Studio 2013官网,找到红框处后点击"下载":
-
选择简体中文后下载即可:
-
下载完成后,双击打开安装文件:
-
按红框处配置,然后进行下一步:
-
按红框处配置,然后进行安装:
-
等待安装中:
-
安装完成后点击启动即可:
-
按需登录:
-
自定义配置,然后启动:
-
来到这个界面后,就已经完成安装了:
-
做完以上操作后,重启计算机
2.1.1.3、下载安装PTVS
-
因为我们整个项目需要使用Visual Studio 2013(以下简称vs2013)的环境,所需要配置vs2013的Python开发环境,故需要下载安装PTVS。我们只需要来到PTVS下载地址下载相关插件:
-
按下图配置,然后进行安装:
-
安装完成:
-
安装完成后启动vs2013,然后新建一个Python项目:
-
可以看到系统已经帮我们自动创建了Python环境:
-
输入下图中红框所示代码后,点击"启动",发现可以正常打印,这就意味着已经成功配置好了vs2013中的Python环境:
2.1.2、使用源码安装系统
-
首先来到KernelFuzzer的GitHub地址下载源码:
-
将下载好的KernelFuzzer保存到某个位置并解压:
-
此时其实就已经安装好了,比较简单,可以说也不需要安装,进入到源代码目录,简单介绍一下源代码目录中的主要文件夹/文件作用:
- crash_processing:处理crash
- crashes:产生的crash会存放在这个目录
- library_calls:需要Fuzz的库调用
- reproducer:复现crash
- worker_setup:设置环境,启动Fuzzer
- bughunt.c:启动Fuzz线程
- bughunt.h:提供一些返回随机字符或数组的函数
- bughunt_build_x64_debug.bat:编译用的bat文件
- bughunt_build_x64_release.bat:编译用的bat文件
- bughunt_build_x86_release.bat:编译用的bat文件
- bughunt_loop.py:处理发现的crash
- bughunt_syscall.asm:进行系统调用的汇编文件
- bughunt_syscall_x64.asm:进行系统调用的汇编文件
- bughunt_syscalls.h:要Fuzz的系统调用
- bughunt_thread.h:bughunt.c启动的进行Fuzz的线程
- handles_database.h:生成各种各样的handle
- helpers.h:一些辅助函数
- hooking.h:设置和取消hook
- library_calls.h:library_calls目录下要Fuzz的库调用
- LICENSE:KernelFuzzer协议
- logger.h:日志功能
- README.md:KernelFuzzer帮助文档
-
然后运行源代码目录中的此脚本:
-
成功运行:
-
该脚本的目的就是生成Fuzz测试工具,当成功执行该脚本后,会在"kernelfuzzer"目录中生成一个名为"bughunt.exe"的可执行文件,该名为"bughunt.exe"的可执行文件就是KernelFuzzer工具最终进行内核Fuzz的核心组件:
-
此时我们就彻底完成了关于KernelFuzzer的下载安装与配置,后面就可以对其进行各种测试了
2.2、使用方法
在本章,我们以Windows 10系统为目标进行Fuzz作为KernelFuzzer的演示实例,在其它系统上关于KernelFuzzer的使用方法也和本章节介绍的一样。更多测试细节,或者对更多的目标进行Fuzz测试请参考第3章,本章只是对KernelFuzzer的使用方法进行通用的演示。关于KernelFuzz的具体使用方法如下介绍。
-
首先然后来到"C:\Windows\System32"目录下,使用管理员权限运行cmd.exe:
-
进入KernelFuzzer的源代码目录:
-
然后进入"worker_setup"目录:
-
启动KernelFuzzer:
-
可以发现,已经成功启动KernelFuzzer了,之后系统会自动重启:
-
系统重启后就自动开始检测了:
-
我们可以看一下具体哪个进程在执行检测,可以打开"任务计划程序"来进行查看:
-
可以发现,是"Bug Hunter"计划任务在每次开机的时候执行Fuzz测试,这就说明我们的程序运行没有任何问题:
-
最后,如果检测到漏洞,会将检测到的漏洞保存到"C盘"的"Dumps"文件夹中:
3、测试用例
3.1、对Windows 10内核进行Fuzz测试
首先需要按照第2章所介绍的内容在Windows 10系统上安装KernelFuzzer,并配置好相关环境后,才能在Windows 10系统上进行下面的操作。
-
首先然后来到"C:\Windows\System32"目录下,使用管理员权限运行cmd.exe:
-
进入KernelFuzzer的源代码目录:
-
然后进入"worker_setup"目录:
-
启动KernelFuzzer:
-
可以发现,已经成功启动KernelFuzzer了,之后系统会自动重启:
-
系统重启后就自动开始检测了:
-
我们可以看一下具体哪个进程在执行检测,可以打开"任务计划程序"来进行查看:
-
可以发现,是"Bug Hunter"计划任务在每次开机的时候执行Fuzz测试,这就说明我们的程序运行没有任何问题:
-
最后,如果检测到漏洞,会将检测到的漏洞保存到"C盘"的"Dumps"文件夹中:
3.2、对Windows 7内核进行Fuzz测试
首先需要按照第2章所介绍的内容在Windows 7系统上安装KernelFuzzer,并配置好相关环境后,才能在Windows 7系统上进行下面的操作。
值得注意的是,我们不需要在Windows 7系统上再次下载安装与配置KernelFuzzer了,只需要将在Windows 10系统中已经下载安装配置好的KernelFuzzer拷贝一份到Windows 7系统上即可。这么做是因为我们最终需要使用编译出来的"bughunt.exe"可执行文件,而在Windows 7系统上不知为何无法成功将"bughunt.exe"可执行文件编译出来,故只能使用在Windows 10系统中已经下载安装配置好的KernelFuzzer(其中已经包含了成功编译出来的"bughunt.exe"可执行文件)来进行后续操作了。除了这一点需要注意,其余与对其它系统的配置与使用过程完全一致。
-
首先然后来到"C:\Windows\System32"目录下,使用管理员权限运行cmd.exe:
-
进入KernelFuzzer的源代码目录:
-
然后进入"worker_setup"目录:
-
启动KernelFuzzer:
-
可以发现,已经成功启动KernelFuzzer了,之后系统会自动重启:
-
系统重启后就自动开始检测了:
-
我们可以看一下具体哪个进程在执行检测,可以打开"任务计划程序"来进行查看:
-
可以发现,是"Bug Hunter"计划任务在每次开机的时候执行Fuzz测试,这就说明我们的程序运行没有任何问题:
-
最后,如果检测到漏洞,会将检测到的漏洞保存到"C盘"的"Dumps"文件夹中:
4、总结
4.1、部署架构
关于KernelFuzzer部署的架构图,如下所示。
对于以上架构图,我们具体来看KernelFuzzer是否对其中的组件进行了修改。详情可参见下方的表格。
是否有修改 | 具体修改内容 |
---|---|
主机内核 | 无 |
主机操作系统 | 无 |
Guest内核 | 无 |
Guest操作系统 | 无 |
4.2、漏洞检测对象
- 检测的对象为Guest内核
- 针对的内核版本为Windows 7和Windows 10
- 针对的漏洞类型为崩溃性错误
4.3、漏洞检测方法
- 使用Windows系统的
syscall
汇编指令通过库函数和系统调用对其内核进行Fuzz测试 - 将测试结果保存到主机中
- 目前可以进行测试的库文件只有一类,即:
- BH_GetSysColorBrush
- 目前可以进行测试的系统调用有两类,包括:
- 0x12F5
- 0x12D4
4.4、种子生成/变异技术
- 初始种子由KernelFuzzer生成
- 没有变异的过程,直接生成目标种子(即库文件和系统调用)的参数
- 生成目标种子参数的策略基于随机,即随机生成特定类型的参数的具体值(比如bool类型、int类型和char类型等)
5、参考文献
- 内核漏洞挖掘技术系列(5)------KernelFuzzer
- API函数的调用过程(三环到零环)以及重写WriteProcessMemory三环
- 使用 SYSENTER 和 SYSEXIT 指令执行对系统过程的快速调用
总结
以上就是本篇博文的全部内容,可以发现,KernelFuzzer的部署与使用的过程并不复杂,并且KernelFuzzer的Fuzz测试过程的脉络也比较清楚,是一个典型的KernelFuzzer测试的过程。总而言之,KernelFuzzer是一个不错的Fuzz测试的工具,值得大家学习。相信读完本篇博客,各位读者一定对KernelFuzzer有了更深的了解。