KernelForge
A library to develop kernel level Windows payloads for post HVCI era
Install / Use
/learn @Cr4sh/KernelForgeREADME
Kernel Forge library for Windows
General information
Contents
How does it work
Kernel Forge API
Usage example
Interfacing Secure Kernel with Kernel Forge
General information
Today <a href="https://www.microsoft.com/en-us/windowsforbusiness/windows10-secured-core-computers">more and more Windows machines</a> comes with VBS enabled by default which forces rootkits and kernel exploits developers to accept new challenges. Windows Virtualization-based Security (VBS) uses hardware virtualization features and Hyper-V to host a number of security services, providing them with greatly increased protection from vulnerabilities in the operating system, and preventing the use of malicious exploits which attempt to defeat protections. One of such services is Hypervisor-Enforced Code Integrity (HVCI) that uses VBS to significantly strengthen code integrity policy enforcement.
-
Q1: On HVCI enabled target I can't execute my own kernel code anymore, even with most awesome local privileges escalation kernel exploit that gives powerful arbitrary memory read write primitives.
-
A1: You can use data only attack to overwrite process token, gain Local System and load any <a href="https://github.com/hfiref0x/KDU#currently-supported-providers">legitimate 3-rd party WHQL signed driver</a> that provides an access to I/O ports, physical memory and MSR registers.
-
Q2: But what if I want to call an arbitrary kernel functions with arbitrary arguments?
-
A2: That's why I made Kernel Forge library, it provides convenient API for this exact purpose.
Kernel Forge consists from two main components: <a href="https://github.com/Cr4sh/KernelForge/blob/master/kforge_library/kforge_library.cpp">the first library</a> implements main functionality required to call an arbitrary kernel functions and <a href="https://github.com/Cr4sh/KernelForge/blob/master/kforge_driver/kforge_driver.cpp">the second library</a> used to delegate arbitrary memory read write primitives: it can be local privileges escalation exploit or just some wrapper around 3-rd party WHQL signed loldriver. For this project I'm using WinIo.sys variation that provides full physical memory access and works just fine even with enabled HVCI:
Contents
Kernel Forge code base consists from the following files:
-
kforge_driver/− Static library ofWinIo.sysdriver wrapper that provides memory read/write API. -
kforge_library/− Static library that implements main functionality of the Kernel Forge. -
kforge/− DLL version of the Kernel Forge library for its interfacing with different languages <a href="https://en.wikipedia.org/wiki/Foreign_function_interface">using CFFI</a>. -
include/kforge_driver.h−kforge_driver.libprogram interface. -
include/kforge_library.h−kforge_library.libprogram interface. -
kforge_example/− An example program that useskforge_library.libAPI to perform <a href="https://github.com/Cr4sh/s6_pcie_microblaze/blob/c13c744ddbc5b3e8dd89dde03ceaa3c7d0240f8e/python/payloads/DmaBackdoorHv/backdoor_client/vm_exec_kernel/vm_exec_kernel.cpp#L185">a classical</a> kernel mode to user mode DLL injection attack. -
dll_inject_shellcode.cpp/dll_inject_shellcode.h− Shellcode used bykforge_example.exeto handle injected DLL image imports and do other things. -
dummy/− Dummy DLL project to use withkforge_example.exethat shows message box after its injection into some process.
How does it work
The idea behind Kernel Forge is very simple, there's no any innovative exploitation techniques, just common things already known for security researches but in more convenient form of the library to use it with 3-rd party projects.
Many kernel mode payloads can be considered just as sequence of function calls, but as long as we can't have any attacker controlled executable code in kernel space because of HVCI, Kernel Forge uses the following approach to perform such kernel function calls from user mode:
- Create new event object and new dummy thread that calls
WaitForSingleObject()on this event to switch itself into the wait state. At this moment dummy thread call stack has the following look:
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
-
Meanwhile, main thread uses
NtQuerySystemInformation()native API function withSystemHandleInformationinformation class to find dummy thread_KTHREADstructure address. -
Arbitrary memory read primitive is used to obtain
StackBaseandKernelStackfields of_KTHREADstructure that keeps an information about dummy thread kernel stack location. -
Arbitrary memory read primitive is used to traverse dummy thread kernel stack starting from its bottom to locate return address from
nt!NtWaitForSingleObject()back to thent!KiSystemServiceCopyEnd()function of system calls dispatcher. -
Then Kernel Forge <a href="https://github.com/Cr4sh/KernelForge/blob/e4f5f10f474c9316776c6679e3347d8e8fe1bf0a/kforge_library/kforge_library.cpp#L583">constructs some ROP chain</a> to call desired kernel function with specified arguments, save its return value into the user mode memory and pass execution to
nt!ZwTerminateThread()for graceful shutdown of dummy thread after the ROP chain execution. Arbitrary memory write primitive is used to overwrite previously located return address with an address of the first ROP gadget:
- And finally, Kernel Forge main thread sets event object to signaled state which resumes dummy thread and triggers ROP chain execution.
As you can see, it's pretty reliable technique with no any magic involved. Of course, this approach has a plenty of obvious limitations:
-
You can't use Kernel Forge to call
nt!KeStackAttachProcess()function that changes current process address space. -
You can execute your calls at <a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-hardware-priorities">passive IRQL level</a> only.
-
You can't call any functions that registers kernel mode callbacks, like
nt!IoSetCompletionRoutine(),nt! PsSetCreateProcessNotifyRoutine()and others.
In addition, kforge_driver.lib is relying on WinIo.sys driver that provides only physical memory access. To achieve virtual memory access having this we need to find PML4 page map location of the kernel virtual address space. Currently <a href="https://github.com/Cr4sh/KernelForge/blob/e4f5f10f474c9316776c6679e3347d8e8fe1bf0a/kforge_driver/kforge_driver.cpp#L60">I'm using</a> PROCESSOR_START_BLOCK structure scan approach to get PML4 address from one of its fields. However, PROCESSOR_START_BLOCK is not present on machines with legacy boot, but this fact is rather not a real problem because you can't have HVCI support on such machines due to <a href="https://docs.microsoft.com/en-us/windows/security/identity-protection/credential-guard/credential-guard-requirements">its strict requirements</a>.
However, even with mentioned limitations you still can develop pretty much useful kernel mode payloads for HVCI enabled targets. On the picture you can see kforge_example.exe utility that calls appropriate kernel functions with kfroge_library.lib API to perform DLL injection into the user mode process with KernelMode value of the KPROCESSOR_MODE which might be suitable for EDR/HIPS security products bypass:
Kernel Forge API
Kernel Forge library provides the following 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 argume
