Every few months a cheat forum thread rediscovers PatchGuard, declares it defeated, posts a proof of concept that runs for eleven seconds, and then goes quiet after the author's test box blue screens with CRITICAL_STRUCTURE_CORRUPTION. The pattern is old enough to have grandchildren. Kernel Patch Protection shipped with x64 Windows Vista in 2005, and after two decades of academic bypasses, marketed rootkits, and hobbyist attempts, the design still works because it is deliberately narrow. PatchGuard does not try to secure the kernel. It secures a specific, published list of structures and code sections, and it BSODs the box the moment any of them drift from their expected hashes.
That narrow scope is the entire story of modern kernel cheat design. If a driver never touches the protected set, PatchGuard never fires. If it touches anything on the list, the box dies in seconds. KyTech's engineering team has been building around that boundary since 2025, and this post walks the boundary in detail: what KPP watches, how it schedules the checks, what it ignores, and which historical bypasses actually landed before Microsoft closed them.
Why PatchGuard exists at all
By 2004, 32-bit Windows kernel patching had escalated into a full ecosystem. Norton, McAfee, and half of the third-party security industry were inline-hooking nt!KiFastCallEntry, replacing SSDT entries wholesale, and injecting into ntoskrnl.exe's code sections. Rootkits like FU and Hacker Defender used the same techniques with different intent. Crashes attributed to third-party kernel drivers were a top telemetry item at Microsoft, and the fingerprint of a hooked kernel was indistinguishable between antivirus and malware.
Microsoft's answer for the new x64 platform was to draw a line. On x64 Windows, kernel drivers must be signed, the HAL cannot be replaced, and the specific structures used by rootkits and legitimate security software alike would be periodically measured. Any drift produces BugCheck 0x109. The Kernel Patch Protection FAQ at learn.microsoft.com/en-us/windows-hardware/drivers/kernel/patchguard has been the canonical reference since the Vista launch. Read the FAQ once and most cheat forum arguments about PatchGuard evaporate.
The protected set
The precise list has grown across Windows releases, but the core Vista set is still the baseline. As of Windows 11 24H2, PatchGuard verifies at least:
- The System Service Descriptor Table (
KeServiceDescriptorTable) and its shadow (KeServiceDescriptorTableShadow), including the target routine pointers. - The Interrupt Descriptor Table (IDT) on every processor.
- The Global Descriptor Table (GDT) on every processor.
- Model-specific registers involved in syscall dispatch:
MSR_LSTAR,MSR_STAR,MSR_CSTAR, andMSR_SFMASK. - Code sections of
ntoskrnl.exe,hal.dll,ci.dll,ndis.sys, and a growing list of core drivers. - The
IRP_MJ_*dispatch tables of protected driver objects. - The
KdpStub/ kernel debugger data block, to catch attempts to enable local kernel debugging on retail kernels. - The
PsInvertedFunctionTableused for x64 unwind, which historical bypasses tried to poison. - A handful of internal timer and DPC queues that PatchGuard uses to reschedule itself, so that overwriting the schedule looks like tampering.
The list is intentionally not exhaustive in Microsoft's public docs, and Microsoft rotates additions between servicing releases. Community reverse engineering of the Vista through Windows 11 24H2 kernels agrees on the above as the reliable core.
How the checks run
PatchGuard is not a driver you can see in !drivers. It is a set of self-modifying, self-scheduling contexts scattered across the kernel, initialized during KiInitializePatchGuard. On boot, nt!KiFilterFiberContext builds a series of _KPRCB-linked callback contexts, encrypts them, and hides the actual verification code behind indirect jumps through junk trampolines. The contexts get scheduled through several unrelated kernel primitives: DPC timers, work items, APCs into system threads, and PRCB-linked IPIs. Every scheduling path was chosen because a driver that disables it would break something visible before PatchGuard itself.
When a scheduled context wakes, it decrypts its state, samples a subset of the protected regions, compares to expected hashes stored at boot, and if any check fails, calls KeBugCheckEx(0x109, ...). The four bug check parameters follow a defined pattern that is useful for triage. A live dump from a hooked SSDT looks approximately like this:
CRITICAL_STRUCTURE_CORRUPTION (109)
This bugcheck is generated when the kernel detects that critical
kernel code or data have been corrupted.
Arguments:
Arg1: a3a039d89fef1601, Reserved
Arg2: b3b7465ef279da52, Reserved
Arg3: fffff80329e00000, Failure type dependent information
Arg4: 0000000000000001, Type of corrupted region:
0 : A generic data region
1 : Modification of a function or .pdata
2 : A processor IDT
3 : A processor GDT
4 : Type 1 process list corruption
5 : Type 2 process list corruption
6 : Debug routine modification
7 : Critical MSR modification
8 : Object type
9 : A processor IVT
a : Modification of a system service function
Arg1 and Arg2 are opaque salted hashes that only WinDbg with symbols and a matching !analyze -v extension can decode past the first byte. Arg3 is the address of the offending region when applicable. Arg4 is the class. An Arg4 = a in the wild almost always means someone wrote to an SSDT entry. An Arg4 = 7 almost always means someone tried to hook syscall entry through MSR_LSTAR.
Timing is asynchronous and randomized. Public reversing puts the mean interval on Windows 11 24H2 between roughly two and ten minutes, with jitter that prevents a driver from correlating checks to any external event. That is the reason "patch, do work, unpatch before the check" strategies always eventually get caught: the check is unpredictable and the sampling window is small enough that a single unlucky slice ends the process.
What PatchGuard deliberately ignores
The unwritten half of the FAQ is more important than the written half. PatchGuard makes no attempt to verify:
- Contents of driver objects beyond the
IRP_MJ_*table (device extensions, private state, allocation lists). EPROCESS,ETHREAD,PEB,TEB, or any per-process bookkeeping. Nothing in the object manager's private structures is measured.- Handle tables, VAD trees, working set lists, or any of the memory manager's runtime data.
- Kernel pool allocations belonging to third-party drivers.
- The registered callback arrays behind
ObRegisterCallbacks,PsSetCreateProcessNotifyRoutineEx,PsSetLoadImageNotifyRoutine, and friends, so long as registration went through the documented API. - Your own signed driver's code, data, or IAT. PatchGuard measures Microsoft's kernel image, not yours.
- Anything in user mode. PatchGuard has nothing to do with
ntdllinline hooks, DLL injection, or usermode anti-cheat, which is the domain we cover in How Kernel Cheats Bypass Usermode Anti-Cheat.
That list is why modern kernel cheats and modern anti-cheats coexist with PatchGuard. Neither side needs to hook the SSDT to accomplish anything they care about, because Windows now exposes documented callback APIs (ObRegisterCallbacks for handle interception, PsSetCreateProcessNotifyRoutineEx for process-creation intercept, PsSetLoadImageNotifyRoutine for module-load intercept, CmRegisterCallbackEx for registry) that provide the same semantics without touching a protected structure.
Reading the SSDT is fine
A common misconception is that even reading the SSDT trips PatchGuard. It does not. PatchGuard hashes memory it owns; it does not intercept reads by other drivers. The following is a fully legal, PatchGuard-safe walk of the shadow SSDT that returns the number of Win32k syscalls in the table on any kernel where the symbol is available. It is used in KyTech's diagnostic driver to sanity-check that the kernel has not already been tampered with by a preinstalled security product before a game starts:
#include <ntddk.h>
typedef struct _SSDT {
PVOID pServiceTable;
PVOID pCounterTable;
ULONG_PTR NumberOfServices;
PVOID pArgumentTable;
} SSDT, *PSSDT;
extern PSSDT KeServiceDescriptorTableShadow;
NTSTATUS DumpShadowSsdtCount(_Out_ PULONG_PTR pCount)
{
if (KeServiceDescriptorTableShadow == NULL)
return STATUS_NOT_FOUND;
// Shadow entry [1] is the win32k half; [0] mirrors nt!KeServiceDescriptorTable
PSSDT win32k = &KeServiceDescriptorTableShadow[1];
// Read-only probe. No writes, no MDL, no double-map. PatchGuard is not
// watching who reads its hashed regions, only whether the hash still matches.
*pCount = win32k->NumberOfServices;
return STATUS_SUCCESS;
}
Note the symbol is not documented and is not guaranteed across Windows versions; resolving it in practice requires walking MmGetSystemRoutineAddress on a hint symbol or scanning the kernel image. That is separate from the PatchGuard question. The read itself is invisible to KPP.
What breaks PatchGuard immediately
The following actions provoke a 0x109 reliably within the sampling window. This is the historical top-of-the-list for rootkit crashes and the reason no shipping kernel cheat in 2026 does any of them:
- Overwriting an SSDT entry with a trampoline. Even a single write to
KiServiceTable[__NR_NtOpenProcess]will produceArg4 = awithin minutes. - Writing to
MSR_LSTARto redirect the syscall entry. ProducesArg4 = 7. - Modifying any byte of
ntoskrnl.exe's.textsection, including inline hooks onNtCreateFileor friends. ProducesArg4 = 1. - Replacing an entry in an IDT or GDT. Produces
Arg4 = 2or3. - Overwriting an
IRP_MJ_*entry in a monitored driver object such asntfs.sysortcpip.sys. - Modifying
KdpDebugRoutineSelector related debug callouts. ProducesArg4 = 6.
The comparison below summarizes the practical picture:
| Technique | Pre-Vista 32-bit | x64 Vista through Win11 24H2 | Result |
|---|---|---|---|
| SSDT hook | Standard | Immediate BSOD 0x109 (Arg4=a) | Dead |
| IDT hook | Standard | Immediate BSOD 0x109 (Arg4=2) | Dead |
| MSR_LSTAR redirect | N/A (32-bit uses sysenter) | Immediate BSOD 0x109 (Arg4=7) | Dead |
| ntoskrnl inline patch | Standard | Immediate BSOD 0x109 (Arg4=1) | Dead |
| ObRegisterCallbacks | N/A | Fully supported | Alive |
| Ps*NotifyRoutine | Limited | Fully supported | Alive |
| Read EPROCESS fields | Standard | Fully supported | Alive |
| MmCopyVirtualMemory | Standard | Fully supported | Alive |
Historical bypasses that briefly worked
For completeness, four PatchGuard bypasses actually functioned in the wild long enough to be interesting.
- Uninformed.org "Bypassing PatchGuard on Windows x64" (Skywing and Skape, 2005 and 2007 revisions). The original demonstration that scheduled contexts could be found through the DPC queue and neutralized. Microsoft closed most of it in Vista SP1 and everything remaining in Windows 7.
DisableThreadForWow64/KiWaitAlwaysXOR key extraction. Various forum posts through 2010 to 2014 documented recovering the encryption key that PatchGuard uses to protect its contexts, then rewriting the callback to a no-op. Progressively harder each release; effectively unusable after the encryption widened past 128 bits and after HVCI arrived.- Timer-DPC replacement. A cheat forum technique from around 2013 that swapped the DPC used to reschedule the check with a benign one. Broke when Microsoft cross-verified DPC identities in Windows 8.1.
- HVCI-off boot chain attacks. Not a PatchGuard bypass in itself, but on machines with Virtualization Based Security disabled, several signed-driver-abuse chains (CVE-2020-15368 on ASRock's driver being the most famous) allowed enough raw physical memory access to disable PatchGuard offline. Microsoft's response was HVCI-on-by-default on Windows 11, which our team has covered in the context of the Win11 24H2
MmMapIoSpaceandMiShowBadMapperchanges.
Every one of the above required either an unpatched kernel or a physical-memory primitive that HVCI now denies. There is no known-working PatchGuard bypass on a fully patched Windows 11 24H2 machine with VBS and HVCI enabled.
Triaging a 0x109 in WinDbg
When a machine drops a CRITICAL_STRUCTURE_CORRUPTION dump, !analyze -v in WinDbg walks the four bug check arguments and, on a symbolicated kernel, tells you which class of region PatchGuard caught drifting. Microsoft's own reference for the stop code lives at BugCheck 0x109: CRITICAL_STRUCTURE_CORRUPTION, and the argument table on that page matches what WinDbg prints. A minidump from a lab box that suffered a stray write to KiServiceTable[__NR_NtQueryInformationProcess] produces output that looks like this:
0: kd> !analyze -v
CRITICAL_STRUCTURE_CORRUPTION (109)
Arguments:
Arg1: a3a039d89fef1601, Reserved (opaque salted hash)
Arg2: b3b7465ef279da52, Reserved (second opaque hash)
Arg3: fffff8032a1c9d80, Failure type dependent information (offending address)
Arg4: 000000000000000a, Type of corrupted region:
0x0a : Modification of a system service function
BUGCHECK_CODE: 109
BUGCHECK_P4: a
FAILURE_BUCKET_ID: 0x109_a_nt!KiServiceTable_MODIFIED
PROCESS_NAME: System
STACK_TEXT:
fffffa60`032becd8 fffff800`029d6a1c nt!KeBugCheckEx
fffffa60`032becd8 fffff800`029d6c11 nt!KiFilterFiberContext+0x8d
fffffa60`032bed10 fffff800`028f3a05 nt!PgCheckSsdt+0x231
fffffa60`032bef40 fffff800`028f2711 nt!KiTimerListExpire+0x2c1
0: kd> dps nt!KeServiceDescriptorTable L1
fffff800`02b1c500 fffff800`029d0000 nt!KiServiceTable
Arg1 and Arg2 stay opaque without private Microsoft symbols; they are keyed by boot-random data. Arg3 is the useful field: the exact address inside the protected region that hashed differently than it did at boot. Arg4 is the class, and 0x0a maps to modification of a system service function, per the values enumerated in the earlier table. When you see PgCheckSsdt or KiFilterFiberContext sitting next to KeBugCheckEx, you are looking at a PatchGuard kill, not a random kernel fault, and the next diagnostic question is which driver on that machine touched the SSDT.
Why this shapes cheat driver design
If a kernel driver wants to survive on modern Windows, it has a small number of legal moves:
- Read game memory using
MmCopyVirtualMemoryafter resolving the game process withPsLookupProcessByProcessId. - Register handle-access callbacks with
ObRegisterCallbacksto stripPROCESS_VM_READfrom anti-cheat opens of the cheat's own process. - Register a process-creation callback with
PsSetCreateProcessNotifyRoutineExto know when the game launches. - Never write to a Microsoft kernel structure. Never modify a Microsoft driver's dispatch table. Never inline-hook a syscall.
Everything else is either detectable by anti-cheat's own scanners or lethal to PatchGuard. The interesting engineering happens inside those constraints: how to time reads, how to make the driver's own code look boring to signature scanners, how to allocate pool that does not stand out, how to survive an anti-cheat callback registration that arrived first.
How KyTech handles this
KyTech was founded in 2025 by two engineers with backgrounds in Windows internals and game security. Everything we ship for Apex Legends, Counter-Strike 2, Overwatch 2, Black Ops 7, Forza Horizon 6, and Roblox uses a kernel driver that reads and never writes. No SSDT touch, no MSR redirect, no inline patch on ntoskrnl.exe, no modification of any driver object we do not own. That is a hard product rule, not a marketing line, because a single 0x109 on a paying customer's machine ends the relationship immediately.
Our reads go through MmCopyVirtualMemory after a PsLookupProcessByProcessId on the target game PID. The driver's own code section is written to be indistinguishable from a generic filter driver on static scan, and its pool allocations use tags that mimic common Windows components. When we need process-launch notification, we use PsSetCreateProcessNotifyRoutineEx, which is a documented API that PatchGuard has no opinion about. Our HWID spoofer, currently in Apex-only beta, operates in the same read-first mindset and is documented alongside the rest of the current product line on the purchase page. If you want the flip side of the anti-cheat conversation, the companion post on how kernel cheats bypass usermode anti-cheat covers what the game-side scanner is doing while our driver is reading. This read-only architecture is what ships in KyTech Apex, our flagship kernel driver for Apex Legends, and the same rules govern every other title in the catalog. Kernel Patch Protection is not our adversary. It is the reason our driver is designed the way it is.
Enough theory. get in.
The loader is one click away. Ring-0 kernel driver, polymorphic per download, memory-only injection. Six games across VAC, EAC, Ricochet, Byfron, and Warden.
Get in ›