Alt Syscalls for Windows 11

Keep Calm and Thunk On!


Intro

You can check this project out on GitHub! To help you find exact code samples see the below links:

Alternate Syscalls have been documented previously for Windows 10, starting with the research by 0xcpu, and then a nice write up by Xacone. There is a problem however; the mechanisms outlined by 0xcpu don’t work on Windows 11.

With this, I started my own research into the changes in the Windows 11 Kernel that Microsoft have done for alternate syscalls; its fair to say they have completely reworked the mechanism. I’ll start by saying, in my testing, PatchGuard does not monitor the structures used in this - if you find different, please let me know either by email (fluxsec@proton.me), or at X - @0xfluxsec. I haven’t had chance to test this with HyperGuard (I bricked my HyperGuard VM and need to set it all up again with the jailbreak) - however, I do not think HyperGuard will currently monitor this, I’ll explain why later, but this is something on my radar to test. I expect in the future, HyperGuard will likely monitor these structures, but who knows! The kernel is a big place, and things will occasionally get missed by human nature. I have a section on this page dedicated to HyperGuard, so keep reading to find out more of my observations.

The code samples in this report are in Rust (sorry C people), I may make a C port if it’s a popular request.

What are alt syscalls

Alternate Syscalls (or Alt Syscalls) are a fully undocumented feature of the Windows Kernel, which allows a callback routine to receive a copy of trap data for a syscall in the kernel. It has been commented, this is likely an internal only feature Microsoft planned to use in Windows Defender.

The use of Alt Syscalls in the kernel is a massive benefit for AntiVirus or Endpoint Detection and Response software, the ability to hook system call’s has not been possible for a long time thanks to PatchGuard which monitors alteration to key internal structures, such as the System Service Dispatch Table, or the Nt* functions themselves implementing the logic.

As such, security vendors moved out of the kernel and hooked system call stubs in userland via a DLL. I have done this myself, you can check out my blog post on that here.

With the introduction of the undocumented Alt Syscalls in Windows 11, one could essentially get rid of the DLL hooks in userland on syscalls and move them into the kernel. Why would you want to do this? Techniques such as HellsGate (which I have written about here), and Halos Gate, can bypass these userland hooks. Whilst there will be technologies out there to try combat this (such as my Ghost Hunting technique), it is still, in 2025, an exploitable weakness of security products.

So, if we can monitor syscalls in the kernel - we do not need userland hooks.

Conversely a rootkit could utilise kernel syscall hooks to monitor operations on the system, but to be honest - they can get most of that data from Events Tracing for Windows, or other sources. I’m not entirely sure its worth the effort.

Reversing the kernel mechanism

To save duplication; I would recommend first reading Xacones write-up of Windows 10 AltSyscalls to see what the implementation there looked like, the author does a really good job of explaining. That will provide a background on some of what we discuss now.

Type differences

Before we go on; lets look at some new fields on the _EPROCESS:

The only new struct we make use of for this method, is SyscallProviderDispatchContext. This struct contains two fields, Level and Slot.

The SyscallProvider field seems to be used for logging purposes, perhaps an identifier as to who the active provider is.

The system dispatch routine

For those not in the know, I am currently making my own EDR called Sanctum - this has turned into a full time hobby; and its basically my mechanism of learning more about Windows Internals and low level security. During that journey, I looked to implement AltSyscalls. I quickly discovered this wasn’t working in my Windows 11 VM, and raised an issue in Xacone’s EDR project.

I had discovered that the PsAltSystemCallHandlers array is not used in Windows 11, save for in the (still available) function PsRegisterAltSystemCallHandler.

PsRegisterAltSystemCallHandler in Windows 11

Hmm; this symbol was pivotal to the Alt Syscall technique in Windows 10. Alright - lets start from the beginning.

System Calls in Windows go through a set routine which eventually gets them into a function called: KiSystemServiceUser (in Binary Ninja you’ll have to view this as part of KiSystemService64 / KiSystemService).

We can see from the control flow graph of IDA, KiSystemServiceUser has a branch which checks first whether the Header union is flagged with data under DebugActive, and then whether the bit is set (0x24) for AltSyscall.

Control Flow Graph System Calls in Windows 11

We can see, if the right bits are active then we will branch into calling PsSyscallProviderDispatch, passing in the KTRAP_FRAME as its argument.

PsSyscallProviderDispatch

N.b., we will look at this diagram again later; but I think it helps at this point whilst working through this section. These are the structs I landed on to enable alt syscalls:

Windows 11 Alternate Syscalls

This function replaces the Windows 10 PsAltSystemCallDispatch that was used in 0xcpu’s technique, and its now more complex.

First, again, the function checks whether the DebugActive flag is enabled, and checks that the Slot field mentioned above is < 20.

PsSyscallProviderDispatch Windows 11

It then loads the PspServiceDescriptorGroupTable symbol. We can infer the size of each entry from the first part of the operations here, the size of each entry is 0x18 bytes in length. It’s easier to see this with the decompilation view:

PsSyscallProviderDispatch Windows 11

There are a lot of parallels and patterns here which you’ll find in normal syscall dispatch around calculating relative addresses in the registers. The below took me a while to understand ^^.

The function gets the SSN (System Service Number) from the KTRAP_FRAME, and checks it against some value starting at the PspServiceDescriptorGroupTable table entry we got (i.e. the first field). This field is the count of SSN’s we provide for (or, more correctly, the capacity). whilst testing, this was a large source of instability, if the SSN is less than the count of SSNs the table contains, it will just return out of the function.

It then pulls out an address, which ended up being for a thunk, from the descriptor entry. Unlike Windows 10, in Windows 11 you must first allocate an RWX buffer and stitch in a small shellcode “thunk” per SSN (16 byte max each). You will see more on the thunk later. The kernel computes the jump target as:

thunk_address = entry.thunk_base + SSN * thunk_size

As it indexes into the buffer by SSN, you can’t just stick 1 thunk in there (or use it to increase the amount of shellcode you are allowed to write). Thinking like a hacker, I suppose if you could get arbitrary write in the kernel, this would be a good region of memory to target :).

It then extracts some flags out of the table entry; these determine whether we make the call to our Alt Syscall callback routine via PspSyscallProviderServiceDispatch or via PspSyscallProviderServiceDispatchGeneric. I have only gone the PspSyscallProviderServiceDispatch route, so if you wish to try get this working for PspSyscallProviderServiceDispatch feel free! Do note however, if you try set a flag for that route with the same callback I have used, you will get a bug check, unfortunately my callback prototype is not valid for PspSyscallProviderServiceDispatch.

To make it a little clearer:

PsSyscallProviderDispatch Windows 11

PspSyscallProviderServiceDispatchGeneric

Assuming we now take the branch to PspSyscallProviderServiceDispatchGeneric, it takes the encoded QWORD count mentioned above. I haven’t actually found a need for this, I lost the variable whilst debugging it - so devised an alternate approach (see callback function section). This ‘metadata’ integer is how many QWORDs you want to copy of stack arguments (i.e. arguments 5 and onwards). You can see its implementation here in PspCaptureSystemServiceInMemoryArgs:

PspCaptureSystemServiceInMemoryArgs

Nothing particularly spectacular happens in here, save for dispatching our callback:

PspSyscallProviderServiceDispatchGeneric

I will admit; I don’t have a clue how _guard_dispatch_icall works, if anyone wants to educate me, please message me! Looking at it in the debugger however, it is clear to see what is going on, it makes a call to KscpCfgDispatchUserCallTargetEsSmep:

KscpCfgDispatchUserCallTargetEsSmep

Which in turn looks like:

KscpCfgDispatchUserCallTargetEsSmep

This is a jmp rax instruction! Whilst working through this; it threw me for a while. Originally I was assuming this was a function pointer to the callback routine; but actually, its the address of our RWX thunk I talked about above, as the jmp will of course start executing instructions!

After fiddling about with bit-shifting and getting memory right, my thunk table looks like (and you can see the rip for where I am in this particular case):

Windows 11 Alt Syscall thunk bootstrap shellcode

The above just jumps to an offset that we calculate when setting up the table! We just pad the rest out with nops to ensure correct memory alignment.

If we examine the memory at 0FFFFF8011C118740h, its my callback function!

Windows 11 Alt Syscall callback routine

The callback routine

With understanding the general mechanisms of the new Windows 11 Alt Syscall dispatch routines, we can now look at the callback! Again, this took a little while to figure out, debugging and tracking registers and the stack, but the callback routine looks as follows (in Rust, again apologies C people!).

It’s worth noting also, we have to return 1 from this function (see below IDA graph):

Windows 11 Alt Syscall callback routine

The arguments are explained in the function comments:

/// The callback routine which we control to run when a system call is dispatched via my alt syscall technique.
/// 
/// # Args:
/// - `p_nt_function`: A function pointer to the real Nt* dispatch function (e.g. NtOpenProcess)
/// - `ssn`: The System Service Number of the syscall
/// - `args_base`: The base address of the args passed into the original syscall rcx, rdx, r8 and r9
/// - `p3_home`: The address of `P3Home` of the _KTRAP_FRAME
/// 
/// # Note:
/// We can use the `p3_home` arg that is passed into this callback to calculate the actual address of the 
/// `KTRAP_FRAME`, where we can get the address of the stack pointer, that we can use to gather any additional 
/// arguments which were passed into the syscall.
/// 
/// # Safety
/// This function is **NOT** compatible with the `PspSyscallProviderServiceDispatch` branch of alt syscalls, it 
/// **WILL** result in a bug check in that instance. This can only be used with 
/// `PspSyscallProviderServiceDispatchGeneric`.
pub unsafe extern "system" fn syscall_handler(
    _p_nt_function: c_void,
    ssn: u32,
    args_base: *const c_void,
    p3_home: *const c_void,
) -> i32 {
    // ..
}

Without re-explaining what I have written there, the 3 important args are the last 3. We have the SSN in rdx, the base address of 0x20 bytes for the first 4 arguments passed into the Nt* function by the syscall caller, you can get these just with some pointer arithmetic.

p3_home is where things get interesting. As I alluded to above, I couldn’t figure out where the buffer we pass to PspCaptureSystemServiceInMemoryArgs went (on reflection maybe its arg 5 on the current stack), so I took an alternative approach after realising that the 4th param is part of the KTRAP_FRAME. With some pointer arithmetic, we can get the rsp of the syscall caller, and grab the arguments that way :).

Seeing as through I’m building an EDR, I wanted to monitor syscalls for common process injection:

We can match (for C people, thats a switch) on the SSN, monitoring for the numbers to which the syscall relates. You can also see the pointer arithmetic used to get the arguments from the callee (only demo’d for NtAllocateVirtualMemory):

pub unsafe extern "system" fn syscall_handler(
    _p_nt_function: c_void,
    ssn: u32,
    args_base: *const c_void,
    p3_home: *const c_void,
) -> i32 {

    if args_base.is_null() || p3_home.is_null() {
        println!("[sanctum] [-] Args base or arg4 was null??");
        return 1;
    }

    let k_trap = unsafe { p3_home.sub(0x10) } as *const KTRAP_FRAME;
    if k_trap.is_null() {
        println!("[sanctum] [-] KTRAP_FRAME was null");
        return 1;
    }

    const ARG_5_STACK_OFFSET: usize = 0x28;

    let k_trap = &unsafe { *k_trap };
    let rsp = k_trap.Rsp as *const c_void;
    let rcx  = unsafe { *(args_base as *const _ as *const usize) } as usize;

    match ssn {
        0x18 => {            
            let rcx_handle  = unsafe { 
                *(args_base as *const *const c_void)
             };
            let rdx_base_addr = unsafe { 
                *(args_base.add(0x8) as *const *const c_void)
            };
            let r8_zero_bit = unsafe { 
                *(args_base.add(0x10) as *const *const usize)
            };
            let r9_sz = unsafe {
                **(args_base.add(0x18) as *const *const usize)
            };
            let alloc_type = unsafe { *(rsp.add(ARG_5_STACK_OFFSET) as *const _ as *const u32) } as u32;
            let protect = unsafe { *(rsp.add(ARG_5_STACK_OFFSET + 8) as *const _ as *const u32) } as u32;

            println!("[VirtualAllocEx] [i] handle: {:p}, base: {:p}, zero bit: {:p}, Size: {}, alloc_type: {:#x}, protect: {:#x}", rcx_handle, rdx_base_addr, r8_zero_bit, r9_sz, alloc_type, protect);
        },
        0x26 => {
            println!("[NtOpenProcess] [i] Hook. SSN {:#x}, rcx as usize: {}. Stack ptr: {:p}", ssn, rcx, rsp);
        },
        0x3a => {
            println!("[Write virtual memory] [i] Hook. SSN {:#x}, rcx as usize: {}. Stack ptr: {:p}", ssn, rcx, rsp);
        },
        0x4e => {
            println!("[create thread] [i] Hook. SSN {:#x}, rcx as usize: {}. Stack ptr: {:p}", ssn, rcx, rsp);
        },
        0xc9 => {
            println!("[create thread ex] [i] Hook. SSN {:#x}, rcx as usize: {}. Stack ptr: {:p}", ssn, rcx, rsp);
        },
        _ => {
            // println!("SSN: {:#x}", ssn);
        },
    }

    1
}

As you can see, it works! These are the prints that you can see above when running “malware.exe” which performs some basic process injection.

Windows 11 Alternate Syscalls

Initialising the structures

Now; onto the structures we need to work with. The definitions I landed on:

#[repr(C)]
pub struct PspServiceDescriptorGroupTable {
    rows: [PspServiceDescriptorRow; 0x20],
}

#[repr(C)]
#[derive(Copy, Clone)]
struct PspServiceDescriptorRow {
    thunk_base: *const c_void,
    ssn_dispatch_table: *const AltSyscallDispatchTable,
    _reserved: *const c_void,
}

#[repr(C)]
struct PspSyscallProviderDispatchContext {
    level: u32,
    slot: u32,
}

#[repr(C)]
struct AltSyscallDispatchTable {
    pub count: u32,
    pub pad: u32,
    pub descriptors: [u32; SSN_COUNT],
}

For the Rust uninitiated, #[repr©] is an attribute that tells the compiler to adhere to C’s memory layout.

To visualise these structs:

Windows 11 Alternate Syscalls

So, we now need to populate these with data. I’ve provided some thorough comments in the below code, but in general handfuls:

  1. Allocate a non-paged executable area of memory in which we will store the thunks. Each thunk is to be 16 bytes; so the total size is MAX_SSN * 16.
  2. Get our callback routines address as a function pointer.
  3. For each individual thunk; write the shellcode which will jump to our callback routine (and pad with NOPs).
  4. Fill the ‘Alt Syscall Dispatch Table’ with a count (capacity) of the max SSN’s we are providing for, and fill the descriptor array with zeros.
  5. For each descriptor, shift in the index, followed by the flags to get us to the Generic dispatch routine + number of QWORDs to copy from the callee’s stack.
  6. Leak the table so the memory is consistent (for Rust’s RAII - ill fix the memory leak later).
  7. Resolve the PspServiceDescriptorGroupTable symbol.
  8. Write a new Row for the PspServiceDescriptorGroupTable which:
    1. Stores the base address of the thunk ‘blob’.
    2. Stores the address of the descriptor table.
  9. Walk all processes to enable the relevant bits so running processes use our alt syscall :).
pub fn enable(driver: &mut DRIVER_OBJECT) {
    const METADATA: u32 = 0x0;
    // These flags ensure we go the PspSyscallProviderServiceDispatchGeneric route
    const FLAGS: u32 = 0x10;

    // Enforce the SLOT_ID rules at compile time
    const _: () = assert!(SLOT_ID <= 20, "SLOT_ID for alt syscalls cannot be > 20");

    //
    // Allocate a non-paged executable region of memory for us to put our shellcode thunks which will
    // essentially 'bootstrap' our callback routine.
    // This thunk array contains 16 bytes per thunk, indexed by the SSN. We will write beyond the usual ntdll
    // SSNs (there are lots more. Not doing so results in system instability. I dont actually know the max number of
    // SSNs, so I've set this to a ridiculous number found in the constant: `SSN_COUNT`.
    //
    let thunk_bytes = SSN_COUNT * 16;
    let p_thunk_array = unsafe {
        ExAllocatePool2(
            POOL_FLAG_NON_PAGED_EXECUTE,
            thunk_bytes as _,
            b"stb!"[0] as _,
        )
    } as *mut u8;
    if p_thunk_array.is_null() {
        println!("[sanctum] [-] failed to alloc stubs");
        return;
    }

    //
    // For each syscall out of `SSN_COUNT`, we want to write our bootstrap thunk
    // so that we jump to our callback routine.
    //
    let callback_address = syscall_handler as usize as u64;

    for i in 0..SSN_COUNT {
        let dest = unsafe { p_thunk_array.add(i * 16) };

        unsafe {
            // mov rax, imm64
            *dest.offset(0) = 0x48;
            *dest.offset(1) = 0xB8;
            // write the 8-byte address of the callback routine
            core::ptr::write_unaligned(dest.offset(2) as *mut u64, callback_address);
            // jmp rax
            *dest.offset(10) = 0xFF;
            *dest.offset(11) = 0xE0;
            // pad to 16 bytes, write nps
            for pad in 12..16 {
                *dest.offset(pad) = 0x90;
            }
        }
    }

    //
    // Now build the 'mini dispatch table':  one per descriptor. Using multiple descriptor ID's should enable us to use different
    // callback routines I think. I haven't experimented with it, but I imagine thats why. When the ID indexes into the table, the offset
    // of the thunk is where we would jump to a different callback routine.
    //
    // low–4 bits   = metadata (0x10 = generic path + N args to capture via a later memcpy),
    // high bits    = descriptor index<<4.
    //
    // Setting FLAGS |= (METADATA & 0xF) means generic path, capture N args
    //
    let mut metadata_table = Box::new(AltSyscallDispatchTable {
        count: SSN_COUNT as u32,
        pad: 0,
        descriptors: [0; SSN_COUNT],
    });
    for i in 0..SSN_COUNT {
        metadata_table.descriptors[i] = ((i as u32) << 4) | (FLAGS | (METADATA & 0xF));
    }
    // Leak the box so that we don't (for now) have to manage the memory; yes, this is a memory leak in the kernel, I'll fix it later.
    let p_metadata_table = Box::leak(metadata_table) as *const AltSyscallDispatchTable;
    println!(
        "[sanctum] [+] Address of the alt syscalls metadata table: {:p}",
        p_metadata_table
    );

    // Get the address of PspServiceDescriptorGroupTable from the kernel by doing some pattern matching; I don't believe
    // we can link to the symbol.
    let kernel_service_descriptor_table = match lookup_global_table_address(driver) {
        Ok(t) => t as *mut PspServiceDescriptorGroupTable,
        Err(_) => {
            println!("[sanctum] failed to find kernel table");
            return;
        }
    };

    //
    // Insert a new row at index 0 in the PspServiceDescriptorGroupTable; in theory, if these were already occupied by other software
    // using alt syscalls, we would want to find an unoccupied slot.
    // This is what the Slot field relates to on the _PSP_SYSCALL_PROVIDER_DISPATCH_CONTEXT of _EPROCESS - essentially an index into which
    // syscall provider to use.
    //
    let new_row = PspServiceDescriptorRow {
        thunk_base: p_thunk_array as *const c_void,
        ssn_dispatch_table: p_metadata_table,
        _reserved: core::ptr::null(),
    };

    // Write it to the table
    unsafe {
        (*kernel_service_descriptor_table).rows[SLOT_ID as usize] = new_row;
    }

    // Enumerate all active processes and threads, and enable the relevant bits so that the alt syscall 'machine' can work :)
    Self::walk_active_processes_and_set_bits(AltSyscallStatus::Enable);
}

Setting the thread and process bits

There are two functions responsible for setting (and unsetting) the required bits for the alt syscalls on threads & processes. The only difference here really from Windows 10 is setting the Slot field on the _EPROCESS.

pub fn configure_thread_for_alt_syscalls(p_k_thread: PKTHREAD, status: AltSyscallStatus) {
    if p_k_thread.is_null() {
        return;
    }

    // Check if is pico process, if it is, we don't want to mess with it, as I haven't spent time reversing the branch
    // for this in PsSyscallProviderDispatch.
    let dispatch_hdr = unsafe { &mut *(p_k_thread as *mut DISPATCHER_HEADER) };
    if unsafe {
        dispatch_hdr
            .__bindgen_anon_1
            .__bindgen_anon_6
            .__bindgen_anon_2
            .DebugActive
            & 4
    } == 4
    {
        return;
    }

    // Assuming now we are not a pico-process; set / unset the AltSyscall bit on the ETHREAD depending upon
    // the `status` argument to this function.
    unsafe {
        match status {
            AltSyscallStatus::Enable => {
                dispatch_hdr
                    .__bindgen_anon_1
                    .__bindgen_anon_6
                    .__bindgen_anon_2
                    .DebugActive |= 0x20
            }
            AltSyscallStatus::Disable => {
                dispatch_hdr
                    .__bindgen_anon_1
                    .__bindgen_anon_6
                    .__bindgen_anon_2
                    .DebugActive &= !0x20
            }
        }
    }
}

pub fn configure_process_for_alt_syscalls(p_k_thread: PKTHREAD) {
    let p_eprocess = unsafe { IoThreadToProcess(p_k_thread as PETHREAD) } as *mut u8;
    let syscall_provider_dispatch_ctx: &mut PspSyscallProviderDispatchContext =
        if !p_eprocess.is_null() {
            unsafe {
                let addr = p_eprocess.add(0x7d0) as *mut PspSyscallProviderDispatchContext;
                &mut *addr
            }
        } else {
            return;
        };

    // Set slot id
    syscall_provider_dispatch_ctx.slot = SLOT_ID;
}

Monitoring processes

As mentioned, we need to enable this for all processes. This is as simple as enumerating each _EPROCESS and _ETHREAD and setting the bits. We also use the kernel callback routine for new thread creation to set the bits on future threads. We don’t need to do this on process creation, as no syscall will happen until a thread is run - so, we can just use that as a choke-point. You can see me call the function walk_active_processes_and_set_bits above, taking in an enum.

This is as follows. Note: The wdk for Rust didn’t define the _EPROCESS and _ETHREAD structs, so I’ve used some constant offsets to the fields.

This is bad practice, and it will only work until Microsoft change the layout of the structures. But it works for now.

/// Walk all processes and threads, and set the bits on the process & thread to either enable or disable the
/// alt syscall method.
///
/// # Args:
/// - `status`: Whether you wish to enable, or disable the feature
///
/// # Note:
/// This function is specifically crafted for W11 24H2; to generalise in the future after POC
fn walk_active_processes_and_set_bits(status: AltSyscallStatus) {
    // Offsets in bytes for Win11 24H2
    const ACTIVE_PROCESS_LINKS_OFFSET: usize = 0x1d8;
    const UNIQUE_PROCESS_ID_OFFSET: usize = 0x1d0;
    const THREAD_LIST_HEAD_OFFSET: usize = 0x370;
    const THREAD_LIST_ENTRY_OFFSET: usize = 0x578;

    let current_process = unsafe { IoGetCurrentProcess() };
    if current_process.is_null() {
        println!("[sanctum] [-] current_process was NULL");
        return;
    }

    // Get the starting head for the list
    let head = unsafe { (current_process as *mut u8).add(ACTIVE_PROCESS_LINKS_OFFSET) }
        as *mut LIST_ENTRY;
    let mut entry = unsafe { (*head).Flink };

    while entry != head {
        // Get the record for the _EPROCESS
        let p_e_process =
            unsafe { (entry as *mut u8).sub(ACTIVE_PROCESS_LINKS_OFFSET) } as *mut _EPROCESS;

        let pid = unsafe {
            let p = (p_e_process as *mut u8).add(UNIQUE_PROCESS_ID_OFFSET) as *const usize;
            *p
        };

        // Skip the Idle process (PID 0) as there are no threads present and it gave a null ptr deref
        if pid == 0 {
            entry = unsafe { (*entry).Flink };
            continue;
        }

        // Walk threads
        let thread_head =
            unsafe { (p_e_process as *mut u8).add(THREAD_LIST_HEAD_OFFSET) } as *mut LIST_ENTRY;
        let mut thread_entry = unsafe { (*thread_head).Flink };

        while thread_entry != thread_head {
            // Here we have each thread, we can now go and set the bit on the thread and process to make
            // alt syscalls work
            let p_k_thread = unsafe { (thread_entry as *mut u8).sub(THREAD_LIST_ENTRY_OFFSET) }
                as *mut _KTHREAD;

            Self::configure_thread_for_alt_syscalls(p_k_thread, status);
            Self::configure_process_for_alt_syscalls(p_k_thread);

            thread_entry = unsafe { (*thread_entry).Flink };
        }

        // Move on to the next process
        entry = unsafe { (*entry).Flink };
    }
}

HyperGuard

As mentioned earlier, I haven’t yet tested this against HyperGuard. However - I suspect HG doesn’t currently monitor the PspServiceDescriptorGroupTable structure, as I don’t think the new mechanisms are implemented in the Secure Kernel.

When starting the research, one thing I omitted above, was coming across a number of functions which follow the naming convention (I cant wildcard either side because of how I format my blog): SyscallProvider:

Windows 11 Alternate Syscalls

Taking a look at PsInitializeSyscallProviders:

Windows 11 Alternate Syscalls

As you can see; it checks for Hyper-V. I actually paid for Windows 11 Pro (lol) when I came across this, before finding all of the above which enabled me to use Alt Syscalls. I’d like a refund :D!

Assuming Hyper-V is enabled and correct, it makes a call to the secure kernel passing in the control code 0xea.

VslpEnterIumSecureMode(2, 0xea, 0, &var_88);

So, I was curious - I decompiled the Secure Kernel, which shows:

Windows 11 Secure Kernel Hyper-V

The calls into the Secure Kernel relating to Alt Syscalls will cause a bug check. I’m no expert on the Secure Kernel, so again, I need to test this once I re-sort my Hyper-V VM, but it would appear to me that Microsoft have designed an API to use alt syscalls, whilst it may be for internal use only (i.e. Defender), I am speculating that this API could be made available to ELAM signed drivers of AntiVirus / EDR vendors.

Final comments

And thats about it! This is stable on my build, with no PatchGuard bug checks. If you have any questions, or recreate / improve on this, please let me know via email (fluxsec@proton.me), or at X - @0xfluxsec!

Hope you have enjoyed this, it took me a solid few weeks to to!

Ciao!