
Periodically, as a rule, on the second Wednesday of the month, you can hear stories that Windows, after the next update, stops loading, showing the blue screen of death. In most cases, the cause of such a situation is either a rootkit or specific system software that treats the internal structures of the operating system frivolously. The blame, of course, is still an update, because “everything worked before it”. With this attitude, it is not surprising that Microsoft does not encourage the use of everything that is not documented. At some point, namely with the release of Windows Server 2003, MS took a more active position in the fight against third-party miracle crafts. Then a kernel integrity protection mechanism appeared - kernel patch protection, better known as PatchGuard.
From the very beginning, it was not positioned as a protection mechanism against rootkits, since rootkits work in the kernel with the same privileges, and therefore, PatchGuard can be disabled. It is rather a filter that cuts off lazy rootkit developers.
What PatchGuard Protects
The most popular place for modifying the kernel was the system call table. By modifying the pointers to the functions of the system calls, they could easily be intercepted, filtered, logged, etc. Moreover, this patch was popular for both rootkits and antivirus software. Other objects of interest for the patch are descriptor tables (GDT, IDT). Through modifying the global table of descriptors, it was possible to change the attributes of the segments, creating backdoors for the code, and through the table of interrupt descriptors it was possible to intercept ... interrupts! The advanced guys splice the kernel functions themselves.
')
Accordingly, the first version of PatchGuard protected:
- system call tables (SST),
- global descriptor table (GDT),
- interrupt descriptor table (IDT)
- kernel image
- nuclear stacks.
With the development of NT, many components of the kernel were reworked, including PatchGuard. At the moment it is already difficult to list everything that is protected with its help:
- many system images, not only the kernel image (nt, hal, WerLiveKernelApi, tm, clfs, pshed, kdcom, bootvid, ci, msrpc, ndis, ntfs, tcpip, fltmgr),
- critical kernel data structures (for example, a process list),
- MSR set (for example, model specific register IA32_LSTAR),
- KdpStub is a debugger routine that gets control after exceptions.
How PatchGuard Guards
It should be noted that PatchGuard is actively using the new implementation of exception handling, introduced in x64-versions of Windows. It is used both for obfuscating PatchGuard itself and for checking the integrity of protected images.
In previous versions of Windows, the exception handler used data structures directly on the stack, which even made it possible to bypass stack cookies when exploiting vulnerabilities. The main change is the storage of a special table within the executable image with entries for each of its individual functions.
typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY { uint32_t BeginAddress;
Due to the fact that the address of the beginning and end of any function can be obtained directly in runtime, the task of counting the checksum of a single function becomes trivial. For comparison, in the x86 versions, the integrity control of images is impossible because it is not clear how to define the boundaries of a particular function, and the entire image (or even its individual sections) cannot be covered with a checksum because the same core contains functions that are patched by the core itself on the fly.
When loading the OS, PatchGuard creates from 1 to 4 contexts - data structures in which copies of the functions used by it are stored, checksums of the structures being protected and the encryption keys of the context itself. These contexts are stored in an unloaded pool in an encrypted form. Let's talk about context checking a bit later.
PatchGuard contexts are initialized in phase 1 of the OS boot. The function directly involved in creating the context does not have a public symbol (we will call it KiInitializePatchGuardContext), but you can find it inside the KiFilterFiberContext function. We found two places in which to create a PatchGuard context:
... -(call)-> Phase1InitializationDiscard -(call)-> KeInitAmd64SpecificState -(exception)-> KiFilterFiberContext
... -(call)-> Phase1InitializationDiscard -(call)-> sub_14071815C -(call)-> ExpLicenseWatchInitWorker -(call)-> KiFilterFiberContext
The first option always creates at least one context, while the second only in 4% of cases. Also, the first option is notable for the fact that it invokes the KiFilterFiberContext function implicitly, namely, by throwing an exception.
Pseudocode KeInitAmd64SpecificState __int64 KeInitAmd64SpecificState() { signed int v0; // edx@2 __int64 result; // rax@2 // PatchGuard if ( !InitSafeBootMode ) { v0 = __ROR4__(KdPitchDebugger | KdDebuggerNotPresent, 1); // ( -1), // KiFilterFiberContext result = (v0 / ((KdPitchDebugger | KdDebuggerNotPresent) != 0 ? -1 : 17)); } return result; }
The function sub_14071815C obviously does not have a public symbol, since it is associated with checking the OS license.
ExpLicenseWatchInitWorker pseudocode VOID ExpLicenseWatchInitWorker() { PVOID KiFilterParam; NTSTATUS (*KiFilterFiberContext)(PVOID pFilterparam); BOOLEAN ForgetAboutPG; // KiServiceTablesLocked == KiFilterParam KiFilterParam = KiInitialPcr.Prcb.HalReserved[1]; KiInitialPcr.Prcb.HalReserved[1] = NULL; KiFilterFiberContext = KiInitialPcr.Prcb.HalReserved[0]; KiInitialPcr.Prcb.HalReserved[0] = NULL; ForgetAboutPG = (InitSafeBootMode != 0) | (KUSER_SHARED_DATA.KdDebuggerEnabled >> 1); // 96% if (__rdtsc() % 100 > 3) ForgetAboutPG |= 1; if (!ForgetAboutPG && KiFilterFiberContext(KiFilterParam) != 1) KeBugCheckEx(SYSTEM_LICENSE_VIOLATION, 0x42424242, 0xC000026A, 0, 0); }
Below is the pseudo-code of the KiFilterFiberContext function, which selects the method for checking a specific context and calls the function for creating the context itself.
KiFilterFiberContext pseudocode BOOLEAN KiFilterFiberContext(PVOID pKiFilterParam) { BOOLEAN Result = TRUE; DWORD64 dwDpcIdx1 = __rdtsc() % 13; // DPC, DWORD64 dwRand2 = __rdtsc() % 10; // 50 50, DWORD64 dwMethod1 = __rdtsc() % 6; // AntiDebug(); Result = KiInitializePatchGuardContext(dwDpcIdx, dwMethod1, (dwRand2 < 6) + 1, pKiFilterParam, TRUE); if (dwRand2 < 6) { DWORD64 dwDpcIdx2 = __rdtsc() % 13; DWORD64 dwMethod2 = __rdtsc() % 6; do { dwMethod2 = __rdtsc() % 6; } while ((dwMethod1 != 0) && (dwMethod1 == dwMethod2)); Result = KiInitializePatchGuardContext(dwDpcIdx2, dwMethod2, 2, pKiFilterParam, FALSE); } AntiDebug(); return Result; }
The function that creates the PatchGuard context is so obfuscated that automatic tools cannot cope with it, and researchers suddenly find it uninteresting to reverse it. In statics, this is a complete mess, 10K + lines of decompiled head-on code (decompilation itself in IDA Pro takes about 40 minutes).

Everything speaks about the extensive use of macros:
- even the simplest operation, such as taking a random number, is spread over 50+ lines of assembly code;
- all cycles are expanded;
- a lot of dead code is inserted;
- indirect access to variables and external functions is used.
Dynamics is also quite difficult. Here are a couple of examples of what is scattered around the code.
cli xor eax, eax cmp byte ptr cs:KdDebuggerNotPresent, al jnz short loc_140F3CFBD jmp short loc_140F3CFBB sti
What does the anti-debugging trick 1 do?When the debugger is connected, it enters an infinite uninterrupted loop.
cli sidt fword ptr [rbp+320h] lidt fword ptr [rbp+228h] mov dr7, r13 lidt fword ptr [rbp+320h] sti
What does the anti-debugging trick 2 do?Loads a temporary invalid interrupt descriptor table. If we watched access to the debug registers, a debug exception will occur that, under these conditions, will cause a tripple fault followed by a reboot.
Consider the parameters of the KiInitializePatchGuardContext function.
- The DPC index of the function to be called for context checking and can be one of the following:
- KiTimerDispatch
- KiDpcDispatch
- ExpTimerDpcRoutine
- Ioptimerdispatch
- IopIrpStackProfilerTimer
- PopThermalZoneDpc
- CmpEnableLazyFlushDpcRoutine
- CmpLazyFlushDpcRoutine
- KiBalanceSetManagerDeferredRoutine
- ExpTimeRefreshDpcRoutine
- ExpTimeZoneDpcRoutine
- ExpCenturyDpcRoutine
- Scheduling verification method:
- KeSetCoalescableTimer
A timer object is created, which will start the test after 2m: 05s ± 5 s. - Prcb.AcpiReserved
DPC will operate at a specific ACPI event, for example, when it enters a low power state. It works not earlier than in 2m: 05s ± 5 s. - Prcb.HalReserved
DPC will work with the HAL timer tick. Not earlier than in 2m: 05s ± 5 s. - PsCreateSystemThread
A separate system stream is created, sleeping 2 m: 05 s ± 5 s. After this, the context check is invoked. - KeInsertQueueApc
A regular kernel APC is created that works immediately, but waits for 2m: 05s ± 5 seconds inside the work item. - KiBalanceSetManagerPeriodicDpc
DPC will work according to the timer of the balancing manager, not earlier than in 2m: 05s ± 5 s.
- The purpose of the parameter is not completely clear; it is only known that it affects the number of checks in the context.
- Parameter specific to the selected planning method.
- Parameter indicating the need for recalculation of checksums for the context.
DPCs that call validation through an exception within themselves, “look” - whether the DeferredContext parameter is a pointer to non-canonical memory. If the pointer is OK, the DPC does its legitimate work. Otherwise, the DPC calls a chain of recursive functions, which ultimately lead to an exception (due to dereference of the non-canonical address) and the execution of its handler.
Call sequences of recursive functions depending on the DPC functionExpTimerDpcRoutine -> KiCustomAccessRoutine0 -> KiCustomRecurseRoutine0 ... KiCustomRecurseRoutineN
IopTimerDispatch -> KiCustomAccessRoutine1 -> KiCustomRecurseRoutine1 ... KiCustomRecurseRoutineN
IopIrpStackProfilerTimer -> KiCustomAccessRoutine2 -> KiCustomRecurseRoutine2 ... KiCustomRecurseRoutineN
PopThermalZoneDpc -> KiCustomAccessRoutine3 -> KiCustomRecurseRoutine3 ... KiCustomRecurseRoutineN
CmpEnableLazyFlushDpcRoutine -> KiCustomAccessRoutine4 -> KiCustomRecurseRoutine4 ... KiCustomRecurseRoutineN
CmpLazyFlushDpcRoutine -> KiCustomAccessRoutine5 -> KiCustomRecurseRoutine5 ... KiCustomRecurseRoutineN
KiBalanceSetManagerDeferredRoutine -> KiCustomAccessRoutine6 -> KiCustomRecurseRoutine6 ... KiCustomRecurseRoutineN
ExpTimeRefreshDpcRoutine -> KiCustomAccessRoutine7 -> KiCustomRecurseRoutine7 ... KiCustomRecurseRoutineN
ExpTimeZoneDpcRoutine -> KiCustomAccessRoutine8 -> KiCustomRecurseRoutine8 ... KiCustomRecurseRoutineN
ExpCenturyDpcRoutine -> KiCustomAccessRoutine9 -> KiCustomRecurseRoutine9 ... KiCustomRecurseRoutineN
The context check consists of two stages: first, checking the structure of the context itself, which occurs at the DPC level, then a work item is planned, which checks the protected structures in the system flow. If the test was successful, the old context is deleted and a new one is created instead, which will be launched at a random time interval. If the check fails, PatchGuard clears all its tracks, including zeroing the stack, and displays a blue screen with error code 0x109: CRITICAL_STRUCTURE_CORRUPTION.
Gif with self-decrypting context in the first stage of verification:

How to win
There are several approaches to neutralizing PatchGuard:
- Such a patch of the kernel image so that PatchGuard is not initialized at all.
- Patch context checking procedures.
- Hook KeBugCheck with system state recovery.
- Cancellation of scheduled checks is what we implemented.
We liked the latter method, since it is the most “pure”: nothing needs to be hooked and dirty, you just need to replace the value of some variables.
- KeSetCoalescableTimer
You need to scan all timers for which the DPC will contain a DeferredContext with a non-canonical address, and increase the timeout for those found to infinity. - Prcb.AcpiReserved
Just zanulit this field. - Prcb.HalReserved
Just zanulit this field. - PsCreateSystemThread
Scan the sleeping threads and unwind their stack. If it rests on a function from the KiServiceTablesLocked structure, this is our client. We set a hibernation time equal to infinity. - KeInsertQueueApc
Scan all workflows with stack promotion. If there are functions in the stack that are not from the kernel code section, and they are untwisted using the data for the functions FsRtlMdlReadCompleteDevEx and FsRtlUnininitializeSmallMcb, this is exactly the PatchGuard workflow. We neutralize as in the previous version. - KiBalanceSetManagerPeriodicDpc
Restore the "legitimate" procedure - KiBalanceSetManagerDeferredRoutine.
These actions must be completed in 2 minutes for the reasons described above. The result is that the context check will never be launched, and a new one will not be scheduled. PatchGuard will not work.
Windows 10
When viewing KiFilterFiberContext from Windows 10 Technical Preview, we noticed a slight change. All old planning methods remained the same. However, a new one has appeared, which so far unconditionally returns STATUS_HV_FEATURE_UNAVAILABLE. Having a little rummaged, we found the KiSwInterruptDispatch function, inside which there is a clear decryption and a call to the context check. Obviously, the ability to check contexts on the Hyper-V hypervisor request will be added. Under certain conditions, a synthetic interrupt will come from the hypervisor, the handler of which will check the integrity of the kernel.
The story continues
In the article we tried not to specify the names of specific functions, not because we feel sorry for. It's simple: the names of the functions used to decrypt and verify contexts are intentionally changed by the developers of PatchGuard and are changed in different versions of the OS.
Here is an example of the inconsistency of the function name with what it actually does. This is the same function, a copy of which is used to self-interpret the context.

One thing is good - all these functions are nearby, so you can start with the KiFilterFiberContext function. Obviously, they all lie in one source file. However, kernel integrity checking is not limited to PatchGuard alone. Macros are inserted into various parts of the kernel, checking for those or other structures. Each such place has to be searched manually. Example:
... --> Phase1InitializationDiscard --> CcInitializeCacheManager --> CcInitializeBcbProfiler
With a probability of 50%, this function calculates a checksum for an arbitrary kernel function and plans to check it every 2 minutes in DPC with the CcBcbProfiler function.
So good luck with your search! PatchGuard is interesting precisely because it is fun to reverse;)
Links to help:
Kernel patch protection: frequently asked questionsBypassing PatchGuard on Windows x64PatchGuard ReloadedTSS blog: Patch-Guard 1The Windows 8.1 Kernel Patch ProtectionUnderstanding and Defeating Windows 8.1 Kernel
Patch protectionMicrosoft Windows 8.1 Kernel Patch Protection Analysis & Attack Vectors