# Windows 11 24H2 Kernel Patch Protection Analysis
While working on a personal project on [GitHub](https://github.com/NeoMaster831/kurasagi), I also uploaded a [pdf](https://github.com/NeoMaster831/kurasagi/tree/product). Upon reflection, I thought there should be at least one article summarizing this. You can check the same content on GitHub.
It will truly be a long journey. I recommend you prepare yourself before reading.
The original post written in Korean is [here](https://blog.wane.im/posts/pg_bypass/). I used ChatGPT to translate whole post, so if you found errors on this post (or there's anything that you cannot understand), please let me know by Discord: `__readfsqword`.
### Abstract
This study conducts an in-depth analysis of Windows Kernel Patch Protection (PatchGuard, PG), and further presents the first case of a complete analysis and bypass of the internal structure and operation of PG as implemented in the latest Windows 11 24H2 version. This research systematically identified the entire process, from PG initialization in the latest build environment, to cryptographic and obfuscation techniques, and detection and bypass logic. Through this, we established a technology to trace and dismantle the core structure of PG in real time, and by implementing a complete bypass technique operating in a bare-metal environment, we provide decisive mitigation insight for security research and defense strategy development.
# Disclaimer
**This research is conducted solely for educational purposes.**
## Environment
The following software and hardware were used:
+ Windows 11 Home 24H2, Build 26100.4351 (Bare-Metal)
+ Windows 11 Pro 23H2 (VM)
+ 12th Gen Intel(R) Core(TM) i7-12700H
+ VMware Workstation Player 17
# Introduction
KPP (Kernel Patch Protection) is a kernel patch protection mechanism added since Windows XP x64 (2005), also known as PG (PatchGuard). In this document, the term PG will be used.
As its name suggests, PG essentially protects against all illegal patches or modifications to the Windows NT kernel. This not only prohibits code patches but also forbids direct modifications to kernel internal structures such as DKOM.
Specifically, PG protects the following system elements, and more details are thoroughly described in [Microsoft's documentation](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/bug-check-0x109---critical-structure-corruption):
+ All functions of the NT kernel
+ Sensitive processor registers such as GDT, IDT, DR7
+ NT kernel objects and structures (`EPROCESS`, SSDT, various global variables)
If the system fails integrity checks, PG immediately triggers a 0x109 BSOD (`CRITICAL_STRUCTURE_CORRUPTION`) to prevent malicious code from residing in memory. This is its fundamental procedure.
PG effectively restrains rootkits by leaving attackers with ongoing restrictions and risks of detection when attempting bypass. For this reason, most malware and game cheat programs are designed with PG in mind—avoiding direct kernel object writes or refraining from hooking NT kernel core functions.
## Things can be caused if PG is disabled
If PG is neutralized, kernel rootkits can persist. Afterwards, kernel object manipulation via Direct Kernel Object Manipulation can arbitrarily alter and corrupt security products and system policies. The most dangerous among these is the complete takeover of Windows ETW. Once protected by PG, ETW is no longer safe and becomes vulnerable to tampering and deletion.
For example, by hooking ETW-related kernel functions, attackers can filter or drop ETW events, selectively censoring logs reaching SIEM. Analysts cannot determine how attacks continued after ETW was disabled.
Due to gaps in ETW logs, it is impossible to quantify “how far the spread has gone.” Scoping shrinks, leaving residual beacons and backdoors, which later appear as *new incidents*. Consequently, digital forensics of the incident becomes difficult.
In addition, kernel exploits become easier to realize (sustaining DKOM), and trust in all kernel modules drops sharply. Many security solutions operate based on the trustworthiness of Windows kernel modules. If PG bypass reduces this trust, it has a devastating impact on the entire security solution stack running above it.
## Motivation
Most attackers believe PG cannot be bypassed, so they avoid head-on collisions with it. However, some high-level hackers develop APT techniques that bypass PG and cause severe threats.
The purpose of this research is not simply analyzing PG or implementing a bypass, but rather providing **defensive insight** by precisely understanding the internal mechanisms exploited by advanced attackers.
I also conducted this project to gain equal technical capability as PG bypassers.
## Evolution of PG
PG has constantly evolved. In the early days of Windows XP and Vista, PG could be bypassed by directly hooking the BSOD-triggering function `KeBugCheckEx`, a relatively simple level of protection. From Windows 7, however, checksum verification before function entry prevented such bypasses. From Windows 8 onwards, PG itself has been continuously verified with checksums, making bypasses more difficult. With Windows 11, virtualization technology was officially introduced and HyperGuard (HG) appeared, implementing even stronger protection techniques.
Various researchers have published analysis materials on PG, but most were written years ago and almost no information exists regarding the latest Windows 11 versions. Therefore, I directly conducted PG analysis targeting Windows 11 24H2, the latest build as of the writing date.
# Analysis
PG has been actively analyzed since long ago. I began my analysis by reviewing past research materials and learning basic information.
To confuse analysts, PG is sometimes symbolized under entirely different names (e.g., `KiFilterFiberContext`) or lacks symbols altogether. In some cases, PG routines are cleverly hidden within normal routines.
## Init Trigger, KiFilterFiberContext
Basically, PG is initialized along with `ntoskrnl.exe`. Several NT modules secretly call PG Init.

The function that initializes PG is known as `KiInitializePatchGuard`, but among the functions that call it, the most central one is `KiFilterFiberContext`. In other words, the direct initialization is carried out by `KiInitializePatchGuard`, but the most important of the “Stub” functions that prepare and trigger it is `KiFilterFiberContext`. Therefore, this function is one of the most critical in PG analysis. Although there are several other functions, since `KiFilterFiberContext` is the most frequently invoked and noteworthy, we will begin by examining the `KiFilterFiberContext` function.
All of the code presented from here on involves very large contexts, so it will be provided in pseudocode form. During the process of converting such large contexts, there may be unintended omissions or leaps. The pseudocode I have written should be used as a tool for understanding the overall flow at a linguistic level.
```
Algorithm: KiFilterFiberContext
Input: FIBER_CONTEXT_PARAM fiberParam
Output: BOOLEAN result
1. AntiDebug
2. If MaxDataSize not initialized:
- If PsIntegrityCheck:
- create callback "542875F90F9B47F497B64BA219CACF69"
- notify callback
3. r1 ← rand(0, 12)
4. r2 ← rand(0, 5)
5. r3 ← rand(0, 9)
6. result ← KiInitializePatchGuard(r1, r2, r3 ≤ 5, fiberParam, 1)
7. If result:
- If r1 ≤ 5:
- r4 ← rand(0, 12)
- r5 ← rand(0, 5) (≠ r2)
- result ← KiInitializePatchGuard(r4, r5, r3 ≤ 5, fiberParam, 0)
- If result:
- result ← KiInitializePatchGuard(0, 7, 1, 0, Unknown)
8. AntiDebug
9. ClearTails
10. Return result
```
Looking closely at the algorithm, the `KiInitializePatchGuard` function is called with random parameters. In other words, the `KiFilterFiberContext` function acts as a launcher that invokes `KiInitializePatchGuard`, and it implies that `KiInitializePatchGuard` can be called multiple times.
In the case of ClearTails, once initialization is complete, the code makes it impossible to find PG-related context in memory at runtime. Since this part is not crucial for the analysis, it has been represented briefly in a single line. The fiberParam will later be used in Method 3 PatchGuard initialization (Section 3.2.10.4). This parameter can be `NULL`, and it is mostly `NULL`, though there are cases where it is not (Section 3.1.2.2).
As for `MaxDataSize`, it is a PG global pointer that will be explained later. This variable is used as a global variable in the global PatchGuard context (Section 3.2.8). The `rand` function generates randomness over the interval $[a, b]$.
### Where is Callback!?
Looking closely at the algorithm, there is a callback-related part early on, and the decompiled C pseudocode for that part actually looks as follows.
```c++
ObjectAttributes.Length = 48;
ObjectAttributes.ObjectName = (PUNICODE_STRING)L"TV";
ObjectAttributes.RootDirectory = 0LL;
ObjectAttributes.Attributes = 64;
*(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0LL;
if ( ExCreateCallback(&CallbackObject, &ObjectAttributes, 0, 0) >= 0 )
{
ExNotifyCallback(CallbackObject, PgTVCallbackDeferredRoutine, &__24);
ObfDereferenceObject(CallbackObject);
if ( __24 )
__2c = 1;
ExInitializeNPagedLookasideList(&stru_140E0EF80, 0LL, 0LL, 0x200u, 0xB38uLL, 0x746E494Bu, 0);
}
```
Many researchers call the callback name 'TV Callback' because `ObjectName` is set to `"TV"`, but this is a misunderstanding caused by a decompiler error. Since the type is `UNICODE_STRING`, the actual callback name is different.
The real name is `"\Callback\542875F90F9B47F497B64BA219CACF69"`. Separately, since the term 'TV Callback' is more commonly used, this document will continue to use the term 'TV Callback'.
Most kernel callbacks follow the sequence `ExCreateCallback`, `ExRegisterCallback`, and `ExNotifyCallback`. A callback created in `ExCreateCallback` is registered through `ObjectName` in `ExRegisterCallback`, and then `ExNotifyCallback` invokes the function for all registered callbacks. The order of `ExCreateCallback` and `ExRegisterCallback` does not matter much.
However, looking at the code, `ExRegisterCallback` is nowhere to be seen. This means it must have already been used somewhere. In fact, where it was called is not important. What matters is the routine invoked through `ExNotifyCallback`, namely `PgTVCallbackDeferredRoutine` (since this is the routine actually used for checks). This function will be examined in detail in Section 3.3.3.
### References
Let us look into the various modules that call the `KiFilterFiberContext` function. As mentioned earlier, multiple modules call it through diverse and subtle methods.
#### KeInitAmd64SpecificState
Due to this function, `KiFilterFiberContext` is always called deterministically. Judging from the name, it might appear to perform something specific to AMD64 CPUs. In reality, this function is naturally embedded in a legitimate function called `PipInitializeCoreDriversAndElam`.
```
Algorithm: KeInitAmd64SpecificState
Input: None
Output: None
1. If (KdDebuggerNotPresent and KdPitchDebugger):
- KiFilterFiberContext(0)
```
But in reality, it is a function that only calls `KiFilterFiberContext`. For non-debugging environments, this function deliberately triggers a ZeroDivisionError through an Exception Handler and then handles it, which at first glance makes it appear to be a routine that does nothing, thereby confusing analysts.

What can be inferred here is that PG does not initialize in a debug environment. This is intended to hinder analysts and also to prevent kernel module developers from causing conflicts with the PG module when developing drivers.
#### ExpLicenseWatchInitWorker
This function is called from the `ExpWatchProductTypeInitialization` function. The `ExpWatchProductTypeInitialization` function contains many routines that verify Microsoft licenses, and this function, pretending to belong to the same category, naturally initializes PG as well.
```
Algorithm: ExpLicenseWatchInitWorker
Input: None
Output: None
1. fiberParam ← KPRCB.HalReserved[6]
2. If (rand(0, 99) < 4 and KdDebuggerNotPresent and KdPitchDebugger):
- KiFilterFiberContext(fiberParam)
```
In this case, the function calls `KiFilterFiberContext` with a 4% probability, accompanied by fiberParam. The fiberParam uses the pre-initialized `PRCB.HalReserved[6]` member of the CPU. Of course, once initialization is complete, any trace of this member disappears, making it impossible to verify the passed fiberParam.
The data type of fiberParam, `FIBER_CONTEXT_PARAM`, is as follows.
```c++
struct FIBER_CONTEXT_PARAM {
UCHAR Code[4]; // prefetchw byte ptr [rcx]; ret
DWORD Unk0;
PVOID PsCreateSystemThreadPtr; // Used in Method 3
PVOID PsCreateSystemThreadDeferredStub; // Used in Method 3
PVOID KiBalanceSetManagerPeriodicDpcPtr; // Used in Method 5
}
```
This structure later becomes an important one in the `KiInitializePatchGuard` function.
#### Extra: KiVerifyXcpt15
This function calls the `KiInitializePatchGuard` routine without going through `KiFilterFiberContext`. While it originally performs a legitimate role, when not in a debugging state it invokes the PG initialization routine through an Exception Handler, similar to the `KeInitAmd64SpecificState` function.
This function is a member of the `KiVerifyXcptRoutines` function array, and it is triggered when the `KiVerifyScopesExecute` function calls all the functions within that array.
```
Algorithm: KiVerifyXcpt15
Input: Unknown
Output: Unknown
...
1. If (KdDebuggerNotPresent and KdPitchDebugger):
- KiInitializePatchGuard(rand(0, 12), 1, 2, 0, 0)
...
2. Return Unknown
```
## Initialization: KiInitializePatchGuard
The `KiInitializePatchGuard` function originally has no symbol and is invoked with the `KeExpandKernelStackAndCallout` function from both the `KiFilterFiberContext` and `KiVerifyXcpt15` functions.
This function is the longest among the functions in `ntoskrnl.exe`. In fact, when decompiled, it produces about 32,000 lines of code. Of course, this length includes the effects of obfuscation and inlined blocks, but fundamentally it is a function that performs a large number of tasks. Practically all hints about the PG module can be obtained from this function. Therefore, the better this function is analyzed, the deeper the understanding of PG, ultimately leading to a successful bypass. I also placed great emphasis on analyzing this function.
However, the developers were also aware of this fact and applied various obfuscation and inlining techniques to this initialization function. As a result, the function grew to nearly 32,000 lines of C pseudocode, making it extremely difficult to analyze.
Nevertheless, when looking at the obfuscated code, it can be observed that virtualization is not applied. Since non-virtualized code is relatively easier to analyze, persistent effort allows one to roughly grasp the initialization routine.
```
Algorithm: KiInitializePatchGuard
Input:
DWORD dpcIndex, DWORD method, DWORD vUnk0,
FIBER_CONTEXT_PARAM fiberParam, BOOLEAN largeCheck
Output: BOOLEAN result
--- Initialization Phase ---
1. If ("TESTSIGNING" and "DISABLE_INTEGRITY_CHECK" are in Boot Options and code patches are allowed):
- return False
2. If (method == 5 and fiberParam == NULL):
- method ← 0
3. crArr ← CriticalRoutines
4. PG_Context ← MemoryAllocate(size = 0x100000 + 0xAE8 + α)
5. PG_Context.AddRandomPadding()
6. PG_Context.Fill(CmpAppendDllSection's bytecode)
7. For f in NT Functions (used in this routine):
- PG_Context.Fill(f)
- // Disturb analyzers; behave like a tiny VM
- // e.g., store ExAllocatePool2 at (PG_Context + 32) and call via (PG_Context + 32)(...)
8. PG_Context.Fill(PTEs of crArr)
// Mitigation for "hooking KeBugCheckEx" — used to recover on BSOD trigger
9. For f in NT Functions:
- PG_Context.AddDetection(f)
10. If (largeCheck):
- For f in NT Functions (More):
- PG_Context.AddDetection(f)
11. PG_Context.AddDetection(IDT)
12. PG_Context.AddDetection(GDT)
13. PG_Context.AddStructureDetections()
14. PG_Context.SetPerProcessorState()
15. PG_Context.SetCodeSectionInContext()
16. PG_Context.SetEntrypointProperly()
// Set code section inside PG context itself
17. PG_Context.FinalizeCodeStuff()
--- Apply Phase ---
18. If (method == 3):
- pgThread ← PG_Context.InitializePgThread()
- PsCreateSystemThread(pgThread)
19. If (method == 4):
- pgApc ← PG_Context.InitializePgApc()
20. If (method == 5):
- PG_Context.InitializeHook(fiberParam)
21. If (method == 7):
- InitializeGlobalContext(MaxDataSize, PG_Context)
22. PG_Context.finalize()
// Final routine verifies itself and stored routines
23. PG_Context.EncryptSelf()
// Encrypt self — ready to launch
24. pgDpc ← PgDpcRoutines[dpcIndex]
25. // Launch PatchGuard contexts; now ready to detect malicious routines
26. switch (method):
case 0:
KeSetCoalescableTimer(pgDpc)
case 1:
KPRCB.AcpiReserved ← pgDpc
case 2:
KPRCB.HalReserved[7] ← pgDpc
case 4:
KeInsertQueueApc(pgApc)
case 3, 5, 7:
Unknown
27. return True
```
I did not analyze every routine line by line; instead, I used various techniques to extract and analyze only the parts that mattered. In other words, I only included in the pseudocode what I found to have an actual impact on the checks during my investigation. Since it would be unreasonable to fully understand this vast routine through such a short algorithm, I will explain it in detail in the subsections.
The `KiInitializePatchGuard` routine can be broadly divided into two parts: the "initialization part" and the "application part." In the current environment, up to around 26,000 lines of C pseudocode belong to the initialization part, and everything after that is the application part.
Naturally, the important section is the application part. If one can fully understand the application part, bypassing is often quite straightforward. Conversely, while understanding the initialization part is helpful, from the perspective of achieving the goal of "runtime bypass," it cannot modify the execution flow, making it harder to exploit. Therefore, I will focus on explaining the application part.

### PG Context
The PG Context is a large structure that stores all kinds of context related to PG, including variables, code, and lists of inspection routines. In the current 24H2 build, its measured size is a massive 2792 (`0xAE8`) bytes.
```c++
struct PG_Context {
UCHAR CmpAppendDllSectionCode[sizeof(CmpAppendDllSection)];
UnkSz0 vUnk0;
struct _FnVm {
PVOID strnicmpPtr;
PVOID KiFreezeDataTableEntryPtr;
// ...
} FnVm;
UnkSz1 vUnk1;
struct _DetectionRoutine {
UINT64 Type;
PVOID TargetRoutine;
DWORD DetectionSize;
DWORD Hash;
UINT64 Desire0, Desire1, Desire2;
} *DetectionRoutines;
UnkSz2 vUnk2;
UnkSz3 MethodSpecific;
UnkSz4 vUnk3;
// ======== After 0xAE8 Size =========
struct _PgCodeSection {
UnkSz5 vUnk4;
UCHAR InitKDbgSection[sizeof(INITKDBG)];
UnkSz6 vUnk5;
} PgCodeSection;
}
```
- `CmpAppendDllSectionCode`: Stores the bytecode of the `CmpAppendDllSection` function. Later, it is used as a context entry point. (Section 3.2.5)
- `FnVm`: Stores pointers to NT functions used in the `KiInitializePatchGuard` function. This appears to be intended to hinder analysts (for obfuscation purposes). After this is declared, almost all functions are invoked indirectly.
- `DetectionRoutines`: A dynamic array containing the functions used for inspection.
- `MethodSpecific`: Parameters individually stored for each `method` parameter (the second argument).
- `PgCodeSection`: PG separately stores code regions in the context to prevent flow instrumentation such as breakpoints, hooking, and routine discovery. This is the reason why an additional size of `0x100000` is allocated.
- `InitKDbgSection`: A copy of the `INITKDBG` section from `ntoskrnl.exe`.
### Random Padding
Structures related to PG, including the PG Context, add a random number of random bytes (a total of 2047, `0x7FF`) to both the front and back of the structure. This makes it even harder to locate PG-related structures in memory.

However, there is also an advantage to this: the 16-byte alignment is broken. While most other NT structures are aligned to 16 bytes, PG structure pointers are not, making it easier to identify whether a structure is PG-related.
### Critical Routines
There are certain routines that PG treats with particular importance, which I have decided to call Critical Routines. The list is as follows.

The first element is currently `NULL`, but at runtime it is initialized to `HaliHaltSystem`.
These Critical Routines are functions that affect safe system shutdown if modified, so their PTEs are also stored separately. When a check later detects tampering and a BSOD must be triggered, they are restored.
### Encryption
PG encrypts itself for storage. This is executed when initialization is nearly complete, and encryption is finalized before PG is launched. By default, it uses an XOR key and employs a dual-encryption structure.
The first encryption is one that is decrypted by `CmpAppendDllSection` and `KiDpcDispatch`. This encryption covers the entire region including the code section, serving to make the PG Context itself undiscoverable in memory.
The second encryption is data encryption, decrypted by various inspection routines (notably `FsRtlMdlReadCompleteDevEx`). This encryption prevents analysts from accessing data even if they somehow manage to reach the code section.
### Entry Point
The entry point of every PG Context directly connects to one of three functions: `CmpAppendDllSection`, `KiDpcDispatch`, or `KiTimerDispatch`. Depending on the first parameter `dpcIndex` of `KiInitializePatchGuard`, `pgDpc` is determined by `PgDpcRoutines[dpcIndex]`. The `PgDpcRoutines` array is as follows.
```c++
PVOID PgDpcRoutines[13] = {
CmpEnableLazyFlushDpcRoutine,
CmpLazyFlushDpcRoutine,
ExpTimeRefreshDpcRoutine,
ExpTimeZoneDpcRoutine,
ExpCenturyDpcRoutine,
ExpTimerDpcRoutine,
IopTimerDispatch,
IopIrpStackProfilerDpcRoutine,
KiBalanceSetManagerDeferredRoutine,
PopThermalZoneDpc,
KiDpcDispatch, // Or KiTimerDispatch
KiDpcDispatch, // Or KiTimerDispatch
KiDpcDispatch // Or KiTimerDispatch
};
```
These are all DPC routines that serve as the initial entry points for PG, and like other legitimate DPCs, they also have a `DeferredContext`.
Looking closely at the first ten routines, it can be confirmed that they are all related to `KiCustomAccessRoutineN`. These DPC routines normally perform legitimate roles, but when a non-canonical address is passed as the `DeferredContext`, they invoke `KiCustomAccessRoutineN`. Each of these `KiCustomAccessRoutineN` functions internally calls its corresponding `KiCustomRecurseRoutineN`.
The `KiCustomRecurseRoutineN` functions then continue to call the next one in sequence. For example, `...Routine0` calls `...Routine1`, `...Routine1` calls `...Routine2`, and `...Routine9` loops back to `...Routine0`.
Through this recursive loop, the routine eventually attempts to dereference the value of `DeferredContext` as a pointer. Since `DeferredContext` is not a valid address, an exception occurs, transferring execution to a hidden handler, which ultimately calls `CmpAppendDllSection`.

The `CmpAppendDllSection` function now decrypts its context and then proceeds to the inspection routines. The `KiDpcDispatch` function, on the other hand, decrypts its context and calls the inspection routines directly, without such a complex process.
A PG Context that has `KiTimerDispatch` as its entry point also makes a direct call like `KiDpcDispatch`, but it does not perform the first decryption step that decrypts its own code.
### Detection Structure
When PG checks the integrity of NT functions, it uses a specific structure. This is the `_DetectionRoutine` structure mentioned earlier.
```c++
struct _DetectionRoutine {
UINT64 Type;
PVOID TargetRoutine;
DWORD DetectionSize;
DWORD Hash;
UINT64 Desire0, Desire1, Desire2;
} *DetectionRoutines;
```
- `Type`: The type of bug check. This matches the fourth parameter of `CRITICAL_STRUCTURE_CORRUPTION KeBugCheckEx`.
- `TargetRoutine`: The address of the target function. This does not necessarily have to be a function; it could also be a specific kernel object (e.g., IDT, SSDT, etc.).
- `DetectionSize`: The size to be checked.
- `Hash`: The hash value that is produced if the inspection result is valid.
- `Desire0, Desire1, Desire2`: Control specific actions depending on `Type`.
- For example: when checking the IDT, an Exception may be deliberately triggered,
- When checking the `EPROCESS` structure, it defines the verification of `Flink` and `Blink`.
- These fields are used in the inspection routine to perform specific operations.
### VM Detection
I conducted many of my tests in a VM, and I observed that PG behaves differently in a virtualized environment. This is a new feature introduced in Windows 11: when a hypervisor is present, PG’s behavior changes significantly. A representative example is the use of the `KeExitRetpoline` function to completely skip certain checks in specific VM environments.
Additionally, functions in the `INITKDBG` section whose names start with `KiErrata` all employ non-standard methods of detecting hypervisor modules.
I have not investigated this in detail, so I cannot determine how many hypervisor detection techniques exist. However, what is clear is that VM environments are not suitable for analyzing PG.
### Global PG Context
When `method` is 7, the PG Context is initialized in the global variable `MaxDataSize`. While the symbol `MaxDataSize` is not directly visible, IDA automatically interprets it as `MaxDataSize` upon decompilation.
The context stored in the global variable always lacks the first stage of decryption (the one that decrypts the code), and therefore always uses the `KiTimerDispatch` function as its entry point. `MaxDataSize` is later used during inspections in the `KiSwInterruptDispatch` function and also in `PgTVCallbackDeferredRoutine`, which is passed to the TV Callback.
### Code Section
PG stores its related functions—such as `FsRtlMdlReadCompleteDevEx` and `CmpAppendDllSection`—in a separate context code section. When these PG functions are executed, they run from the PG Context instead of `ntoskrnl.exe`. This prevents analysts from hooking or setting breakpoints on PG functions.
### Applies
Now let’s examine how the initialized PG Context is actually applied. The behavior of PG in practice depends on the value of `method`.
#### Method 0
When `method` is 0, a timer is initialized using the `pgDpc` determined by `dpcIndex`, and the timer is queued with the `KeSetCoalescableTimer` function.
The key point here is that the `Period` member of the queued `KTIMER` object is 0, while the `DueTime` member contains a value of about 2 minutes.
This may raise the question: “If PG needs to perform continuous checks, why use `DueTime` instead of `Period`?”
Looking at the inspection routines, we can see that after completing all checks, they re-queue the timer again with `KeSetCoalescableTimer`. In other words, to enhance security, PG chooses `DueTime` and schedules a new timer after each inspection cycle.
#### Method 1
When `method` is 1, the determined `pgDpc` is inserted into the `KPRCB.AcpiReserved` member. Although it is unclear exactly how this DPC is invoked, it was confirmed to eventually be executed.
#### Method 2
When `method` is 2, the process is almost identical to `method 1`, but the DPC is inserted into the `KPRCB.HalReserved[7]` member. As with `method 1`, it was confirmed to eventually be executed.
#### Method 3
When `method` is 3, `pgDpc` is not used. Instead, a thread is created using the `PsCreateSystemThreadPtr` function pointer inside `fiberParam`, and the `PsCreateSystemThreadDeferredStub` function is inserted.
In addition, six other functions are inserted, but they are not invoked.
Within this function, the following three functions are used to maintain inspection intervals:
- `KeDelayExecutionThread`
- `KeWaitForSingleObject`
- `KeWaitForMultipleObjects`
At first glance, it seems that a function pointer from `fiberParam` is necessary. However, whether `fiberParam` is truly required is questionable. In fact, I observed `PsCreateSystemThread` being called even when `fiberParam` was absent, and the PG Context still initialized successfully. It appears that another path is used in such cases.
#### Method 4
When `method` is 4, PG injects an APC with about a 2-minute interval into an existing random system thread using the `KeInsertQueueApc` function. Similar to `method 3`, this APC uses functions that delay procedure execution to maintain inspection intervals.
#### Method 5
When `method` is 5 and `fiberParam` is `NULL`, the `method` falls back to 0. Therefore, in practice, PG rarely operates under `method` 5.
This approach hooks the global variable `KiBalanceSetManagerPeriodicDpc` by using the `KiBalanceSetManagerPeriodicDpcPtr` member of `fiberParam`. Internally, a counter is maintained so that the normal routine runs until the counter reaches 0. When the counter is 0, `pgDpc` is queued, and the counter is reset with a new random value.

#### Method 7
Cases of `method` being 6 have not been observed, and the reason for this remains unclear.
When `method` is 7, `KiInitializePatchGuard` itself does not perform any actions. It has been observed that it initializes a DPC with either the `KiInterruptThunk` or `KiMachineCheckControl` function, but it terminates without queuing the DPC.
In fact, even when bypass code was written excluding `KiMachineCheckControl` and `KiInterruptThunk`, the bypass still worked perfectly fine without any issues.
`method 7` is always called at least once during system boot. This is because `KiFilterFiberContext` always uses a fixed parameter at the end. `method 7` initializes the global variable `MaxDataSize`, which serves as the global PG Context pointer. Since `KiSwInterruptDispatch` expects this global variable to always be initialized, this appears to be the reason.
For more details, refer to Section 3.3.4.
## Detections
Now let’s look at how PG detects system corruption.
The inspection method varies greatly depending on the `Type` field of the `_DetectionRoutine` structure (Section 3.2.6). In addition, the `DesireN` members define the specific actions taken for each `Type`.
### BugCheck Parameter
There are many inline blocks like the following.
```c++
*(_QWORD *)(v2 + 2336) = v2 - 0x5C5FC0A76E374B18LL;
*(_QWORD *)(v2 + 2344) = (char *)v38 - 0x4C48B4211BBACBEBLL;
v68 = *v38;
*(_QWORD *)(v2 + 2360) = v67;
v69 = *(_DWORD *)(v2 + 2520);
*(_QWORD *)(v2 + 2352) = v68;
*(_DWORD *)(v2 + 2328) = 1;
if ( (v69 & 0x20000000) == 0 && (*(_DWORD *)(v2 + 2524) & 0x200000) != 0 && (v69 & 1) != 0 )
{
v70 = *(unsigned int *)(v2 + 2676);
v71 = *(_QWORD *)(v2 + 2104);
v72 = *(_QWORD *)(v2 + 2680);
v73 = (_QWORD *)(v70 + v2);
v74 = v70 + v2 + 8 * ((unsigned __int64)(unsigned int)(*(_DWORD *)(v2 + 2052) - v70) >> 3);
while ( v73 != (_QWORD *)v74 )
{
*v73 ^= v72;
v72 = ((v71 ^ *v73++) + __ROR8__(v72, v72 & 0x3F)) ^ 0xEFA;
}
*(_DWORD *)(v2 + 2524) &= ~0x200000u;
if ( v72 != *(_QWORD *)(v2 + 2688) )
{
v75 = *(_DWORD *)(v2 + 2052);
v76 = *(_QWORD *)(v2 + 1416);
*(_QWORD *)v76 = v2;
*(_DWORD *)(v76 + 16) = v75;
if ( !*(_DWORD *)(v2 + 2328) )
*(_QWORD *)(*(_QWORD *)(v2 + 1416) + 24LL) = v72 ^ *(_QWORD *)(v2 + 2688);
sub_140BCC384(a1: v2, a2: 0LL, a3: v72, a4: 256LL);
}
}
```
This is the common routine called when a check fails, defining the bug check parameters passed into `KeBugCheckEx`. In other words, it reveals the identity of the reserved parameters.
- `Parameter1`: The address of the PG Context that performed the inspection
- `Parameter2`: The address of the `_DetectionRoutine` structure that failed the inspection
- `Parameter3`: The address of the corrupted data
- `Parameter4`: The Type of the corrupted data
`Parameter1` and `Parameter2` each undergo subtraction with the constants `0x5C5FC0A76E374B18` and `0x4C48B4211BBACBEB` respectively, serving as a first layer of defense against analysts attempting to access them.
Regarding `Parameter4`, undocumented Types such as `265` exist, most of which are related to corruption of the PG Context itself.
This inline block is very useful when statically analyzing inspection routines, as it makes it easy to determine where an inspection failed.
### Main: FsRtlMdlReadCompleteDevEx
This is a massive function that performs inspections for all `Type` values at once. The function first decrypts the data portion, then performs checks for all `Type` values defined in Microsoft’s BugCheck documentation. Finally, it executes cleanup routines, such as re-queuing expired timers.
If an inspection fails, the PTEs of `CriticalRoutines` are restored, and the `SdbpCheckDll` function is called. This function is a wrapper around `KeBugCheckEx`, but it clears the stack before invoking it. This is a technique meant to hinder analysts attempting to analyze memory dumps.

#### Nb: PgDetectionN
There are also some inspection functions without symbols. I simply named these `PgDetectionN`. These functions behave almost identically to `FsRtlMdlReadCompleteDevEx`, with the difference that their inspection `Type` range is narrower.
It was confirmed that these functions operate only when a VM is running, and I was unable to capture them functioning in a Bare-Metal environment.
The reason I did not analyze them in detail is precisely because they only operate in VM environments. Our goal is to implement a bypass in a Bare-Metal environment, not in a VM.
### Sub: PgTVCallbackDeferredRoutine
`PgTVCallbackDeferredRoutine` is the function passed as a parameter to the TV Callback. It performs the same role as `FsRtlMdlReadCompleteDevEx`, but without re-queuing timers or threads at the end. Therefore, it is necessary to investigate how this callback is reinvoked.
This function uses the `MaxDataSize` PG global context initialized in **Method 7**.
### Sub: KiSwInterruptDispatch
This appears to be a recently added inspection routine. It is a routine called from the `KiSwInterrupt` IDT function. Despite its name, it actually performs PG inspections.
This also uses the `MaxDataSize` initialized in **Method 7**.
### Sub: CcBcbProfiler
This function operates independently of the PG Context, and strictly speaking, it is not directly related to `KiInitializePatchGuard`.
It initializes a timer using the `KeSetCoalescableTimer` function in `CcInitializeBcbProfiler`, and every two minutes it inspects a random NT kernel function. There also exists a twin function `CcBcbProfiler2` (originally without a symbol). These two functions cooperate, being re-queued with `KeSetCoalescableTimer` after each inspection routine is finished.
The difference is that BSOD is triggered not via `SdbpCheckDll`, but through `CcAdjustBcbDepth`. Both functions serve the same purpose.
### Sub: KiMcaDeferredRecoveryService
In addition, several other routines operate independently (e.g., `PspProcessDelete`, `KiInitializeUserApc`). These functions do not perform independent inspection routines but instead carry out lightweight checks just before executing their normal roles.
When triggering a BSOD, these functions commonly call `KiMcaDeferredRecoveryService`.
`KiMcaDeferredRecoveryService` is a function that, like `SdbpCheckDll` and `CcAdjustBcbDepth`, clears the stack before calling `KeBugCheckEx`.
## Approach
As seen above, to analyze PG, one cannot use breakpoints, hooking, or exception catching. Due to the anti-debugging modules embedded in PG routines themselves, debugging-based approaches are difficult. This makes dynamic analysis extremely hard to perform effectively.
In addition, obfuscation and inlining techniques greatly increase the difficulty of statically analyzing PG. Within these constraints, I employed creative and diverse analysis techniques to study PG.
The results presented in Section 3 were derived through the methods explained from here onward.
### Static Analysis
In fact, I spent the majority of the overall analysis time on static analysis. The fortunate point is that PG functions are not virtualized but only obfuscated, meaning that with enough effort, code analysis can be carried out.
During the analysis, I discovered many areas that appeared to have been inlined.

For example, an inline block starting with the `rdtsc` instruction and containing many branches is actually inline assembly used to generate four random alphabetic characters. This ensures that when a pool is allocated, it cannot be identified by a pool tag.
During analysis, I focused on identifying such inline blocks and moving past them quickly. While performing static analysis, I did not rely on special tools or tricks; instead, I proceeded honestly by investing time and effort.
Paradoxically, however, this turned out to be the least productive method.
### Length Trick
As mentioned earlier, PG employs obfuscation, and one characteristic of this obfuscation technique is that it tends to unnecessarily increase the length of code.
Based on this observation, I sorted the functions in `ntoskrnl.exe` by code length in descending order, and indeed, I confirmed that many PG routines were included among them.

### prefetchnta Trick
Important routines related to PG typically include the instruction `prefetchnta byte ptr [rax]`. The exact reason for invoking this opcode is unknown, but it can be assumed that it is intended to improve performance during inspections.

Therefore, when analyzing PG, searching for the signature `0F 18 00` can effectively extract routines for analysis.
### xref Tracing
Since NT-related functions rarely use indirect calls, examining the cross references of a specific function often directly reveals the parent functions that call it.
### Obfuscation makes sense!
Paradoxically, what allows us to answer the question, “Is this a PG function?” is the **obfuscation itself**.
PG functions frequently use rarely employed assembly instructions such as `rdtsc`, `rol`, and `ror` for obfuscation purposes. These serve as clues that make it easier to determine whether a function is PG-related.
### PG Constants
There are two constants used when triggering a BSOD:
- `0x5C5FC0A76E374B18`
- `0x4C48B4211BBACBEB`
Unlike the bug check code `0x109`, these constants appear in plaintext rather than encrypted, which makes them useful for signature searches.
Since the second constant (`0x4C48B4211BBACBEB`) can sometimes be `NULL`, I relied on the first constant (`0x5C5FC0A76E374B18`) for more stable searching.
## Dynamic Analysis
PG is one of the programs that are extremely difficult to analyze dynamically. The Windows-specific debugger [WinDbg](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/) cannot be used. To use WinDbg, Windows must run in debug mode, but PG is completely disabled in debug mode.
One might think, “Can’t we just restore the `Kd`-related flags to their original state?” However, it is difficult to even determine the exact number of debugging-related flags, and numerous anti-debugging routines exist (such as `KdDisableDebugger`, `cli`-`sti`, etc.). Circumventing or restoring all of them would be excessively costly.
### VM
Using a hypervisor (VM) as an external debugger allows completely **out-of-band debugging** from the guest OS perspective, leaving no traces such as interrupts or debug register changes.
The key is to **convert VM-exit traps into debugger events**. For example, in Intel VT-x, operations that manipulate Debug Registers in the guest OS are first delivered to the hypervisor, triggering a VM-exit. Hijacking this and converting it into a debugger event is the specific approach.

[VMware](https://www.vmware.com/) provides support for this feature. You can add the following entries to the VM configuration file (`.vmx`):
```
debugStub.listen.guest64 = "TRUE"
monitor.debugOnStartGuest64 = "TRUE"
debugStub.port.guest64 = "1234"
```
Once configured and executed, a [gdb](https://sourceware.org/gdb/) server will open on the `debugStub.port.guest64` port, and you can connect to the VM using [IDA](https://hex-rays.com/)’s **Remote GDB Debugger** frontend to debug the VM properly.
As analyzed earlier, when using a VM, the control flow changes, meaning PG does not operate normally. However, it is extremely advantageous for analyzing the initialization routine (`KiInitializePatchGuard`).
In particular, it is useful for identifying functions invoked during initialization and tracing indirect calls. It also allows setting R/W traces on specific parts of the PG Context to intuitively observe which values are copied at which points. In fact, most of the behavior related to timers could be inferred using this method.
The downside is that debugging starts not from the `KiSystemStartup` function but from the **EFI loading stage**, making it difficult to find the exact entry point. At this stage, essential information such as the memory address where `ntoskrnl.exe` is allocated cannot be confirmed.
Since our goal is to analyze PG as it initializes together with the NT kernel, this limitation is quite critical.
To overcome this, I focused on the `KUSER_SHARED_DATA` structure. `KUSER_SHARED_DATA` resides at a fixed system address (`FFFF'F780'0000'0000`) and is used during system initialization. Indeed, I confirmed that the `KiInitializeKernel` function references `KUSER_SHARED_DATA+280`.

Now the method is simple. We can set a **Hardware R/W Breakpoint** at `FFFF'F780'0000'0280`, and in practice, this breakpoint is triggered immediately after `ntoskrnl.exe` is allocated in memory.
However, performing all these steps manually each time during debugging is inefficient. To automate this, I wrote an **IDAPython Script**, which can be found in the Research folder of the Git repository: [GitHub - kurasagi/research](http://github.com/NeoMaster831/kurasagi/tree/7d8c832dea17e0b35971ddde0c6afa0bba315dd9/research)
With this process completed, the initialization routine can be analyzed with greater precision. It also becomes possible to **suspend** the OS at any time during execution for analysis.
### Barricade
This method played a very critical role in PG analysis. Rather than being a standardized technique, it is a methodology derived from a close examination of PG’s characteristics.
The PG Context stores itself in an encrypted state, and when the context uses `CmpAppendDllSection` or `KiDpcDispatch` as its entry point, it decrypts itself through these functions. Since it contains executable assembly code, execution (X) permission is naturally required. Furthermore, because the context within the page must be decrypted, read/write (R/W) permissions are also necessary.
Therefore, **PatchGuard entry points that use these two functions always have RWX permissions.**

From this, we can infer the question: “Are PG Context entry points always RWX?” In fact, after checking the page PTEs of all three functions, including `KiTimerDispatch`, it was confirmed that they all had RWX permissions. This was especially surprising because `KiTimerDispatch` does not require RWX permissions, yet it still had them.
There is an important reason why this fact matters. According to Microsoft’s driver signing policy, **a signed driver must not contain even a single byte in an RWX section.**
[Reference: Driver compatibility with Device Guard](https://learn.microsoft.com/en-us/windows-hardware/test/hlk/testref/driver-compatibility-with-device-guard)
Additionally, Inf2Cat is configured to fail testing if a `NonPagedPool` (=`NonPagedPoolExecute`) pool exists.

In other words, on a normal Windows 11 system, no driver code should exist with RWX sections. From this, we can derive the idea that **the only module with RWX sections is PG itself**. (Some parts of `ntoskrnl.exe` also use RWX sections, but they are very few.) This fact also connects to why the implementation later results in fewer overheads and errors.
Therefore, by traversing all pages and removing the X permission from RWX sections, when PG attempts to execute its entry point code, it will lack execution rights, causing a **page fault (`#PF`) exception**.
The function responsible for handling `#PF` is the `KiPageFault` function in the IDT. When the exception is specifically due to missing X permissions, execution is redirected to the `MmAccessFault` function.
Thus, by hooking the `MmAccessFault` function, it becomes possible to handle only exceptions related to PG. During my analysis, I intentionally included stack information to trigger BSODs, which made the analysis process easier.
The main reasons PG is so difficult to analyze are twofold:
1. The context is encrypted, making it hard to locate PG routines when they are not running.
2. It is difficult to determine where and how the routines are invoked.
However, this method is a powerful technique that solves both problems simultaneously.
I thought the idea of **“triggering a `#PF` to block PG routines before they even enter”** resembled a barricade blocking entry. For this reason, I named this method **Barricade**.

This methodology, derived through a simple computer science proof, can also be effectively used later in PG bypassing.
# Bypass
I considered how to achieve a bypass. Of course, the easiest methods include booting the system itself in debug mode or using VT-x with EPT hooking for an effortless bypass. However, since our goal is to **bypass PG in a bare-metal environment during normal system runtime**, these approaches cannot be used.
The fact that we cannot directly alter the flow of the `KiInitializePatchGuard` function is an important constraint. However, as mentioned earlier, if the **application part and the sequence of function calls** are clearly understood, we can design individual bypass scenarios for each function.
## Timers
The timer-related application and inspection routines discovered so far are as follows. All of these modules must be terminated:
- `KiInitializePatchGuard` Method 0
- `CcBcbProfiler` & `CcBcbProfiler2`
- DPC: `KiInitializePatchGuard` Method 1
- DPC: `KiInitializePatchGuard` Method 2
- Hook: `KiInitializePatchGuard` Method 5
Methods 1 and 2 are ultimately related to timers as well, so they are included here.
Timers are generally stored in the form of linked lists in `PRCB.TimerTable`. Therefore, by traversing all processors and scanning their `PRCB.TimerTable`, it is possible to inspect all registered timers.
The challenge is that the `Dpc` member, which defines the DPC routine inside the timer entry, is stored in an encrypted state with `KiWaitAlways` and `KiWaitNever`. This was discovered at the beginning of the `KeSetTimer` function.
```c++
v4 = (unsigned __int64)Timer ^ _byteswap_uint64(KiWaitAlways ^ (unsigned __int64)Dpc);
v42 = Dpc;
v5 = (unsigned int)KiWaitNever;
v6 = DueTime;
v7 = (_KDPC *)(KiWaitNever ^ __ROR8__(v4, KiWaitNever));
```
Based on the findings, the function for decryption can be written as follows.
```c++
PKDPC wsbp::Timer::GetDecryptedDpc(PKTIMER pTimer) {
return (PKDPC)(*gl::RtVar::KiWaitAlwaysPtr ^ _byteswap_uint64(\
(UINT64)pTimer ^ _rotl64(\
(UINT64)pTimer->Dpc ^ *gl::RtVar::KiWaitNeverPtr, \
(UCHAR)*gl::RtVar::KiWaitNeverPtr \
)));
}
```
### KiInitializePatchGuard Method 0
I previously mentioned that the `DeferredContext` value passed as pgDpc in Method 0 is not a canonical address. (Section 3.2.5)
Therefore, by checking whether `DeferredContext` is a non-canonical address and, if detected, setting the `Dpc` itself to `NULL`, the issue can be resolved.
MSDN also states that `Dpc` may be `NULL`.
[Reference: KeSetTimer function](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kesettimer)
There are two reasons for not checking `DeferredRoutine`.
1. The original function performs a legitimate role, so it does not serve as a basis for determination.
2. As mentioned earlier, the `KiDpcDispatch` and `KiTimerDispatch` functions are moved into the PG Context, making them difficult to identify.
### CcBcbProfiler Twins
This case is simpler because it can be determined based on `DeferredRoutine`.
Check whether `DeferredRoutine` is `CcBcbProfiler` or `CcBcbProfiler2`, and if so, similarly set the `Dpc` to `NULL` to resolve it.
### KiInitializePatchGuard Method 1, 2 DPCs
- Method 1: `KPRCB.AcpiReserved`
- Method 2: `KPRCB.HalReserved[7]`
These are originally `NULL`. Therefore, if a non-`NULL` value is observed, simply change it back to `NULL`.
### KiInitializePatchGuard Method 5
Restore the `KiBalanceSetManagerPeriodicDpc` variable to its original state. The original `DeferredRoutine` is `KiBalanceSetManagerDeferredRoutine`.
### Implementation
The complete implementation code can be found in the Git repository.
[GitHub - kurasagi/Module/Timer.cpp](http://github.com/NeoMaster831/kurasagi/blob/master/kurasagi/Module/Timer.cpp)
## MaxDataSize References
The routines that access and use the PG global context `MaxDataSize`, which is initialized in Method 7, are as follows.
- `KiSwInterruptDispatch`
- `PgTVCallbackDeferredRoutine`
In this case, simply setting `MaxDataSize` to `NULL` is generally sufficient. However, there is a problem. Most other routines check whether `MaxDataSize` is `NULL` before dereferencing it, but the `KiSwInterruptDispatch` function does not check and immediately attempts to dereference it, causing a BSOD.
To address this, you can **patch the `KiSwInterruptDispatch` function itself to skip the inspection**. This method can be applied when no other PG routines are active.
### Implementation
The complete implementation code can be found in the Git repository.
[GitHub - kurasagi/Module/Context7.cpp](http://github.com/NeoMaster831/kurasagi/blob/master/kurasagi/Module/Context7.cpp)
## Miscellaneous
Other routines are as follows.
- `KiMcaDeferredRecoveryService`
- System Thread: `KiInitializePatchGuard` Method 3
- APC: `KiInitializePatchGuard` Method 4
### KiMcaDeferredRecoveryService
Patch this function so that `KeBugCheckEx` does not execute.
Routines that call `SdbpCheckDll` and `CcAdjustBcbDepth` are structured to trap on failure via a `JUMPOUT` instruction, but routines that call `KiMcaDeferredRecoveryService` lack such a mechanism. This serves as a **vulnerability**.
### System Thread & APC
Threads and APCs internally call `KeDelayExecutionThread`, `KiWaitForSingleObject`, and `KiWaitForMultipleObjects` to delay procedure execution. (See Section 3.2.10.4)
Therefore, by traversing system threads and examining their call stacks, if the return address includes any of these three functions, the thread can be terminated by invoking `KeDelayExecutionThread` with `DueTime` set to a very large value.
### Implementation
The complete implementation code can be found in the Git repository.
[GitHub - kurasagi/Module/Misc.cpp](http://github.com/NeoMaster831/kurasagi/blob/master/kurasagi/Module/Misc.cpp)
## Barricade
The Barricade method is a **comprehensive bypass technique** that combines the barricade debugging method (Section 4.2.2) with the inspection bypass methods presented thus far.
This method triggers a `#PF` at the PG entry point, and through the hooked `MmAccessFault` function, additional checks are performed to identify PG.
The problem is that the `MmAccessFault` function is called from multiple places. Therefore, it would normally be difficult to determine whether an exception was caused by a PG routine, and the overhead would be significant. However, as we demonstrated, **a simple RWX permission check can greatly increase detection accuracy while reducing overhead**.
### Implementation
The complete implementation code for bypassing PG can be found in the Git repository.
[GitHub - kurasagi/Module/Barricade.cpp](http://github.com/NeoMaster831/kurasagi/blob/master/kurasagi/Module/Barricade.cpp)
Although the explanation is simple, the actual implementation is quite difficult. I strongly recommend checking the code directly.
# Mitigations
Reflecting on the bypass methods discussed so far, let’s examine where defenses could be applied.
## Barricade: PatchGuard is too OLD!
Currently, the most critical method is Barricade. However, there is no clear way to block Barricade. If RWX permissions are not used, PG can easily be defeated with signature scans.
Thus, a **paradigm shift** is needed: instead of inspecting the kernel from within the kernel, inspections must be performed from a higher privilege level. Microsoft is indeed aware of this and has attempted to respond with HyperGuard (HG). However, it is known that HyperGuard is still incomplete and limited in functionality.
I place significant value on HG. As virtualization technology becomes more prevalent, more devices will use it, and HG enforcement will likely become possible. Therefore, I recommend integrating PG into HG.
Of course, I am not suggesting radical integration right now. It will take time for virtualization technology to become fully mainstream. But this also means there is **sufficient development time available**. If HG evolves *slow and steady*, it can make Windows security much stronger.
## Something Else
Other vulnerabilities I leveraged, which could be improved, include:
- Removal of `KeBugCheckEx 0x109` parameters (and inline blocks)
- Removal of the `prefetchnta` instruction
- Adding traps after `KiMcaDeferredRecoveryService`
- Adding more indirect calls
- Removing unnecessary obfuscation
- Obfuscating fixed constants (e.g., `0x4C48B4211BBACBEB`) using `rol` and `ror`, as with the bug check code
- Adding virtualization techniques where performance impact is acceptable
- Removing W permission from the `KiTimerDispatch` section
# Conclusion
As observed through the PG bypass, PatchGuard is an innovative protection technique powerful enough to confuse most reverse engineers. However, it still has limitations in completely blocking skilled attackers.
Therefore, to advance Windows kernel security further, it is necessary to reinforce PatchGuard by addressing its potential weaknesses and by adopting **hypervisor-based multilayered defense systems**.
I hope this study will serve as a small guide for future kernel security research and practical defense strategy development. Thank you.
## Precautions with Demonstration
The full code is available in the Git repository:
[GitHub - kurasagi](http://github.com/NeoMaster831/kurasagi)
This code is a **complete PG bypass**. In other words, once the driver is loaded, PG is disabled.
The code can be built with **Microsoft Visual Studio 2022**, but both the **WDK** and **Windows SDK** must be installed on the system to build it.
When demonstrating, please keep the following precautions in mind:
1. This PG bypass code works **only on Windows 11 24H2 Build 26100.4351**.
2. All options related to **Core Isolation** must be disabled.
3. I used [kdmapper](https://github.com/TheCruZ/kdmapper/tree/master) to load an unsigned driver into the system for PG bypass testing. However, the code works normally even if the driver is loaded by standard means.
4. You can forcibly trigger PG by hooking NT functions with `VerifyPg`. When the `kurasagi` driver is loaded, PG is successfully bypassed, and no BSOD occurs.