Skip to content
λmaldev wiki/
pagesIAT Obfuscation
T1027WindowsC / C++ASMWinAPI

IAT Obfuscation

updated 2026-08-047 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

The Import Address Table (IAT) is a PE structure that lists every DLL and function the binary calls. Static analysis tools, AV engines, and YARA rules routinely scan it: a PE that imports VirtualAllocEx, WriteProcessMemory, and CreateRemoteThread is trivially flagged. IAT obfuscation removes these declarations entirely and resolves the function addresses at runtime, leaving the static IAT clean.

The spectrum of approaches ranges from simple LoadLibrary + GetProcAddress calls (still visible in API monitoring) to PEB-walking with API hashing (no strings, no imports, no calls to GetProcAddress) to compiler-level tricks like /DELAYLOAD abuse and custom import resolvers.

Note.

Stripping the IAT does not hide the APIs from a running sandbox — API monitoring hooks fire on the actual call regardless of how the address was obtained. IAT obfuscation defeats static analysis and signature-based scanners; combine it with direct syscalls or unhooking to address runtime inspection.

The call chain

  1. 1
    locate kernel32 via PEB InMemoryOrderModuleList
    Walk the PEB loader list to find kernel32 without calling GetModuleHandle (which itself appears in the IAT).
  2. 2
    parse kernel32 export table
    Walk IMAGE_EXPORT_DIRECTORY to resolve GetProcAddress by name or by hash.
  3. 3
    resolve target APIs via GetProcAddress or manual export walk
    Obtain function pointers for every API the payload needs, storing them in global variables or a struct.
  4. 4
    call APIs through function pointers
    Indirect calls through variables do not appear as imported symbols and break static YARA rules keyed on API names.

Reference implementation

PEB walk to find kernel32 (no imports needed)

peb_walk.cC
#include <windows.h>
#include <winternl.h>

// Walk PEB InMemoryOrderModuleList to locate kernel32.dll base
HMODULE get_kernel32(void) {
  PEB *peb = (PEB*)__readgsqword(0x60);
  LIST_ENTRY *head = &peb->Ldr->InMemoryOrderModuleList;
  LIST_ENTRY *cur  = head->Flink;

  while (cur != head) {
      LDR_DATA_TABLE_ENTRY *entry = CONTAINING_RECORD(
          cur, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

      // FullDllName.Buffer is a wide string — check for "kernel32"
      WCHAR *name = entry->FullDllName.Buffer;
      if (name) {
          // simple case-insensitive substring match
          for (int i = 0; name[i]; i++) {
              if ((name[i]|0x20) == 'k' && (name[i+1]|0x20) == 'e' &&
                  (name[i+2]|0x20) == 'r' && (name[i+3]|0x20) == 'n') {
                  return (HMODULE)entry->DllBase;
              }
          }
      }
      cur = cur->Flink;
  }
  return NULL;
}

API hashing — resolve by djb2 hash, no strings

api_hash.cC
#include <windows.h>

// djb2 hash of an ASCII string
static DWORD hash_api(const char *name) {
  DWORD h = 5381;
  while (*name) h = ((h << 5) + h) + (unsigned char)*name++;
  return h;
}

// Walk export table and find the function whose name hashes to target_hash
FARPROC resolve_by_hash(HMODULE mod, DWORD target_hash) {
  BYTE *base = (BYTE*)mod;
  PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
  PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);

  DWORD exp_rva = nt->OptionalHeader.DataDirectory[0].VirtualAddress;
  if (!exp_rva) return NULL;

  PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(base + exp_rva);
  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 *fn_name = (const char*)(base + names[i]);
      if (hash_api(fn_name) == target_hash) {
          return (FARPROC)(base + funcs[ordinals[i]]);
      }
  }
  return NULL;
}

// Pre-computed hashes (compute offline with a separate tool)
#define HASH_VIRTUAL_ALLOC       0x91afca54
#define HASH_VIRTUAL_PROTECT     0x7946c61b
#define HASH_CREATE_THREAD       0xb18726a3

typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL   (WINAPI *pVirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);

void resolve_apis(HMODULE k32, pVirtualAlloc *va, pVirtualProtect *vp) {
  *va = (pVirtualAlloc)  resolve_by_hash(k32, HASH_VIRTUAL_ALLOC);
  *vp = (pVirtualProtect)resolve_by_hash(k32, HASH_VIRTUAL_PROTECT);
}

XOR-encoded string resolution (lightweight alternative)

xor_strings.cC
#include <windows.h>

#define XOR_KEY 0x42

// XOR-encoded "VirtualAlloc" — generated by a build-time tool
static const BYTE enc_virtual_alloc[] = {
  0x14,0x29,0x27,0x33,0x27,0x20,0x27,0x2d,0x27,0x2d,0x2f,0x00
};

static char decode_str(const BYTE *enc, size_t len, char *out) {
  for (size_t i = 0; i < len; i++)
      out[i] = enc[i] ^ XOR_KEY;
  out[len] = 0;
  return 0;
}

FARPROC resolve_xor(HMODULE mod, const BYTE *enc_name, size_t len) {
  char name[64] = {0};
  decode_str(enc_name, len, name);
  FARPROC fp = GetProcAddress(mod, name);
  // zero out the decoded string immediately
  SecureZeroMemory(name, sizeof name);
  return fp;
}

Hash computation utility (build-time, run offline)

compute_hashes.pyPython
# Run offline to pre-compute API hashes for embedding in the implant
APIs = [
  "VirtualAlloc", "VirtualProtect", "CreateThread",
  "WriteProcessMemory", "OpenProcess", "NtAllocateVirtualMemory",
]

def djb2(s):
  h = 5381
  for c in s:
      h = ((h << 5) + h + ord(c)) & 0xFFFFFFFF
  return h

for api in APIs:
  print(f"#define HASH_{api.upper().replace('.','_'):40s} 0x{djb2(api):08x}")

Technique comparison

Approach Strings visible GetProcAddress in IAT Runtime detection
Direct imports Yes Trivial
LoadLibrary + GetProcAddress Yes (in .rdata) Yes Easy
XOR-encoded strings No Yes Moderate
API hashing + PEB walk No No Hard
Hashing + direct syscalls No No Very hard

API hashing with a PEB walk leaves zero clues in the static binary: no import table entries, no plaintext strings, no calls to GetProcAddress itself.

Detection

YARA
PE with no imports section (no IMAGE_IMPORT_DESCRIPTOR) or imports only LoadLibrary/GetProcAddress — classic dynamic resolution pattern.
BEHAVIOURAL
Process that resolves dozens of APIs via GetProcAddress calls in the first 500ms of execution.
MEMORY SCAN
XOR-encoded or reversed API name strings in the .data or .rdata section.
SANDBOX
API monitor log shows calls to GetProcAddress with common malware API names (VirtualAlloc, WriteProcessMemory, etc.).

The most reliable runtime detection is API monitoring that logs GetProcAddress arguments — even if the strings are decoded at the call site, the argument passed to GetProcAddress is briefly plaintext. Sandbox hooks on GetProcAddress can capture this. Fully PEB-based resolution evades this by never calling GetProcAddress.

Was this page useful?edit this page ↗