Skip to content
Obfuscationintermediate

API Hashing

Malware replaces imported function names with pre-computed hash values and resolves addresses at runtime by walking the PE export table, hiding API usage from static analysis.

Rather than listing Windows API names in the Import Address Table (IAT), malware stores 32-bit hash values of the names it needs. At runtime, the loader iterates the export directory of kernel32.dll (and other modules), hashes each export name with the same algorithm, and resolves the address when a match is found. The IAT is empty or minimal, and strings like "CreateRemoteThread" never appear in the binary.

Common hash algorithms used: ROR-13 (Metasploit/Cobalt Strike default), custom ADD-based, djb2, FNV-1a.

How it works

c
// ROR-13 hash — the de-facto standard in shellcode loaders
DWORD ror13_hash(const char *name)
{
    DWORD h = 0;
    while (*name) {
        h = (h >> 13) | (h << 19);   // rotate right 13
        h += (BYTE)*name++;
    }
    return h;
}

// Walk a module's export table and find a function by hash
FARPROC resolve_by_hash(HMODULE hMod, DWORD target_hash)
{
    BYTE *base = (BYTE *)hMod;
    IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
    IMAGE_NT_HEADERS *nt  = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
    IMAGE_EXPORT_DIRECTORY *exp = (IMAGE_EXPORT_DIRECTORY *)
        (base + nt->OptionalHeader.DataDirectory[0].VirtualAddress);

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

    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        const char *name = (const char *)(base + names[i]);
        if (ror13_hash(name) == target_hash)
            return (FARPROC)(base + funcs[ordinals[i]]);
    }
    return NULL;
}

Many implementations obtain the kernel32.dll base address without any imports by walking the PEB's InMemoryOrderModuleList:

asm
mov  eax, fs:[30h]        ; PEB
mov  eax, [eax+0Ch]       ; PEB.Ldr
mov  eax, [eax+14h]       ; InMemoryOrderModuleList.Flink (first = ntdll)
mov  eax, [eax]           ; second entry (kernel32)
mov  eax, [eax+10h]       ; DllBase

Detection & analysis

Static analysis:

  • Identify the absence of imports combined with a small resolver function containing a rotate/XOR loop.
  • IDA/Ghidra: apply a script that pre-computes the hash for every export of common DLLs and renames indirect calls accordingly. Community scripts exist for ROR-13 and several variants.
  • CAPA rule U0217 / B0032.001 flags export-table iteration patterns combined with hash comparisons.

Dynamic analysis:

  • Hook GetProcAddress — many implementations eventually fall back to it for modules loaded at runtime.
  • Use API monitor or Frida to log all resolved function addresses against known exports.
  • Memory dumps after the resolver runs will reveal a populated function-pointer table suitable for comparison against known API addresses.
Votes

Comments(0)