Import Table Obfuscation
Destroying or hiding the original Import Address Table so static tools see an empty or tiny import list, while the packer reconstructs the real API pointers at runtime.
Most static triage starts with the import table: the APIs a binary pulls from
kernel32.dll, ws2_32.dll or advapi32.dll reveal its intent before a single
instruction is read. Import table obfuscation breaks that shortcut. The packer
strips the original IMAGE_DIRECTORY_ENTRY_IMPORT, leaving the IAT empty or
holding only a couple of bootstrap functions, then rebuilds every needed pointer
at runtime.
Because the Windows loader never resolves the real APIs, a disassembler shows indirect calls through addresses that are zero on disk, and cross-references to named imports simply vanish.
How it works
At load time the stub resolves one or two anchor functions (typically
LoadLibraryA and GetProcAddress), then walks its own obfuscated tables to
populate the IAT. APIs are usually referenced by a hash of the name rather than
the name itself, so no readable strings survive in the file:
/* Resolve an API by ROR-13 hash instead of by name — no strings on disk */
FARPROC resolve(HMODULE mod, uint32_t want_hash) {
BYTE *base = (BYTE *)mod;
IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
DWORD rva = nt->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
IMAGE_EXPORT_DIRECTORY *exp = (IMAGE_EXPORT_DIRECTORY *)(base + 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 *name = (const char *)(base + names[i]);
if (ror13(name) == want_hash) /* compare hashes, not strings */
return (FARPROC)(base + funcs[ords[i]]);
}
return NULL;
}The resolved pointers are written into a private table that the unpacked code
calls indirectly (call [rbx+8]), so the real IAT may stay empty even after
execution begins.
Detection & analysis
Static analysis: The frontmatter is the tell — dumpbin /imports or PE
viewers (CFF Explorer, PE-bear) show only LoadLibraryA/GetProcAddress, or an
import directory of size zero, against a .text section full of indirect calls
through uninitialised pointers. Look for the export-walking loop and a hashing
routine (ROR-13, CRC32, or a custom rotate) operating on .dll export names.
Dynamic analysis: Let the stub finish resolving, then dump the process and
rebuild imports. Breakpoint on GetProcAddress and log each (module, name)
pair to reconstruct the API map; Scylla and pe-sieve can scan mapped memory
for valid API pointers and synthesise a fresh import directory at the OEP. A
debugger conditional log on GetProcAddress returns the full list the malware
hid.
Detection rule hint: Flag PEs whose import directory resolves to fewer than
~5 functions yet whose .text entropy and indirect-call density are high; pair
this with a YARA rule matching a ROR-13/CRC32 export-hash loop and references to
AddressOfNameOrdinals in code, which together strongly indicate runtime import
reconstruction rather than a genuinely tiny program.