For the complete documentation index, see llms.txt. This page is also available as Markdown.

Indirect Syscalls — Preserving a Legitimate Stack Trace

Why Direct Syscalls Are Not Enough

Direct syscalls represent a significant step forward against EDRs that operate exclusively in userland. However, mature EDRs implement detection based on call stack analysis: when a syscall arrives at the kernel, the system can examine the process stack to verify that the call frames match what is expected.

In a legitimate execution, the frame sequence for a call like NtAllocateVirtualMemory would be:

┌─────────────────────────────────────────────────────────────────┐
│          Call Stack — Legitimate Execution                      │
│                                                                 │
│  [0] ntdll!NtAllocateVirtualMemory       ← expected frame      │
│  [1] KERNELBASE!VirtualAllocEx                                  │
│  [2] kernel32!VirtualAlloc                                      │
│  [3] MyApplication!main+0x42                                    │
│  [4] KERNEL32!BaseThreadInitThunk                               │
│                                                                 │
│          Call Stack — Direct Syscall (suspicious)               │
│                                                                 │
│  [0] MyApplication!syscall_stub+0x09    ← anomaly!             │
│  [1] MyApplication!inject+0x1A                                  │
│  [2] MyApplication!main+0x42                                    │
│                                                                 │
│  The ntdll frame is absent → EDR flags as suspicious.          │
└─────────────────────────────────────────────────────────────────┘

The syscall instruction occurs inside the attacker's code, and when the kernel performs stack unwinding during callbacks, the return address points outside ntdll.dll. This is a reliable indicator of compromise (IoC) for EDRs such as Cortex XDR and SentinelOne.

Indirect syscalls solve this problem: the syscall instruction executes within the legitimate address space of ntdll.dll, preserving the appearance of a normal call.


The Concept of Indirect Syscall

The core idea is simple: instead of issuing the syscall instruction in our own code, we jump directly to the syscall; ret inside ntdll's original stub, after correctly setting up the SSN and arguments.

The EDR sees the syscall instruction executing from an address within ntdll.dll — exactly what would be expected in a legitimate execution.


Finding the syscall; ret Offset in ntdll

The challenge is locating the exact offset of the syscall; ret pair within each function's stub. There are three scenarios:

  1. Unhooked stub: Original bytes are intact. We can read syscall; ret at the default offset (+0x12 on x64).

  2. Stub hooked at the beginning (JMP): The first bytes were overwritten. The syscall; ret at subsequent offsets may still be intact.

  3. Completely overwritten stub: We need to find syscall; ret in another clean stub and reuse that address.

Hell's Gate Strategy

Hell's Gate (by am0nsec and RtlMateusz) was the first public technique to dynamically resolve SSNs. The original logic reads the stub bytecode directly from memory.

Limitation: Fails when the stub is hooked (bytes were modified).

Halo's Gate Strategy

Halo's Gate extends Hell's Gate to handle hooked stubs: if the target stub is modified, it searches adjacent stubs (neighbors) in the export table — which may not be hooked — and uses their SSN as a reference, adding or subtracting the positional delta.

Tartarus' Gate Strategy

Tartarus' Gate goes further: sorts all exports by address (RVA) and uses the index in the sorted list as the SSN — without depending on the bytecode. Works even when all stubs are hooked.


Complete Indirect Syscall Implementation


SysWhispers3 with Indirect Syscalls

SysWhispers3 natively supports indirect syscalls via the --method jumper flag:

The generated code includes the runtime logic for locating syscall; ret and guarantees the syscall instruction always executes within ntdll.dll.


Technique Comparison


Advanced Detection

More recent EDRs implement multiple layers of verification:

  • Return address validation: Checks whether the syscall return address is in a memory region that belongs to a legitimate PE module.

  • CFG (Control Flow Guard): Jumping to addresses not registered as valid CFG targets generates an exception.

  • Kernel stack walking: During PsSetCreateThreadNotifyRoutine and similar callbacks, the kernel walks the process stack to inspect frames.

The answer to this is stack spoofing — a separate technique that manipulates the call stack to make it appear the call originated from legitimate code. That topic is covered in depth in the Thread Stack Spoofing article.


References

  • am0nsec & RtlMateusz, "Hell's Gate" — github.com/am0nsec/HellsGate (2020)

  • trickster0, "Halo's Gate" — github.com/trickster0/TartarusGate (2021)

  • Paul Laîné, "Tartarus' Gate" — turtleseason.github.io (2021)

  • klezVirus, "SysWhispers3 — Evasion using Indirect Syscalls" — github.com/klezVirus/SysWhispers3

  • MDSec, "Bypassing EDR Hooks: Direct and Indirect Syscalls" — mdsec.co.uk

  • WithSecure Labs, "Detecting Syscall Evasion" — labs.withsecure.com (2022)

  • Elastic Security Labs, "Hunting for Syscall-based Evasion" — elastic.co/security-labs (2023)

  • Alex Matrosov, "EDR Internals" — REcon 2022

Last updated