Overview
The generator::gadget policy generates stubs that jump to syscall; ret gadgets found in ntdll.dll’s .text section. This provides more legitimate call stacks and can evade some EDR detection techniques.
Platform Support: This policy is only available on Windows x64. It is not supported on x86.
Constants
Indicates whether this policy requires syscall gadgets from ntdll.dll static constexpr bool bRequiresGadget = true ;
Gadget-based stubs require discovering syscall; ret sequences in ntdll.dll
Method Signatures
Returns the size of the generated stub in bytes static constexpr size_t getStubSize () { return 32 ; }
Generates the syscall stub that jumps to a gadget static void generate (
uint8_t* pBuffer ,
uint32_t uSyscallNumber ,
void* pGadgetAddress
)
Pointer to the buffer where the stub will be written
The syscall number to embed in the stub
Address of the syscall; ret gadget to jump to
Generated Shellcode
x64 Stub (32 bytes)
From syscall.hpp:265-291:
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 ;
}
Assembly breakdown:
mov r10, rcx - Move first parameter to R10 (syscall convention)
mov eax, <syscall_number> - Load syscall number
mov r11, <gadget_address> - Load the address of the syscall; ret gadget
push r11 - Push the gadget address onto the stack
ret - “Return” to the gadget address, which executes syscall; ret
Gadget Discovery
The Manager class discovers gadgets by scanning ntdll.dll’s .text section for the byte sequence 0x0F 0x05 0xC3 (syscall; ret):
// From syscall.hpp:895-924
bool findSyscallGadgets ()
{
ModuleInfo_t ntdll;
if ( ! getModuleInfo ( SYSCALL_ID ( "ntdll.dll" ), ntdll))
return false ;
IMAGE_SECTION_HEADER * pSections = IMAGE_FIRST_SECTION ( ntdll . m_pNtHeaders );
uint8_t * pTextSection = nullptr ;
uint32_t uTextSectionSize = 0 ;
// Find .text section
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 ;
}
}
if ( ! pTextSection || ! uTextSectionSize)
return false ;
// Scan for syscall; ret gadgets (0x0F 0x05 0xC3)
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 ();
}
Gadgets are selected randomly using rdtscp() for each syscall invocation, adding variability to execution patterns.
Usage Example
#include <syscalls-cpp/syscall.hpp>
#if SYSCALL_PLATFORM_WINDOWS_64
// Use the pre-defined type alias
SyscallSectionGadget syscallManager;
if ( ! syscallManager . initialize ()) {
std ::cerr << "Failed to initialize (gadget discovery may have failed) \n " ;
return 1 ;
}
// Or compose manually
using GadgetManager = syscall ::Manager <
syscall :: policies :: allocator ::heap,
syscall :: policies :: generator ::gadget
> ;
GadgetManager manager;
manager . initialize ();
// Invoke syscalls
PVOID pAddress = nullptr ;
SIZE_T uSize = 0x 1000 ;
manager . invoke < NTSTATUS > (
SYSCALL_ID ( "NtAllocateVirtualMemory" ),
NtCurrentProcess (),
& pAddress, 0 , & uSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
#endif
Security Benefits
Call Stack Legitimacy The return address points into ntdll.dll’s .text section, making stack traces appear more legitimate.
Return Address Validation Some EDR solutions validate return addresses—this technique places them inside ntdll, bypassing such checks.
Randomization Random gadget selection adds entropy, making behavior analysis harder.
No Direct Syscall The stub itself doesn’t contain a syscall instruction, reducing direct detection risk.
Trade-offs
Aspect Description Performance Slightly slower (indirect jump through ret) Stealth High (legitimate return addresses) Complexity Moderate (requires gadget discovery) Platform Support x64 only Use Case EDR evasion, security research
If gadget discovery fails during initialization, the Manager will fail to initialize. Ensure ntdll.dll has not been stripped or modified.
See Also
generator::direct Direct syscall stub generation
generator::exception Exception-based stub generation
Gadget Syscalls Example Complete working example