Skip to main content

Overview

FreshyCalls is a robust SSN (System Service Number) resolution technique that determines syscall numbers by sorting NT function exports by their virtual addresses. Unlike traditional methods that read potentially-hooked opcode bytes, FreshyCalls only examines export table addresses — making it highly resistant to inline hooks.
FreshyCalls is the default resolution method in SysWhispers4 due to its excellent balance of speed, reliability, and hook resistance.

The Problem: Inline Hooks

AV/EDR products commonly place inline hooks on NT functions in ntdll.dll by modifying the first few bytes:
; Original NtAllocateVirtualMemory stub:
4C 8B D1              mov r10, rcx
B8 18 00 00 00        mov eax, 0x18      ; SSN = 0x18
0F 05                 syscall
C3                    ret

; After EDR hook:
E9 XX XX XX XX        jmp <EDR_Handler>  ; Overwrites first 5 bytes!
XX XX XX XX           (garbage)
0F 05                 syscall            ; Never reached
C3                    ret
Techniques like Hell’s Gate that read the mov eax, <SSN> opcode fail when hooks overwrite these bytes.

How FreshyCalls Works

Core Principle

NT syscall stubs in ntdll.dll are laid out sequentially in memory, ordered by their syscall numbers. The virtual address order directly corresponds to SSN order:
VA: 0x180001000  →  Nt* function with SSN 0
VA: 0x180001020  →  Nt* function with SSN 1
VA: 0x180001040  →  Nt* function with SSN 2
...
VA: 0x180001600  →  Nt* function with SSN 48 (e.g., NtAllocateVirtualMemory)
By sorting all Nt* exports by VA and using the sorted index as the SSN, we never need to read potentially-hooked function bytes.

Algorithm

// 1. Parse ntdll export table → collect all Nt* functions
for each export in ntdll.exports:
    if name starts with "Nt":
        exports.add({ name, VA })

// 2. Sort by virtual address (ascending)
exports.sort_by(VA)

// 3. Index in sorted list = SSN
for (i = 0; i < exports.length; i++):
    ssnTable[exports[i].name] = i

Why It Works

  1. VA order is canonical: Microsoft’s NT kernel assigns SSNs sequentially during build. ntdll.dll stubs are laid out in memory matching this order.
  2. Export table is rarely hooked: EDR hooks target function code, not the export directory entries. VAs remain accurate.
  3. No opcode reading: Unlike Hell’s/Halo’s Gate, we never inspect potentially-tampered bytes inside the function.

Implementation Details

Parsing the Export Table

BOOL SW4_FreshyCalls(PVOID pNtdll) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)pNtdll;
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((PBYTE)pNtdll + dos->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY exports = 
        (PIMAGE_EXPORT_DIRECTORY)((PBYTE)pNtdll + 
        nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    PDWORD nameRvas = (PDWORD)((PBYTE)pNtdll + exports->AddressOfNames);
    PDWORD funcRvas = (PDWORD)((PBYTE)pNtdll + exports->AddressOfFunctions);
    PWORD  ordinals = (PWORD)((PBYTE)pNtdll + exports->AddressOfNameOrdinals);

    SW4_EXPORT* ntExports = (SW4_EXPORT*)calloc(exports->NumberOfNames, sizeof(SW4_EXPORT));
    DWORD ntCount = 0;

    // Collect all Nt* exports
    for (DWORD i = 0; i < exports->NumberOfNames; i++) {
        PCHAR name = (PCHAR)((PBYTE)pNtdll + nameRvas[i]);
        if (name[0] == 'N' && name[1] == 't') {
            DWORD funcRva = funcRvas[ordinals[i]];
            ntExports[ntCount].Address = (PVOID)((PBYTE)pNtdll + funcRva);
            ntExports[ntCount].Hash = djb2_hash(name);
            ntCount++;
        }
    }

    // Sort by address
    qsort(ntExports, ntCount, sizeof(SW4_EXPORT), compare_va);

    // Index = SSN
    for (DWORD i = 0; i < ntCount; i++) {
        // Match against our function table by hash
        for (DWORD j = 0; j < SW4_FUNC_COUNT; j++) {
            if (ntExports[i].Hash == SW4_FunctionHashes[j]) {
                SW4_SsnTable[j] = i;  // SSN = sorted index
                break;
            }
        }
    }

    free(ntExports);
    return TRUE;
}

Strengths

Hook Resistant

Works even when every Nt* stub is hooked — never reads function bytes, only VAs from export table

Fast

Single ntdll parse + qsort — completes in ~1-2ms on modern systems

Simple

No complex neighbor scanning or opcode validation logic

Reliable

Handles all common EDR hook patterns (E9 JMP, FF 25 JMP, EB short JMP, CC int3)

Limitations

Export Table Manipulation

If an EDR modifies the export directory itself (extremely rare, as it breaks Windows API resolution), FreshyCalls can be defeated:
// Hypothetical: EDR reorders export RVAs to break VA sorting
// In practice, this would break legitimate API calls and is not observed
exports->AddressOfFunctions[hooked_index] = trampoline_rva;
Mitigation: Use RecycledGate, which combines FreshyCalls with opcode validation for cross-checking.

Syscall Number Changes

SSNs change between Windows builds (e.g., NtAllocateVirtualMemory is SSN 0x18 on Win10 21H2, but 0x1A on Win11 24H2). FreshyCalls automatically adapts because it derives SSNs from the running system’s ntdll layout.

Comparison with Other Methods

MethodHook ResistanceSpeedComplexityExport Table Dependency
StaticNoneInstantLow
Hell’s GateLowFastLow
Halo’s GateMediumFastMedium
Tartarus’ GateHighFastHigh
FreshyCallsVery HighFastLow
SyscallsFromDiskMaximumSlowMedium
RecycledGateMaximumMediumMedium

When to Use

  • Maximum paranoia required: Use SyscallsFromDisk or RecycledGate
  • Export table manipulation detected: Extremely rare; switch to SyscallsFromDisk
  • Kernel callbacks in use: No user-mode technique bypasses ETW-Ti; requires kernel-mode evasion

Usage in SysWhispers4

Generate with FreshyCalls (Default)

# FreshyCalls is the default — no flag needed
python syswhispers.py --preset common

# Explicit specification
python syswhispers.py --preset injection --resolve freshycalls

Integration

#include "SW4Syscalls.h"

int main(void) {
    // Initialize FreshyCalls SSN resolution
    if (!SW4_Initialize()) {
        fprintf(stderr, "[!] FreshyCalls initialization failed\n");
        return 1;
    }

    // SSNs are now resolved — use syscalls
    PVOID base = NULL;
    SIZE_T size = 0x1000;
    NTSTATUS st = SW4_NtAllocateVirtualMemory(
        GetCurrentProcess(), &base, 0, &size,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
    );

    return NT_SUCCESS(st) ? 0 : 1;
}

Detection Considerations

What EDRs Can See

  1. Export table enumeration: Iterating ntdll exports is common for legitimate software (loaders, debuggers)
  2. qsort usage: Generic sorting is not inherently suspicious
  3. Memory reads of ntdll: Standard PE parsing behavior

Detection Vectors

  • Behavioral: Syscall execution with RIP outside ntdll (if using embedded method)
  • Memory scanning: Signature detection of SysWhispers4 code patterns
  • Call stack analysis: Abnormal return addresses (mitigated by --stack-spoof)

Best Practices

1

Use indirect invocation

Combine with --method indirect to keep RIP inside ntdll during syscalls:
python syswhispers.py --resolve freshycalls --method indirect
2

Enable obfuscation

Randomize stub ordering and inject junk instructions:
python syswhispers.py --resolve freshycalls --obfuscate
3

Unhook ntdll first (optional)

Remove hooks before FreshyCalls runs for maximum stealth:
SW4_UnhookNtdll();  // Remove all inline hooks
SW4_Initialize();    // FreshyCalls reads clean stubs

Further Reading

RecycledGate

Enhanced version combining FreshyCalls with opcode validation

SyscallsFromDisk

Alternative that maps clean ntdll from disk

Invocation Methods

How to execute syscalls after SSN resolution

Original Research

FreshyCalls by crummie5

Build docs developers (and LLMs) love