Part 15: Kernel Exploitation -> UAF

Hola, and welcome back to part 15 of the Windows exploit development tutorial series. Today we have another post on pwning @HackSysTeam's extreme vulnerable driver. In this post we will be exploiting the Use-After-Free vulnerability, in what will be the first of the "complex" vuln classes! I recommend readers get a leg up and review the resources listed below as they provide a comprehensive explanation of kernel pool memory and reserve objects. For more details on setting up the debugging environment see part 10.

Recon the challenge

The recon portion of this post is slightly different as there are a number of driver functions involved in the UAF vulnerability. We will look at each of them in turn as provide such detail as is appropriate.

The function allocates a non-paged pool chunk, fills it with A's, prepends a callback pointer and adds a null terminator. Pretty much the same story in IDA, the screenshot below can be used for reference. Notice the object size is 0x58 bytes and the pool tag is "Hack" (little endian).

Fairly straight forward, this frees the pool chunk by referencing the tag value. This is the function that contains the vulnerability in that "g_UseAfterFreeObject" is not set to null after the object is freed and so retains a stale object pointer. Again let's quickly try that with the following POC.

This function reads in the "g_UseAfterFreeObject" value and executes the object callback. If we call this function with the following POC we essentially end up calling volatile memory because the system is free to re-purpose the, previously freed, pool chunk for whatever reason it sees fit.

Finally, and slightly contrived, we have a driver function which allows us to allocate a fake object on the non-paged pool. Highly convenient as this function allows us to allocate objects identical to the original UAF object.

Pwn all the things!

Game Plan

Ok, the basic principle is straight forward. We (1) allocate the UAF object, (2) we free the UAF object, (3) we replace the pool chunk with our fake object, (4) we call the stale UAF pointer and end up executing code with the callback function from our fake object. Nice and simple!

The only issues we may face here are memory alignment and pool chunk coalescing, again I recommend you read Tarjei's paper. Essentially, if we free the UAF object and it is adjacent to other free pool chunks then the allocator will coalesce these chunks for performance reasons. If this happens it will be highly unlikely that we can replace the UAF object with our own fake object. To avoid this we need to get the non-paged pool in a predictable state and force the driver to allocate the UAF object in a location we can reliably overwrite later!

Derandomize the Non-Paged Pool

Our first objective here is to fill in, as best as possible, all the empty spaces at the "start" of the non-paged kernel pool. To do this we will create a ton of objects with a size close to our UAF object. IoCompletionReserve objects are a perfect candidate for this as they are allocated on the non-paged pool and have a size of 0x60!

First, let's have a look at the IoCompletionReserve object type before we spray the pool (object types can be listed with => "!object \ObjectTypes").

We can use the NtAllocateReserveObject function to create IoCo objects. This function returns a handle to the created object and as long as we don't release the handle, the object will remain allocated in the pool. In the POC below I am spraying these objects in two sittings, (1) x10000 objects to fill in the fragmented pool space and (2) x5000 which should hopefully be consecutive.

For debugging purposes the script dumps the last 10 handles to stdout and then automatically initializes a breakpoint in WinDBG.

If we take another look at the IoCompletionReserve type we can see we did in fact allocate 15000 objects!

Let's inspect one of the handles we dumped to stdout.

As expected, it's an IoCompletionReserve object. Also, considering this is one of the last handles of our spray, we should have consecutive allocations on the non-paged pool.

Woot, we can see the size of our object is 0x60 (96) bytes and some stable consecutive allocations! As a final step we will add a routine to our POC to free every second IoCompletionReserve object from our second allocation (2500 in total) to create holes in the non-paged pool!