An alternative way for SSDT hooking that bypasses KPP (Kernel Patch Protection/PatchGuard). Traditional SSDT hooking is a suicide mission on modern Windows because PatchGuard obsessively monitors the KeServiceDescriptorTable. Instead of fighting the kernel, we play by its rules. We use the System Call Provider infrastructure which is a legitimate framework designed for subsystems like WSL or Pico to route syscalls through our own custom dispatch logic.
To hijack the flow, we have to populate a specific set of undocumented structures that the kernel uses to identify "Alternative" providers.
-
The Group Table: A global array of 32 "Slots." We find an empty one and claim it as our own.
-
The Dispatch Context: A small struct we write into the
_EPROCESS(typically at offset0x7D0) that tells the kernel, "Hey, this process belongs to Slot X". -
The Dispatch Table: This is our custom SSDT. It contains a
max_ssnand an array of RVAs (Relative Virtual Addresses) that point to our hook functions.
typedef struct {
UINT32 Level; // Priority
UINT32 Slot; // Index into the Group Table
} PspSyscallProviderDispatchContext;
typedef struct {
UINT32 max_ssn;
UINT32 ssn_entry_rva[1]; // Encoded as (Offset << 4) | Flags
} ssn_dispatch_table;
To understand why you're stripping the lower bits, you have to look at how the Windows Kernel (specifically nt!PsSyscallProviderDispatch) treats the values you put into your Alternative SSDT (ssn_entry_rva). It isn't just a list of pointers; it’s a packed bitfield that controls the entire dispatch pipeline.
Each entry in your ssn_dispatch_table is encoded using the following logic:
Value = (RVA << 4) | Flags
When the kernel pulls this value during a syscall, it uses the lower 4 bits to decide which path of the pipeline to take.
When a thread with the 0x20 bit set in DebugActive triggers a syscall, the kernel diverts from the standard path and calls nt!PsSyscallProviderDispatch. It uses the Slot in your EPROCESS (Offset 0x7D4) to find your row in the PspServiceDescriptorGroupTable.
The kernel looks at the bottom nibble (0–3 bits) of your entry to choose between two distinct internal engines:
-
The Fast Path (Bit 4 /
0x10is NOT set): The kernel callsPspSyscallProviderServiceDispatch. This is a "raw" jump to your handler. It's fast, but it does not capture stack arguments. If you're hooking a syscall with more than 4 arguments, your handler will likely read garbage from the stack or crash. -
The Generic Path (Bit 4 /
0x10IS set): The kernel callsPspSyscallProviderServiceDispatchGeneric. This is the "safe" route. The kernel uses the remaining lower bits to determine a QWORD count for argument copying. It captures the user-mode stack arguments and places them into a clean buffer for your handler.
Once the path is chosen, the kernel has to find where your code actually lives. Since you provided an RVA, it performs the following math:
-
Shift Right: It shifts your value right by 4 to remove the flags and recover the raw RVA.
-
Add Base: It adds this RVA to the
DriverBasepointer stored in yourPspServiceDescriptorRow. -
The Call: It finally jumps to that absolute address, passing the captured arguments to your
AltSyscallHandler.
Once the kernel completes the address reconstruction and clears the flag bits, it enters the final stage of the AltSyscall Pipeline This is where the transition from "Kernel Logic" to "Your Logic" happens.
After the kernel has calculated the absolute address of your handler and prepared the argument buffer (in the Generic Path), it performs a CALL to your driver.
-
Context Passing: The kernel doesn't just jump to your code; it passes a
CONTEXTstructure or a set of registers that contain the original user-mode state. This includes the original SSN, the return address, and the pointers to the parameters. -
Execution Isolation: Your handler now runs with full kernel privileges (
CPL 0). Because you are in the "Generic" pipeline, the kernel has already stabilized the stack for you. You can read or even modify the arguments before they are ever passed to the original function.
Your handler's return value determines what the kernel does next. This is the "Fork in the Road":
-
Return TRUE (Handled): You tell the kernel, "I’ve got this." The kernel skips the original SSDT function entirely. It takes the result you placed in
RAXand begins thesysretprocess to return to user-mode. This is how you "block" or "spoof" a syscall likeNtQuerySystemInformation. -
Return FALSE (Pass-Through): You tell the kernel, "I just wanted to watch." After your code finishes logging the activity, the kernel continues the original execution flow, eventually calling the real function in the
KeServiceDescriptorTable.