System Calls and SSDT: How User Mode Reaches the Kernel
Objective: Understand how Windows user-mode code transitions to ring 0 via the
SYSCALLinstruction, how the System Service Descriptor Table (SSDT) dispatches those calls, and why SSDT hooking, direct syscalls, and modern kernel hardening (PatchGuard, HVCI, MWTI ETW) are central to both offensive tradecraft and defensive telemetry.
1. Why System Calls Exist
User-mode code runs at CPL 3 (ring 3). The kernel runs at CPL 0 (ring 0). Privileged operations — opening another process, mapping physical pages, accessing the file system, talking to drivers — require ring 0. The CPU enforces this with segment descriptors and page-table permissions; a direct CALL into kernel memory from user mode faults immediately.
The bridge is a controlled transition: the user-mode side specifies what it wants by number, the CPU switches to ring 0 at a fixed, kernel-controlled entry point, and the kernel validates and dispatches. That number is the System Service Number (SSN), and the dispatch table is the SSDT.
This design has two consequences that drive everything in this post:
- The kernel entry point is fixed and well-known, so an attacker who can write to ring 0 memory (a kernel rootkit) can redirect every syscall by patching one table.
- The user-mode side of the syscall (the stub in
ntdll.dll) is not privileged, so an EDR can hook it — and a red teamer can bypass that hook by issuing theSYSCALLinstruction themselves.
2. The Mechanics of SYSCALL on x64
SYSCALL is a dedicated x86-64 instruction designed for fast ring-3 → ring-0 transitions. It does not use the legacy interrupt gate (int 2Eh); it reads MSRs and jumps.
| MSR | Address | Role |
|---|---|---|
IA32_LSTAR | 0xC0000082 | Kernel RIP to jump to on SYSCALL from 64-bit user mode. Holds KiSystemCall64 (or KiSystemCall64Shadow with KPTI). |
IA32_STAR | 0xC0000081 | Encodes the kernel and user CS/SS selectors for SYSCALL/SYSRET. |
IA32_FMASK | 0xC0000084 | RFLAGS mask — bits cleared on entry (notably IF, masking interrupts during the prologue). |
The x64 Windows syscall ABI:
EAXholds the SSN (the index intoKiServiceTable).R10holds the first argument. The user-mode stub copiesRCXintoR10becauseSYSCALLitself clobbersRCXwith the returnRIP.RDX,R8,R9, then stack — match the standard x64 calling convention for the remaining arguments.
A minimal user-mode stub, exactly as ntdll lays it out:
; NtFooBar — illustrative ntdll-style syscall stub (x64)
NtFooBar:
mov r10, rcx ; SYSCALL clobbers RCX; preserve arg0 in R10
mov eax, 0x???? ; SSN — VERSION-SPECIFIC, resolve at runtime
syscall ; ring-3 -> ring-0 via LSTAR
ret ; SYSRET returns hereThe 32-bit predecessor was SYSENTER (with entry stored in IA32_SYSENTER_EIP). On modern 64-bit Windows, SYSENTER is only relevant inside the Wow64 path.

3. KiSystemCall64: The Kernel Entry Point
When the CPU executes SYSCALL from user mode:
- It loads
RIPfromIA32_LSTAR(→KiSystemCall64). - It loads
CS/SSfromIA32_STAR(kernel selectors). - It saves the old user
RIPinRCXand oldRFLAGSinR11. - It clears
RFLAGSbits perIA32_FMASK.
KiSystemCall64 then:
- Swaps
GSviaSWAPGSto access the per-CPUKPCR. - Switches from the user stack to the kernel stack stored in the
KPCR. - Builds a
KTRAP_FRAMEcapturing the user context. - Indexes
KeServiceDescriptorTable(or the Shadow variant for Win32k GUI calls) usingEAX. - Calls the resolved
Nt*function. - On return, restores the frame and executes
SYSRETto drop back to ring 3.
Selected KTRAP_FRAME fields (see WDK wdm.h for the full layout):
| Field | Description |
|---|---|
Rip | Saved user-mode instruction pointer (from RCX at entry). |
Rsp | Saved user-mode stack pointer. |
EFlags | Saved RFLAGS (from R11). |
ErrCode | Processor error code; 0 for syscalls. |
With Kernel Page-Table Isolation (KPTI) active, IA32_LSTAR points instead at KiSystemCall64Shadow, a thin trampoline that swaps from the user CR3 (which maps only a minimal kernel trampoline) to the full kernel CR3 before falling through into the normal dispatcher. This is the Meltdown mitigation.
4. The SSDT and KSERVICE_TABLE_DESCRIPTOR
The “SSDT” in casual use refers to two related objects:
| Symbol | Description |
|---|---|
KeServiceDescriptorTable | Exported KSERVICE_TABLE_DESCRIPTOR. Covers the core Nt* services in ntoskrnl.exe. |
KeServiceDescriptorTableShadow | Not exported. Adds a second entry for win32k!W32pServiceTable — the GUI/USER/GDI syscall surface. Rootkits historically located it by pattern scanning around KeAddSystemServiceTable or via debugger symbols. |
KiServiceTable | The actual function-pointer table referenced by the descriptor. |
KiArgumentTable | Parallel array of argument byte counts per service. |
Approximate layout from public symbols:
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
PULONG_PTR ServiceTable; // -> KiServiceTable (encoded offsets on x64)
PULONG CounterTable; // call counters (typically NULL in retail)
ULONG TableSize; // number of services
PUCHAR ArgumentTable; // bytes of stack args per service
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;The SSN (EAX) is split: the low 12 bits index the table, and bit 12 selects which descriptor — 0 for KeServiceDescriptorTable, 1 for the Win32k shadow table. This is how GUI syscalls (NtUserCreateWindowEx, NtGdiBitBlt, …) coexist with kernel-proper syscalls in the same SSN space.

5. The x64 Encoded-Offset Format
A critical detail anyone writing an SSDT scanner gets wrong the first time: on x64 Windows, KiServiceTable entries are not function pointers. Each entry is a 32-bit value encoding a signed offset from the base of KiServiceTable itself, with the low 4 bits used to communicate the argument-count category to the dispatcher.
The decode is:
// Recover the real Nt* function address from KiServiceTable[i]
ULONG_PTR DecodeSsdtEntry(PULONG ServiceTable, ULONG index)
{
LONG encoded = (LONG)ServiceTable[index]; // signed 32-bit
LONG offset = encoded >> 4; // arithmetic shift
return (ULONG_PTR)ServiceTable + offset; // base + offset
}The arithmetic right shift matters — it preserves the sign, allowing functions located before KiServiceTable in memory to be addressed. A naive unsigned >> 4 will silently miss those entries and produce a corrupt scanner.
6. Tracing a Syscall End-to-End: NtOpenProcess
Following an OpenProcess call from a user-mode debugger target:
kernel32!OpenProcess
└─> kernelbase!OpenProcess
└─> ntdll!NtOpenProcess ; the syscall stub
mov r10, rcx
mov eax, <SSN> ; version-specific
syscall
ret
─────────── ring 3 / ring 0 boundary ───────────
CPU: RIP <- LSTAR (KiSystemCall64[Shadow])
nt!KiSystemCall64
├─ SWAPGS, switch to kernel stack
├─ build KTRAP_FRAME
├─ idx = EAX & 0xFFF
├─ desc = (EAX & 0x1000) ? Shadow : KeServiceDescriptorTable
├─ fn = desc->ServiceTable + (desc->ServiceTable[idx] >> 4)
└─ call nt!NtOpenProcess
nt!NtOpenProcess
├─ ObReferenceObjectByName / ByHandle
├─ SeAccessCheck (DesiredAccess vs token)
└─ ObOpenObjectByPointer -> HANDLE
SYSRET back to user-mode RIP saved in RCXThe SSN for NtOpenProcess changes between Windows builds; never hardcode it. Tooling either resolves it from the on-disk ntdll.dll, parses the in-memory stub, or consults a versioned table such as j00ru’s syscall reference.
A practical SSN extractor parses the Nt* export’s first instructions and reads the MOV EAX, imm32 (B8 xx xx xx xx) byte pattern:
# Parse SSNs from a clean on-disk ntdll.dll (illustrative)
import pefile, struct
pe = pefile.PE(r"C:\Windows\System32\ntdll.dll", fast_load=False)
pe.parse_data_directories()
image = pe.get_memory_mapped_image()
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
name = exp.name.decode() if exp.name else ""
if not name.startswith("Nt"):
continue
stub = image[exp.address: exp.address + 24]
# Classic stub: 4C 8B D1 B8 ss ss 00 00 F6 04 25 ... 0F 05 C3
if stub[0:3] == b"\x4c\x8b\xd1" and stub[3] == 0xB8:
ssn = struct.unpack("<I", stub[4:8])[0]
print(f"{name:40s} SSN=0x{ssn:04x}")Red-team loaders use the same idea at runtime — sometimes against a fresh copy of ntdll read from disk to defeat in-memory EDR hooks (the “Perun’s Fart” / fresh-copy pattern).
7. Wow64 and Heaven’s Gate
A 32-bit process on 64-bit Windows still ultimately issues a 64-bit SYSCALL, because the only kernel entry the CPU honors from a 64-bit process is KiSystemCall64. The Wow64 layer bridges this:
32-bit app -> wow64cpu!CpupReturnFromSimulatedCode
-> far jmp 0x33:<addr> ; CS=0x23 (32-bit) -> CS=0x33 (64-bit)
-> wow64.dll / 64-bit ntdll
-> SYSCALLThe 0x33 / 0x23 CS selector switch is the so-called Heaven’s Gate (community label, not an official Microsoft term). Malware abuses it to:
- Execute 64-bit shellcode from a process that defenders are monitoring as a 32-bit target.
- Issue syscalls that bypass 32-bit ntdll hooks if the EDR only instruments the Wow64 layer.
Analysts should treat any unexpected far jmp to CS=0x33 in 32-bit code as a strong IOC.
8. SSDT Hooking: The Classic Rootkit Technique
Pre-Vista x64, kernel rootkits manipulated KiServiceTable directly:
- Locate the descriptor (
KeServiceDescriptorTableis exported; the Shadow descriptor was pattern-scanned). - Disable write protection (clear
CR0.WP) or remap the page as writable. - Save the original entry for the target SSN (e.g.,
NtQueryDirectoryFile,NtEnumerateValueKey). - Overwrite the entry with a pointer to attacker code.
- The hook calls the original after filtering results — hiding files, registry keys, processes, or network connections.
The illustrative read-only inspection (do not modify) inside a signed test driver:
extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
VOID DumpSsdtSizeAndSample(VOID)
{
PKSERVICE_TABLE_DESCRIPTOR d = KeServiceDescriptorTable;
PULONG table = (PULONG)d->ServiceTable;
DbgPrint("[SSDT] TableSize = %lu\n", d->TableSize);
for (ULONG i = 0; i < 4 && i < d->TableSize; i++) {
LONG enc = (LONG)table[i];
ULONG_PTR addr = (ULONG_PTR)table + (enc >> 4);
DbgPrint("[SSDT] [%lu] encoded=0x%08x -> 0x%p\n", i, enc, (PVOID)addr);
}
}
// Reading LSTAR to confirm KiSystemCall64[Shadow]
VOID DumpLstar(VOID)
{
ULONG64 lstar = __readmsr(0xC0000082);
DbgPrint("[MSR] IA32_LSTAR = 0x%llx (KiSystemCall64[Shadow])\n", lstar);
}Live inspection from WinDbg on a kernel-debugged target:
0: kd> dt nt!_KSERVICE_TABLE_DESCRIPTOR nt!KeServiceDescriptorTable
0: kd> dq nt!KeServiceDescriptorTable L4
0: kd> dd nt!KiServiceTable L20
0: kd> u poi(nt!KiServiceTable) L5
0: kd> rdmsr c00000829. PatchGuard (KPP) and Why SSDT Hooking Died
Since x64 Vista, Kernel Patch Protection periodically validates a set of protected structures, including KiServiceTable, IDT, GDT, MSR_LSTAR, kernel image code sections, and several driver objects. On mismatch, KPP issues bugcheck 0x109 — CRITICAL_STRUCTURE_CORRUPTION. The checks run from randomized timers and contexts to resist disablement.
The practical result:
- SSDT hooking is no longer a viable persistence or hiding primitive on supported 64-bit Windows. Any survival window is short and ends in a BSOD.
- Modern kernel-mode attackers use driver callbacks (
PsSetCreateProcessNotifyRoutine,ObRegisterCallbacks, minifilters) rather than SSDT patching, because those are the supported extension points and are not policed by KPP. - With HVCI/Memory Integrity enabled, even loading the malicious driver is gated: kernel pages cannot be both writable and executable, and unsigned kernel code cannot enter ring 0 at all. The hypervisor enforces this at the EPT level — PatchGuard becomes a second line, not the first.
10. Direct and Indirect Syscalls (Modern Red Team TTPs)
Because KPP closed the kernel-side door, evasion moved into user mode. Many EDRs hook the Nt* stubs in ntdll.dll by overwriting the first bytes with a JMP into their inspection DLL. Two techniques bypass that:
- Direct syscalls. The loader embeds its own
mov eax, ssn; syscall; retstub in attacker memory and calls it instead ofntdll!NtXxx. The hooked ntdll is never touched. SSNs are resolved at runtime (parsing ntdll, sortingNt*exports by address — the “Hell’s Gate” / “Halo’s Gate” patterns). - Indirect syscalls. The
mov eax, ssnhappens in attacker memory, but thesyscallinstruction itself is reached by jumping to thesyscallbyte sequence insidentdll.dll. The kernel-side return address therefore points back into ntdll, matching what legitimate code looks like in stack-walk telemetry.
The detection signal flips between the two:
| Technique | What it bypasses | What still sees it |
|---|---|---|
| Direct syscall | ntdll user-mode hooks | Stack walk shows syscall from unbacked / private memory. |
| Indirect syscall | ntdll hooks and naive stack-walk checks | Kernel ETW (Microsoft-Windows-Threat-Intelligence) sees the syscall regardless of where it was issued from. |
ETW-TI is the answer to indirect syscalls: it fires from inside the kernel dispatcher, after the SYSCALL has already landed in KiSystemCall64, so the user-mode evasion is irrelevant.

11. Common Attacker Techniques
| Technique | Description |
|---|---|
| SSDT hook (legacy) | Overwrite KiServiceTable[SSN] to filter results for hiding rootkit artifacts; killed by PatchGuard on x64. |
| Shadow SSDT hook | Same against W32pServiceTable to intercept GUI/keyboard/clipboard syscalls. |
| Direct syscall stub | Embedded mov eax, ssn; syscall in attacker memory to bypass ntdll hooks. |
| Indirect syscall | Jump to the syscall gadget inside ntdll so call stacks look legitimate. |
| Hell’s Gate / Halo’s Gate | Runtime SSN resolution by parsing/sorting Nt* exports in mapped ntdll. |
| Fresh-copy ntdll | Read clean ntdll.dll from disk to re-derive unhooked stubs and SSNs. |
| Heaven’s Gate | Far jump from 32-bit (CS=0x23) to 64-bit (CS=0x33) to execute 64-bit syscalls from a Wow64 process. |
| Driver-based hooking | Where HVCI is off, signed-but-vulnerable drivers (“BYOVD”) are used to write to MSRs or protected pages. |
12. Defensive Strategies & Detection
The detection model has shifted from “watch the SSDT” (PatchGuard already does that) to watch how syscalls are issued from user mode and consume kernel ETW.
Sysmon
| Event ID | Field | Why it matters |
|---|---|---|
1 | ParentImage, CommandLine | Baseline; correlates injection target lineage. |
10 | GrantedAccess, CallTrace | The CallTrace field is the primary direct-syscall tell — legitimate stacks contain ntdll.dll; direct syscalls show UNKNOWN(...) or RWX private memory regions. |
25 | — | Process image tampering / hollowing. |
Sigma — direct-syscall NtOpenProcess against LSASS
title: Process Access to LSASS via Direct Syscall (Unbacked Call Stack)
id: 8d0c2a4e-syscall-lsass-unbacked
status: experimental
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 10
TargetImage|endswith: '\lsass.exe'
GrantedAccess:
- '0x1010'
- '0x1410'
- '0x1fffff'
unbacked:
CallTrace|contains:
- 'UNKNOWN'
- 'UNKNOWN('
filter_legit:
SourceImage|endswith:
- '\MsMpEng.exe'
- '\MsSense.exe'
condition: selection and unbacked and not filter_legit
level: high
tags:
- attack.credential_access
- attack.t1003.001
- attack.t1106ETW Providers Worth Subscribing To
| Provider | Use |
|---|---|
Microsoft-Windows-Threat-Intelligence | Kernel ETW provider exposing AllocVm, ProtectVm, MapViewOfSection, ReadVm/WriteVm events. Fires from inside the kernel dispatcher, so direct and indirect syscalls are still visible. Consumer must run as PPL. |
Microsoft-Windows-Kernel-Process | Process and thread creation, image loads. |
Microsoft-Windows-Kernel-Audit-API-Calls | Audits selected Nt API calls (verify against current SDK). |
Audit Policy
- Audit Sensitive Privilege Use — catches
SeDebugPrivilegeenabling, a near-universal precursor to syscall-based cross-process injection. - Audit Process Creation with command-line capture.
- Audit Handle Manipulation with object SACLs on
lsass.exe.
Hardening
- HVCI / Memory Integrity — single highest-value control. Blocks unsigned and W^X-violating kernel code; defeats BYOVD primitives that try to disable PatchGuard, patch the SSDT, or clear
CR0.WP. - VBS + Credential Guard — keeps LSASS secrets off the path even if a syscall reaches
NtOpenProcess. - KPTI — Meltdown mitigation; also implies
KiSystemCall64Shadowis the LSTAR target. - Driver Signature Enforcement + Microsoft vulnerable-driver blocklist — limits BYOVD options.
- EDR ntdll instrumentation — still valuable as a low-cost filter against commodity malware; layer with kernel ETW for the sophisticated cases.
13. Tools for Syscall and SSDT Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg | Kernel debugger; resolves nt!KeServiceDescriptorTable, nt!KiServiceTable, reads MSRs via rdmsr. | learn.microsoft.com |
| Process Hacker | Live handle, thread, and module inspection; surfaces RWX private memory regions. | processhacker.sourceforge.io |
| Process Monitor | Boot-time and runtime Nt* activity captured via minifilter. | learn.microsoft.com |
| SysmonView / Sysmon | EID 10 CallTrace, EID 25 telemetry. | learn.microsoft.com |
| HollowsHunter / pe-sieve | Detects unbacked / hollowed / patched modules — strong correlator for direct-syscall loaders. | github.com/hasherezade |
| SwishDbgExt | WinDbg extension with SSDT dumping and decode of the encoded-offset format. | github.com |
| Volatility 3 | Memory forensics; windows.ssdt plugin walks the descriptor and decodes entries. | volatilityfoundation.org |
| j00ru syscall tables | Authoritative per-version SSN reference. | j00ru.vexillium.org |
| SilkETW / SealighterTI | User-friendly consumers for ETW providers including Microsoft-Windows-Threat-Intelligence. | github.com |
14. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Native API | T1106 | EID 10 CallTrace containing UNKNOWN; ETW-TI AllocVm/ProtectVm from unbacked memory. |
| Process Injection | T1055 | Cross-process NtAllocateVirtualMemory + NtWriteVirtualMemory + NtCreateThreadEx chain via ETW-TI. |
| DLL Injection | T1055.001 | EID 7/8 plus ETW-TI write/protect events into a remote PID. |
| PE Injection | T1055.002 | RWX private allocations followed by remote thread creation. |
| Process Hollowing | T1055.012 | NtUnmapViewOfSection followed by NtWriteVirtualMemory into the primary image base. |
| Rootkit | T1014 | PatchGuard 0x109 bugchecks; SSDT integrity scans in memory forensics. |
| Impair Defenses: Disable/Modify Tools | T1562.001 | Driver loads with revoked or vulnerable signatures; HVCI/DSE violations. |
Summary
- Every Windows syscall is a
SYSCALLinstruction that lands atKiSystemCall64viaMSR_LSTARand is dispatched throughKiServiceTableusing theEAXSSN. - The SSDT on x64 stores encoded offsets, not raw pointers —
base + (entry >> 4)— and theEAXbit 12 selects between the core and Win32k Shadow tables. - PatchGuard killed SSDT hooking on x64; modern offense has moved to direct and indirect syscalls in user mode and to BYOVD when ring 0 is required.
- HVCI/VBS is the strongest defense against the kernel half; kernel ETW (
Microsoft-Windows-Threat-Intelligence) is the strongest defense against direct/indirect syscalls because it fires after the transition. - Detect with Sysmon EID 10
CallTrace(unbacked memory in the stack), enrich with ETW-TI, and map to MITRE T1106 / T1055 for response.
Related Tutorials
- User Mode vs Kernel Mode: Privilege Rings and the Boundary
- Fibers: User-Mode Cooperative Threads
- Access Tokens and Privileges: The Kernel’s Security Context
- APCs: Asynchronous Procedure Calls and Thread Hijacking Surface
- DPCs: Deferred Procedure Calls and Interrupt Deferral
References
- Using Nt and Zw Versions of the Native System Services Routines — Microsoft Learn (Windows Drivers)
- Libraries and Headers (Ntdll.dll & System Calls) — Microsoft Learn (Windows Drivers)
- Native API (T1106) — MITRE ATT&CK Enterprise
- Input Capture: Credential API Hooking (T1056.004) — MITRE ATT&CK Enterprise
- Glimpse into SSDT in Windows x64 Kernel — Red Team Notes (ired.team)
- Exploring Malicious Windows Drivers (Part 1): Introduction to the Kernel and Drivers — Cisco Talos Intelligence
HAL and Ntoskrnl: The Kernel Core Components
Objective: Understand the architecture and division of labor between
hal.dll(the Hardware Abstraction Layer) andntoskrnl.exe(the NT kernel and Executive), how they are loaded during boot, the structures and routines each exposes, and how defenders inspect, detect tampering against, and harden these Ring 0 core components.
1. HAL and Ntoskrnl Overview
Two binaries sit at the bottom of Windows kernel mode and everything else builds on them. ntoskrnl.exe is the NT kernel plus the Executive — the policy and service layer of the OS. hal.dll is the Hardware Abstraction Layer — a thin platform shim that hides interrupt controllers, bus topology, timers, and DMA behind a uniform interface so the rest of the kernel stays hardware-independent.
| Binary | Full name | Loaded by | Ring |
|---|---|---|---|
ntoskrnl.exe | NT OS Kernel + Executive | winload.efi | Ring 0 |
hal.dll | Hardware Abstraction Layer | winload.efi | Ring 0 |
Both reside in %SystemRoot%\System32\. On multiprocessor systems the SMP-aware image ntkrnlmp.exe is selected by the loader and presented as ntoskrnl.exe; modern Windows 10/11 ships only the SMP variant. Verify image identity and signature on a live host with sigcheck, dumpbin /headers, or the WinDbg lm command. The separation exists for portability (HAL absorbs platform differences) and layering (the kernel implements scheduling and policy, not chipset quirks).
2. Boot Handoff: From Bootloader to KiSystemStartup
winload.efi loads ntoskrnl.exe and hal.dll into memory, then transfers control to the kernel entry point KiSystemStartup, passing a pointer to a LOADER_PARAMETER_BLOCK. That structure carries the memory descriptor list, the ARC hardware tree, NLS data, and other boot-time state the kernel needs before it can manage its own memory.
winload.efi
└─ loads ntoskrnl.exe + hal.dll
└─ ntoskrnl!KiSystemStartup(PLOADER_PARAMETER_BLOCK)
├─ HalInitializeProcessor() ; HAL brings up per-CPU hardware
├─ KiInitializeKernel() ; KPCR/KPRCB, IDT, GDT
├─ Executive phase init:
│ Mm/Ob/Se/Io/Cm/Ps InitSystem()
└─ PsInitialSystemProcess() ; System process (PID 4)
└─ Phase 1: smss.exe launchedHAL initializes the processor before the Executive runs a single line of policy code. Secure Boot validates the winload.efi → ntoskrnl.exe / hal.dll chain in firmware, so tampering with either binary on disk breaks the boot chain on a properly configured machine.

3. The HAL: Abstracting the Hardware
The HAL translates abstract requests into platform-specific operations: programming the APIC, translating bus-relative addresses, allocating DMA-coherent buffers, and calibrating the stall timer. Drivers and the kernel call HAL routines instead of touching hardware registers directly.
| Routine | Purpose |
|---|---|
HalGetInterruptVector | Translate a bus IRQ to a system interrupt vector and required IRQL |
HalTranslateBusAddress | Convert a bus-relative address to a logical address |
HalAllocateCommonBuffer | Allocate DMA-coherent memory visible to CPU and device |
KeStallExecutionProcessor | Calibrated busy-wait (HAL-implemented on most platforms) |
HalRequestSoftwareInterrupt | Request a software interrupt at a given IRQL to trigger DPC delivery |
On modern ACPI systems the HAL is far thinner than in the NT 4 era. Many classic Hal* exports such as HalGetInterruptVector are deprecated; the PnP/ACPI stack and IoConnectInterruptEx now handle interrupt wiring. Since Windows 8, HAL Extensions (halextpcat.dll, halextintc.dll, and similar PE images loaded by HAL itself) carry SoC- and OEM-specific code without replacing the whole HAL.
4. IRQL: The Kernel’s Preemption Ladder
Interrupt Request Level (IRQL) is the central arbitration mechanism shared by HAL and the kernel. The HAL programs the interrupt controller to enforce IRQL in hardware; running at an IRQL masks all interrupts at or below that level on the current CPU.
| IRQL (x64) | Symbolic name | Used for |
|---|---|---|
| 0 | PASSIVE_LEVEL | Normal thread execution |
| 1 | APC_LEVEL | APC delivery; paging allowed |
| 2 | DISPATCH_LEVEL | Scheduler, spin locks; no paging, no blocking |
| 3–12 | Device IRQLs | Hardware ISRs |
| 13 | CLOCK_LEVEL | Clock interrupt |
| 14 | PROFILE_LEVEL | Profiling interrupt |
| 15 | HIGH_LEVEL | NMI, machine check |
The cardinal rule: at DISPATCH_LEVEL or above you may not touch pageable memory or block, because the scheduler and page fault handler cannot run. A driver that dereferences paged-out memory at elevated IRQL produces the classic IRQL_NOT_LESS_OR_EQUAL bug check. Query the current level with KeGetCurrentIrql(). IRQL numeric values are architecture-specific; the table above is the canonical x64 mapping.

5. The Kernel Layer (Ke): Scheduling and Synchronization
The Ke layer sits directly above HAL and implements thread scheduling, interrupt and exception dispatch, and the low-level synchronization primitives the rest of the system depends on.
| Routine | What it does |
|---|---|
KeInitializeSpinLock | Initialize a spin-lock object |
KeAcquireSpinLock | Raise IRQL to DISPATCH_LEVEL and acquire the lock |
KeReleaseSpinLock | Release the lock and restore the saved IRQL |
KeInsertQueueDpc | Queue a Deferred Procedure Call |
KeWaitForSingleObject | Wait on a dispatcher object (event, mutex, timer, thread) |
KeSetEvent | Set a kernel event to the signaled state |
Dispatcher objects — events, mutexes, semaphores, timers, threads — share a common DISPATCHER_HEADER carrying Type, SignalState, and WaitListHead. The wait machinery keys off that header. The synchronization pattern below runs at PASSIVE_LEVEL, where blocking is legal:
KEVENT readyEvent;
KeInitializeEvent(&readyEvent, NotificationEvent, FALSE);
// ... another thread eventually calls KeSetEvent(&readyEvent, IO_NO_INCREMENT, FALSE);
NTSTATUS status = KeWaitForSingleObject(
&readyEvent, // dispatcher object
Executive, // wait reason
KernelMode, // processor mode
FALSE, // non-alertable
NULL); // no timeoutPer-CPU scheduler state lives in the KPCR (Kernel Processor Control Region), reachable via gs:[0] on x64, with an embedded KPRCB holding CurrentThread, NextThread, IdleThread, and the DPC queue.
6. The Executive Layer (Ex and Friends)
The Executive comprises the higher-level managers, each identified by a two-letter prefix. They build on Ke primitives and HAL services.
| Manager | Prefix | Responsibilities |
|---|---|---|
| Object Manager | Ob | Object lifecycle, handles, reference counting |
| Process/Thread Manager | Ps | EPROCESS/ETHREAD creation and teardown |
| Memory Manager | Mm | VAD trees, PTEs, page faults, pool |
| I/O Manager | Io | IRP lifecycle, driver loading |
| Security Reference Monitor | Se | Access checks, tokens, privileges |
| Configuration Manager | Cm | Registry hive management |
| Executive Support | Ex | Pool allocation, lookaside lists, callbacks |
Correct pool usage on modern Windows uses ExAllocatePool2 (the successor to ExAllocatePoolWithTag, deprecated starting Windows 10 build 19041) paired with ExFreePoolWithTag:
// Allocate non-paged pool with a 4-byte tag (read in WinDbg as 'XgAT').
PVOID buffer = ExAllocatePool2(POOL_FLAG_NON_PAGED, 0x1000, 'TAgX');
if (buffer != NULL) {
// ... use buffer at IRQL <= DISPATCH_LEVEL ...
ExFreePoolWithTag(buffer, 'TAgX');
}The Object Manager exposes ObReferenceObjectByHandle to convert a handle into a referenced kernel object pointer — the gateway every component crosses when validating access.
7. Key Kernel Structures
A handful of structures are the backbone of process, thread, and CPU state. Defenders and rootkit authors alike walk these every day.
| Structure | Key fields |
|---|---|
EPROCESS | UniqueProcessId, ActiveProcessLinks, Token, VadRoot, Peb, ImageFileName[15], ThreadListHead |
ETHREAD | Cid (CLIENT_ID), ThreadListEntry, Win32StartAddress, embedded KTHREAD |
KTHREAD | Header (DISPATCHER_HEADER), KernelStack, State, WaitIrql, Teb |
KPCR | Per-CPU; IRQL, IDT/GDT pointers, pointer to KPRCB |
KPRCB | CurrentThread, NextThread, IdleThread, DPC queue |
KDPC | DeferredRoutine, DeferredContext, DpcListEntry |
ActiveProcessLinks is a doubly linked LIST_ENTRY chaining every EPROCESS. The Task Manager view of “all processes” is, at bottom, a walk of this list. That makes it a prime DKOM target: unlinking an EPROCESS hides the process from list-based enumeration while it continues to run and be scheduled — covered in Section 10.
8. The SSDT and System Call Dispatch
A user-mode SYSCALL instruction transfers Ring 3 → Ring 0 and lands in ntoskrnl!KiSystemCall64. The dispatcher indexes the System Service Dispatch Table via KeServiceDescriptorTable, which points at KiServiceTable (an array of service routine offsets) and KiArgumentTable (argument byte counts). GUI calls into win32k.sys route through the shadow table KeServiceDescriptorTableShadow.
Patching KiServiceTable so a service index points at attacker code is the classic SSDT hook, historically used by rootkits to intercept NtQuerySystemInformation, NtOpenProcess, and similar. On x64 this is exactly the kind of structure modification PatchGuard validates, so SSDT hooking is loud and largely obsolete on modern systems — but understanding the dispatch path is essential for reading both live disassembly and integrity-check telemetry.

9. Live Analysis with WinDbg and Volatility
Load Microsoft symbols and the entire layout becomes navigable. List the core modules and dump structures directly:
0: kd> lm m nt ; ntoskrnl base, range, symbols
0: kd> lm m hal ; hal.dll base and range
0: kd> dt nt!_EPROCESS ; full EPROCESS field layout
0: kd> !process 0 0 ; enumerate processes via ActiveProcessLinks
0: kd> !pcr 0 ; KPCR for CPU 0
0: kd> !prcb 0 ; KPRCB: CurrentThread / IdleThread
0: kd> dps nt!KeServiceDescriptorTable ; SSDT pointer + service count
0: kd> !idt ; IDT vectors (HAL-programmed interrupt routing)For dead-box memory forensics, Volatility 3 reconstructs the same view from a dump and is the natural cross-check against a possibly compromised live host:
# Enumerate processes and loaded kernel modules from a memory image.
vol -f memory.dmp windows.pslist
vol -f memory.dmp windows.modules
# psscan walks pool tags instead of ActiveProcessLinks; a process that
# appears in psscan but NOT in pslist is a candidate DKOM-unlinked process.
vol -f memory.dmp windows.psscanA delta between windows.pslist (list-based) and windows.psscan (pool-scan-based) is a high-fidelity indicator of ActiveProcessLinks tampering.
10. Common Attacker Techniques
Kernel-core abuse turns on either modifying ntoskrnl structures from a loaded driver or exploiting a vulnerability to reach Ring 0 in the first place.
| Technique | Description |
|---|---|
| SSDT hooking | Patch KiServiceTable entries to intercept syscalls |
| DKOM unlinking | Splice an EPROCESS out of ActiveProcessLinks to hide a process |
| Kernel callback removal | Strip PsSetCreateProcessNotifyRoutine entries to blind EDR |
| BYOVD | Load a vulnerable signed driver to gain a Ring 0 primitive |
| Kernel exploitation | Abuse an ntoskrnl/HAL bug to escalate Ring 3 → Ring 0 |
| In-memory image patch | Patch ntoskrnl.exe code pages at runtime |
A malicious driver is still loaded through the documented path — a Services registry key of Type = 1 followed by a load — which is exactly where detection begins. Bring-Your-Own-Vulnerable-Driver remains popular precisely because it sidesteps the need to find a fresh kernel bug.

11. Defensive Strategies & Detection
Detection centers on driver loads, integrity events, and kernel structure cross-checks.
| Sysmon Event ID | Name | Relevance |
|---|---|---|
6 | Driver Loaded | Kernel driver load with Signed, Hashes, Signature fields |
7 | Image Loaded | Module loads in unusual contexts |
13 | Registry Value Set | New Services driver entries |
Pair Sysmon with Windows event sources: System Event ID 7045 (new kernel-mode service installed), Security Event ID 5038 (image hash invalid — DSE failure), and Event ID 6281 (page hash mismatch). The Microsoft-Windows-Kernel-Memory ETW provider surfaces pool allocations useful for hunting pool-based implants.
title: Suspicious Unsigned Kernel Driver Load
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 6
Signed: 'false'
filter_legit:
ImageLoaded|startswith:
- 'C:\Windows\System32\drivers\'
- 'C:\Windows\System32\DriverStore\'
condition: selection and not filter_legit
level: high| Mechanism | Description |
|---|---|
| PatchGuard (KPP) | Validates SSDT, IDT, GDT, KPCR, and kernel code; bug check 0x109 on tampering |
| Driver Signature Enforcement | ci.dll requires Authenticode-signed drivers |
| HVCI | VTL1 enforces signed Ring 0 code; blunts BYOVD and runtime patching |
| Secure Boot | Validates the winload → ntoskrnl/hal chain in firmware |
Operational hardening: enable HVCI (Core Isolation → Memory Integrity), confirm Secure Boot in msinfo32, audit SeLoadDriverPrivilege use, deploy the Microsoft Vulnerable Driver Blocklist (DriverSiPolicy.p7b), monitor HKLM\SYSTEM\CurrentControlSet\Services\ for new Type = 1 entries, and baseline loaded-module hashes against periodic WinPmem/Volatility snapshots.
12. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Rootkit | T1014 | Volatility pslist/psscan delta; PatchGuard bug check 0x109 |
| Kernel Modules and Extensions | T1547.006 | Sysmon EID 6; Event ID 7045; Services key writes |
| Exploitation for Privilege Escalation | T1068 | Crash telemetry, anomalous Ring 0 transitions |
| Impair Defenses | T1562.001 | Missing kernel callbacks; EDR self-protection alerts |
| Process Injection | T1055 | Kernel KeStackAttachProcess/MmCopyVirtualMemory use |
| Modify System Image | T1601.001 | Code integrity Event ID 5038/6281; PatchGuard |
13. Tools for Kernel Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg | Live and dump kernel debugging, structure walks | microsoft.com |
| Volatility 3 | Memory forensics, pslist/psscan/modules | volatilityfoundation.org |
| WinPmem | Live memory acquisition | github.com |
| Process Hacker | Driver and handle inspection | processhacker.sourceforge.io |
| Sysmon | Driver-load and registry telemetry | sysinternals.com |
| sigcheck | Image signature and hash verification | sysinternals.com |
| Ghidra | Static analysis of drivers and ntoskrnl | ghidra-sre.org |
14. Summary
- HAL and ntoskrnl are the two Ring 0 binaries every other Windows component is built on — HAL abstracts hardware, ntoskrnl implements the kernel and Executive policy layers.
- The kernel layer (
Ke) supplies scheduling and synchronization; the Executive (Ob,Ps,Mm,Io,Se,Cm,Ex) builds managers on top, all arbitrated by IRQL that the HAL enforces in hardware. - Core structures —
EPROCESS,ETHREAD,KPCR, the SSDT — are the backbone of process and CPU state and the prime targets for SSDT hooks, DKOM unlinking, and callback removal. - Detect kernel tampering via Sysmon Event ID
6, Event IDs7045/5038/6281, and Volatility pslist-vs-psscan deltas; prevent it with HVCI, DSE, Secure Boot, and the vulnerable-driver blocklist.
Related Tutorials
- Access Tokens and Privileges: The Kernel’s Security Context
- System Calls and SSDT: How User Mode Reaches the Kernel
- User Mode vs Kernel Mode: Privilege Rings and the Boundary
- SIDs and Security Descriptors: Identity in Windows Security
- Fibers: User-Mode Cooperative Threads
References
- Windows Kernel-Mode HAL Library – Microsoft Learn (Windows Drivers)
- Windows Kernel-Mode Kernel Library – Microsoft Learn (Windows Drivers)
- Overview of Windows Components (Kernel-Mode) – Microsoft Learn
- User Mode and Kernel Mode – Microsoft Learn (Windows Drivers)
- Boot or Logon Autostart Execution: Kernel Modules and Extensions (T1547.006) – MITRE ATT&CK
- Deeper into Windows Architecture (HAL, ntoskrnl, Executive) – Microsoft Learn Archive
User Mode vs Kernel Mode: Privilege Rings and the Boundary
Objective: Understand the architectural separation between user mode (Ring 3) and kernel mode (Ring 0) on Windows — how Intel hardware enforces it, how the Windows OS layers process isolation and the system call dispatch path on top, and why this boundary is the central battleground for rootkits, EDR, and modern kernel hardening.
1. Why Rings Exist — The Hardware Contract
Intel x86/x64 CPUs define four hardware privilege levels — called rings — numbered 0 (most privileged) through 3 (least privileged). The currently executing privilege level is encoded in the low two bits of the CS segment register and is referred to as the Current Privilege Level (CPL). Every memory access, every instruction fetch, and every attempt at a privileged instruction is checked against this value by the CPU itself, before any OS code runs.
Windows collapses Intel’s four rings into two:
| Feature | User Mode | Kernel Mode |
|---|---|---|
| Ring / CPL | Ring 3 (CPL = 3) | Ring 0 (CPL = 0) |
| Memory access | User VA only | Full kernel + user VA |
| Privileged instructions | Faults with #GP | Allowed |
| Address space isolation | Per-process private | Single shared VA across all drivers |
| Crash blast radius | Process termination | Bug check (BSOD) |
| Entry mechanism | Native execution | SYSCALL / interrupt / exception |
Rings 1 and 2 exist in hardware but are unused by Windows — using only Ring 0 and Ring 3 maps cleanly to the “supervisor vs. user” model and is portable to architectures (ARM64, older RISC) that don’t expose intermediate levels. The instant Ring 3 code attempts to execute LGDT, LIDT, RDMSR, WRMSR, HLT, CLI, STI, or any I/O instruction outside its IOPB, the CPU raises a General Protection Fault (#GP) and the kernel terminates the offending thread.
This single hardware guarantee — CPL is checked by silicon, not software — is what makes the user/kernel boundary trustworthy in the first place.

2. User Mode: The Sandboxed World
When Windows launches an application, it creates a process with its own private virtual address space, its own handle table, and a security token. On x64, the user-mode half of the address space spans 0x0000000000000000 – 0x00007FFFFFFFFFFF (128 TB). Anything above that canonical boundary is kernel territory and is unmapped from user mode page tables (especially under KVA Shadow).
User-mode code can:
- Allocate memory in its own VA via
VirtualAlloc. - Open handles to kernel objects through documented APIs.
- Spawn threads and processes via the Win32 subsystem (
csrss.exe).
User-mode code cannot:
- Read or write another process’s memory without explicit handle access.
- Touch kernel VA, modify page tables, or read MSRs directly.
- Service interrupts, install drivers, or hook the IDT/GDT.
Every meaningful operation that touches hardware, files, networking, or kernel objects must therefore traverse the user/kernel boundary through a system call.
3. Kernel Mode: The Shared Kingdom
In contrast to user mode’s per-process isolation, all kernel-mode code shares a single virtual address space. ntoskrnl.exe, the HAL, file system drivers, network stack drivers, and every third-party driver loaded on the system all coexist in the same address space, on the same privilege level, with no memory protection between them.
| Region | Purpose |
|---|---|
| Non-paged pool | Kernel allocations that must remain resident (DPC/ISR code, kernel objects) |
| Paged pool | Kernel allocations that can be paged out |
| System PTE region | Kernel-managed page table entries for I/O mapping |
| HAL / driver image range | Loaded driver .text/.data sections |
A buggy driver writing to the wrong pointer can corrupt another driver’s structures or the kernel’s own state. A crash in any kernel component triggers a bug check (BSOD) because, unlike user mode, there is no isolation boundary to contain the damage. This is also exactly why attackers want Ring 0: once executing in kernel mode, malicious code has the same authority over the OS as the OS itself.
4. Crossing the Boundary — The SYSCALL Path
Every Win32 API that touches the kernel eventually reaches an ntdll.dll stub. On x64 those stubs all have the same shape:
; ntdll!NtReadFile (representative)
mov r10, rcx ; preserve arg1 (RCX is clobbered by SYSCALL)
mov eax, 0x06 ; syscall number (build-specific; illustrative)
syscall ; user -> kernel transition
retThe SYSCALL instruction is the choreography of the boundary crossing. The CPU performs all of the following atomically:
| Step | CPU action |
|---|---|
| 1 | Saves user RIP into RCX |
| 2 | Saves user RFLAGS into R11 |
| 3 | Masks RFLAGS per IA32_FMASK (MSR 0xC0000084) — clears IF so interrupts are off at kernel entry |
| 4 | Loads new CS/SS selectors from IA32_STAR (MSR 0xC0000081) |
| 5 | Loads RIP from IA32_LSTAR (MSR 0xC0000082) — points to nt!KiSystemCall64 |
| 6 | Transitions CPL from 3 to 0 |
From here, control is in Windows. The kernel-side dispatch chain is:
| Function | Role |
|---|---|
nt!KiSystemCall64 | Entry point loaded from IA32_LSTAR. Executes swapgs to swap user GS for kernel GS, switches to the kernel stack, allocates and populates a _KTRAP_FRAME with the saved user-mode register state. With KVA Shadow (KPTI) enabled, the variant KiSystemCall64Shadow is used to swap page tables first. |
nt!KiSystemServiceUser | Locates the current _KTHREAD via GS:[0x188] and sets KTHREAD.PreviousMode = UserMode (1) so the kernel knows arguments came from Ring 3 and must be probed. |
nt!KiSystemServiceStart | Splits the syscall number in EAX into a table identifier (high bits) and a service index (low bits). |
nt!KiSystemServiceRepeat | Selects KeServiceDescriptorTable (Nt* executive calls) or KeServiceDescriptorTableShadow (Win32k GUI calls), validates the argument count, and dispatches. |
Service routine (e.g. nt!NtReadFile) | Validates user pointers (ProbeForRead / ProbeForWrite) and performs the work. |
SYSRET | Restores RIP from RCX, RFLAGS from R11, transitions CPL from 0 back to 3, and the caller returns from ntdll. |
The key takeaway for defenders: every user-mode action eventually appears in EAX as a syscall number — and EDR products that hook only in user space (in ntdll) can be bypassed by re-implementing this exact stub in attacker code (direct/indirect syscalls).

5. The SSDT — Routing Calls Inside the Kernel
The System Service Descriptor Table (SSDT) is the array of function pointers that turns EAX into a kernel routine address.
| Symbol | Description |
|---|---|
KeServiceDescriptorTable | Exported; primary SSDT for Nt* executive system calls |
KeServiceDescriptorTableShadow | Not exported; adds the Win32k.sys GUI calls used by threads with a Win32 subsystem context |
ServiceTable | Field inside each descriptor — pointer to an array of encoded function offsets (on x64 these are relative offsets right-shifted by 4) |
NumberOfServices | Count of valid entries |
Patching SSDT entries to redirect kernel calls was the classic 32-bit rootkit technique (and the canonical kernel hook for early HIPS products). On x64, PatchGuard (KPP) periodically verifies the SSDT and several other critical structures; modification triggers Bug Check 0x109 — CRITICAL_STRUCTURE_CORRUPTION.
6. Key Kernel Structures at the Boundary
The kernel maintains per-CPU and per-thread state that defenders inspect to understand mode transitions.
// Conceptual layout — verify offsets against your build's symbols.
typedef struct _KPCR {
// ...
struct _KPRCB Prcb; // at +0x180 on x64; embedded
} KPCR, *PKPCR;
typedef struct _KPRCB {
// ...
struct _KTHREAD *CurrentThread; // GS:[0x188] in kernel mode
} KPRCB, *PKPRCB;
typedef struct _KTHREAD {
// ...
UCHAR PreviousMode; // 0 = KernelMode, 1 = UserMode
PKTRAP_FRAME TrapFrame; // saved register state from SYSCALL
} KTHREAD, *PKTHREAD;PreviousMode is one of the most consequential bytes in the system: kernel routines branch on it to decide whether to probe and capture caller-supplied pointers (user mode) or trust them directly (kernel mode). Bugs in that check have been the root cause of multiple Windows LPE CVEs.
Inspect any of these live in WinDbg on a kernel debug target:
0: kd> rdmsr 0xC0000082 ; IA32_LSTAR -> KiSystemCall64
0: kd> dg cs ; show CS selector + CPL
0: kd> dt nt!_KPCR @$pcr
0: kd> dt nt!_KTHREAD @$thread PreviousMode TrapFrame
0: kd> dt nt!_KTRAP_FRAME @$thread->TrapFrame
0: kd> dps KeServiceDescriptorTable L47. Hardening the Boundary
Microsoft has spent two decades hardening the user/kernel boundary in layers. Each mechanism closes a class of attacks against Ring 0.
| Mechanism | What it enforces |
|---|---|
| PatchGuard (KPP) | Periodic integrity checks on SSDT, IDT, GDT, KPCR, MSRs, and kernel code sections. Tampering triggers Bug Check 0x109. |
| Driver Signature Enforcement (DSE) | All kernel drivers must be signed. Enforced by ci.dll. Disabling DSE (bcdedit /set testsigning on) is a strong adversary indicator. |
| Secure Boot | UEFI-rooted trust chain prevents unsigned bootloaders/drivers from loading before Windows starts. |
| HVCI (Memory Integrity) | A VTL1 hypervisor enforces W^X on kernel pages — unsigned code cannot execute even from Ring 0. |
| KVA Shadow (KPTI) | User page tables contain only minimal kernel mappings; full mapping is installed only while CPL = 0. Mitigates Meltdown-class speculative leaks. |
| Microsoft Vulnerable Driver Blocklist | Maintained list of known-abused drivers; enforced by HVCI/CI. |
Together these turn Ring 0 from “anything goes once you’re in” into a far more constrained environment — and explain why modern attackers gravitate toward Bring Your Own Vulnerable Driver (BYOVD) as their cleanest path to kernel code execution.

8. Common Attacker Techniques
The boundary is a target precisely because Ring 0 sits underneath every defensive product. Attackers care about three categories of abuse:
| Technique | Description |
|---|---|
| Direct / indirect syscalls | Rebuild the ntdll stub (mov r10, rcx; mov eax, <N>; syscall) inside the implant to bypass user-mode hooks placed by EDR. |
| BYOVD | Load a legitimately signed but vulnerable driver, then exploit it to gain arbitrary Ring 0 read/write — used to disable EDR, blank tokens, or clear callbacks. |
| Kernel exploitation (LPE) | Exploit a kernel vulnerability (write-what-where, type confusion, double-fetch on user pointers when PreviousMode == UserMode) to escalate Ring 3 → Ring 0. |
| SSDT hooking (legacy) | Patch entries in KeServiceDescriptorTable to intercept syscalls — blocked on x64 by PatchGuard but still relevant for 32-bit forensics. |
| DKOM (Direct Kernel Object Manipulation) | Unlink _EPROCESS entries from ActiveProcessLinks to hide processes; clear PsActiveProcessHead linkages. |
| Callback removal | Walk PsSetCreateProcessNotifyRoutine / PsSetLoadImageNotifyRoutine arrays and null EDR callbacks. |
PreviousMode overwrite | Set KTHREAD.PreviousMode = KernelMode (0) to make subsequent Nt* calls skip user-pointer validation. |
9. Defensive Strategies & Detection
The fact that all roads cross the boundary is a defender’s leverage: even attackers using direct syscalls leave telemetry at driver load, privilege use, and kernel object access layers.
Sysmon coverage
| Event ID | Name | Relevance |
|---|---|---|
1 | Process Create | Parent/child + command line; catches bcdedit, sc.exe create … type= kernel |
6 | Driver Loaded | Fires on every kernel driver load; fields include ImageLoaded, Hashes, Signed, Signature — primary BYOVD signal |
7 | Image Loaded | DLL loads; detect ntdll.dll loaded from non-standard paths |
10 | Process Access | Cross-process handle opens with sensitive GrantedAccess masks (precursor to injection) |
255 | Sysmon Error | Tampering with the Sysmon kernel driver may surface here |
Windows audit policies
| Policy | Event IDs | Detects |
|---|---|---|
Audit Sensitive Privilege Use | 4673 | Use of SeLoadDriverPrivilege — required to load any kernel driver |
Audit Security System Extension | 4697, 7045 | New service / kernel driver installed |
Audit Kernel Object | 4656, 4663 | Access to kernel objects via SACL-tagged handles |
Audit Policy Change | 4719 | Audit-policy tampering (a common pre-attack step) |
High-value ETW providers
Microsoft-Windows-Kernel-Process— process/thread/image events at the kernel boundary.Microsoft-Windows-Kernel-File/Microsoft-Windows-Kernel-Registry— kernel-side file and registry ops, useful for catching driver-stage persistence.Microsoft-Windows-Threat-Intelligence(ETWTI) — emits high-fidelity events forReadProcessMemory,WriteProcessMemory,MapViewOfSection,QueueUserApc. Consumption requires a PPL or kernel consumer; verify provider availability on your build withlogman query providers.
Sigma — BYOVD pattern
title: Suspicious Kernel Driver Load - BYOVD Pattern
logsource:
product: windows
category: driver_load
detection:
selection:
EventID: 6
Signed: 'false'
filter_legit_path:
ImageLoaded|startswith:
- 'C:\Windows\System32\drivers\'
- 'C:\Windows\SysWOW64\drivers\'
condition: selection and not filter_legit_path
fields:
- ImageLoaded
- Hashes
- Signature
- SignatureStatus
level: highSigma — SeLoadDriverPrivilege exercised by non-system principal
title: SeLoadDriverPrivilege Use by Non-System Account
logsource:
product: windows
service: security
detection:
selection:
EventID: 4673
PrivilegeList|contains: 'SeLoadDriverPrivilege'
filter_machine_accounts:
SubjectUserName|endswith: '$'
condition: selection and not filter_machine_accounts
level: mediumHardening checklist
- Enable HVCI / Memory Integrity (
HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard). - Enable Secure Boot in UEFI.
- Apply the Microsoft Vulnerable Driver Blocklist (HVCI-enforced).
- Verify Meltdown mitigations / KVA Shadow via
Get-SpeculationControlSettings. - Alert on
bcdedit /set testsigning onand on driver loads whereSigned=falseor hashes matchloldrivers.io. - Enable Kernel DMA Protection for laptops with Thunderbolt/USB4.
- Limit
SeLoadDriverPrivilegeassignment and monitor every use via Event4673.
10. Tools for Boundary Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg | Kernel debugger; inspect _KPCR, _KTHREAD, _KTRAP_FRAME, MSRs, SSDT | aka.ms/windbg |
| Sysmon | Process/driver/handle telemetry — EIDs 1/6/7/10 | sysinternals.com |
| Process Hacker | View loaded drivers, handles, tokens, KPP-safe inspection | processhacker.sourceforge.io |
| Process Monitor | File/registry/thread activity at the boundary | sysinternals.com |
| Volatility 3 | Memory forensics; walk _EPROCESS, hidden processes via DKOM | volatilityfoundation.org |
| DriverView / DriverQuery | Enumerate loaded kernel drivers and signing state | nirsoft.net |
ETW / logman | Enumerate and capture kernel-mode ETW providers | built-in |
| loldrivers.io | Catalog of known-vulnerable signed drivers | loldrivers.io |
11. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Rootkit | T1014 | Volatility scans for unlinked _EPROCESS; PatchGuard bug checks 0x109 |
| Process Injection | T1055 | Sysmon EID 8/10; ETWTI WriteProcessMemory / QueueUserApc |
| Exploitation for Privilege Escalation | T1068 | Bug check telemetry, unusual PreviousMode transitions, EDR kernel callbacks |
| Create or Modify System Process: Service | T1543.003 | Security EID 4697, System EID 7045, Sysmon EID 6 |
| Impair Defenses | T1562.001 | Driver loads correlated with subsequent loss of EDR telemetry; EID 4673 with SeLoadDriverPrivilege |
| Exploitation for Defense Evasion (BYOVD) | T1211 | Sysmon EID 6 with unsigned driver or known-vulnerable hash; loldrivers.io match |
Summary
- The user/kernel boundary is enforced by silicon —
CPLinCS— not by software, which is what makes it trustworthy. - Windows uses only Ring 0 and Ring 3; user mode runs in a per-process private VA, kernel mode runs in a single shared VA where any bug is a BSOD.
- Every user→kernel transition flows through
SYSCALL→IA32_LSTAR→KiSystemCall64→ SSDT dispatch, leavingEAXandKTHREAD.PreviousModeas the canonical fingerprints. - Modern hardening — PatchGuard, DSE, HVCI, KVA Shadow, and the vulnerable driver blocklist — has pushed attackers toward BYOVD and direct syscalls.
- Defenders watch the boundary through Sysmon EID 6, Security EID 4673 (
SeLoadDriverPrivilege), ETWTI, and kernel-callback EDR telemetry — every Ring 0 attack eventually touches one of them.
Related Tutorials
- System Calls and SSDT: How User Mode Reaches the Kernel
- Fibers: User-Mode Cooperative Threads
- Access Tokens and Privileges: The Kernel’s Security Context
- HAL and Ntoskrnl: The Kernel Core Components
- SIDs and Security Descriptors: Identity in Windows Security
References
- User Mode and Kernel Mode – Windows Drivers | Microsoft Learn
- Kernel-Mode Driver Architecture Design Guide – Windows Drivers | Microsoft Learn
- Exploitation for Privilege Escalation, Technique T1068 – Enterprise | MITRE ATT&CK®
- Privilege Escalation, Tactic TA0004 – Enterprise | MITRE ATT&CK®
- CPU Rings, Privilege, and Protection | Many But Finite (Gustavo Duarte)