Skip to content

Instantly share code, notes, and snippets.

@Kristal-g
Created May 12, 2025 09:02
Show Gist options
  • Save Kristal-g/eec050b3fcea2a77715ef0cff4acf841 to your computer and use it in GitHub Desktop.
Save Kristal-g/eec050b3fcea2a77715ef0cff4acf841 to your computer and use it in GitHub Desktop.

Syscall Provider

Background

SyscallProvider is a feature available from Windows 11 22H2, that allows for inline hooking of syscalls.
This unfinished research was done on Windows 11 22H2. The feature is fully undocumented at the moment and it looks like it's locked to Microsoft-signed drivers.
All of the information here was gathered by manual reverse engineering of securekernel.exe, skci.dll and ntoskrnl.exe.
The kernel exports three functions to work with the new feature: PsRegisterSyscallProvider, PsQuerySyscallProviderInformation, PsUnregisterSyscallProvider.
This writeup will explore how this feature is initialized, how it works internally, and how to interact with it and use it.

Initialization

Some structures and flags have to be checked and initialized when the machine boots, so later drivers could add their on provider.
The first condition that's checked in initialization is that the flag VslVsmEnabled is True.
A way to enable VSM on your machine is:

reg add "HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard" /v "EnableVirtualizationBasedSecurity" /t REG_DWORD /d 1 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard" /v "Locked" /t REG_DWORD /d 0 /f

It shows us that this feature depends on VSM (Virtual Secure Mode) being enabled, and actually a lot of the inner workings of this feature lie in securekernel.exe.
The call flow that leads to the initialization in the secure kernel is:

... -> Phase1Initialization -> IoInitSystem -> IoInitSystemPreDrivers -> PsInitializeSyscallProviders -> VslpEnterIumSecureMode(SECURESERVICE_INITIALIZE_SYSCALL_PROVIDERS) --- securekernel.exe ---> IumInvokeSecureService -> SkmmInitializeSyscallProviders

The arguments that are passed to the secure kernel are the global variable PspServiceDescriptorGroupTable and size 32. This variable, which is in the "CFGRO" section, represents a table of maximum of 16 pairs of syscall providers callback arrays. When a new syscall provider is registered, a pair of callback arrays have to be provided - one for normal Nt syscalls, and the other for win32k (gui-related) syscalls.
The secure kernel does sanity checks about the size of the table and on its location in the CFGRO, section and then creates a SecurePool allocation for saving internal information about it later.
It then saves the pointer to this table in the global variable SkmiNtServiceDescriptorGroupTable and the size of it at SkmiNtServiceDescriptorGroupTableEntries.
So this initialization just makes room for storing information about the service descriptor tables.
If the initialization succeeds, the kernel marks it through the global flag PspSyscallProvidersEnabled.

Then, the kernel initializes the "metadata" of the tables. The metadata is information about each Service Descriptor Table defined in the OS, and more specifically, the number of arguments for each syscall.
To do this, it calls the function PsRegisterSyscallProviderServiceTableMetadata(unsigned int service_table_index) and the full callstack to the secure kernel looks like:

PsInitializeSyscallProviders -> PsRegisterSyscallProviderServiceTableMetadata(index=0) -> VslRegisterSyscallProviderServiceTableMetadata -> VslpEnterIumSecureMode(SECURESERVICE_REGISTER_SYSCALL_PROVIDER_SERVICE_TABLE_MD) --- securekernel.exe ---> IumInvokeSecureService -> SkmmRegisterSyscallProviderServiceTableMetadata

The parameter is index into KeServiceDescriptorTableShadow, from which it grabs a pointer to the Argument Table of the specified SDT and tells the secure kernel to fully copy the table to its own memory space.
SkmmRegisterSyscallProviderServiceTableMetadata stores the argument table at SkmiServiceTableMetadata, which is able to contain only two pointers.
Further down the boot process, win32k calls KeAddSystemServiceTable to register the SDT for win32k syscalls, and that in turns registers its metadata with PsRegisterSyscallProviderServiceTableMetadata(index=1).
In the securekernel there's a check that only indexes 0 and 1 are registered and it doesn't accept other values, so the slots are full and there's no way for us to register arbitrary table metadata.

To recap, at this stage the secure kernel variable SkmiNtServiceDescriptorGroupTable contains room for syscall provider service descriptor groups, and the SkmiServiceTableMetadata variable contains the number of arguments each syscall takes.

Syscall hooks flow

When a usermode application triggers a syscall instruction, it ends up in the KiSystemCall64 function (or in KiSystemCall64Shadow if KVAS is enabled but that doesn't matter for us here).
In the KiSystemCall64 function there's now a check if the bits for the flags Minimal and AltSyscall are set in KTHREAD->DISPATCHER_HEADER->DebugActive.
Minimal refers to Pico processes, and the AltSyscall refers to an old Windows feature that got replaced by Syscall Provider.
If either of these flags is set, it calls PsSyscallProviderDispatch. That means that the SyscallProvider is atleast on a per-thread basis (later we'll see it's actually per process).

Instead of explaining AltSyscalls, I'll refer to some resources on that and that great summary from one of them:

In conclusion, it seems that at the moment, the whole alternative system call handler mechanism is closed for internal use only, as the first member of the handler array is reserved for use by pico providers and the second one for use by core Windows drivers, which are Microsoft signed drivers that are designed to be loaded before any other driver. So the only way to have a use for this mechanism right now is to bypass/disable PatchGuard and modify the pointer in the handlers array.

Inside the dispatcher function

First, if the thread belongs to a Pico process it calls the Pico syscalls handler, and otherwise it continues to the SyscallProvider flow. Every process has these new fields:

struct _PS_SYSCALL_PROVIDER* SyscallProvider;                                 //0xb50
struct _LIST_ENTRY SyscallProviderProcessLinks;                               //0xb58
struct _PSP_SYSCALL_PROVIDER_DISPATCH_CONTEXT SyscallProviderDispatchContext; //0xb68 

SyscallProviderDispatchContext is actually the only struct related to this feature that's documented. Its definition:

struct _PSP_SYSCALL_PROVIDER_DISPATCH_CONTEXT
{
  unsigned int Level;
  unsigned int Slot;
};

Currently, Level should always be zero, otherwise it crashes. It's used in a process's opt-in flow to determine the slot of the SyscallProvider. I guess this field would be used in later versions to allow syscall providers to have more than one context, each with its own callback array.

The dispatcher uses the Slot field as an index to the PspServiceDescriptorGroupTable array. I define the descriptor group as:

struct _PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR_GROUP
{
  void *DriverBaseAddress;
  _PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR *PspDescriptorArray[2];
};

It then uses the 12 bit (0x1000) of the syscall num to access to ServiceDescriptorGroup it took from the array. This bit identifies whether to get the Win32k syscalls (GUI) array, or the normal nt syscalls array. 1 == Win32k, 0 == NT.
I define Syscall Provider Service Descriptor as:

struct _PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR
{
  DWORD Size;
  DWORD Entries[];
};

Indexing to the Entries array is through the unmodifed syscall number, and each value in this array contains both the number of arguments of the syscall and an RVA to the handler function, in a packed way:

struct _PS_SYSCALL_PROVIDER_SERVICE_ENTRY {
union {
  DWORD serviceEntry;
  struct {
    DWORD ArgsCount : 4;
    DWORD IsGenericFlag : 1;
    DWORD FunctionRVA : 27;
    };
  };
}

The RVA is relative to the DriverBaseAddress in the ServiceDescriptorGroup struct. Specific branches are taken if the RVA is 0 or 1. In case of RVA==0 the dispatcher returns and continues to the original syscall handler, and in the case of RVA==1 then the kernel raises an exeception.

The first four bits are the argument count, and the fifth bit is used as a flag to pick to correct dispatcher function. It that bit is set, PspSyscallProviderServiceDispatchGeneric is called and otherwise PspSyscallProviderServiceDispatch is called. I call it the "NonGeneric" callback. PspSyscallProviderServiceDispatchGeneric calls the default callback that was chosen in registration for all syscalls. More on that later.
With the "Generic" callback you can decide whether to let the kernel to continue to the original syscall handler or not, but with the NonGeneric callback the kernel never calls the original syscall.
Both callbacks receive as arguments the first four parameters to the syscalls, by getting the registers Rcx, Rdx, R8 and R9. Other registers are saved on the stack before the callback is called.

Provider registration

To use the SyscallProvider mechanism, a driver must have all the registration information compiled into its binary, organized in a section called scpcblk (probably short for Syscall Provider Callbacks Block).
When a driver is loaded the secure kernel creates for it a Secure Image Section object and a Nar object.
A driver's Nar object holds a lot of information about the driver, among them the SyscallProvider registration data and a pointer to the driver's Secure Image Section object.
Registration data is parsed in the SkmiCaptureSyscallProviderImageData function, which is called from SkmiPrepareDriverCfgState where the secure kernel parses a lot of data from the image.

SkmiCaptureSyscallProviderImageData

At the begining, this function verifies a few things: that the SyscallProvider registration data wasn't parsed already from the driver, and that the driver's Secure Image Object has a specific bit turned on.

if ( !DriverSecureImageObject
    || DriverNar->pSyscallProviderDescriptorGroup
    || (*(DriverSecureImageObject + 152) & 0x200) == 0 )
  {
    return 0;
  }

That bit is initialized in SkmmFinishSecureImageValidation -> SkciFinishImageValidation -> SkciDetermineAuthorizedPageUse, where various signing-related information is parsed:

// Simplified
if (CiCompareSigningLevels(driverSigningLevel, SE_SIGNING_LEVEL_WINDOWS))
  *flags |= 0x200;
if (CiCompareSigningLevels(driverSigningLevel, SE_SIGNING_LEVEL_MICROSOFT))
{
  if (MincryptIsEKUInPolicy(v6, "1.3.6.1.4.1.311.76.8.1") || ((g_CiOptions & 8 /*TestModeEnabled*/) != 0))
  {
    *flags |= 0x200;
  }
}

Basically the driver must have a Windows Signing Level, or if it has a Microsft Signing Level it requires TestMode enabled or to have the Microsoft Publisher in its EKU.
Third party drivers can't get a Windows Signing Level and can't get the Microsoft Publisher in their certificate, so it means this feature is locked for Microsoft usage only.

Usermode opt-in flow

NtSetInformationProcess (ProcessAltSystemCallInformation = 100)

It calls:
PspSyscallProviderOptIn
PspAttachProcessToSyscallProvider

PspSyscallProviderOptIn

Search through the list of SyscallProviders for the given Guid (or other similar) and get the specific SyscallProvider. Then it tests for several things:

  • Make sure the Level is not 0 and that the Slot of the level is initialized
  • Set the AltSyscall flag on the EPROCESS (used as a lock)
  • Make sure the process wasn't attached to a syscall provider already

Then it calls the SyscallProvider's callback (filter) function that takes as parameters (_EPROCESS * pProcess, DWORD * ParentPid).
If the filter return success value (>=0) then it attaches the provider to the process.
It takes the process' lock, and goes through its list of threads; for each thread it sets the bit for AltSyscall.
That way, all the current threads of the process are marked for AltSyscall.
On new threads creation, the kernel updates the AltSyscall flag on the thread in PspInsertThread. On new process creations under a monitored parent process:
PspInsertProcess -> PspInheritSyscallProvider -> PspAttachProcessToSyscallProvider.

You can't detach a process from a Syscall Provider after it was attached.
The only reference to PspDetachProcessFromSyscallProvider is from PspProcessDelete.

Reversed internals

ntoskrnl:
__int64 __fastcall PspSyscallProviderServiceDispatch(_KTRAP_FRAME *pTrapFrame, __int64 (__fastcall *callbackPtr)(unsigned __int64, unsigned __int64, unsigned __int64, unsigned __int64), BYTE ArgsCount)
void __fastcall PspDereferenceSyscallProvider(_PS_SYSCALL_PROVIDER *pSyscallProvider)
_EX_PUSH_LOCK **__fastcall PspInitializeSyscallProvider(_PS_SYSCALL_PROVIDER *, _PS_SYSCALL_PROVIDER_OBJECT *someObject, _GUID *syscall_provider_id_struct_maybe, PVOID functionPtr)
__int64 __fastcall PspLookupSyscallProviderById(_PS_SYSCALL_PROVIDER_OPT_IN *someSyscallProviderIdStruct, _PS_SYSCALL_PROVIDER *pSyscallProvider)
__int64 __fastcall PspInheritSyscallProvider(_EPROCESS *NewProcess, _EPROCESS *ParentProcess)
__int64 __fastcall PsRegisterSyscallProviderServiceTableMetadata(unsigned int service_table_index)
__int64 __fastcall VslPublishSyscallProviderServiceTables(_QWORD *pSyscallProviderServiceDescriptorGroup, ULONG *output)
__int64 __fastcall VslRegisterSyscallProviderServiceTableMetadata(unsigned int service_table_index, int service_table_limit, int *p_args_table)
__int64 __fastcall PsQuerySyscallProviderInformation(_PS_SYSCALL_PROVIDER *pSyscallProvider, int unused_should_be_null, _QWORD *outputBuffer, unsigned __int64 outputBufferSize, _QWORD *bufferSizeNeeded)
__int64 __fastcall PsRegisterSyscallProvider(_PS_SYSCALL_PROVIDER_OBJECT *someObj, _PS_SYSCALL_PROVIDER_REGISTRATION *someStruct, _PS_SYSCALL_PROVIDER **ppSyscallProvider)
__int64 __fastcall PsSyscallProviderDispatch(_KTRAP_FRAME *pTrapFrame)
void __fastcall PsUnregisterSyscallProvider(_PS_SYSCALL_PROVIDER *pSyscallProvider)
__int64 __fastcall PspAttachProcessToSyscallProvider(_EPROCESS *pProcess, struct _PS_SYSCALL_PROVIDER *pSyscallProvider, unsigned int Level)
void __fastcall PspDestroySyscallProvider(_PS_SYSCALL_PROVIDER *pSyscallProvider)
void __fastcall PspDetachProcessFromSyscallProvider(_EPROCESS *pProcess)
_EPROCESS *__fastcall PspGetNextSyscallProviderProcess(_PS_SYSCALL_PROVIDER *pSyscallProvider, _EPROCESS *pProcess)
__int64 __fastcall PspInsertSyscallProvider(_PS_SYSCALL_PROVIDER *pSyscallProvider)
__int64 __fastcall PspLookupSyscallProviderByIdNoLock(_PS_SYSCALL_PROVIDER_OPT_IN *syscallProviderOptInStruct, _PS_SYSCALL_PROVIDER *pSysProvider)
__int64 __fastcall PspQuerySyscallProviderProcessList(_PS_SYSCALL_PROVIDER *pSyscallProvider, unsigned __int64 *pdwArrSize_maybe, _QWORD *outputBuffer)
__int64 __fastcall PspSyscallProviderOptIn(_EPROCESS *pProcess, _PS_SYSCALL_PROVIDER_OPT_IN *syscallProviderOptInStruct)
__int64 __fastcall PspSyscallProviderServiceDispatchGeneric(_KTRAP_FRAME *pTrapFrame, __int64 (__fastcall *callbackPtr)(__int64, _QWORD, __int64 *, _QWORD *), BYTE ArgsCount, unsigned int syscallNumber, _QWORD *return_value)
void PsInitializeSyscallProviders()


securekernel:
_IMAGE_SECTION_HEADER *__fastcall SkmiCaptureSyscallProviderImageData(__int64 imageBase, __int64 a2, _IMAGE_NT_HEADERS64 *NtHeader, _SK_NAR_NODE_R *DriverNar)
__int64 __fastcall SkmiCaptureSyscallProviderServiceTableData(__int64 imageBase, unsigned int SizeOfImage, unsigned int RVA, _PS_SYSCALL_PROVIDER_SERVICE_TABLE_ENTRIES_TABLE *serviceTableEntriesDescriptor)
__int64 __fastcall SkmiCleanSyscallProviderServiceDescriptorGroup(_PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR_GROUP *pScpServiceDescriptorGroup)
__int64 __fastcall SkmiCreateSyscallProviderServiceTable(_PS_SYSCALL_PROVIDER_REGISTRATION_SK *pSyscallProvider, int service_table_limit_maybe, unsigned int syscall_provider_service_table_index, _PS_SYSCALL_PROVIDER_SERVICE_TABLE_ENTRY_DESCRIPTOR *pScpServiceTableEntryDescriptor, _PS_SYSCALL_PROVIDER_SERVICE_TABLE_ENTRIES_TABLE **p_syscall_provider_service_table_entries_SK_mem)
__int64 __fastcall SkmiFreeSyscallProviderImageData(_SK_NAR_NODE_R *)
__int64 __fastcall SkmiFreeSyscallProviderInfo(_PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR_GROUP *descriptorGroup)
__int64 __fastcall SkmiMakeSyscallProviderServiceTableEntry(_PS_SYSCALL_PROVIDER_REGISTRATION_SK *FunctionsBase, unsigned int someLimitMaybe, PVOID callbackPtr, int someFlag, BYTE ArgsCount, _DWORD *pCompactedSyscallEntry)
__int64 __fastcall SkmiPublishSyscallProviderServiceTables(_SK_NAR_NODE_R *NarOfObject, _PS_SYSCALL_PROVIDER_REGISTRATION_SK **p_obj_from_kernel)
__int64 __fastcall SkmiRevokeSyscallProviderServiceTables(_PS_SYSCALL_PROVIDER_REGISTRATION_SK *obj_from_kernel, _PS_SYSCALL_PROVIDER_REGISTRATION_SK *obj_from_kernel_2)
__int64 __fastcall SkmmInitializeSyscallProviders(__int64 *PspServiceDescriptorGroupTable_nt_ptr, __int64 size_maybe)
__int64 __fastcall SkmmPublishSyscallProviderServiceTables(__int64 somedata_from_kernel, _DWORD *output_to_km_maybe)
__int64 __fastcall SkmmRegisterSyscallProviderServiceTableMetadata(unsigned int service_table_index, _MDL *sdt_args_table_DataMdl, __int64 pfn)
__int64 __fastcall SkmmRevokeSyscallProviderServiceTables(__int64)


ntoskrnl:
struct _PSP_SYSCALL_PROVIDER_DISPATCH_CONTEXT
{
  unsigned int Level;
  unsigned int Slot;
};

struct _PS_SYSCALL_PROVIDER
{
  _LIST_ENTRY pspSyscallProviders_list;
  _GUID guid;
  _PS_SYSCALL_PROVIDER_OBJECT *someObjPtr;
  _QWORD *filterFunctionPtr;
  _QWORD ref_count;
  _EX_RUNDOWN_REF run_ref;
  _EX_PUSH_LOCK *pushLock;
  _LIST_ENTRY procSyscallProviderProcessLinks;
  _PSP_SYSCALL_PROVIDER_DISPATCH_CONTEXT providerDispatchContext[];
};

struct __declspec(align(4)) _PS_SYSCALL_PROVIDER_REGISTRATION
{
  int int_should_end_in_one;
  _GUID guid;
  int pad0;
  PVOID filterFunctionPtr;
};

struct _PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR_GROUP
{
  _QWORD *DriverBaseAddress;
  _PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR *PspDescriptorArray[2];
};

struct _PS_SYSCALL_PROVIDER_OBJECT
{
  _QWORD qword0;
  _QWORD qword8;
  _QWORD proc_ref_count;
  _QWORD *service_descriptor_group;
};

struct _PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR
{
  DWORD size;
  DWORD Entries[];
};

struct _PS_SYSCALL_PROVIDER_OPT_IN
{
  _GUID guid;
  _QWORD level;
};


securekernel:
struct _SK_NAR_NODE_R
{
  _RTL_BALANCED_NODE balancedNode;
  _QWORD BaseAddress;
  _QWORD *NtInvertedFunctionTableReference;
  IMAGE_LOAD_CONFIG_DIRECTORY64 *pLoadConfig;
  _QWORD qword30;
  _QWORD DriverSecureImageObject;
  unsigned int r_sparseTableRelated;
  unsigned int cfgroSection_VirtualAddress;
  unsigned int cfgroSection_PhysicalAddress;
  unsigned int unsigned_int4c;
  _QWORD SizeInPages;
  unsigned int secureImageSizeInPages;
  _WORD refcount;
  _WORD someFlags;
  _SK_NAR_NODE_R *selfPtr;
  _QWORD qword68;
  _BYTE gap70[16];
  _QWORD pPoolAllocation3;
  _DWORD dword88;
  _BYTE gap8C[12];
  _QWORD pPoolAllocation2;
  _QWORD pPoolAllocation1;
  _DWORD dwordA8;
  _SK_NAR_NODE_R *SomeDependantNar;
  _QWORD qwordB8;
  _DWORD dwordC0;
  _QWORD pLoadConfigIpValidationTablesStruct;
  _PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR_GROUP *pSyscallProviderDescriptorGroup;
};

struct __declspec(align(4)) _PS_SYSCALL_PROVIDER_SERVICE_TABLE_ENTRIES_TABLE
{
  _DWORD tableSize;
  _DWORD compactedCallbackPtrs[4096];
};

struct _PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR_GROUP
{
  _QWORD *FunctionsBaseAddress;
  _PS_SYSCALL_PROVIDER_SERVICE_TABLE_ENTRIES_TABLE *DescriptorBase_KM[2];
};

struct _PS_SYSCALL_PROVIDER_SERVICE_DESCRIPTOR
{
  DWORD size;
  DWORD RVAs[];
};

struct _PS_SYSCALL_PROVIDER_SERVICE_TABLE_ENTRY_DESCRIPTOR
{
  _DWORD tag;
  _DWORD reserved;
  PVOID genericCallbackPtr;
  _DWORD nonGenericCallbacksArraySize;
  _PS_SYSCALL_PROVIDER_SERVICE_TABLE_ENTRY_DESCRIPTOR_NON_GENERIC_CALLBACK nonGenericCallbacksArray[];
};

struct _PS_SYSCALL_PROVIDER_SERVICE_TABLE_ENTRY_DESCRIPTOR_NON_GENERIC_CALLBACK
{
  _DWORD syscallNumber;
  _DWORD someFlag;
  _QWORD *nonGenericCallbackPtr;
};

References

Syscalls: https://alice.climent-pommeret.red/posts/a-syscall-journey-in-the-windows-kernel/
Secure Pool: https://windows-internals.com/secure-pool/

Hyperguard:
https://windows-internals.com/hyperguard-secure-kernel-patch-guard-part-1-skpg-initialization/

Secure kernel objects:
https://ntnuopen.ntnu.no/ntnu-xmlui/bitstream/handle/11250/2448948/18109_FULLTEXT.pdf
https://i.blackhat.com/USA-20/Thursday/us-20-Amar-Breaking-VSM-By-Attacking-SecureKernal.pdf

SKCI:
https://csrc.nist.gov/csrc/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp2607.pdf

Signing levels:
https://n4r1b.com/posts/2022/09/smart-app-control-internals-part-2/
http://2012.ruxconbreakpoint.com/assets/Uploads/bpx/alex-breakpoint2012.pdf

Alt Syscalls:
https://lesnik.cc/hooking-all-system-calls-in-windows-10-20h1/
https://github.com/DownWithUp/CallMon
https://github.com/0xcpu/WinAltSyscallHandler

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment