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.
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
HalDispatchTableor 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.
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 (
VTLs). The hypervisor allows creating a system where most things run in
VTL0, but some, more privileged things, run in higher
VTLs (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
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.
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
This series will cover some of those functions, starting from the first ones being called during boot:
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 time
HyperGuardchecks need to run and will invoke the target function
SkpgPatchGuardTimer– a secure kernel timer object that is going to control the execution of some
HyperGuardchecks. 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 to
- Intercept function pointers – other than the periodic checks controlled by the timer,
HyperGuardalso 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 to
ShvlpHandleRegisterIntercept– points to
ShvlpHandleRepHypercallIntercept– points to
- 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:
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
There are three ways to call SkpgConnect and all start from a call by the normal kernel:
Connect Software Interrupt – the PatchGuard Path
The most interesting
HyperGuard activation path is through
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 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
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.
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
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
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.
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
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
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.
- 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
- HyperGuard – Secure Kernel Patch Guard: Part 2 – SKPG Extents
- HyperGuard – Secure Kernel Patch Guard: Part 1 – SKPG Initialization
- IoRing vs. io_uring: a comparison of Windows and Linux implementations
- I/O Rings – When One I/O Operation is Not Enough
- Thread and Process State Change