Hi all! And welcome to part 3 of the HyperGuard chronicles!
In the previous blog post I introduced SKPG extents – the data structures that describe the memory ranges and system components that should be monitored by HyperGuard. So far, I only covered the initialization extent and various types of memory extents, but those are just the beginning. In this post I will cover the rest of the extent types and show how they are used by HyperGuard to protect other areas of the system.
The next extent group to look into is MSR and Control Register extents:
MSR and Control Register Extents
This group contains the following extent types:
These extent types are received from the normal kernel, but they are never added into the array at the end of the
SKPG_CONTEXT or get validated during the runtime checks that I’ll describe in one of the next posts. Instead, they are used in yet another part of SKPG initialization.
After initializing the
SkpgConnect performs an IPI (Inter-Processor Interrupt). It performs this IPI by calling
SkeGenericIpiCall with a target function and input data, and the function will call the target function on every processor and send the requested data. In this case, the target function is
SkpgxInstallIntercepts and the input data contains the number of input extents and the matching array:
I will go over intercepts in a lot more detail in a future blog post, but to give some necessary context: SKPG can ask the hypervisor to intercept certain actions in the system, like memory access, register access or instructions. HyperGuard uses that ability to intercept access to certain MSRs and Control Registers (and other things, which I will talk about later) to prevent malicious modifications. HyperGuard uses the input extents to choose which MSRs and Control Registers to intercept, out of the list of accepted options.
Since each processor has its own set of MSRs and registers, HyperGuard needs to intercept the requested one on all processors. Therefore,
SkpgxInstallIntercepts is called through an IPI, to make sure it’s called in the context of each processor.
SkpgxInstallIntercepts, the function iterates over the array of input extents and handles the three types included in this group based on the data supplied in them. If you remember, each extent contains
0x18 bytes of type-specific data. For this group, this data contains the number of the MSR/Register to be intercepted as well as the processor number that it should be intercepted on. This means that there might be more that one input extent for each MSR or control register, each for a different processor number. Or MSRs and control registers might only be intercepted on certain processors but not on others, if that is what the normal kernel requested. The data structure in the input extent for MSRs and control register extents looks something like this:
typedef struct _MSR_CR_DATA
} MSR_CR_DATA, *PMSR_CR_DATA;
While iterating over the extents, the function checks if the extent type is of one of the three in this group, and if so whether the processor number in the extent matches the current processor. If so, it checks if the number of the MSR or control register matches one of the accepted ones. If the extent matches one of the accepted registers, a mask is fetched from an array in the
SKPRCB – this array contains the needed masks for all accepted MSRs and control registers so the hypervisor can be asked to intercept them. All masks are collected, and when all extents have been examined the final mask is sent to
ShvlSetRegisterInterceptMasks to be installed. The mask that is used to install the intercepts is the union
HV_REGISTER_CR_INTERCEPT_CONTROL. It is documented and can be found here.
Now that the general process is covered, we can look into the accepted MSRs and control registers and understand why HyperGuard might want to protect them from modifications, starting from the MSRs:
Patching certain MSRs is a popular operation for exploits and rootkits, allowing them to do things such as hooking system calls or disabling security features. Some of those MSRs are already periodically monitored by PatchGuard, but there are benefits to intercepting them through HyperGuard that I will cover later. The list of MSRs that can be intercepted keeps growing over time and receives new additions as new features and registers get added to CPUs, such as the implementation of CET which added multiple MSRs that might be a target for attackers. As of Windows 11 build 22598, the MSRs that can be intercepted by SKPG are:
0xC0000080) – among other things, this MSR contains the NX bit, enforcing a mitigation that doesn’t allow code execution in addresses that aren’t specifically marked as executable. It also contains flags related to virtualization support.
0xC0000081) – contains the address of the
x86system call handler.
0xC0000082) – contains the address of the
x64system call handler – should normally be pointing to
0xC0000083) – contains the address of the system call handler on
x64when running in compatibility mode – should normally be pointing to
0xC0000084) – system call flags mask. Any bit set here when a system call is executed will be cleared from
0xC0000103) – usage depends on the operating system, but this MSR is generally used to store a signature, to be read together with a time stamp.
0x1B) – contains the
0x174) – contains the
CSvalue for ring
0code when performing system calls with
0x175) – contains the stack pointer for the kernel stack when performing system calls with
0x176) – contains the
EIPvalue for ring
0entry when performing system calls with
0x1A0) – controls multiple processor features, such as Fast Strings disable, performance monitoring and disable of the XD (no-execute) bit.
0x6A2) – controls kernel mode
0x6A4) – contains the ring
0shadow stack pointer.
0x6A5) – contains the ring
1shadow stack pointer.
0x6A6) – contains the ring
2shadow stack pointer.
0x6A8) – contains a pointer to the interrupt shadow stack table.
0xDA0) – contains a mask to be used when
XRESTORinstructions are called in kernel-mode. For example, it controls the saving and loading of the registers used by Intel Processor Trace (
By modifying certain control registers an attacker can disable security features or gain control of execution. Currently SKPG supports intercepts of two control registers:
CR0– controls certain hardware configuration such as paging, protected mode and write protect.
CR4– controls the configuration of different hardware features. For example, driver signature enforcement,
UMIPbits control security features that make
CR4an interesting target for attackers using an arbitrary write exploit.
Currently only one extended control register exists –
XCR0. It’s used to toggle storing or loading of extended registers such as
CET registers, and can be intercepted and protected by SKPG.
Installing the Intercepts
Now that we know that registers can be intercepted and why, we can get back and look at the installation of the intercepts through
ShvlSetRegisterInterceptMasks. The function receives a
HV_REGISTER_CR_INTERCEPT_CONTROL mask to know which intercepts to install, as well as the values for a few of the intercepted registers –
IA32_MISC_ENABLE MSR. These are all placed in a structure that is passed into the function, which looks like this:
} REGISTER_INTERCEPT_INFORMATION, *PREGISTER_INTERCEPT_INFORMATION;
InterceptControl mask is built while iterating over the input extents, and the values for
IA32_MISC_ENABLE are read from the
SKPRCB (their values, together with the values for all other potentially-intercepted registers, are placed there in
SkeInitSystem, triggered from a secure call with code
This structure is sent to
ShvlSetRegisterInterceptMasks which in turn calls
ShvlSetVpRegister on each of the four values in the input structure to register an intercept. Setting the register values is done by initiating a fast hypercall with a code of
0x51), sending on four arguments (for anyone interested, all hypercall values are documented here). The last two arguments are of types
HV_REGISTER_VALUE – these types are documented so it’s easy to see what registers are being set:
Looking at the function, we see that it’s setting the required values for
IA32_MISC_ENABLE, and finally setting the mask for intercept control, so from this point all requested registers are intercepted by the hypervisor and any access to them will be forwarded to the SKPG intercept routine.
Secure VA Translation Extents
In the previous post I introduced the secure extents – extents indicating
VTL1 memory or data structures to be protected. I also covered memory extents, including the secure memory extents. Here is another kind of secure extents, which are initialized internally in the secure kernel, without using input extents from
VTL0. They are called Secure VA Translation Extents and are initialized inside
SkpgCreateSecureVaTranslationExtents. These extents are used to protect Virtual->Physical address translations for different pages or memory regions that are a common target for attack:
Though they are called secure extents, the data they protect is mostly
VTL0 data, such as the
VTL0 mapping of the
KCFG bitmap or the inverted function table. The exact validations done differ between the types: for example, the zero page should never be mapped so a successful virtual->physical address translation of the zero page should not be acceptable, while the kernel
CFG bitmap should have valid translations but the
VTL0 mapping of those pages should always be read-only.
SkpgCreateSecureVaTranslationExtents, we can see that the extents are initialized with no input data or memory ranges:
This is because all of these extents correlate to specific data structures which are all initialized elsewhere so the data doesn’t need to be part of the extent itself, so the type is the only part that needs to be set. We can also see that some of these extents are only initialized when
KCFG is enabled, since without it they are not needed. I will cover the checks done for each of these extents in a later blog post, which will describe SKPG extent verification.
Finally, if HotPatching is enabled, two more extents are added, both with type
These extents protect the
SkpgNtExtension variables, which keep track of HotPatching data.
There are two more extents that are processor-specific, since the data they protect exists separately in each processor. However, unlike the MSR and Control Register extents, no intercepts need to be installed and no function needs to be executed on all processors (for now). These extents are also received from the normal kernel and added to the array of extents in the
SKPG_CONTEXT structure. The data received for each of these two extents includes base address, limit and a processor number, so multiple entries might exist for these extent types, with different processor numbers:
These extents contain the memory range for the
IDT tables on each processor, so HyperGuard will protect them from malicious modifications.
0x1013 and 0x1018 never get initialized anywhere in
SecureKernel.exe and don’t seem to be used anywhere. They may be deprecated or not fully implemented yet.
- 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