Overview
Stub generation policies control how syscalls are executed . Each policy generates different machine code that ultimately invokes the kernel via a system call, but with varying levels of indirection and evasion capability.
All stub generation policies must satisfy the IsStubGenerationPolicy concept by providing:
static constexpr bool bRequiresGadget - Whether the policy needs syscall gadgets from ntdll
static constexpr size_t getStubSize() - Size of the generated stub in bytes
static void generate(uint8_t* pBuffer, uint32_t uSyscallNumber, void* pGadgetAddress) - Generate stub code
generator::direct
Overview
The direct generator creates classic, self-contained syscall stubs that directly execute the syscall instruction (x64) or call dword ptr fs:[0xC0] (x86). This is the most straightforward approach.
Platform Support : Works on both x86 and x64 Windows platforms with architecture-specific shellcode.
Implementation Details
Location: syscall.hpp:309-341
struct direct
{
static constexpr bool bRequiresGadget = false ;
#if SYSCALL_PLATFORM_WINDOWS_64
inline static constinit std ::array < uint8_t , 18 > arrShellcode =
{
0x 51 , // push rcx
0x 41 , 0x 5A , // pop r10
0x B8 , 0x 00 , 0x 00 , 0x 00 , 0x 00 , // mov eax, 0x00000000 (syscall number placeholder)
0x 0F , 0x 05 , // syscall
0x 48 , 0x 83 , 0x C4 , 0x 08 , // add rsp, 8
0x FF , 0x 64 , 0x 24 , 0x F8 // jmp qword ptr [rsp-8]
};
#elif SYSCALL_PLATFORM_WINDOWS_32
inline static constinit std ::array < uint8_t , 15 > arrShellcode =
{
0x B8 , 0x 00 , 0x 00 , 0x 00 , 0x 00 , // mov eax, 0x00000000 (syscall number placeholder)
0x 89 , 0x E2 , // mov edx, esp
0x 64 , 0x FF , 0x 15 , 0x C0 , 0x 00 , 0x 00 , 0x 00 , // call dword ptr fs:[0xC0]
0x C3 // ret
};
#endif
static void generate ( uint8_t* pBuffer , uint32_t uSyscallNumber , void* /*pGadgetAddress*/ )
{
std :: copy_n ( arrShellcode . data (), arrShellcode . size (), pBuffer);
if constexpr ( platform ::isWindows64)
* reinterpret_cast < uint32_t *> (pBuffer + 4 ) = uSyscallNumber;
else
* reinterpret_cast < uint32_t *> (pBuffer + 1 ) = uSyscallNumber;
}
static constexpr size_t getStubSize () { return arrShellcode . size (); }
};
x64 Shellcode Breakdown
; Stub size: 18 bytes
push rcx ; Save RCX
pop r10 ; Move RCX to R10 (syscall calling convention)
mov eax , 0x00000000 ; Load syscall number (patched at runtime)
syscall ; Execute syscall instruction
add rsp , 8 ; Adjust stack
jmp qword ptr [ rsp - 8 ] ; Return to caller
Why this sequence?
Windows x64 syscall calling convention requires the first parameter in R10 (not RCX)
EAX holds the syscall number
The syscall instruction directly transitions to kernel mode
Stack adjustment and indirect jump handle the return
x86 Shellcode Breakdown
; Stub size: 15 bytes
mov eax , 0x00000000 ; Load syscall number (patched at runtime)
mov edx , esp ; EDX points to arguments on stack
call dword ptr fs :[ 0xC0 ] ; Call KiFastSystemCall (WOW64)
ret ; Return to caller
Why this sequence?
x86 Windows uses fs:[0xC0] to reach the syscall dispatcher (WOW64 or native)
EAX holds the syscall number
EDX points to the stack frame with arguments
Security Characteristics
Advantages :
Simple and fast execution
No dependencies on ntdll gadgets
Works on all platforms
Minimal overhead
Limitations :
Contains direct syscall instruction (easily detected by EDR)
No obfuscation or indirection
May trigger userland hooks if memory is scanned
Best for : Compatibility and performance when detection is not a concern
generator::gadget
Overview
The gadget generator avoids direct syscall instructions by jumping to existing syscall; ret gadgets found in ntdll.dll. This provides indirection and uses legitimate Windows code for the actual syscall.
Platform Limitation : Only available on x64 Windows. x86 does not support this approach.
Implementation Details
Location: syscall.hpp:263-293
#if SYSCALL_PLATFORM_WINDOWS_64
struct gadget
{
static constexpr bool bRequiresGadget = true ;
static constexpr size_t getStubSize () { return 32 ; }
static void generate ( uint8_t* pBuffer , uint32_t uSyscallNumber , void* pGadgetAddress )
{
// mov r10, rcx
pBuffer [ 0 ] = 0x 49 ;
pBuffer [ 1 ] = 0x 89 ;
pBuffer [ 2 ] = 0x CA ;
// mov eax, syscallNumber
pBuffer [ 3 ] = 0x B8 ;
* reinterpret_cast < uint32_t *> ( & pBuffer [ 4 ]) = uSyscallNumber;
// mov r11, gadgetAddress
pBuffer [ 8 ] = 0x 49 ;
pBuffer [ 9 ] = 0x BB ;
* reinterpret_cast < uint64_t *> ( & pBuffer [ 10 ]) = reinterpret_cast < uint64_t > (pGadgetAddress);
// push r11
pBuffer [ 18 ] = 0x 41 ;
pBuffer [ 19 ] = 0x 53 ;
// ret
pBuffer [ 20 ] = 0x C3 ;
}
};
#endif
x64 Shellcode Breakdown
; Stub size: 32 bytes (21 bytes used, rest is padding)
mov r10 , rcx ; Move first parameter to R10
mov eax , 0x00000000 ; Load syscall number (patched)
mov r11 , 0x0000000000000000 ; Load gadget address (patched)
push r11 ; Push gadget address to stack
ret ; Jump to gadget (syscall; ret)
Execution flow :
Set up syscall number in EAX
Move first parameter to R10
Load the address of a syscall; ret gadget from ntdll
Jump to the gadget using push + ret
Gadget executes syscall then returns normally
Gadget Discovery
Location: syscall.hpp:896-924
The library scans ntdll’s .text section for syscall; ret sequences:
bool findSyscallGadgets ()
{
// ... get ntdll module info ...
// Find .text section
IMAGE_SECTION_HEADER * pSections = IMAGE_FIRST_SECTION ( ntdll . m_pNtHeaders );
uint8_t * pTextSection = nullptr ;
uint32_t uTextSectionSize = 0 ;
for ( int i = 0 ; i < ntdll . m_pNtHeaders -> FileHeader . NumberOfSections ; ++ i)
{
if ( hashing :: calculateHashRuntime ( reinterpret_cast < const char *> ( pSections [i]. Name ))
== hashing :: calculateHash ( ".text" ))
{
pTextSection = ntdll . m_pModuleBase + pSections [i]. VirtualAddress ;
uTextSectionSize = pSections [i]. Misc . VirtualSize ;
break ;
}
}
// Scan for 0x0F 0x05 0xC3 (syscall; ret)
m_vecSyscallGadgets . clear ();
for (DWORD i = 0 ; i < uTextSectionSize - 2 ; ++ i)
{
if ( pTextSection [i] == 0x 0F && pTextSection [i + 1 ] == 0x 05 && pTextSection [i + 2 ] == 0x C3 )
m_vecSyscallGadgets . push_back ( & pTextSection [i]);
}
return ! m_vecSyscallGadgets . empty ();
}
Random Gadget Selection
For OPSEC, a random gadget is selected at stub generation time:
const size_t uRandomIndex = native :: rdtscp () % uGadgetsCount;
pGadgetForStub = m_vecSyscallGadgets [uRandomIndex];
Security Characteristics
Advantages :
No direct syscall instruction in your code
Uses legitimate Windows code from ntdll
Harder to detect with simple signature scanning
Random gadget selection increases entropy
Limitations :
Requires gadget discovery (initialization overhead)
x64 only
Slightly larger stub size (32 bytes vs 18 bytes)
Returns to stack-based address (may trigger stack checks)
Best for : EDR evasion when you want to avoid direct syscall instructions
generator::exception
Overview
The exception generator uses a Vectored Exception Handler (VEH) to execute syscalls. Instead of a syscall instruction, it generates an illegal instruction (ud2) that triggers an exception. The VEH catches this exception and redirects execution to a syscall gadget.
Novel Approach : This technique is unique because the syscall never appears in the stub code itself—only an illegal instruction that gets handled at runtime.
Implementation Details
Location: syscall.hpp:296-307
struct exception
{
static constexpr bool bRequiresGadget = true ;
static constexpr size_t getStubSize () { return 8 ; }
static void generate ( uint8_t* pBuffer , uint32_t /*uSyscallNumber*/ , void* /*pGadgetAddress*/ )
{
pBuffer [ 0 ] = 0x 0F ; // ud2 instruction (illegal instruction)
pBuffer [ 1 ] = 0x 0B ;
pBuffer [ 2 ] = 0x C3 ; // ret
std :: fill_n (pBuffer + 3 , getStubSize () - 3 , 0x 90 ); // NOP padding
}
};
Stub Code
; Stub size: 8 bytes
ud2 ; Trigger EXCEPTION_ILLEGAL_INSTRUCTION
ret ; (never reached normally)
nop ; Padding
nop
nop
nop
nop
Exception Handler
Location: syscall.hpp:77-109
static LONG NTAPI VectoredExceptionHandler (PEXCEPTION_POINTERS pExceptionInfo)
{
if ( ! pExceptionContext . m_bShouldHandle )
return EXCEPTION_CONTINUE_SEARCH;
if ( pExceptionInfo -> ExceptionRecord -> ExceptionCode == EXCEPTION_ILLEGAL_INSTRUCTION &&
pExceptionInfo -> ExceptionRecord -> ExceptionAddress == pExceptionContext . m_pExpectedExceptionAddress )
{
pExceptionContext . m_bShouldHandle = false ;
#if SYSCALL_PLATFORM_WINDOWS_64
// Set up syscall arguments and redirect to gadget
pExceptionInfo -> ContextRecord -> R10 = pExceptionInfo -> ContextRecord -> Rcx ;
pExceptionInfo -> ContextRecord -> Rax = pExceptionContext . m_uSyscallNumber ;
pExceptionInfo -> ContextRecord -> Rip = reinterpret_cast < uintptr_t > ( pExceptionContext . m_pSyscallGadget );
#else
// x86 handling
uintptr_t uReturnAddressAfterSyscall = reinterpret_cast < uintptr_t > ( pExceptionInfo -> ExceptionRecord -> ExceptionAddress ) + 2 ;
pExceptionInfo -> ContextRecord -> Edx = pExceptionInfo -> ContextRecord -> Esp ;
pExceptionInfo -> ContextRecord -> Esp -= sizeof ( uintptr_t );
* reinterpret_cast < uintptr_t *> ( pExceptionInfo -> ContextRecord -> Esp ) = uReturnAddressAfterSyscall;
pExceptionInfo -> ContextRecord -> Eip = reinterpret_cast < uintptr_t > ( pExceptionContext . m_pSyscallGadget );
pExceptionInfo -> ContextRecord -> Eax = pExceptionContext . m_uSyscallNumber ;
#endif
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
Exception Context Guard
Location: syscall.hpp:57-75
A RAII guard sets up the exception context before each syscall:
class CExceptionContextGuard
{
public:
CExceptionContextGuard ( const void* pExpectedAddress , void* pSyscallGadget , uint32_t uSyscallNumber )
{
pExceptionContext . m_bShouldHandle = true ;
pExceptionContext . m_pExpectedExceptionAddress = pExpectedAddress;
pExceptionContext . m_pSyscallGadget = pSyscallGadget;
pExceptionContext . m_uSyscallNumber = uSyscallNumber;
}
~CExceptionContextGuard ()
{
pExceptionContext . m_bShouldHandle = false ;
}
CExceptionContextGuard ( const CExceptionContextGuard & ) = delete ;
CExceptionContextGuard & operator = ( const CExceptionContextGuard & ) = delete ;
};
Execution Flow
Setup Exception Context
Before invoking a syscall, CExceptionContextGuard stores:
Expected exception address (the ud2 instruction)
Target syscall gadget address
Syscall number
Execute ud2
The stub executes ud2, triggering EXCEPTION_ILLEGAL_INSTRUCTION
VEH Catches Exception
The vectored exception handler checks if:
Exception code is EXCEPTION_ILLEGAL_INSTRUCTION
Exception address matches the expected stub address
Context Modification
The handler modifies the CPU context:
Sets RAX/EAX to the syscall number
Sets R10/EDX to the first parameter
Redirects RIP/EIP to the syscall gadget
Resume Execution
Exception handling returns with EXCEPTION_CONTINUE_EXECUTION, and the thread resumes at the gadget, executing the syscall
Invocation Code
Location: syscall.hpp:797-816
if constexpr ( std ::is_same_v < IStubGenerationPolicy, policies :: generator ::exception > )
{
#if SYSCALL_PLATFORM_WINDOWS_64
const size_t uGadgetCount = m_vecSyscallGadgets . size ();
if ( ! uGadgetCount)
{
if constexpr ( std ::is_same_v < Ret, NTSTATUS > )
return native ::STATUS_UNSUCCESSFUL;
return Ret{};
}
const size_t uRandomIndex = native :: rdtscp () % uGadgetCount;
void * pRandomGadget = m_vecSyscallGadgets [uRandomIndex];
#else
void * pRandomGadget = ( void * ) __readfsdword ( 0x C0 );
#endif
CExceptionContextGuard contextGuard (pStubAddress, pRandomGadget, it -> m_uSyscallNumber );
return reinterpret_cast < Function_t > (pStubAddress)( std :: forward < Args >(args)...);
}
Security Characteristics
Advantages :
Smallest stub size (8 bytes)
No syscall instruction in stub code
Maximum obfuscation—stub looks like broken/invalid code
Difficult to detect with static analysis
VEH provides additional layer of indirection
Random gadget selection on each invocation
Limitations :
Exception handling overhead (slower than direct)
Requires VEH registration
More complex debugging
VEH can be monitored by security products
Thread-local exception context required
Best for : Maximum stealth when performance is less critical than evasion
Comparison Matrix
Feature directgadgetexceptionStub Size (x64) 18 bytes 32 bytes 8 bytes Platform x86 + x64 x64 only x86 + x64 Requires Gadgets ❌ No ✅ Yes ✅ Yes Performance Fastest Fast Slower (exception overhead) Stealth Low Medium High Contains syscall ✅ Yes ❌ No ❌ No Complexity Low Medium High Detection Risk High Medium Low
Choosing the Right Policy
Maximum Performance
Use generator::direct when speed is critical and detection is not a concern. Simple and reliable.
Balanced Evasion
Use generator::gadget (x64 only) to avoid direct syscall instructions while maintaining good performance.
Maximum Stealth
Use generator::exception when evading advanced detection is the priority and you can accept the performance overhead.
Usage Examples
Direct Generator
Gadget Generator (x64 only)
Exception Generator
using DirectManager = syscall ::Manager <
syscall :: policies :: allocator ::section,
syscall :: policies :: generator ::direct, // Fast, simple
syscall ::DefaultParserChain
> ;
DirectManager manager;
manager . initialize ();
Next Steps
Parsing Policies Learn how syscall numbers are resolved from ntdll