This will be a multi-part series of posts describing the internal mechanisms and purpose of Secure Kernel Patch Guard, also known as HyperGuard. This first part will focus on what SKPG is and how it’s being initialized.
Overview
In the world of Windows security, PatchGuard is a uniquely undocumented and hardly any “unofficial” documentation. Thus, there are conflicting opinions and rumors about the way it operates and different “PatchGuard bypasses” that get published aren’t very reliable. Still, every few years some helpful PG analysis gets published, shedding some light on this mysterious feature. This blog post is not about PatchGuard so we won’t go into much detail about it, but it discusses a similar and related feature, so some basic knowledge of PatchGuard is needed. Here are a couple of things needed to understand of the rest of the post:
- The purpose of PatchGuard is to monitor the system for changes in kernel space that should not happen on a normal system and crash it when those are detected. This doesn’t mean any unusual data change – PatchGuard monitors a pre-determined list of data structures that are common targets for kernel exploitation or rootkits, such as modifications to
HalDispatchTable
or callback arrays, or changes to control registers or MSRs to disable security features. The full list of monitored structures and pointers is not documented and the information that does get published by Microsoft is left vague on purpose. - PatchGuard doesn’t monitor everything, all the time. It runs periodically, checking for certain changes every time it runs – it won’t necessarily crash the system right when a malicious change is done and a system might run for a long time with such changes. There is no guarantee that PatchGuard will ever detect and crash the system. This also means it is hard to validate potential bypasses.
The main weakness of PatchGuard and the reason for all the obscurity around its implementation is the fact that it monitors Ring 0
code and data – from code that runs in Ring 0
. There is nothing preventing a rootkit that already gained Ring 0
code execution privileges from patching the code for PatchGuard itself and disabling or bypassing it. The only thing stopping this scenario is PatchGuard’s obscurity and the fact that its code is hard to find and uses a range of obfuscation techniques to make itself hard to analyze and disable.
There is a lot more to say about PatchGuard but, like I mentioned, this is not the topic of the post. So, I’ll skip right to discussing PatchGuard’s newer sibling – HyperGuard, also known as Secure Kernel Patch Guard, or SKPG
. This new feature leverages the existence of Hyper-V and VBS to create a new monitoring and protection capability that is similar to PatchGuard but not susceptible to the same weaknesses since it is not running as normal Ring 0
code and cannot be tampered by normal rootkits.
Finding HyperGuard
HyperGuard takes advantage of VBS
– Virtualization Based Security. This capability that was added in the past few years is made possible by the creation of Hyper-V and Virtual Trust Levels (VTL
s). The hypervisor allows creating a system where most things run in VTL0
, but some, more privileged things, run in higher VTL
s (currently the only one implemented is VTL1
) where they are not accessible to normal processes regardless of their privilege level – including VTL0
kernel code. Put simply, no VTL0
code can interact with memory in VTL1
in any way.
Having memory that cannot be tampered with even from normal kernel code allows for many new security features, some of which I’ve written about in the past and others are documented in other blogs, conference talks and official Microsoft documentation. A few examples include KCFG
, HVCI
and KDP
.
This is also what allows Microsoft to implement HyperGuard
– a feature similar to PatchGuard
that can’t be tampered with even by malicious code that managed to elevate itself to run in the kernel. For this reason, HyperGuard
doesn’t need to hide or obfuscate itself in any way, and it’s so much easier to analyze using static analysis tools.
The VTL1
kernel, also known as the secure kernel, is managed through SecureKernel.exe
. This is also the binary where HyperGuard
is implemented. If we open securekernel.exe
in IDA we can easily find all the code implementing HyperGuard
, which all uses the prefix Skpg
:
This series will cover some of those functions, starting from the first ones being called during boot: SkpgInitSystem
:
HyperGuard Initialization
HyperGuard
initialization mostly happens during the normal kernel’s Phase 1
initialization, but requires multiple steps. The first step starts with a secure call where SKSERVICE=SECURESERVICE_PHASE3_INIT
. This leads to SkInitSystem
which will initialize SKCI
(Secure Kernel Code Integrity) and call into SkpgInitSystem
. This function sets up the basic components of SKPG
– its callback, timer, extension table and intercept functions, all of which I’ll discuss in more detail later in this series. At this point SKPG
is not fully initialized – that only happens later in response to another request from the normal kernel. For now, only a few SKPG
globals are being set:
Some interesting components to notice at this stage are:
SkpgPatchGuardCallback
– a callback which is going to be called every timeHyperGuard
checks need to run and will invoke the target functionSkpgPatchGuardCallbackRoutine
.SkpgPatchGuardTimer
– a secure kernel timer object that is going to control the execution of someHyperGuard
checks. It gets set to run at a random time so checks will happen at different intervals, making periodic checks harder to avoid. The function set its callback function toSkpgPatchGuardTimerRoutine
.- Intercept function pointers – other than the periodic checks controlled by the timer,
HyperGuard
also has a few intercept functions, which execute every time a certain operation is being intercepted by the Hypervisor. The operation being intercepted is pretty clear from the function names, but I’ll cover them in more detail later anyway. The global variables for these are:ShvlpHandleMsrIntercept
– points toSkpgxInterceptMsr
ShvlpHandleRegisterIntercept
– points toSkpgxInterceptRegister
ShvlpHandleRepHypercallIntercept
– points toSkpgInterceptRepHypercall
- Optional variables – there are a few other global variables that did not fit in the screenshot and get initialized based on the flags received as part of the input argument, or other optional configuration:
SkpgInhibitKernelVaProtection
SkpgNtKvaShadow
SkpgSecureExtension
After initializing all the global variables, the function returns and the rest of the secure kernel initialization continues. For now, the timer is not scheduled and HyperGuard
is effectively “dormant”. HyperGuard
is only fully “activated” later – through a call to SkpgConnect
.
There are three ways to call SkpgConnect and all start from a call by the normal kernel:
HyperGuard Activation
Connect Software Interrupt – the PatchGuard Path
The most interesting HyperGuard
activation path is through PatchGuard
. This SKPG
activation path, like all others, begins with a secure call. This secure call, with SKSERVICE= SECURESERVICE_CONNECT_SW_INTERRUPT
, originates from the normal kernel function VslConnectSwInterrupt
. This leads, as usual, to the secure kernel handler which calls into IumpConnectSwInterrupt
and from there to SkpgConnect
, passing it all the data that was sent by the normal kernel.
When we search for calls to VslConnectSwInterrupt
we see two calls – one from PsNotifyCoreDriversInitialized
that I’ll cover soon and a second one from KiConnectSwInterrupt
:
KiConnectSwInterrupt
is only called by one caller – an anonymous function in ntoskrnl.exe
that has no name in the public symbols. This is an extremely large function that calls into other anonymous functions and has a lot of weird and seemingly unrelated functionality. This is one of the PatchGuard
initialization routines, which does the “real” activation of HyperGuard
, supplying the secure kernel with memory protection ranges and targets which I will discuss later when talking about SKPG
extents.
I encourage you to follow the call stack yourselves and get a bit of insight into the mysteries of PatchGuard
initialization, but if I start covering PatchGuard
details this series will quickly become a book so I will skip the details here. Let’s just trust me when I say that this all also happens in the context of Phase 1
initialization and is the first point where HyperGuard
is activated.
Once HyperGuard
is fully activated, a global variable SkpgInitialized
is set to TRUE
. This variable is checked every time SkpgConnect
is called, and if set the function will return immediately and not make any changes to any SKPG
initialization data. This means that the two other activation paths that will be described here will only activate HyperGuard
if PatchGuard
is not running and will result in less thorough protection of the machine. If PatchGuard
is active, then the other two activation paths will return without doing anything.
Connect Software Interrupt – Phase1 Initialization
The second code path into VslConnectSwInterrupt
goes through PsNotifyCoreDriversInitialized
. This is also happening as part of Phase 1
initialization, but later than the PatchGuard
path:
As we can see here, the call to VslConnectSwInterrupt
is done with empty input variables, meaning no memory ranges or extra data is sent to HyperGuard
and it will only use its basic functionality. If PatchGuard
is running, then at this point SKPG
should already be initialized and the call will return with no changes to SKPG
, so this path is only needed if PatchGuard
is not active.
Phase3 Initialization
The last case where HyperGuard
is activated happens during Phase 3
initialization. This happens in response to a secure call with SKSERVICE=SECURESERVICE_REGISTER_SYSTEM_DLLS
. It will also call into SkpgConnect
with no input data, simply to initialize it if nothing else has already.
On the normal kernel side: In PspInitPhase3
the system checks the VslVsmEnabled
global variable to learn whether Hyper-V is running and VSM
is enabled. If it is, the system calls VslpEnterIumSecureMode
– a common function to generate a secure call with a given service code and arguments packed into an MDL
. The system enters secure mode with service code SECURESERVICE_REGISTER_SYSTEM_DLLS
:
Once a secure call reaches the secure kernel it is handled by IumInvokeSecureService
, which is pretty much just a big switch statement, calling the correct function or functions for each service code. In the case of code SECURESERVICE_REGISTER_SYSTEM_DLLS
, it calls SkpgConnect
and then uses the data passed in by the kernel to register system DLL
s:
As I mentioned, this is the last time SkpgConnect
is called, right at the end of system initialization. This is done in case SKPG
hasn’t been initialized at an earlier stage already. In this case, SkpgConnect
is called with almost no input data, to only initialize the most basic SKPG
functionality. If SKPG
has already been initialized earlier, this call will return without changing anything.
HyperGuard Activation – Diagram
This is it for part 1
of this series. So far, we only covered the general idea of what HyperGuard
is and its initialization paths. Next time we will dive into SkpgConnect
to see what happens during SKPG
activation and learn more about the types of data SKPG
protects and how.
- Secure Kernel Research with LiveCloudKd
- Troubleshooting a System Crash
- KASLR Leaks Restriction
- Investigating Filter Communication Ports
- An End to KASLR Bypasses?
- Understanding a New Mitigation: Module Tampering Protection
- One I/O Ring to Rule Them All: A Full Read/Write Exploit Primitive on Windows 11
- One Year to I/O Ring: What Changed?
- HyperGuard Part 3 – More SKPG Extents
- An Exercise in Dynamic Analysis