KernelFuzzer部署、使用与原理分析

文章目录

  • 前言
  • 1、概述
    • 1.1、整体架构
    • 1.2、工作流程
      • 1.2.1、环境配置流程
      • 1.2.2、计划任务执行流程
      • 1.2.3、Fuzz测试流程
        • 1.2.3.1、整体资源调度
        • 1.2.3.2、选取Fuzz测试目标
        • 1.2.3.3、生成Fuzz测试参数
        • 1.2.3.4、进行Fuzz测试
  • 2、安装与使用
    • 2.1、源码安装
      • 2.1.1、部署系统依赖组件
        • 2.1.1.1、下载安装Python 3.5
        • 2.1.1.2、下载安装Visual Studio 2013
        • 2.1.1.3、下载安装PTVS
      • 2.1.2、使用源码安装系统
    • 2.2、使用方法
  • 3、测试用例
    • 3.1、对Windows 10内核进行Fuzz测试
    • 3.2、对Windows 7内核进行Fuzz测试
  • 4、总结
    • 4.1、部署架构
    • 4.2、漏洞检测对象
    • 4.3、漏洞检测方法
    • 4.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工作,故将本章节的标题命名为“环境配置流程”。具体来说:

  1. 安装WinDbg调试工具(install_windbg()函数调用)。
  2. 安装CouchDB Python模块(install_couch_module()函数调用)。
  3. 修改注册表以禁用UAC(用户账户控制)、锁屏、Windows错误报告和Windows更新(change_registry()函数调用)。
  4. 启用内核转储,以便在系统崩溃时生成转储文件(enable_kernel_dumps()函数调用)。
  5. 创建一个计划任务,使得目标脚本在用户登录时自动执行(schedule_task()函数调用)。
  6. 启用特殊池,以便对win32k.sys系统文件启用特殊池(enable_special_pool()函数调用)。
  7. 重启系统(reboot_system()函数调用)。

  在这里我们主要关注上面标红的部分,即schedule_task()函数调用,schedule_task()函数实现在“/kernelfuzzer/worker_setup/worker_setup.py”的第54行。
在这里插入图片描述

  该函数的目的是创建一个名为“Bug Hunter”的计划任务,在用户登录时执行指定的程序“bughunt_loop.py”。具体来说。

  1. 打印提示消息“[!] Scheduling Task…”。
  2. 调用subprocess.call函数执行命令schtasks。
  3. 使用schtasks命令创建一个名为“Bug Hunter”的计划任务,该任务在用户登录时执行,执行的程序为“bughunt_loop.py”,路径为当前工作目录的上级目录。
  4. 函数结束执行。

  可以发现该函数的核心逻辑是上面的红色部分,即在每次系统启动时,都要执行“/kernelfuzzer/bughunt_loop.py”程序,故我们接下来就要分析该程序。

1.2.2、计划任务执行流程

  通过上一章节的分析,我们清楚,KernelFuzzer启动后,首先创建一个名为“Bug Hunter”的计划任务,在每次系统启动后,都会执行这个计划任务。而该计划任务最终执行的就是“/kernelfuzzer/bughunt_loop.py”程序。该程序的全部代码如下所示。

	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测试工具,并在测试过程中监测系统崩溃并将崩溃信息提交到数据库。具体来说,该脚本的代码执行逻辑如下。

  1. 处理内存转储文件:
    • 通过os.walk()函数遍历指定文件夹中的文件。
    • 使用fnmatch.filter()函数过滤出扩展名为“.dmp”的文件。
    • 对每个找到的转储文件执行以下操作:
      • 创建一个以当前时间戳命名的新文件夹,并将其路径保存在folder变量中。
      • 将转储文件移动到新创建的文件夹中。
      • 使用kd.exe分析转储文件,并将分析结果保存到名为“windbg.log”的日志文件中。
      • 如果成功分析转储文件,则设置crash_found标志为True,表示发现了崩溃。
  2. 提交崩溃信息到数据库:
    • 尝试使用Python脚本“couchdb_submit.py”将崩溃信息提交到CouchDB数据库。
    • 如果提交失败,则继续执行脚本而不中断。
  3. 运行Fuzz测试工具(即“bughunt.exe”):
    • 尝试使用给定的超时时间运行Fuzz测试工具(即“bughunt.exe”)。
    • 如果超时,打印相应的消息。
    • 捕获所有其他异常,以便脚本可以继续执行。
  4. 清理临时文件和进程:
    • 终止所有Notepad进程。
    • 删除临时文件和日志文件。
    • 最后,重新启动计算机。

  以上代码的核心逻辑就是上面标红的部分,即启动“bughunt.exe”可执行文件,那么问题就来了。

  1. “bughunt.exe”可执行文件是怎么得到的?
  2. “bughunt.exe”可执行文件都做了什么事情?

  让我们首先解决第一个问题,还记得我们在安装KernelFuzzer时执行的一个脚本么?即“/kernelfuzzer/bughunt_build_x64_release.bat”脚本文件。
在这里插入图片描述

  该脚本用于设置环境变量,并在当前目录下编译“bughunt.c”和汇编“bughunt_syscall_x64.asm”文件,生成可执行文件“bughunt.exe”。具体来说。

  1. 设置环境变量:
    • 调用Visual Studio的vcvarsall.bat脚本来设置64位环境变量。
  2. 删除旧文件:
    • 删除当前目录下的所有日志、目标文件和可执行文件。
  3. 汇编目标文件:
    • 使用ml64.exe汇编“bughunt_syscall_x64.asm”文件,生成目标文件“bughunt_syscall_x64.obj”。
  4. 编译和链接:
    • 使用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行。

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);  }  

  整体来看,该程序是一个多线程程序,根据命令行参数创建一定数量的线程,并等待所有线程执行完毕后退出。具体来说,该段程序的逻辑为。

  1. 变量声明和初始化:
    • 声明了一些变量,包括子进程数量、线程索引、线程句柄数组等。
    • 声明了两个无符号整数变量seedcontrol_seed,并初始化为NULL
  2. 命令行参数解析和错误检查:
    • 通过命令行参数argcargv[],获取传递给程序的参数。
    • 如果参数数量不为4,打印使用说明并返回错误码0xDEADBEEF
  3. 初始化句柄:
    • 调用make_HANDLES()函数,用于初始化一组不同类型的句柄(Handle)。
  4. 参数转换:
    • 使用strtol()函数将命令行参数转换为无符号整数,分别表示子进程数量、系统调用次数和种子值。
  5. 线程创建:
    • 使用CreateThread()函数创建多个线程,根据传入的参数动态调整子进程数量。
    • 每个线程执行相同的函数bughunt_thread(),并传递种子值作为参数。
    • 检查线程句柄是否创建成功,若失败则打印错误信息并返回错误码。
  6. 等待线程结束:
    • 使用WaitForMultipleObjects()函数等待所有线程执行完毕。
    • 当所有线程都退出时,程序继续执行。
  7. 清理资源:
    • 使用CloseHandle()函数关闭每个线程的句柄,释放内存。
  8. 返回退出码:
    • 最后返回退出码0,表示程序成功结束。

  以上代码虽然比较多,不过我们并不需要全部分析,我们只需要关注上面标红的两处重点逻辑即可。下面将会对其进行详细分析。

  • 初始化句柄
    此处的逻辑由make_HANDLES()函数调用完成,该函数实现在“kernelfuzzer/handles_database.h”的第105行。
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;  }  

  这段代码的目的是初始化一组不同类型的句柄,以便后续的程序可以使用这些句柄进行各种操作,如创建窗口、文件、位图等。其主要代码执行逻辑如下。

  1. 变量声明和初始化:
    • 声明了一些变量,包括句柄索引、临时句柄、临时整数等。
    • 定义了一个BITMAP结构体和一个字节型二维数组bits,用于创建位图。
  2. 句柄数组初始化:
    • 初始化了一个叫做HANDLES的句柄数组,长度为HANDLES_N
    • 通过循环将句柄数组中的所有元素初始化为0
  3. 句柄创建:
    • 在循环中,根据随机选择的方式,尝试创建不同类型的句柄。
    • 使用不同的WinAPI函数创建句柄,如GetDesktopWindow()MonitorFromPoint()CreateFile()等。此外还有一个名为OpenNotepad的自定义函数来创建关于notepad.exe的句柄
    • 检查句柄是否有效,若有效则记录到HANDLES数组中,并记录句柄创建方式到HANDLE_CREATOR数组中。
  4. 可用句柄索引初始化:
    • 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行。

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);  }  

  这段代码是用于模拟多线程环境下的系统调用执行过程,通过随机选择系统调用和参数,以及记录执行过程中的详细信息,来进行系统调用的测试和分析。具体来说,该函数的代码执行逻辑如下:

  1. 变量声明和初始化:
    • syscall_idx:用于迭代系统调用数组的索引。
    • syscall:指向SYSCALL结构体的指针,用于存储随机选择的系统调用。
    • syscall_argument_datatype_idx:用于迭代系统调用参数数据类型的索引。
    • syscall_arguments[]:存储系统调用参数值的数组。
    • stream:文件指针,用于日志记录。
    • syscall_handle_argumentBH_Handle结构体,用于存储随机选择的句柄。
    • syscall_log_string:用于构建系统调用日志字符串的字符数组,初始化为全零。
  2. 随机数种子初始化:
    • 检查传入的种子参数seed是否为1,如果是,则将种子重新设置为基于当前时间、当前进程ID和当前线程ID的组合值,并使用srand()函数初始化随机数生成器。同时记录下种子值。
    • 如果种子不为 1,则直接使用传入的种子值,并记录下种子值。
  3. 系统调用循环:
    • 使用for循环遍历系统调用数组。
    • 在每次循环中,执行以下操作:
      • 随机选择一个库调用或者对其进行钩子(hook)操作(不过目前作者并没有实现该功能)。
      • 随机选择一个系统调用,并将其赋值给syscall
      • 构建系统调用日志字符串,记录系统调用的UID以及参数。
      • 生成随机选择的系统调用的对应用于Fuzz测试的参数。
      • 使用bughunt_syscall(*)函数(其中*表示该函数接收的参数)调用系统调用,并传入参数数组中的值。
  4. 参数值获取:
    • while循环中,通过调用get_fuzzed_*()函数(其中*表示各种参数类型)获取不同数据类型的模糊值,并将其存储在syscall_arguments[]数组中。
    • while循环中,对于句柄类型的参数,调用get_random_HANDLE()函数获取随机句柄,并记录在日志中。
  5. 系统调用日志记录:
    • while循环结束后,在for循环中,使用sprintf()函数将系统调用的UID和参数值格式化成字符串,并记录在syscall_log_string中。
    • while循环结束后,在for循环中,调用logger()函数记录系统调用日志字符串。
  6. 系统调用执行:
    • while循环结束后,在for循环中,调用bughunt_syscall()函数(其中*表示该函数接收的参数)执行系统调用,传入系统调用的UID和参数数组中的值。
  7. 返回值:
    • 函数执行完毕后,返回一个值为0的DWORD类型。

  在该函数中,核心逻辑为上面标红的部分,这几部分操作又可以归为三类,即分别为:

  1. 选取Fuzz测试目标
    该逻辑由(*random_LIBRARY_CALL())();函数调用和random_SYSCALL ();函数调用来完成。
  2. 生成Fuzz测试参数
    该逻辑由((DWORD)get_fuzzed_*());函数(其中*表示各种参数类型)调用来完成。
  3. 进行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的全过程。具体来说,该函数的代码逻辑为:

  1. 函数声明和变量定义:
    • 函数BH_GetSysColorBrush()被定义为VOID类型,没有返回值。
    • 函数内部声明了两个变量result_BH_GetSysColorBrushtempInt_BH_GetSysColorBrush,分别用于存储系统颜色刷的句柄和Fuzz的整数参数。
  2. 生成日志标识字符串:
    • 使用get_time_in_ms()函数获取当前时间(毫秒级),结合rand() % 1024的随机数,生成一个字符串vid作为日志标识。
  3. 获取Fuzz整数参数:
    • 调用get_fuzzed_int32()函数,获取一个Fuzz的整数作为参数,用于调用GetSysColorBrush()函数。
  4. 调用系统函数:
    • 使用tempInt_BH_GetSysColorBrush作为参数,调用GetSysColorBrush()函数获取系统颜色刷,并将结果存储在result_BH_GetSysColorBrush中。
  5. 记录日志和存储句柄:
    • 在日志中记录函数调用和参数信息,包括获取的系统颜色刷的句柄。
    • 使用put_random_HANDLE()函数将获取的系统颜色刷的句柄存储起来,用于后续的处理。

  该函数的核心逻辑为上面标红的两部分,即:

  1. 调用get_fuzzed_int32()函数来生成Fuzz参数。get_fuzzed_int32()函数实现在“/kernelfuzzer/bughunt.h”的第258行。
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测试了。

  1. 调用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函数生成一个0n之间的随机索引。然后,函数返回指向所选系统调用的指针。

       很明显,在这里最重要的就是SYSCALLS这个系统调用数组,因为选取的过程是随机的,选取的目标就是这个SYSCALLS系统调用数组。而这个SYSCALLS系统调用数组又定义在“/kernelfuzzer/bughunt_syscalls.h”的第23行。
在这里插入图片描述

  该数组包含了一系列系统调用的信息,具体来说:

  1. 定义SYSCALLS数组:
    • 代码开始处定义了一个名为SYSCALLS的数组,用于存储系统调用的信息。
    • 此数组包含了一系列SYSCALL结构体元素,每个元素表示一个系统调用。
  2. SYSCALL结构体:
    • 每个SYSCALL结构体包含三个主要字段:唯一标识符(uid)、参数类型数组(argument_datatypes)、返回值类型(return_type)。
    • 唯一标识符(uid)用于标识每个系统调用,以便在代码中引用。
    • 参数类型数组(argument_datatypes)列出了系统调用的参数类型,以便在调用时使用正确的参数。
    • 返回值类型(return_type)指定了系统调用的返回值类型,以便调用者了解调用结果的类型。
  3. 系统调用定义:
    • 此处列出了两个Windows 7 x64用户界面(user32)库的系统调用,分别为0x12F50x12D4
    • 第一个系统调用(即0x12F5)没有参数,返回一个布尔类型的值。
    • 第二个系统调用(即0x12D4)包含四个参数类型(void指针、void指针、void指针、句柄),返回一个布尔类型的值。

  最终,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值。具体分析如下:

  1. 定义了一个bool类型的数组bool_BH[],包含两个bool值01
  2. 声明了一个变量n,用于存储随机选择的bool值。
  3. 使用rand() % sizeof(bool_BH) / sizeof(bool_BH[0])生成一个随机索引,取余操作确保索引在数组范围内,除以数组中元素的大小可以得到数组的长度。
  4. 根据随机生成的索引,从数组中选择一个bool值并将其赋值给变量n
  5. 记录选中的bool值到日志中。
  6. 返回选中的bool值n

  上述代码是关于bool类型参数的生成函数,下面我们还可以来分析关于char类型参数的生成函数。比如,get_fuzzed_char8()实现在“/kernelfuzzer/bughunt.h”的第143行。
在这里插入图片描述

  这段代码定义了一个函数get_fuzzed_char8(),其作用是从一个包含各种字符的数组中随机选择一个字符,并返回选中的字符。具体分析如下:

  1. 定义了一个char8_t类型的数组char8_BH[],包含了一系列字符,如空格、制表符、换行符、特殊符号等。
  2. 声明了一个变量n,用于存储随机选择的字符。
  3. 使用rand() % sizeof(char8_BH) / sizeof(char8_BH[0])生成一个随机索引,取余操作确保索引在数组范围内,除以数组中元素的大小可以得到数组的长度。
  4. 根据随机生成的索引,从数组中选择一个字符并将其赋值给变量n
  5. 记录选中的字符到日志中。
  6. 返回选中的字符n

  其余的Fuzz测试参数生成函数也都是基本一样的逻辑,不过在这里有一个Fuzz测试生成函数需要强调一下。即get_random_HANDLE()函数,该函数实现在“/kernelfuzzer/handles_database.h”的第36行。
在这里插入图片描述

  这段代码定义了一个函数get_random_HANDLE(),其作用是从存储句柄的数组中随机选择一个句柄,并返回选中的句柄及其索引。具体分析如下:

  1. 声明了一个BH_Handle类型的结构体变量temp_handle,用于存储随机选择的句柄及其索引。
  2. 声明了一个无符号整数变量n,用于存储随机生成的索引值。
  3. 如果句柄数组已经完全填满(即HANDLE_ARRAY_FULLY_POPULATED为真),则计算句柄数组的长度,随机生成一个索引值n
  4. 如果句柄数组未完全填满,则随机生成一个不超过可用句柄索引的随机数n
  5. 将选中的句柄及其索引赋值给temp_handle结构体变量的对应成员。
  6. 记录选中的句柄及其相关信息到日志中。
  7. 返回包含选中句柄及其索引的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行。

	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指令调用系统调用服务例程。具体解释如下:

  1. 参数传递和栈帧设置:
    • RCX, RDX, R8, R9寄存器用于传递函数的前四个参数。
    • push rbpmov rbp, rsp用于保存当前函数调用之前的栈帧,以便后续恢复。
    • sub rsp, 118h分配局部变量或栈空间的大小。
  2. 参数备份:
    • 一系列的movpush指令将函数参数传递到栈上,以便在syscall调用中使用。
  3. 系统调用指令:
    • mov r9, [rbp + 30h]mov rcx, r10将栈中保存的参数重新加载到寄存器中,以备syscall调用。
    • syscall是x64架构下的系统调用指令,触发相应的系统调用服务例程。
  4. 栈帧恢复和返回:
    • mov rsp, rbppop 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
  1. 首先来到Python官网下载Python 3.5:
    在这里插入图片描述

  2. 选择红框的版本下载:
    在这里插入图片描述

  3. 下载好后双击打开,按照下图选择:
    在这里插入图片描述

  4. 点击“Next”:
    在这里插入图片描述

  5. 按照下图选择,最后点击“Install”:
    在这里插入图片描述

  6. 点击“Close”:
    在这里插入图片描述

  7. 然后在命令提示符(cmd)窗口输入“python”后出现下图内容,说明Python 3.5已经安装成功了:
    在这里插入图片描述

2.1.1.2、下载安装Visual Studio 2013
  1. 首先来到Visual Studio 2013官网,找到红框处后点击“下载”:
    在这里插入图片描述

  2. 选择简体中文后下载即可:
    在这里插入图片描述

  3. 下载完成后,双击打开安装文件:
    在这里插入图片描述

  4. 按红框处配置,然后进行下一步:
    在这里插入图片描述

  5. 按红框处配置,然后进行安装:
    在这里插入图片描述

  6. 等待安装中:
    在这里插入图片描述

  7. 安装完成后点击启动即可:
    在这里插入图片描述

  8. 按需登录:
    在这里插入图片描述

  9. 自定义配置,然后启动:
    在这里插入图片描述

  10. 来到这个界面后,就已经完成安装了:
    在这里插入图片描述

  11. 做完以上操作后,重启计算机

2.1.1.3、下载安装PTVS
  1. 因为我们整个项目需要使用Visual Studio 2013(以下简称vs2013)的环境,所需要配置vs2013的Python开发环境,故需要下载安装PTVS。我们只需要来到PTVS下载地址下载相关插件:
    在这里插入图片描述

  2. 按下图配置,然后进行安装:
    在这里插入图片描述

  3. 安装完成:
    在这里插入图片描述

  4. 安装完成后启动vs2013,然后新建一个Python项目:
    在这里插入图片描述

  5. 可以看到系统已经帮我们自动创建了Python环境:
    在这里插入图片描述

  6. 输入下图中红框所示代码后,点击“启动”,发现可以正常打印,这就意味着已经成功配置好了vs2013中的Python环境:
    在这里插入图片描述

2.1.2、使用源码安装系统

  1. 首先来到KernelFuzzer的GitHub地址下载源码:
    在这里插入图片描述

  2. 将下载好的KernelFuzzer保存到某个位置并解压:
    在这里插入图片描述

  3. 此时其实就已经安装好了,比较简单,可以说也不需要安装,进入到源代码目录,简单介绍一下源代码目录中的主要文件夹/文件作用:
    在这里插入图片描述

    • 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帮助文档
  4. 然后运行源代码目录中的此脚本:
    在这里插入图片描述

  5. 成功运行:
    在这里插入图片描述

  6. 该脚本的目的就是生成Fuzz测试工具,当成功执行该脚本后,会在“kernelfuzzer”目录中生成一个名为“bughunt.exe”的可执行文件,该名为“bughunt.exe”的可执行文件就是KernelFuzzer工具最终进行内核Fuzz的核心组件:
    在这里插入图片描述

  7. 此时我们就彻底完成了关于KernelFuzzer的下载安装与配置,后面就可以对其进行各种测试了

2.2、使用方法

  在本章,我们以Windows 10系统为目标进行Fuzz作为KernelFuzzer的演示实例,在其它系统上关于KernelFuzzer的使用方法也和本章节介绍的一样。更多测试细节,或者对更多的目标进行Fuzz测试请参考第3章,本章只是对KernelFuzzer的使用方法进行通用的演示。关于KernelFuzz的具体使用方法如下介绍。

  1. 首先然后来到“C:\Windows\System32”目录下,使用管理员权限运行cmd.exe:
    在这里插入图片描述

  2. 进入KernelFuzzer的源代码目录:
    在这里插入图片描述

  3. 然后进入“worker_setup”目录:
    在这里插入图片描述

  4. 启动KernelFuzzer:
    在这里插入图片描述

  5. 可以发现,已经成功启动KernelFuzzer了,之后系统会自动重启:
    在这里插入图片描述

  6. 系统重启后就自动开始检测了:
    在这里插入图片描述

  7. 我们可以看一下具体哪个进程在执行检测,可以打开“任务计划程序”来进行查看:
    在这里插入图片描述

  8. 可以发现,是“Bug Hunter”计划任务在每次开机的时候执行Fuzz测试,这就说明我们的程序运行没有任何问题:
    在这里插入图片描述

  9. 最后,如果检测到漏洞,会将检测到的漏洞保存到“C盘”的“Dumps”文件夹中:
    在这里插入图片描述

3、测试用例

3.1、对Windows 10内核进行Fuzz测试

  首先需要按照第2章所介绍的内容在Windows 10系统上安装KernelFuzzer,并配置好相关环境后,才能在Windows 10系统上进行下面的操作。

  1. 首先然后来到“C:\Windows\System32”目录下,使用管理员权限运行cmd.exe:
    在这里插入图片描述

  2. 进入KernelFuzzer的源代码目录:
    在这里插入图片描述

  3. 然后进入“worker_setup”目录:
    在这里插入图片描述

  4. 启动KernelFuzzer:
    在这里插入图片描述

  5. 可以发现,已经成功启动KernelFuzzer了,之后系统会自动重启:
    在这里插入图片描述

  6. 系统重启后就自动开始检测了:
    在这里插入图片描述

  7. 我们可以看一下具体哪个进程在执行检测,可以打开“任务计划程序”来进行查看:
    在这里插入图片描述

  8. 可以发现,是“Bug Hunter”计划任务在每次开机的时候执行Fuzz测试,这就说明我们的程序运行没有任何问题:
    在这里插入图片描述

  9. 最后,如果检测到漏洞,会将检测到的漏洞保存到“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”可执行文件)来进行后续操作了。除了这一点需要注意,其余与对其它系统的配置与使用过程完全一致。

  1. 首先然后来到“C:\Windows\System32”目录下,使用管理员权限运行cmd.exe:
    在这里插入图片描述

  2. 进入KernelFuzzer的源代码目录:
    在这里插入图片描述

  3. 然后进入“worker_setup”目录:
    在这里插入图片描述

  4. 启动KernelFuzzer:
    在这里插入图片描述

  5. 可以发现,已经成功启动KernelFuzzer了,之后系统会自动重启:
    在这里插入图片描述

  6. 系统重启后就自动开始检测了:
    在这里插入图片描述

  7. 我们可以看一下具体哪个进程在执行检测,可以打开“任务计划程序”来进行查看:
    在这里插入图片描述

  8. 可以发现,是“Bug Hunter”计划任务在每次开机的时候执行Fuzz测试,这就说明我们的程序运行没有任何问题:
    在这里插入图片描述

  9. 最后,如果检测到漏洞,会将检测到的漏洞保存到“C盘”的“Dumps”文件夹中:
    在这里插入图片描述

4、总结

4.1、部署架构

  关于KernelFuzzer部署的架构图,如下所示。
请添加图片描述
  对于以上架构图,我们具体来看KernelFuzzer是否对其中的组件进行了修改。详情可参见下方的表格。

是否有修改具体修改内容
主机内核
主机操作系统
Guest内核
Guest操作系统

4.2、漏洞检测对象

  1. 检测的对象为Guest内核
  2. 针对的内核版本为Windows 7和Windows 10
  3. 针对的漏洞类型为崩溃性错误

4.3、漏洞检测方法

  1. 使用Windows系统的syscall汇编指令通过库函数和系统调用对其内核进行Fuzz测试
  2. 将测试结果保存到主机中
  3. 目前可以进行测试的库文件只有一类,即:
    • BH_GetSysColorBrush
  4. 目前可以进行测试的系统调用有两类,包括:
    • 0x12F5
    • 0x12D4

4.4、种子生成/变异技术

  1. 初始种子由KernelFuzzer生成
  2. 没有变异的过程,直接生成目标种子(即库文件和系统调用)的参数
  3. 生成目标种子参数的策略基于随机,即随机生成特定类型的参数的具体值(比如bool类型、int类型和char类型等)

5、参考文献

  1. 内核漏洞挖掘技术系列(5)——KernelFuzzer
  2. API函数的调用过程(三环到零环)以及重写WriteProcessMemory三环
  3. 使用 SYSENTER 和 SYSEXIT 指令执行对系统过程的快速调用

总结

  以上就是本篇博文的全部内容,可以发现,KernelFuzzer的部署与使用的过程并不复杂,并且KernelFuzzer的Fuzz测试过程的脉络也比较清楚,是一个典型的KernelFuzzer测试的过程。总而言之,KernelFuzzer是一个不错的Fuzz测试的工具,值得大家学习。相信读完本篇博客,各位读者一定对KernelFuzzer有了更深的了解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/29520.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

硫碳复合材料可用作固态电池正极材料 锂硫电池是重要下游

硫碳复合材料可用作固态电池正极材料 锂硫电池是重要下游 硫碳复合材料&#xff0c;是半固态电池、固态电池的正极材料&#xff0c;主要用于金属硫电池制造领域&#xff0c;在锂硫电池应用中研究热度最高。 锂硫电池&#xff0c;一种二次电池&#xff0c;以硫元素为正极&#x…

HarmonyOS 页面路由(Router)

1. HarmonyOS页面路由(Router) 页面路由指在应用程序中实现不同页面之间的跳转和数据传递。HarmonyOS提供了Router模块&#xff0c;通过不同的url地址&#xff0c;可以方便地进行页面路由&#xff0c;轻松地访问不同的页面。本文将从页面跳转、页面返回和页面返回前增加一个询问…

VBA学习(9):按指定名单一键删除工作表

今天继续给大家聊VBA编程中工作表对象的常用操作&#xff0c;主要内容是如何批量删除工作表&#xff1b;也就是删除单个工作表、删除全部工作表和删除指定名单内的工作表。 1.删除单个工作表 删除工作表需要使用到工作表对象的delete方法&#xff0c;语法格式如下&#xff1a…

聚类分析 #数据挖掘 #Python

聚类分析&#xff08;Cluster Analysis&#xff09;是一种无监督机器学习方法&#xff0c;主要用于数据挖掘和数据分析中&#xff0c;它的目标是将一组对象或观测值根据它们之间的相似性或相关性自动分组&#xff0c;形成不同的簇或类别。聚类分析并不预先知道每个观测值的具体…

可燃气体报警器:户外工地安全预警先锋,定期检定保障安全无忧

在现代化的建设进程中&#xff0c;户外工地作为城市发展的重要推动力&#xff0c;其安全问题一直备受关注。 工地现场往往涉及多种易燃易爆气体&#xff0c;一旦发生泄漏&#xff0c;后果不堪设想。因此&#xff0c;如何有效预警并防范可燃气体泄露&#xff0c;成为户外工地安…

新手小白从Windows转Linux,或许manjaro更适合你!

网管小贾 / sysadm.cc 野生动物园里有一块并不怎么大的水塘&#xff0c;一群火烈鸟就生活在这里。 它们在水塘里悠闲地漫步&#xff0c;饿了就找些小鱼小虾&#xff0c;困了就伸个懒腰、打个盹。 就这样日复一日&#xff0c;过着百无聊赖的日子&#xff0c;直到有一天…… 这…

easyexcel和poi版本冲突报错深入解析v2

easyexcel报错解决 问题 项目由poi改用easyexcel&#xff0c;报错如下&#xff1a; java.lang.NoSuchMethodError: ‘org.apache.poi.ss.usermodel.CellType org.apache.poi.ss.usermodel.Cell.getCellType()’ 原因 easyexcel中的poi和项目原本的poi版本冲突问题。 由于之前做…

数据结构---排序算法

个人介绍 hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的…

数据中心机房建设标准

数据中心机房是专门设计用于存放、管理和维护计算机服务器、网络设备、存储设备以及其他IT基础设施的物理空间。它们是信息化社会的基石&#xff0c;为各种在线服务提供硬件和网络支持。数据中心机房的主要功能包括数据存储、处理、传输以及提供计算资源。 数据中心机房建设涉及…

用python绘制三维条形图

用python绘制三维条形图 三维条形图特点与用途 效果代码 三维条形图 三维条形图是一种在三维空间中表示数据的方法&#xff0c;它通过垂直或水平的条形长度来显示类别之间的差异。与传统的二维条形图相比&#xff0c;三维条形图增加了深度或高度的维度&#xff0c;使得数据可视…

数理化解题研究杂志社数理化解题研究编辑部2024年第12期目录

教学设计与教学策略研究 聚焦数学思想 贯彻核心素养——以“函数的奇偶性”的教学设计为例 宋方宁;李硕; 2-4 高中数学课堂案例研究——探讨“教-学-评”一体化模式的应用 赖琰媛;曹小燕; 5-7 漫谈体验式教学在高中数学教学中的运用策略 林素珍; 15-17《数理化解题…

洁盟超声波清洗机怎么样?2024爆款机型声波清洗机测评、一篇看懂

随着现在近视率的逐年上升&#xff0c;戴眼镜的人群越来越多&#xff01;当然他们也在面临着一个眼镜清洗的问题&#xff01;因为长期佩戴眼镜&#xff0c;镜框还有镜片上面都是会积累灰尘、油污、污垢以及细菌&#xff0c;脏脏的不仅令眼镜不美观&#xff0c;同时在长期的佩戴…

【嵌入式】SD NAND:SD卡的集成与优化

嵌入式SD卡&#xff0c;也称为SD NAND或贴片式SD卡&#xff0c;是一种专为空间受限的设备设计的存储解决方案。这种存储卡与传统的SD卡不同&#xff0c;它采用贴片式封装&#xff0c;可以直接焊接到设备的PCB上&#xff0c;从而为电子设备提供内置存储功能。以下是嵌入式SD卡的…

简单介绍vim

文章目录 前言一、Vim的特点二、安装Vim三、设置Vim配置文件的位置&#xff1a;编辑配置文件&#xff1a;添加配置选项&#xff1a;保存并退出编辑器&#xff1a;快速配置验证设置&#xff1a; 总结 前言 Vim是一款强大的文本编辑器&#xff0c;被广泛用于各种编程和文本编辑任…

canvas实现画布拖拽效果 适配Uniapp和Vue (开箱即用)

需求:我司是做AIGC项目最近和地铁项目有关需要实现海报效果图&#xff0c;并且需要使用画布拖拽和修改上传删除等等功能 当时连续加班花了10个工作日搓出来 实现挺简单的但是Canvas数据处理还是挺麻烦的 大概功能如图下 首先我们需要引入Fabric.js 这个库封装好了原生的Canva…

C#唯一进程的处理Winform/WPF

C#唯一进程的处理 1.使用进程&#xff08;Process&#xff09;判断winformWPF执行效果&#xff1a; 2.使用互斥体&#xff08;Metux&#xff09;实现winformWPF实现效果&#xff1a; 在C#客户端&#xff08;Winform/WPF&#xff09;开发过程中&#xff0c;有的情况需要确保程序…

三分钟搞懂AI Agent是什么!

点击下方“JavaEdge”&#xff0c;选择“设为星标” 第一时间关注技术干货&#xff01; 免责声明~ 任何文章不要过度深思&#xff01; 万事万物都经不起审视&#xff0c;因为世上没有同样的成长环境&#xff0c;也没有同样的认知水平&#xff0c;更「没有适用于所有人的解决方案…

mindmapper17软件最新版下载-MindMapper17(思维导图软件)下载附加详细安装步骤

​​MindMapper​​​是一款专业的可视化思维导图软件&#xff0c;通过智能绘图方法&#xff0c;在管理信息和处理工作流程中&#xff0c;帮助提高组织、审查、合作、分享和交流能力。《思维导图三招十八式》作者也一直力荐 MindMapper这款软件。思维导图是表达发射性思维的有效…

《详解》如何在ROS中建立MQTT通信

观前提醒&#xff1a;本期主要内容为ROS中MQTT通信节点的编程&#xff0c;和ROS部分底层通信机制的浅析 一、复习一下&#xff1a;ROS通信机制&MQTT通信异同点 ROS通信机制概述 ROS中的主要通信机制有以下几种&#xff1a; 话题 (Topics) 发布/订阅模型&#xff08;Pu…