I was always there from the start
我一直想学习引导程序工具包 (bootkit) 并编写一个。这篇博客解释了什么是引导程序工具包以及我们编写的工具包是如何工作的。
引导程序工具包
引导程序工具包是一种在系统引导过程中感染系统的恶意软件,通常在引导加载程序之前加载,使其能够修补或钩住其之后的任何内容。在本例中,我们的目标是修补 Windows 内核ntoskrnl.exe
。引导程序工具包本质上是隐蔽的,使其非常难以被检测到,因此也使得防病毒软件更难检测到它们或它们可能对系统做出的任何更改。
UEFI
统一可扩展固件接口 (UEFI) 是为替代 BIOS 而创建的,它充当操作系统和固件之间的接口,为引导加载程序或预引导应用程序提供标准环境。
UEFI 应用程序(如引导加载程序或驱动程序)是存储在 FAT32 分区上的可移植可执行文件 (PE),在启动期间由固件加载。虽然 UEFI 应用程序和驱动程序相似,但它们在关键方面有所不同。UEFI 应用程序只运行一次,退出后从内存中卸载,而 UEFI 驱动程序即使在操作系统初始化后仍保留在内存中。
UEFI 提供两种类型的服务:引导服务和运行时服务。
引导服务
引导服务是仅在操作系统初始化之前可用的功能。一旦调用ExitBootServices
,这些服务就无法访问。它们包括检索系统内存映射、访问图形输出协议 (GOP) 等操作。由于它们仅一次性可用,引导服务主要由引导加载程序等 UEFI 应用程序使用。
运行时服务
运行时服务是即使在操作系统完全初始化后仍然可访问的功能。这些服务包括获取或设置系统时间、关闭或重置系统以及访问固件的环境变量等任务。与引导服务不同,运行时服务在调用ExitBootServices
后仍然可用,这使得它们对需要与操作系统交互的 UEFI 驱动程序特别有用。
我们从哪里开始?
我们的目标是在系统引导过程中获得控制权,所以我们想要钩住引导加载程序本身。在内存中寻找引导加载程序并修补它以调用钩子是很繁琐的。幸运的是有一个更好的方法。我们可以钩住ExitBootServices
。
ExitBootServices
ExitBootServices 是一个由引导加载程序在将控制权转交给操作系统之前调用的函数。此时,内核和所有必要的依赖项都已加载到内存中并准备执行。引导加载程序的唯一剩余任务是将控制权交给内核并传递LOADER_PARAMETER_BLOCK
。
Winload 在将控制权交给内核之前调用 ExitBootServices
钩住ExitBootServices
很简单。我们禁用写保护,覆盖全局引导服务对象中的ExitBootServices
函数指针,然后重新启用写保护。
钩住 ExitBootServices
钩子函数很简单,只捕获调用者的返回地址,该地址指向OslFwpKernelSetupPhase1
内部。之后,它调用原始的ExitBootServices
以继续引导过程。
我们遍历winload.efi
并使用移动签名来获取LOADER_PARAMETER_BLOCK
的指针。然而,我们遇到了一个问题,尝试访问LOADER_PARAMETER_BLOCK
会导致崩溃,因为我们尚未切换到 NT 使用的正确地址空间。
显示加载器块位置的反汇编
EVT_SIGNAL_VIRTUAL_ADDRESS_CHANGE
引导程序工具包的下一步涉及在驱动程序入口点注册一个处理函数,该函数在事件触发时被调用。此事件在新地址空间设置完成后发生,使我们能够访问LOADER_PARAMETER_BLOCK
和内核。要定位LOADER_PARAMETER_BLOCK
,我们将搜索代码移到处理程序中,并使用前面提到的加载签名。一旦我们有了LOADER_PARAMETER_BLOCK
,我们就解析它以找到ntoskrnl.exe
并提取其基地址。虚拟地址更改事件处理程序
修补内核
现在我们有了内核基地址,我们可以扫描内核以找到任何函数并根据需要修改它。在这种情况下,我们的目标是修补在引导过程早期调用的函数。一个这样的函数IoInitSystem
作为理想目标脱颖而出。我们钩住的部分是在核心系统驱动程序(如文件系统、ACPI 等)加载之后,但在任何引导驱动程序加载之前调用的。
原始 IoInitSystem 函数的反汇编
通过执行另一个签名搜索,我们定位到IoInitSystem
的地址,在那里我们修补一个 retpoline 跳转。修补过程如下
-> 首先,我们从函数中复制原始代码。
-> 接下来,我们准备 retpoline 的 shellcode 如下
mov r10, IoInitSystem
push r10 ; Keep the original as ourreturn address
mov r10, IoinitSystemHook
jmp r10
在钩子函数结束时,我们恢复原始代码,由于栈上的最后一个地址是原始函数,当我们返回时执行会正常继续。
修补后的 IoInitSystem 函数的反汇编
由于我们的钩子函数位于 UEFI 驱动程序中,我们必须使用ConvertPointer
来获取 NT 使用的地址空间中的地址。
一旦进入钩子函数,我们就获得了内核级别的控制权。我们可以解析内核的IMAGE_EXPORT_DIRECTORY
来查找函数地址,这使我们能够使用任何 NT 函数。
Bootkit 工作流程图
演示
演示视频展示了 bootkit 如何在启动过程中修补 NT,使用ZwDisplayString
显示 Hello World。
完整代码可以在这里[1]找到。
结论
通过这个项目,我们 (NSG650[2]和Pdawg[3]) 学到了很多关于 UEFI 和 Windows 启动过程的知识。这也提醒我们这类恶意软件是多么可怕和强大,以及为什么需要像安全启动这样的安全措施。
参考资料
这里: https://github.com/Pdawg-bytes/UEFIBootKit
[2]NSG650: https://github.com/NSG650
[3]Pdawg: https://github.com/Pdawg-bytes
原文始发于微信公众号(securitainment):我从一开始就在那里 - 初步理解 Windows UEFI Bootkit 开发
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论