Welcome to Part
2 of the series about Secure Kernel Patch Guard, also known as HyperGuard. This part will start describing the data structure and components of SKPG, and more specifically the way it’s activated. If you missed Part
1, you can find it right here.
Inside HyperGuard Activation
1 of the series I introduced
HyperGuard and described its different initialization paths. Whichever path we went through, we end up reaching
SkpgConnect when the normal kernel finished its initialization. This is when all important data structures in the kernel have already been initialized and can start being monitored and protected by
After a couple of standard input validations,
SkpgConnectionLock and checks the
SkpgInitialized global variable to tell if
HyperGuard has already been initialized. If the variable is set, the function will return
STATUS_SUCCESS, depending on the information received. In either of those cases, it will do nothing else.
If SKPG has not been initialized yet,
SkpgConnect will start initializing it. First it calculates and saves multiple random values to be used in several different checks later on. Then it allocates and initializes a context structure, saved in the global
SkpgContext. Before we move on to other SKPG areas, it’s worth spending a bit of time talking about the SKPG context.
This SKPG context structure is allocated and Initialized in
SkpgConnect and will be used in all SKPG checks. It contains all the data needed for
HyperGuard to monitor and protect the system, such as the NT PTE information, encryption algorithms, KCFG ranges, and more, as well as another timer and callback, separate to the ones we saw in the first part of the series. Unfortunately, like the rest of
HyperGuard, this structure, which I’ll call
SKPG_CONTEXT, is not documented and so we need to do our best to figure out what it contains and how it’s used.
First, the context needs to be allocated. This context has a dynamic size that depends on the data received from the normal kernel. Therefore, it is calculated at runtime using the function
SkpgComputeContextSize. The minimal size of the structure is
0x378 bytes (this number tends to increase every few Windows builds as the context structure gains new fields) and to that will be added a dynamic size, based on the data sent from the normal kernel.
That input data, which is only sent when SKPG is initialized through the
PatchGuard code paths, is an array of structures named Extents. These extents describe different memory regions, data structures and other system components to be protected by
HyperGuard. I will cover all of these in more detail later in the post, but a few examples include the
IDT, data sections in certain protected modules and MSRs with security implications.
After the required size is calculated, the
SKPG_CONTEXT structure is allocated and some initial fields are set in
SkpgAllocateContext. A couple of these fields include another secure timer and a related callback, whose functions are set to
SkpgHyperguardRuntime. It also sets fields related to PTE addresses and other paging-related properties, since a lot of the
HyperGuard checks validate correct Virtual->Physical page translations.
SkpgInitializeContext is called to finish initializing the context using the extents provided by the normal kernel. This basically means iterating over the input array, using the data to initialize internal extent structures, that I’ll call
SKPG_EXTENT, and sticking them at the end of the
SKPG_CONTEXT structure, with a field I chose to call
ExtentOffset pointing to the beginning of the extent array (notice that none of these structures are documented, so all structure and field names are made up):
There are many different types of extents, and each
SKPG_EXTENT structure has a
Type field indicating its type. Each extent also has a hash, used in some cases to validate that no changes were done to the monitored memory region. Then there are fields for the base address of the monitored memory and the number of bytes, and finally a union that contains data unique to each extent type. For reference, here is the reverse engineered
typedef struct _SKPG_EXTENT
} SKPG_EXTENT, *PSKPG_EXTENT;
I mentioned that the input extents used by
HyperGuard were provided by the
PatchGuard initializer function in the normal kernel. But SKPG initializes another kind of extents as well – secure extents. To initialize those,
SkpgInitializeContext calls into
SkpgCreateSecureKernelExtents, providing the
SKPG_CONTEXT structure and the address where the current extent array ends – so the secure extents can be placed there. Secure extents use the same
SKPG_EXTENT structure as regular extents and protect data in the secure kernel, such as modules loaded into the secure kernel and secure kernel memory ranges.
Like I mentioned, there are many different types of extents, each used by
HyperGuard to protect a different part of the system. However, we can split them into a few groups that share similar traits and are handled in a similar way. For clarity and to separate normal extents from secure extents, I will use the naming convention
SkpgExtent for normal extent types and
SkpgExtentSecure for secure extent types.
The first extent that I’d like to cover is a pretty simple one that always gets sent to
SkpgInitializeContext regardless of other input:
There is one extent that doesn’t belong in any of the groups since it is not involved in any of the
HyperGuard validations. This is extent
SkpgExtentInit – this extent is not copied to the array in the context structure. Instead, this extent type is created by
SkpgConnect and sent into
SkpgInitializeContext to set some fields in the context structure itself that were previously unpopulated. These fields have additional hashes and information related to hotpatching, such as whether it is enabled and the addresses of the retpoline code pages. It also sets some flags in the context structure to reflect some configuration options in the machine.
Memory and Module Extents
This group includes the following extent types:
The thing all these extent types have in common is that they all indicate some memory range to be protected by
HyperGuard. Most of these contain memory ranges in the normal kernel, however
VTL1 memory ranges and modules. Still, all these extent types are handled in a similar way regardless of the memory type or
VTL so I grouped them together.
When normal memory extents are being added to the SKPG Context, all normal kernel address ranges get validated to ensure that the pages have a valid mapping for SKPG protection. For a normal kernel page to be valid for SKPG protection, the page can’t be writable. SKPG will monitor all requested pages for changes, so a writable page, whose contents can change at any time, is not a valid “candidate” for this kind of protection. Therefore, SKPG can only monitor pages whose protection is either “read” or “execute”. Obviously, only valid pages (as indicated by the Valid bit in the PTE) can be protected. There are slight differences to some of the memory extents when HVCI is enabled as SKPG can’t handle certain page types in those conditions.
Once mapped and verified, each memory page that should be protected gets hashed, and the hash gets saved into the
SKPG_EXTENT structure where it will be used in future
HyperGuard checks to validate that the page wasn’t modified.
Some memory extents describe a generic memory range, and some, like
SkpgExtentImagePage, describe a specific memory type that needs to be treated slightly differently. This extent type mentions a specific image in the normal kernel, but
HyperGuard should not be protecting the whole image, only a part of it. So the input extent has the image base, the page offset inside the image where the protection should start and the requested size. Here too the memory region to be protected will be hashed and the hash will be saved into the
SKPG_EXTENT to be used in future validations.
SKPG_EXTENT structures that get written into the SKPG Context normally only describe a single memory page while the system might want to protect a much larger area in an image. It is simply easier for
HyperGuard to handle memory validations one page at a time, to make for more predictable processing time and avoid taking up too much time while hashing large memory ranges, for example. So, when receiving an input extent where the requested size is larger than a page (
SkpgInitializeContext iterates over all the pages in the requested range and creates a new
SKPG_EXTENT for each of them. Only the first extent, describing the first page in the range, receives the type
SkpgExtentImage. All the other ones that describe the following pages receive a different type,
0x1014, which I chose to call
SkpgExtentPartialMemory, and the original extent type is placed in the first
2 bytes in the type-specific data inside the
Every extent in the array can be marked by different flags. One of these is the
Protected flag, which can only be applied to normal kernel extents, meaning that the specified address range should be protected from changes by SKPG. In this case,
SkpgInitializeContext will call
SkmmPinNormalKernelAddressRange on the requested address range to pin in and prevent it from being freed by
The secure memory extents essentially behave very similar to the normal memory extent, with the main differences being that they are initialized by the secure kernel itself and the details of what they are protecting.
Extents of type
SkpgExtentSecureModule are generates to monitor all images loaded into the secure kernel space. This is done by iterating the
SkLoadedModuleList global list, which, like the normal kernel’s
PsLoadedModuleList, is a linked list of
KLDR_DATA_TABLE_ENTRY structures representing all loaded modules. For each one of those modules,
SkpgCreateSecureModuleExtents is called to generate the extents.
To do so,
SkpgCreateSecureModuleExtents receives a
KLDR_DATA_TABLE_ENTRY for one loaded DLL at a time, validates that it exists in
PsInvertedFunctionTable (a table containing basic information for all loaded DLLs, mostly used for quick search for exception handlers) and then enumerates all the sections in the module. Most sections in a secure module are monitored using an
SKPG_EXTENT but are not protected from modifications. Only one section is being protected, the
TABLERO section is a data section that exists in only a handful of binaries. In the normal kernel it exists in Win32k.sys, where it contains the win32k system service table. In the secure kernel a
TABLERO section exists in securekernel.exe, where it contains global variables such as
SkmiNtPteBase, and others:
SkpgCreateSecureModuleExtents encounters a
TABLERO section, it calls
SkmmProtectKernelImageSubsection to change the PTE for the section pages from the default read-write to read only.
Then for each section, regardless of its type, an extent with type
SkpgExtentSecureModule is created. Each memory region gets hashed a flag in the extent marks if the section is executable. The number of extents generated per section can vary: If HotPatching is enabled on the machine a separate extent will be generated for every page in the protected image ranges. Otherwise, every protected section generates one extent that might cover multiple pages, all of them with type
If HotPatching is enabled, one last secure module extent gets created for each secure module. The variable
SkmiHotPatchAddressReservePages will indicate how many pages are reserved for HotPatch use at the end of the module, and an extent gets created for each of those pages. Similar to the way described earlier for normal kernel module extents, each extent describes a single page, the extent type is
SkpgExtentPartialMemory and the type
SkpgExtentSecureModule is placed in one of the type-specific fields of the extent.
Another secure extent type is
SkpgExtentSecureMemory. This is a generic extent type used to indicate any memory range in the secure kernel. However, for now it is only used to monitor the
GDT pointed to by the secure kernel processor block – the
SKPRCB. This is an internal structure that is similar in its purpose to the normal kernel’s
KPRCB (and similarly, an array of them exists in
SkeProcessorBlock). There will be one extent of this type for each processor in the system. Additionally, the function sets a bit in the Type field of each
KGDTENTRY64 structure to indicate that this entry has been accessed and prevent it from being modified later on – but the entry for the
TSS at offset
0x40 gets skipped:
This pretty much covers the initialization and uses of the memory extents. But this is just the first group of extents, and there are many others that monitor various different parts of the system. In the next post I’ll talk about more of these other extent types, which interact with system components like MSRs, control registers, the
KCFG bitmap and more!
- 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