PatchGuard Analysis - Part 4

PatchGuard Analysis - Part 4

August 9, 2025·0xbekoo
0xbekoo

Verification Routines

This phase is the heart of PatchGuard’s self-defense mechanism. Once PatchGuard is triggered—either by a timer, a DPC, a system thread, or other dispatch mechanisms—it enters the validation routine. The validation code decrypts and inspects internal PatchGuard state and memory-resident kernel structures.

There are several checking routines involved, with the main one called FsRtlMdlReadCompleteDevEx. Other routines include:

  • TVCallBack Routine: The routine looks very much like FsRtlMdlReadCompleteDevEx. We showed previously that it was called from a global variable set up from KiFilterFiberContext.

  • CcBcbProfiler We saw that this function is used to check a randomly choosen routine from ntoskrnl.

In this section, we will focus mainly on FsRtlMdlReadCompleteDevEx. This function is really long and the function can be summed up into multiple parts:

  1. Prologue
  2. Check of Structures
  3. Epilogue

Prologue

To summarize, here are the main steps that will be described:

Verifying PatchGuard Context Integrity (Part 1, 2, 3)

At this stage, the entire PatchGuard context is decrypted and resides in plaintext in memory. The first operation involves computing checksums for all three parts of the context and comparing them to the expected values that were originally generated during the initialization phase (KiInitPatchGuardContext).

Before this validation occurs, certain volatile fields—such as the context’s checksum itself or temporary WorkItem structures—are temporarily removed from the memory region or moved to the stack to ensure a consistent hash output. These values are restored immediately after checksum verification completes. We can look at 0x140BB308F address:

The stack and r13 register is prepared at beginning of the function:

```asm
FsRtlMdlReadCompleteDevEx:
...
lea rbp, [rsp-898h]
sub rsp, 998h
mov r13, rcx ; Pass the Context to r13

This step ensures that no part of the PatchGuard context has been altered since initialization, effectively acting as a self-checking integrity mechanism.

Re-Encrypting the Primary Context Region (Part 1)

Once the first validation pass is complete, PatchGuard proceeds to re-encrypt the first section of its context structure. This part contains critical operational metadata and is never meant to persist in plaintext form.

Interestingly, PatchGuard does not immediately re-encrypt the remaining sections (Parts 2 and 3), although they also contain sensitive information. The exact reason behind this partial re-encryption strategy remains unclear—it may be a performance optimization or a strategic trade-off to support upcoming operations.

Checksum Verification of Parts 2 and 3

With Part 1 now encrypted again, PatchGuard focuses on validating the integrity of Parts 2 and 3. These sections typically hold:

  • Copies of essential ntoskrnl routines (used later for memory restoration), and

  • Arrays describing critical kernel structures and their precomputed checksums.

These regions remain unencrypted at this stage to allow direct access during subsequent verifications. As with Part 1, any inconsistency here would suggest tampering and would eventually lead to a system bug check.

Timed Delay via Sleep or Wait Routine

To prevent frequent checks and thwart certain timing-based attacks, PatchGuard includes an enforced wait period between validation cycles. This wait typically ranges between 2 minutes and 2 minutes 10 seconds and can be implemented using one of three methods:

  1. SelfEncryptWaitAndDecrypt (sub_140510430): Encrypts the context, invokes KeDelayExecutionThread, then decrypts it after waiting.

An example for SelfEncryptWaitAndDecrypt (named it in literature) is that it called at 0x140BC274E:

As an extra, this function takes the address of KeDelayExecutionThread as a parameter. These address both SelfEncryptWaitAndDecrypt and KeDelayExecutionThread are prepared at 0x140BC252F. Also the timer is prepared at the same address:

loc_140BC252F:
mov rax, [rsi+2C8h] ; Get the address of KeWaitForSingleObject
mov r9, [rsi+170h]  ; Get the address of KeDelayExecutionThread
mov [rbp+8D0h+var_8D8], rax
mov rax, [rsi+340h] ; Get the address of SelfEncryptWaitAndDecrypt
mov [rbp+8D0h+var_8F0], rax
mov [rbp+8D0h+var_950], r9

[...]

mov [rbp+8D0h+var_850], rcx ; Pass the timer

In SelfEncryptWaitAndDecrypt, it firstly encrypts the context and calls KeDelayExecutionThread directly at 0x140510567:

After the execution of KeDelayExecutionThread, it then decrypts the context at 0x1405105AB:



  1. KeWaitForSingleObject: Waits on a kernel event or timer object—can return immediately if already signaled.

An example of KeWaitForSingleObject is that it is called from 0x140BC278A near the section where SelfEncryptWaitAndDecrypt is called:


loc_140BC2770:
xor     edx, edx
test    r11, r11
jnz     short loc_140BC278A 

[...]

loc_140BC278A:
lea rax, [rbp+8D0h+var_850] ; Get the address of KeWaitForSingleObject
xor r9d, r9d
mov [rsp+9D0h+BugCheckParameter4], rax
xor r8d, r8d
mov rax, [rbp+8D0h+var_8D8]
mov rcx, r11
call KeGuardDispatchICall ; Call KeWaitForSingleObject

Again the address of KeWaitForSingleObject is prepared at the same address, 0x140BC252F.


  1. KeDelayExecutionThread: Passive sleep for the entire duration, used as a fallback. From 0x140BC2770 address:
loc_140BC2770:
xor edx, edx
test r11, r11
jnz short loc_140BC278A ; Call KeWaitForSingleObject

lea r8, [rbp+8D0h+var_850]
xor ecx, ecx
mov rax, r9
call KeGuardDispatchICall ; Call KeDelayExecutionThread

The method used is determined by values in the PatchGuard context, specifically flags and object pointers initialized during KiInitPatchGuardContext. For example:

  • A flag at offset 0xAB8 is used to select the SelfEncryptWaitAndDecrypt method. This flag is initialized at 0x140BC251D and then determines whether to call this function or not:

This flag is then used in parts of the methods we saw above. If this flag is set, SelfEncryptWaitAndDecrypt will be called. Let's look at the code again:
loc_140BC26F1:
...
mov r10, [rbp+8D0h+arg_8] ; Get the Flag

[...]

loc_140BC274E:
test r10, r10 ; If the flag is 0, then jump
jz short loc_140BC2770

; /* Call SelfEncryptWaitAndDecrypt */ 
mov rax, [rbp+8D0h+var_8F0] ; Get the address of SelfEncryptWaitAndDecrypt
lea r8, [rbp+8D0h+var_850]
mov edx, r14d
mov [rsp+9D0h+BugCheckParameter4], r10
mov rcx, rsi
call KeGuardDispatchICall 
jmp short loc_140BC27A8

  • Offset 0xA40 or 0x5D0 can hold Object for KeWaitForSingleObject. When calling KeWaitForSingleObject, the value of r11 register is passed as Object:
loc_140BC2770:
xor edx, edx
test r11, r11        
jnz loc_140BC278A ; If the object is initialized, then jump it
[.. Calling KeDelayExecutionThread Function..]


...


loc_140BC278A:
...
mov rax, [rbp+8D0h+var_8D8] ; Get the address of KeWaitForSingleObject
mov rcx, r11 ; Pass the object as a parameter
call KeGuardDispatchICall 

The value of r11 is set at the same address, 0x140BC251D:


  • If these aren’t valid, a classical thread sleep via KeDelayExecutionThread is used.

Notably, the KiStackProtectNotifyEvent, a global event object, may be signaled by other kernel components like KeBalanceSetManager, enabling early wake-up from wait in specific contexts.

This timing mechanism introduces entropy and variation into PatchGuard’s operation schedule, making it significantly harder to predict, intercept, or bypass.

Decrypt Part 1 of the Context

After the sleep or wait operation concludes, PatchGuard resumes execution and decrypts the first segment of its context structure. This segment was previously re-encrypted for protection.

Revalidate Checksums for Parts 2 and 3

To ensure that no tampering occurred during the waiting phase, PatchGuard recomputes the checksums of Parts 2 and 3 and compares them with the original values stored earlier. These checksums were preserved across the wait by temporarily saving them to the stack using push/pop instructions.

Because the original values never touch static memory during the wait, intercepting and modifying them would be extremely difficult. This step is a crucial part of PatchGuard’s self-integrity enforcement.

Final Checksum of Part 1

In this final integrity check, PatchGuard computes the checksum of the first 0x618 bytes of the context. This region contains internal function pointers but no volatile data such as hash values or runtime variables. The calculated result is compared to the original checksum generated during context initialization (stored at offset 0x8b8).

A mismatch here would indicate memory corruption or tampering, triggering PatchGuard’s defensive response.

Set Thread Affinity to a Target Processor

To finalize the validation setup, PatchGuard selects a specific processor core on which the integrity check should run. This is particularly important because some kernel structures are processor-specific, and executing on a consistent core helps maintain control and reliability.

PatchGuard begins by retrieving the session ID that was stored during context initialization. It then randomly selects a process from the system by generating a value between zero and the total number of active processes. Instead of choosing a process ID directly, it enumerates through the system’s process list and selects the entry based on the generated number.

Once a process is selected, PatchGuard extracts its group affinity—a bitmap indicating which processors that process can run on. It calculates how many processors are available in that affinity by counting the number of set bits (i.e., performing a Hamming weight operation). From this set of allowed processors, it randomly picks one and sets its own execution thread to run exclusively on that processor using the KeSetSystemGroupAffinityThread API.

This step ensures randomness and minimizes predictability, making it harder for attackers to monitor or manipulate PatchGuard’s activity across the system.

Kernel Structure Integrity Check

IDT Verification

To check IDT, PatchGuard goes through some preliminary steps. Firstly, it starts by dispatching the type of the bugcheck. For the IDT the type is 0x2, then the dispatcher goes to 0x140BBFC59:

Since the Interrupt Descriptor Table (IDT) is processor-specific and accessed through the idtr register, selecting the right processor is necessary to control the specific processor that PatchGuard will check. And this information is stored in part 3 of the structure at 0x28 offset (initialized at 0x140BEA994 address). Then PatchGuard therefore proceeds to initialize a KAFFINITY structure with this information and call KeSetSystemGroupAffinityThread to execute the thread. Lastly it calls KiGetGdtIdt to fetch the idr and gdtr values.

Then PatchGuard splits the check into two parts: the first part handle the KxUnexpectedInterrupt functions, and the second the IDT itself. Firstly, PatchGuard fetches the address of KxUnexpectedInterrupt0, which is actually an array of functions.

For each entry, it disables external interrupt with set CR8 to 0xF (0x140BBFCFA):

loc_140BBFCBD:
...
mov r15, cr8        ; Keep Original Value of CR8
mov eax, 0Fh
mov cr8, rax        ; Disable all external interrupt (CR8 = 0xF)

Then it starts the check at 0x140BBFD34:

mov eax, [rcx+4]
mov rcx, [rsi+628h] ; Get the entry from KxUnexpectedInterrupt0
mov eax, edi
lea rdx, [rcx+rax*8]
cmp rbx, rdx

These entries of KxUnexpectedInterrupt0 actually are initialized at 0x140BD724F:

loc_140BD71C0:
...
lea rax, KxUnexpectedInterrupt0
mov [r14+628h], rax

if it matches with the respective KxUnexpectedInterrupt(s) entry, it calls KiGetInterruptObjectAddress to get the KINTERRUPT object at 0x140BBFD4E:

loc_140BBFD4E:
mov rax, [rsi+470h]
mov ecx, edi
call KeGuardDispatchICall ; Call KiGetInterruptObjectAddress

The address of KiGetInterruptObjectAddress is initialized at 0x140BD6D09:

loc_140BD6CE6:
...
lea rax, KiGetInterruptObjectAddress
mov [r14+470h], rax

Then PatchGuard uses RtlSectionTableFromVirtualAddress to check three things:

  • whether the address belongs to a discardable image (IMAGE_SCN_MEM_DISCARDABLE);
  • whether the address belongs to the mapping of ntoskrnl.exe;
  • whether it belongs to one of the exported functions of ntoskrnl.exe (using RtlLookupFunctionEntry).

At 0x140BBFDDA address, it start by calling RtlSectionTableFromVirtualAddress:

loc_140BBFDA2:
...
mov r8d, dword ptr [rbp+8D0h+var_8A8]
mov rcx, [rsi+8F8h]
sub r8d, edx
mov  rax, [rsi+220h]
call KeGuardDispatchICall ; Call RtlSectionTableFromVirtualAddress
test rax, rax
jz loc_140BC010A

Then it executes RtlLookupFunctionEntry routine at 0x140BBFE41:

loc_140BBFDF5:
...
lea rdx, [rbp+8D0h+var_878]
xor r8d, r8d
mov rcx, rbx
call KeGuardDispatchICall ; Call RtlLookupFunctionEntry

var_878 is initialized at 0x140BBFCBD:

loc_140BBFCBD:
mov rax, [rsi+8E8h]
mov r9, r15
mov [rbp+8D0h+var_878], rax

0x8E8 offset contains 0x140000000 value. We can see that this offset is prepared at 0x140BFB409 address:

loc_140BFB402:                          
lea rbx, cs:140000000h
mov [rdi+8E8h], rbx
...

For the second part, PatchGuard calculates a checksum of the table referenced by the IDT register, similar to how it verifies most other structures. After completing the hash calculation, PatchGuard restores the previous processor affinity by calling KeRevertToUserGroupAffinityThread, then compares the computed hash with the one stored in memory.

Epilogue

The epilogue of the check routine can be separated in two part, obviously: the one that happens when a modification is detected, and the one that happens when everything is fine.

Normal Completion: No Issues Detected

After the final hash comparison of a structure, as previously mentioned, if the total amount of data verified is less than the maximum defined in KiInitPatchGuardContext, PatchGuard proceeds to the next structure in the array. Otherwise, it re-arms the PatchGuard context for later use. This process involves multiple steps, but they are essentially the same as the initialization steps.

For methods 0, 1, 2, 4, and 5, the code is almost identical to the implementation found in KiInitPatchGuardContext with respect to the method applied:

  1. KeSetCoalescableTimer is called directly
  2. DPC is stored in KPRCB.AcpiReserved
  3. DPC is stored in KPRCB.HalReserved
  4. APC is inserted with KeInsertQueueApc
  5. DPC was already set in a global variable

The third method, which involves the creation of a system thread, is re-armed but not within the same main function. Once the verification routine finishes, a small dispatcher chooses between calling KeDelayExecutionThread or KeWaitForSingleObject.


  • If KeDelayExecutionThread is selected, a usual timeout between 2 minutes and 2 minutes 10 seconds is set.

  • If KeWaitForSingleObject is selected, a fixed timeout of 2 minutes is applied this time.


Note that the very first time the routine was called, no timeout was provided; only an event was used, which was signaled by KeSetEvent at the end of KiInitPatchGuardContext for the seventh method. However, in the current case, with a 50% chance per boot, the event is reset and, unless overlooked, will never be set again since the signaling occurs only during initialization.

For the seventh method, absolutely nothing is done: the code immediately jumps to the end of the check routine. As mentioned before, this method is cleared right after initialization begins, so its behavior here remains unclear.

Modification Detection: System Integrity Compromised

Once the checksum operation completes, in the case of the Interrupt Descriptor Table (IDT), PatchGuard first restores the previous processor affinity for the current thread. Following this, it compares the newly computed hash with the original hash stored during the KiInitPatchGuardContext initialization. If any modification is detected, PatchGuard executes a controlled sequence of steps that ultimately trigger a Blue Screen of Death (BSOD).

Checksum, Encryption, and Verifications

The initial phase involves handling the PatchGuard context structure. PatchGuard computes a checksum over the entire structure, but before doing so, it transforms the structure into a “common” state by clearing or resetting volatile or dynamic fields that might fluctuate between checks. This is important to ensure consistent checksum results across invocations.

The operations performed before the checksum calculation include:


  • Zeroing out or clearing the checksum field itself (located at offset 0x658) within the full context structure (parts 1, 2, and 3).

  • Setting the total size of the checked data (at offset 0x6C8) to match the size of the first part of the context, replicating the process from initialization.

  • Saving the workitem field (at offset 0x638) onto the stack, then clearing it within the context.


After preparing the structure with these modifications, the checksum is calculated over the entire context.

Once the checksum is calculated, the workitem is restored from the stack back into the context, and the checksum result is saved at offset 0x658.

It is important to note that this newly computed checksum is not directly compared with the previous checksum at this point, which indicates it may serve purposes other than immediate integrity verification, such as internal consistency or other bookkeeping.

Next PatchGuard proceeds to reencrypt the very beginning of the PatchGuard context, which is the code of CmpAppendDllSection. There is no obvious reason for this encryption especially since the rest of the structure remains in clear text for now.

Restore Sensitive Data
Additional Anti-Debug Technique: Overwriting DbgPrint with a RET Instruction

PatchGuard employs multiple anti-debugging techniques throughout its execution. One of the final such measures appears to be a simple but effective tampering of the DbgPrint routine by overwriting its first instruction with 0xC3, which corresponds to the RET opcode.

Clearing Specific Context Entries

PatchGuard clears two specific offsets in the context structure: KxUnexpectedInterrupt0 and KiIsrThunkShadow. The exact reason for this is unclear, especially since the checksum has already been calculated.

KeBugCheckEx or SdpbCheckDll

Near the end of its verification routine, PatchGuard calls KeGuardCheckICall with KeBugCheckEx as the argument. However, a subtle change can be noticed through timeless analysis: if the scheduling method used is 7, then KeGuardCheckICall is rewritten inside the KiInitPatchGuardContext function, at address 0x140BFB3FD, along with KeGuardDispatchICall:

loc_140BFB3EF:
lea rax, KeGuardCheckICall
sub eax, ecx
test r9d, r9d
jz short loc_140BFB402
mov byte ptr [rax+r8], 0C3h ; Ret Instruction

This mean that if method used is not 7, then SdpbCheckDll is called instead of KeBugCheckEx.

SdpbCheckDll is essentially a stub that leads to KeBugCheckEx, but before jumping, it clears the thread stack retrieved from ETHREAD.InitialStack. If the current thread is executing a DPC (indicated by KPCRB.DpcRoutineActive), PatchGuard checks whether the current stack matches the DPC stack (pointed to by KPCRB.DpcStack). In that case, instead of clearing ETHREAD.InitialStack, it clears the DpcStack.

Conclusion

This journey has been a long and demanding one. The research began in April and finally concluded in August, culminating in a comprehensive set of notes spanning approximately 90 pages. As a result, we now possess a far more in-depth understanding of PatchGuard—how it hides itself, the types of integrity checks it performs, and the various methods it uses to initialize and operate.

In this article, we aimed to provide a high-level yet holistic view of PatchGuard’s architecture, focusing on its core components, initialization routines, and operational behavior. The analysis, driven by reverse engineering techniques and tooling, allowed us to demystify how this security mechanism is structured and how it enforces protection within the Windows kernel.

Ultimately, I believe this work will serve as a valuable reference for both academic researchers and hands-on security practitioners interested in low-level Windows internals and kernel protection mechanisms.

References

Throughout this article, I used Tetrane’s “Updated Analysis of PatchGuard on Microsoft Windows 10 RS4” as a main source. This work provided detailed insights into PatchGuard’s mechanisms, including its initialization, kernel-level integrity checks, and protection strategies.

  1. AMD64 Architecture Programmer’s Manual Volume 2, see page 109
  2. Microsoft Docs - KeExpandKernelStackAndCallout
  3. Intel® 64 Architecture x2APIC Specification, see page 2-2
  4. Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 4: Model-Specific Registers, see Page 2-4
  5. Felix Clouiter - CPUID
Last updated on