Syscall layer
- ATT&CK: T1106 (Native API)
- YAML:
build.stub.syscalls - Scope: loader-wide (configures the generated stub)
What it does
Section titled “What it does”Replaces the stub’s suspicious Win32 API calls with NT syscalls whose SSN is
resolved at runtime. In mode: indirect the syscall instruction executes from
a syscall; ret gadget inside ntdll, so the call always appears to originate
in ntdll (not in private stub code), userland hooks in ntdll are bypassed,
and the stub’s IAT no longer contains the suspicious imports.
YAML parameters
Section titled “YAML parameters”build: stub: syscalls: mode: none | indirect # default: none resolver: hells_gate | tartarus_gate | api_hash # default: hells_gatemode—nonemaps thent_*helpers to the plain Win32 APIs;indirectresolves SSNs at runtime and calls the kernel through the ntdll gadget.resolver— only meaningful withmode: indirect:- Hell’s Gate (default) reads each SSN out of the Nt* stub prologue in the
live
ntdll. Fails withPatternMismatchwhen an EDR has hooked an export stub. - Tartarus Gate reads each SSN from a clean copy of
ntdllread from disk, so hooks in the live stubs do not hide the SSN. The gadget still comes from the liventdll’s.text. - API hashing walks the same clean copy but matches each export by a
precomputed FNV-1a 64-bit hash of its name, so neither
GetProcAddressnor any function-name string is needed.
- Hell’s Gate (default) reads each SSN out of the Nt* stub prologue in the
live
YAML example
Section titled “YAML example”build: stub: syscalls: mode: indirect resolver: hells_gateruntime: - technique: process_hollowing params: target: "C:\\Windows\\System32\\svchost.exe"How it works
Section titled “How it works”- Fragment contract. Technique fragments always call the
nt_*helpers (nt_alloc_vm,nt_write_vm,nt_read_vm,nt_resume_thread,nt_unmap_view,nt_protect_vm,nt_suspend_thread,nt_query_info_process,nt_query_info_thread,nt_get_context_thread,nt_set_context_thread,nt_create_thread_ex,nt_wait_for_single_object,nt_close) and never the Win32 APIs directly.$SYSCALLS_BLOCK$gives each helper two implementations — a direct Win32 call formode: none, a syscall through the ntdll gadget formode: indirect— so the fragment code is identical in both modes. - SSN resolution.
init_syscalls()runs once, before the techniques, for the deduplicated union of the active pipeline’sTechniqueDef::syscalls. Hell’s Gate reads the canonical prologue4C 8B D1 B8 ?? ?? ?? ??(mov r10, rcx; mov eax, <SSN>); Tartarus Gate and API hashing walk a clean on-disk copy’s export directory, converting each RVA to a file offset. - The gadget. The in-memory PE section table locates
.text, which is scanned for the first0F 05 C3(syscall; ret). - Trampoline (
nt_invoke). Acore::arch::global_asm!shim with eleven argument slots plus the SSN and the gadget.syscallpushes no return address, so the kernel reads arguments 5+ from exactly the slots the Windows x64 ABI already put them in: the shim only setsr10(argument 1),eax(SSN) andr11(gadget) andjmps, and the gadget’s trailingretreturns straight to the Rust caller. - Helpers. Each
nt_*helper wrapsnt_invokewith the NT function’s signature and returnsNTSTATUS(i32); fragments treat< 0as an error.
What it evades
Section titled “What it evades”- Userland hooks in
ntdll: the call reaches the kernel without passing through the hooked exported stub. - Static IAT analysis:
VirtualAllocEx,WriteProcessMemory,ReadProcessMemory,ResumeThread,Get/SetThreadContext,CreateRemoteThread,WaitForSingleObject,GetExitCodeProcessandCloseHandleleave the import table, and theNtUnmapViewOfSectionstring no longer appears in the binary (docs/measurements.md). GetProcAddress-based resolution and name strings: withresolver: api_hashthe stub never callsGetProcAddressfor the Nt* functions and never contains their names — only the precomputed hash constants.
What detects it
Section titled “What detects it”- Kernel callbacks (PsSetCreateProcessNotifyRoutine, ObRegisterCallbacks) and
ETW-TI: the syscall is indistinguishable from a legitimate
ntdllcall. - Stack inspection / stack-spoofing detectors (the call stack does not show
ntdll). Mitigated by thestack_spoofingtechnique; otherwise pending. - SSN anomalies: signatures built from SSNs that change between Windows builds.
Implementation notes
Section titled “Implementation notes”- Why indirect over direct. A direct syscall fails the syscall-origin check
(the instruction must reside in a Microsoft-signed module); jumping to a
syscall; retgadget insidentdllpasses it. - Rendering is programmatic (
src/engine/syscalls.rs): a new technique only declares its Nt* functions inTechniqueDef::syscalls, and its helpers are emitted only when needed. - The Nt* names never reach the binary: Hell’s/Tartarus Gate rebuild them at
runtime via
StringsConfig; API hashing never emits them. Theno_sensitive_literals_in_generated_stubsguardrail covers these strings. mode: indirectis x64-only; seedocs/x86.md. The PE-file walk helpers (rva_to_offset,pe16_at,pe32_at) are shared between the Tartarus Gate and API-hashing resolvers via$FILE_PE_HELPERS$.
References
Section titled “References”- Hell’s Gate (am0nsec, RtlMateusz — VX-Underground): https://vxug.fakedoma.in/papers/VXUG/Exclusive/HellsGate.pdf
- Tartarus’ Gate (trickster0): https://github.com/trickster0/TartarusGate
- Direct vs indirect syscalls (Outflank): https://www.outflank.nl/blog/2023/12/13/direct-syscalls-vs-indirect-syscalls/
- Syscall detection / instrumentation callbacks (n4r1b): https://github.com/Deputation/instrumentation_callbacks