Indirect syscalls are not a personality
Why indirect syscalls buy less than people think, and what the detection side actually keys on.
- #Syscalls
- #EDR Evasion
- #Detection
- #Implants
§The appeal
Direct syscalls (calling `syscall` instruction directly from your code) bypass userland hooks placed by EDR on ntdll exports. Indirect syscalls go one step further: you jump into ntdll's own `syscall` instruction, so the call stack shows a legitimate ntdll frame. The marketing writes itself. The reality is more boring.
§What indirect syscalls actually do
Instead of `syscall` in your code, you locate the `syscall; ret` gadget inside ntdll (e.g. in `NtAllocateVirtualMemory`) and call it with the syscall number in EAX and arguments in the registers. The stack frame now points into ntdll, which defeats naive "is the caller in a known module" checks.
// Hell's Gate style: read syscall number from ntdll stub
DWORD get_syscall_number(const char *name) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
void *fn = GetProcAddress(ntdll, name);
// stub: mov r10, rcx; mov eax, <ssn>; syscall; ret
unsigned char *p = (unsigned char *)fn;
if (p[0] == 0x4C && p[1] == 0x8B && p[2] == 0xD1 && p[3] == 0xB8) {
return *(DWORD *)(p + 4);
}
return 0;
}
// Indirect: jump into ntdll's syscall gadget
extern void do_syscall(void *gadget, DWORD ssn, ...);
void *find_gadget(const char *name) {
void *fn = GetProcAddress(GetModuleHandleA("ntdll.dll"), name);
unsigned char *p = (unsigned char *)fn;
for (int i = 0; i < 32; i++) {
if (p[i] == 0x0F && p[i+1] == 0x05) return &p[i]; // syscall
}
return NULL;
}§Where detection actually lives
- 01**ETW Threat Intelligence** (requires EDR kernel driver, enabled on Windows 10+): logs `NtAllocateVirtualMemory`, `NtProtectVirtualMemory`, `NtWriteVirtualMemory` regardless of how they are invoked. Direct and indirect syscalls both trigger ETW TI events. This is the single biggest reason syscall-based evasion is dead against modern EDRs.
- 01**Call stack analysis**: indirect syscalls produce a stack with an ntdll frame, but the *return address* into ntdll points to a `syscall` instruction, not a function entry. EDRs that walk the stack (Elastic, CrowdStrike) check whether the return address is at a function boundary. A `syscall` instruction in the middle of `NtAllocateVirtualMemory` is suspicious.
- 01**Frequency/volume**: a process making 1000 syscalls per second with no corresponding ntdll call exports is anomalous. Behavioral models catch this.
- 01**Memory scanning of the gadget**: the gadget address is a known ntdll offset. EDRs can check whether the thread's RIP is at a `syscall` instruction inside ntdll *and* whether the preceding instructions match the expected stub. Indirect calls from unusual callers still stand out in ETW.
§What still works (honestly)
- Against **userland-only** EDRs (older, hook-only), indirect syscalls still bypass hooks.
- Against **specific** detection rules that only check the calling module name, indirect wins over direct.
- Combined with sleep obfuscation and call-stack spoofing (e.g., SilentMoonwalk), the stack-walk detection can be defeated, but ETW TI remains.
§The honest take
Indirect syscalls are a stepping stone, not a destination. They solve the 2018 problem (userland hooks) and fail against the 2024 problem (kernel ETW + behavioral). If your threat model includes a modern EDR with TI enabled, syscalls alone will not save you. Spend the complexity budget on call-stack spoofing, ETW patching (which itself is a detection), or living inside a trusted process.
§Compile
# MinGW
x86_64-w64-mingw32-gcc -O2 -s indirect.c -o indirect.exe -lntdll
# MSVC
cl /O2 /GS- indirect.c /link /OUT:indirect.exe ntdll.lib§Detection (defender side)
- Enable ETW Threat Intelligence (requires ELAM driver or Defender).
- Alert on `NtAllocateVirtualMemory`/`NtProtectVirtualMemory` events where the calling module is not a known signed binary.
- Stack-walk: flag threads whose RIP is at a `syscall` instruction but whose return address is not at a function entry in ntdll.
"
Written by
MJ — offensive security researcher
I break things in a lab so defenders can build better controls. Everything published here is lab-derived, responsibly disclosed, and documented with the detection path in mind.