Chapter 16

Chapter 16: IAT Hiding & API Hashing

The Problem with Static Imports

Every Windows executable contains an Import Address Table (IAT) that lists the DLLs and functions the program requires. This table serves as a roadmap for the Windows loader, telling it which libraries to load and which function addresses to resolve. However, this same roadmap provides security tools with immediate insight into a program's capabilities. When an analyst or automated system sees imports like VirtualAlloc, WriteProcessMemory, and CreateRemoteThread, alarm bells ring before the program even executes.

The IAT problem fundamentally comes down to this: static imports create a permanent record of API usage that survives in the binary forever, visible to anyone who examines the file. The solution lies in resolving API addresses at runtime rather than compile time, eliminating the incriminating IAT entries while preserving the ability to use those functions.

THE IAT PROBLEM VISUALIZED
==========================

                    STATIC IMPORTS (Visible)
                    ========================

PE File on Disk:
┌──────────────────────────────────────────────────────────────┐
│  DOS Header                                                  │
│  PE Header                                                   │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Import Address Table (IAT)                            │ │
│  │  ─────────────────────────────────────────────────────│ │
│  │  kernel32.dll:                                        │ │
│  │    ├── VirtualAlloc         ◄── RED FLAG!             │ │
│  │    ├── VirtualProtect       ◄── RED FLAG!             │ │
│  │    ├── WriteProcessMemory   ◄── CRITICAL!             │ │
│  │    └── CreateRemoteThread   ◄── CRITICAL!             │ │
│  │  ntdll.dll:                                           │ │
│  │    └── NtAllocateVirtualMemory  ◄── EXTREMELY SUS!    │ │
│  └────────────────────────────────────────────────────────┘ │
│  .text (code)                                                │
│  .data (data)                                                │
└──────────────────────────────────────────────────────────────┘

                    DYNAMIC RESOLUTION (Hidden)
                    ===========================

PE File on Disk:
┌──────────────────────────────────────────────────────────────┐
│  DOS Header                                                  │
│  PE Header                                                   │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Import Address Table (IAT)                            │ │
│  │  ─────────────────────────────────────────────────────│ │
│  │  kernel32.dll:                                        │ │
│  │    └── GetSystemTime        ◄── Innocent!             │ │
│  │  user32.dll:                                          │ │
│  │    └── MessageBoxA          ◄── Normal!               │ │
│  └────────────────────────────────────────────────────────┘ │
│  .text (code contains runtime resolution logic)              │
│  .data (contains hash constants: 0x9CE0D4A, 0x10066F2F...)  │
└──────────────────────────────────────────────────────────────┘

At runtime, the code walks the PEB, parses export tables,
and resolves the "suspicious" APIs dynamically using hashes.

Part 1: Understanding the PE Import Structure

Before we can hide imports, we must understand how they work. The Windows Portable Executable (PE) format defines a specific structure for import information that the loader processes when starting a program.

The Import Directory

The PE header's optional header contains a data directory array, where entry index 1 points to the import directory. This directory contains an array of IMAGE_IMPORT_DESCRIPTOR structures, one for each imported DLL, terminated by a null entry.

PE IMPORT STRUCTURE ANATOMY
===========================

IMAGE_IMPORT_DESCRIPTOR (one per DLL):
┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│  OriginalFirstThunk  ──────► Import Lookup Table (ILT)             │
│  TimeDateStamp       ──────► Binding timestamp                      │
│  ForwarderChain      ──────► Forwarded function index              │
│  Name                ──────► RVA to DLL name string ("kernel32")   │
│  FirstThunk          ──────► Import Address Table (IAT)            │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Import Lookup Table (ILT) - Before Loading:
┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│  [0]  ──► IMAGE_IMPORT_BY_NAME { Hint=0x0, "VirtualAlloc" }        │
│  [1]  ──► IMAGE_IMPORT_BY_NAME { Hint=0x1, "VirtualProtect" }      │
│  [2]  ──► IMAGE_IMPORT_BY_NAME { Hint=0x2, "CreateThread" }        │
│  [3]  ──► NULL (terminator)                                         │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Import Address Table (IAT) - After Loading:
┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│  [0]  ──► 0x00007FFE45A20000  (VirtualAlloc actual address)        │
│  [1]  ──► 0x00007FFE45A20180  (VirtualProtect actual address)      │
│  [2]  ──► 0x00007FFE45A21A00  (CreateThread actual address)        │
│  [3]  ──► NULL                                                      │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

When code calls VirtualAlloc, it actually reads the address
from IAT[0] and jumps there. The loader filled in these addresses
at process startup by walking the export tables of each DLL.

The key insight is that the loader performs two operations we can replicate: finding loaded modules and looking up function addresses in their export tables. If we do this ourselves at runtime, we bypass the IAT entirely.


Part 2: Custom GetModuleHandle via PEB Walking

Windows tracks all loaded modules in a data structure accessible through the Process Environment Block (PEB). Each thread has a Thread Environment Block (TEB) containing a pointer to the PEB, and the PEB contains a pointer to the loader data structures.

Navigating the PEB Loader Structures

The PEB's Ldr field points to a PEB_LDR_DATA structure containing three linked lists of loaded modules:

Each list connects LDR_DATA_TABLE_ENTRY structures that contain the module's base address and name.

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

// Custom structures (the official ones are incomplete)
typedef struct _MY_PEB_LDR_DATA {
    ULONG Length;
    BOOLEAN Initialized;
    HANDLE SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
} MY_PEB_LDR_DATA, *PMY_PEB_LDR_DATA;

typedef struct _MY_LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    // Additional fields exist but aren't needed here
} MY_LDR_DATA_TABLE_ENTRY, *PMY_LDR_DATA_TABLE_ENTRY;

Implementing Custom GetModuleHandle

With these structures defined, we can walk the module list to find a specific DLL by name:

HMODULE GetModuleHandleCustom(LPCWSTR wszModuleName) {
    // Access PEB via TEB
    // The TEB is stored in a segment register (GS on x64, FS on x86)
    // The PEB pointer is at offset 0x60 (x64) or 0x30 (x86) from TEB

#ifdef _WIN64
    PPEB pPeb = (PPEB)__readgsqword(0x60);
#else
    PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif

    // Navigate to loader data
    PMY_PEB_LDR_DATA pLdr = (PMY_PEB_LDR_DATA)pPeb->Ldr;

    // Walk the InMemoryOrderModuleList
    // This is a circular doubly-linked list
    PLIST_ENTRY pHead = &pLdr->InMemoryOrderModuleList;
    PLIST_ENTRY pEntry = pHead->Flink;

    while (pEntry != pHead) {
        // The entry is embedded in LDR_DATA_TABLE_ENTRY
        // We use CONTAINING_RECORD to get the full structure
        PMY_LDR_DATA_TABLE_ENTRY pModule = CONTAINING_RECORD(
            pEntry,
            MY_LDR_DATA_TABLE_ENTRY,
            InMemoryOrderLinks
        );

        if (pModule->BaseDllName.Buffer) {
            // Case-insensitive comparison
            if (_wcsicmp(pModule->BaseDllName.Buffer, wszModuleName) == 0) {
                return (HMODULE)pModule->DllBase;
            }
        }

        pEntry = pEntry->Flink;
    }

    return NULL;  // Module not found
}
PEB MODULE LIST TRAVERSAL
=========================

TEB (Thread Environment Block)
┌──────────────────────────────┐
│  ...                         │
│  ProcessEnvironmentBlock ────┼────┐
│  (offset 0x60 on x64)        │    │
│  ...                         │    │
└──────────────────────────────┘    │
                                    │
                                    ▼
PEB (Process Environment Block)
┌──────────────────────────────┐
│  ...                         │
│  Ldr ────────────────────────┼────┐
│  ...                         │    │
└──────────────────────────────┘    │
                                    │
                                    ▼
PEB_LDR_DATA
┌───────────────────────────────────────────────────────────┐
│  InMemoryOrderModuleList (head) ◄──────┐                 │
│            │                            │                 │
│            ▼                            │                 │
│  ┌─────────────────┐    ┌─────────────────┐              │
│  │ ntdll.dll       │───►│ kernel32.dll    │───► ... ────┘│
│  │ DllBase: 0x7FF..│    │ DllBase: 0x7FE..│              │
│  │ BaseDllName     │    │ BaseDllName     │              │
│  └─────────────────┘    └─────────────────┘              │
└───────────────────────────────────────────────────────────┘

By walking this list, we find module base addresses
without calling GetModuleHandle (no IAT entry needed).

Part 3: Custom GetProcAddress via Export Table Parsing

Once we have a module's base address, we can find function addresses by parsing its export table. This is exactly what GetProcAddress does internally.

Understanding Export Tables

DLLs export functions through an IMAGE_EXPORT_DIRECTORY structure containing three parallel arrays:

EXPORT TABLE STRUCTURE
======================

IMAGE_EXPORT_DIRECTORY:
┌─────────────────────────────────────────────────────────────────┐
│  Name                 ──► "kernel32.dll"                        │
│  Base                 ──► Ordinal base (usually 1)              │
│  NumberOfFunctions    ──► Total exported functions              │
│  NumberOfNames        ──► Functions exported by name            │
│  AddressOfFunctions   ──► RVA array of function addresses       │
│  AddressOfNames       ──► RVA array of name string RVAs         │
│  AddressOfNameOrdinals──► Word array of ordinal indices         │
└─────────────────────────────────────────────────────────────────┘

Lookup Process for "VirtualAlloc":

AddressOfNames:         AddressOfNameOrdinals:   AddressOfFunctions:
┌────────────────┐      ┌───────────────────┐    ┌──────────────────┐
│ [0] "CloseH.."│       │ [0] → 42          │    │ [0]  → 0x1000    │
│ [1] "Create.."│       │ [1] → 103         │    │ [1]  → 0x1180    │
│ [2] "Virtual."│ ◄──   │ [2] → 7   ────────┼───►│ [7]  → 0x5A00   │
│ ...           │  Match│ ...               │    │ ...              │
└────────────────┘      └───────────────────┘    └──────────────────┘

1. Find "VirtualAlloc" in AddressOfNames array (index 2)
2. Read AddressOfNameOrdinals[2] = 7
3. Read AddressOfFunctions[7] = 0x5A00 (RVA)
4. Return DllBase + 0x5A00 = actual function address

Implementing Custom GetProcAddress

FARPROC GetProcAddressCustom(HMODULE hModule, LPCSTR szFunctionName) {
    PBYTE pBase = (PBYTE)hModule;

    // Parse PE headers
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBase;
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
        return NULL;  // Invalid PE
    }

    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(pBase + pDosHeader->e_lfanew);
    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
        return NULL;  // Invalid PE
    }

    // Get export directory
    DWORD dwExportRVA = pNtHeaders->OptionalHeader.DataDirectory[
        IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

    if (dwExportRVA == 0) {
        return NULL;  // No exports
    }

    PIMAGE_EXPORT_DIRECTORY pExportDir =
        (PIMAGE_EXPORT_DIRECTORY)(pBase + dwExportRVA);

    // Get the three arrays
    PDWORD pdwAddressOfFunctions = (PDWORD)(pBase + pExportDir->AddressOfFunctions);
    PDWORD pdwAddressOfNames = (PDWORD)(pBase + pExportDir->AddressOfNames);
    PWORD pwAddressOfNameOrdinals = (PWORD)(pBase + pExportDir->AddressOfNameOrdinals);

    // Linear search through names
    for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) {
        LPCSTR szCurrentName = (LPCSTR)(pBase + pdwAddressOfNames[i]);

        if (strcmp(szCurrentName, szFunctionName) == 0) {
            // Found it - get the ordinal, then the function address
            WORD wOrdinal = pwAddressOfNameOrdinals[i];
            DWORD dwFunctionRVA = pdwAddressOfFunctions[wOrdinal];

            // Check for forwarded export (address within export section)
            DWORD dwExportSize = pNtHeaders->OptionalHeader.DataDirectory[
                IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

            if (dwFunctionRVA >= dwExportRVA &&
                dwFunctionRVA < dwExportRVA + dwExportSize) {
                // This is a forwarded export
                // The "address" is actually a string like "NTDLL.RtlAllocateHeap"
                // Full implementation would recursively resolve this
                return NULL;
            }

            return (FARPROC)(pBase + dwFunctionRVA);
        }
    }

    return NULL;  // Function not found
}

Part 4: API Hashing Fundamentals

While custom GetModuleHandle and GetProcAddress eliminate IAT entries, they still require function name strings in the binary. These strings are easily detected: an analyst seeing "VirtualAlloc" or "CreateRemoteThread" in the strings output knows exactly what the program does.

API hashing solves this by replacing strings with numeric hash values. At runtime, the code hashes each export name and compares against the target hash. No function name strings appear in the binary.

The DJB2 Hash Algorithm

DJB2 is the most commonly used API hash algorithm due to its simplicity and good distribution. Created by Daniel J. Bernstein, it processes one character at a time with a multiply-and-add operation.

// DJB2 hash implementation
DWORD HashStringDjb2(LPCSTR szString) {
    DWORD dwHash = 5381;  // Magic starting value

    while (*szString) {
        // hash = hash * 33 + char
        // The multiplication by 33 is done as (hash << 5) + hash
        // because bit shifting is faster than multiplication
        dwHash = ((dwHash << 5) + dwHash) + *szString++;
    }

    return dwHash;
}

// Case-insensitive version for module names
DWORD HashStringDjb2W(LPCWSTR wszString) {
    DWORD dwHash = 5381;

    while (*wszString) {
        WCHAR c = *wszString++;
        // Convert to lowercase for case-insensitive comparison
        if (c >= L'A' && c <= L'Z') {
            c += 32;
        }
        dwHash = ((dwHash << 5) + dwHash) + c;
    }

    return dwHash;
}
DJB2 HASH COMPUTATION EXAMPLE
=============================

Input: "VirtualAlloc"

Initial: hash = 5381 (0x1505)

Step 1: 'V' (86)
  hash = (5381 << 5) + 5381 + 86
       = 172192 + 5381 + 86
       = 177659 (0x2B5DB)

Step 2: 'i' (105)
  hash = (177659 << 5) + 177659 + 105
       = 5685088 + 177659 + 105
       = 5862852 (0x596AC4)

Step 3: 'r' (114)
  hash = (5862852 << 5) + 5862852 + 114
       = 187611264 + 5862852 + 114
       = 193474230 (0xB8765B6)

... (continues for each character) ...

Final: 0x9CE0D4A (for "VirtualAlloc")

This hash value replaces the string in the binary.
At runtime, we hash each export name and compare.

Other Hash Algorithms

Different hash algorithms provide different trade-offs between speed, collision resistance, and detectability:

// Jenkins One-at-a-Time - Better distribution
DWORD JenkinsOneAtATime(LPCSTR szString) {
    DWORD dwHash = 0;

    while (*szString) {
        dwHash += *szString++;
        dwHash += (dwHash << 10);
        dwHash ^= (dwHash >> 6);
    }

    // Final mixing
    dwHash += (dwHash << 3);
    dwHash ^= (dwHash >> 11);
    dwHash += (dwHash << 15);

    return dwHash;
}

// Rotate-based hash (common in shellcode)
DWORD Ror13Hash(LPCSTR szString) {
    DWORD dwHash = 0;

    while (*szString) {
        // Rotate right by 13 bits
        dwHash = (dwHash >> 13) | (dwHash << 19);
        dwHash += *szString++;
    }

    return dwHash;
}

Part 5: Hashed Module and Function Lookup

Combining PEB walking with API hashing creates a complete dynamic resolution system that requires no strings and no IAT entries.

Module Lookup by Hash

// Precomputed module hashes (lowercase)
#define KERNEL32_HASH 0x6DDB9555  // "kernel32.dll"
#define NTDLL_HASH    0x1EDAB0ED  // "ntdll.dll"
#define USER32_HASH   0x63C84283  // "user32.dll"

HMODULE GetModuleByHash(DWORD dwModuleHash) {
#ifdef _WIN64
    PPEB pPeb = (PPEB)__readgsqword(0x60);
#else
    PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif

    PMY_PEB_LDR_DATA pLdr = (PMY_PEB_LDR_DATA)pPeb->Ldr;
    PLIST_ENTRY pHead = &pLdr->InMemoryOrderModuleList;
    PLIST_ENTRY pEntry = pHead->Flink;

    while (pEntry != pHead) {
        PMY_LDR_DATA_TABLE_ENTRY pModule = CONTAINING_RECORD(
            pEntry, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

        if (pModule->BaseDllName.Buffer) {
            // Hash the current module name
            DWORD dwCurrentHash = HashStringDjb2W(pModule->BaseDllName.Buffer);

            if (dwCurrentHash == dwModuleHash) {
                return (HMODULE)pModule->DllBase;
            }
        }

        pEntry = pEntry->Flink;
    }

    return NULL;
}

Function Lookup by Hash

// Precomputed function hashes
#define VIRTUALALLOC_HASH      0x9CE0D4A
#define VIRTUALPROTECT_HASH    0x10066F2F
#define CREATETHREAD_HASH      0x44F575C3
#define WAITFORSINGLEOBJ_HASH  0xE8AFE98

FARPROC GetFunctionByHash(HMODULE hModule, DWORD dwFunctionHash) {
    PBYTE pBase = (PBYTE)hModule;

    // Navigate PE structure
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pBase + pDos->e_lfanew);

    DWORD dwExportRVA = pNt->OptionalHeader.DataDirectory[
        IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

    if (!dwExportRVA) return NULL;

    PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pBase + dwExportRVA);

    // Get export arrays
    PDWORD pdwFunctions = (PDWORD)(pBase + pExport->AddressOfFunctions);
    PDWORD pdwNames = (PDWORD)(pBase + pExport->AddressOfNames);
    PWORD pwOrdinals = (PWORD)(pBase + pExport->AddressOfNameOrdinals);

    // Hash each export name and compare
    for (DWORD i = 0; i < pExport->NumberOfNames; i++) {
        LPCSTR szName = (LPCSTR)(pBase + pdwNames[i]);
        DWORD dwCurrentHash = HashStringDjb2(szName);

        if (dwCurrentHash == dwFunctionHash) {
            WORD wOrdinal = pwOrdinals[i];
            return (FARPROC)(pBase + pdwFunctions[wOrdinal]);
        }
    }

    return NULL;
}

Part 6: Compile-Time Hashing

The techniques above still have one weakness: the hash values appear as constants in the binary. Signature-based detection can target known hash values like 0x9CE0D4A for VirtualAlloc. Compile-time hashing with randomization addresses this.

C++ constexpr Hashing

C++11 introduced constexpr functions that the compiler evaluates at compile time when possible. This allows computing hashes during compilation, ensuring no string ever exists in the source code flow.

// Compile-time DJB2 hash using recursion
constexpr DWORD HashCompileTime(const char* szString, DWORD dwHash = 5381) {
    // Base case: null terminator
    return (*szString == '\0')
        ? dwHash
        : HashCompileTime(
            szString + 1,
            ((dwHash << 5) + dwHash) + *szString
          );
}

// Convenience macro
#define HASH(str) HashCompileTime(str)

// Usage - these are computed at compile time
constexpr DWORD HASH_VirtualAlloc = HASH("VirtualAlloc");
constexpr DWORD HASH_CreateThread = HASH("CreateThread");

// The string "VirtualAlloc" never appears in the binary
// Only the constant 0x9CE0D4A is embedded

Randomized Compile-Time Hashing

To defeat hash-based signatures, we can add a per-build seed that modifies all hash values. Each compilation produces different hashes.

// Generate seed from compile time
constexpr DWORD CompileTimeSeed() {
    // Use __TIME__ macro: "HH:MM:SS"
    return (__TIME__[0] - '0') * 36000 +
           (__TIME__[1] - '0') * 3600 +
           (__TIME__[3] - '0') * 600 +
           (__TIME__[4] - '0') * 60 +
           (__TIME__[6] - '0') * 10 +
           (__TIME__[7] - '0');
}

// Seeded hash - XOR result with seed
constexpr DWORD HashSeeded(const char* szString, DWORD dwSeed, DWORD dwHash = 5381) {
    return (*szString == '\0')
        ? (dwHash ^ dwSeed)  // Apply seed at the end
        : HashSeeded(szString + 1, dwSeed, ((dwHash << 5) + dwHash) + *szString);
}

#define HASH_RANDOM(str) HashSeeded(str, CompileTimeSeed())

// Runtime hash must use same seed
DWORD HashStringWithSeed(LPCSTR szString) {
    DWORD dwHash = 5381;
    DWORD dwSeed = CompileTimeSeed();  // Same seed as compile time

    while (*szString) {
        dwHash = ((dwHash << 5) + dwHash) + *szString++;
    }

    return dwHash ^ dwSeed;
}
COMPILE-TIME vs RUNTIME HASHING
===============================

Traditional Approach:
┌────────────────────────────────────────────────────────────┐
│ Source Code: GetProcAddress(h, "VirtualAlloc")            │
│                              ↓                             │
│ Binary contains: "VirtualAlloc" string                     │
│                                                            │
│ DETECTION: String search finds "VirtualAlloc" → FLAGGED   │
└────────────────────────────────────────────────────────────┘

Runtime Hashing:
┌────────────────────────────────────────────────────────────┐
│ Source Code: GetFunctionByHash(h, 0x9CE0D4A)              │
│                              ↓                             │
│ Binary contains: 0x9CE0D4A constant                        │
│                                                            │
│ DETECTION: Known hash signature → FLAGGED                  │
└────────────────────────────────────────────────────────────┘

Compile-Time Randomized Hashing:
┌────────────────────────────────────────────────────────────┐
│ Source Code: GetFunctionByHash(h, HASH_RANDOM("VirtualA"))│
│                              ↓                             │
│ Binary contains: 0xA7B3F21 (varies per build)              │
│                                                            │
│ DETECTION: No known signature, no string → HARDER!         │
└────────────────────────────────────────────────────────────┘

Part 7: Pseudo-Handle Optimization

Some Windows API functions always return constant values that can be replaced with macros, avoiding even the need for dynamic resolution.

GetCurrentProcess and GetCurrentThread

These functions always return pseudo-handles: GetCurrentProcess() returns (HANDLE)-1 and GetCurrentThread() returns (HANDLE)-2. These aren't real kernel handles but special values the kernel recognizes.

// Avoid IAT entries entirely by using constants
#define NtCurrentProcess() ((HANDLE)(LONG_PTR)-1)
#define NtCurrentThread()  ((HANDLE)(LONG_PTR)-2)

void Example() {
    // Instead of: HANDLE hProc = GetCurrentProcess();
    HANDLE hProc = NtCurrentProcess();  // No API call!

    // Instead of: HANDLE hThread = GetCurrentThread();
    HANDLE hThread = NtCurrentThread();  // No API call!

    // These pseudo-handles work everywhere real handles would
    MEMORY_BASIC_INFORMATION mbi;
    VirtualQueryEx(hProc, someAddress, &mbi, sizeof(mbi));
}

Part 8: Complete Implementation Example

Bringing together all concepts into a practical API resolution system:

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

// =====================================================================
// CONFIGURATION
// =====================================================================

#define DJB2_SEED 5381

// Module hashes (computed offline, case-insensitive)
#define H_KERNEL32 0x6DDB9555  // kernel32.dll
#define H_NTDLL    0x1EDAB0ED  // ntdll.dll

// Function hashes (case-sensitive)
#define H_VIRTUALALLOC        0x9CE0D4A
#define H_VIRTUALPROTECT      0x10066F2F
#define H_VIRTUALFREE         0x30633AC
#define H_CREATETHREAD        0x44F575C3
#define H_WAITFORSINGLEOBJECT 0xE8AFE98

// =====================================================================
// HASH FUNCTIONS
// =====================================================================

__forceinline DWORD HashStringW(LPCWSTR wszString) {
    DWORD dwHash = DJB2_SEED;
    while (*wszString) {
        WCHAR c = *wszString++;
        if (c >= L'A' && c <= L'Z') c += 32;  // Lowercase
        dwHash = ((dwHash << 5) + dwHash) + c;
    }
    return dwHash;
}

__forceinline DWORD HashStringA(LPCSTR szString) {
    DWORD dwHash = DJB2_SEED;
    while (*szString) {
        dwHash = ((dwHash << 5) + dwHash) + *szString++;
    }
    return dwHash;
}

// =====================================================================
// STRUCTURES
// =====================================================================

typedef struct _MY_PEB_LDR_DATA {
    ULONG Length;
    BOOLEAN Initialized;
    HANDLE SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
} MY_PEB_LDR_DATA, *PMY_PEB_LDR_DATA;

typedef struct _MY_LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
} MY_LDR_DATA_TABLE_ENTRY, *PMY_LDR_DATA_TABLE_ENTRY;

// =====================================================================
// API RESOLUTION
// =====================================================================

HMODULE GetModuleByHash(DWORD dwHash) {
#ifdef _WIN64
    PPEB pPeb = (PPEB)__readgsqword(0x60);
#else
    PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif

    PMY_PEB_LDR_DATA pLdr = (PMY_PEB_LDR_DATA)pPeb->Ldr;
    PLIST_ENTRY pHead = &pLdr->InMemoryOrderModuleList;
    PLIST_ENTRY pEntry = pHead->Flink;

    while (pEntry != pHead) {
        PMY_LDR_DATA_TABLE_ENTRY pModule = CONTAINING_RECORD(
            pEntry, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

        if (pModule->BaseDllName.Buffer &&
            HashStringW(pModule->BaseDllName.Buffer) == dwHash) {
            return (HMODULE)pModule->DllBase;
        }
        pEntry = pEntry->Flink;
    }
    return NULL;
}

FARPROC GetFunctionByHash(HMODULE hModule, DWORD dwHash) {
    PBYTE pBase = (PBYTE)hModule;

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pBase + pDos->e_lfanew);

    DWORD dwExportRVA = pNt->OptionalHeader.DataDirectory[
        IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if (!dwExportRVA) return NULL;

    PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pBase + dwExportRVA);

    PDWORD pdwFunctions = (PDWORD)(pBase + pExport->AddressOfFunctions);
    PDWORD pdwNames = (PDWORD)(pBase + pExport->AddressOfNames);
    PWORD pwOrdinals = (PWORD)(pBase + pExport->AddressOfNameOrdinals);

    for (DWORD i = 0; i < pExport->NumberOfNames; i++) {
        if (HashStringA((LPCSTR)(pBase + pdwNames[i])) == dwHash) {
            return (FARPROC)(pBase + pdwFunctions[pwOrdinals[i]]);
        }
    }
    return NULL;
}

// =====================================================================
// FUNCTION POINTER TYPES
// =====================================================================

typedef LPVOID (WINAPI* fnVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL (WINAPI* fnVirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef BOOL (WINAPI* fnVirtualFree)(LPVOID, SIZE_T, DWORD);
typedef HANDLE (WINAPI* fnCreateThread)(
    LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD (WINAPI* fnWaitForSingleObject)(HANDLE, DWORD);

// API table for organized access
typedef struct _API_TABLE {
    fnVirtualAlloc pVirtualAlloc;
    fnVirtualProtect pVirtualProtect;
    fnVirtualFree pVirtualFree;
    fnCreateThread pCreateThread;
    fnWaitForSingleObject pWaitForSingleObject;
} API_TABLE, *PAPI_TABLE;

// =====================================================================
// INITIALIZATION
// =====================================================================

BOOL InitializeAPIs(PAPI_TABLE pApis) {
    HMODULE hKernel32 = GetModuleByHash(H_KERNEL32);
    if (!hKernel32) return FALSE;

    pApis->pVirtualAlloc = (fnVirtualAlloc)GetFunctionByHash(hKernel32, H_VIRTUALALLOC);
    pApis->pVirtualProtect = (fnVirtualProtect)GetFunctionByHash(hKernel32, H_VIRTUALPROTECT);
    pApis->pVirtualFree = (fnVirtualFree)GetFunctionByHash(hKernel32, H_VIRTUALFREE);
    pApis->pCreateThread = (fnCreateThread)GetFunctionByHash(hKernel32, H_CREATETHREAD);
    pApis->pWaitForSingleObject = (fnWaitForSingleObject)GetFunctionByHash(
        hKernel32, H_WAITFORSINGLEOBJECT);

    // Verify all APIs resolved
    return (pApis->pVirtualAlloc && pApis->pVirtualProtect &&
            pApis->pVirtualFree && pApis->pCreateThread &&
            pApis->pWaitForSingleObject);
}

// =====================================================================
// USAGE EXAMPLE
// =====================================================================

int main() {
    API_TABLE apis = { 0 };

    if (!InitializeAPIs(&apis)) {
        return 1;  // Resolution failed
    }

    // Use dynamically resolved APIs - no IAT entries exist for these
    LPVOID pMem = apis.pVirtualAlloc(
        NULL,
        4096,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE
    );

    if (!pMem) return 1;

    // Change protection
    DWORD dwOldProtect;
    apis.pVirtualProtect(pMem, 4096, PAGE_EXECUTE_READ, &dwOldProtect);

    // Clean up
    apis.pVirtualFree(pMem, 0, MEM_RELEASE);

    return 0;
}

Part 9: Defense and Detection

Understanding IAT hiding helps defenders recognize when it's being used and develop appropriate countermeasures.

Detection Strategies

IAT HIDING DETECTION MATRIX
===========================

┌──────────────────────────┬─────────────────────────────┬────────────────┐
│ Indicator                │ Detection Method            │ Difficulty     │
├──────────────────────────┼─────────────────────────────┼────────────────┤
│ Minimal IAT              │ PE static analysis          │ Easy           │
│ (few legitimate imports) │                             │                │
├──────────────────────────┼─────────────────────────────┼────────────────┤
│ PEB/TEB access patterns  │ Behavioral monitoring       │ Hard           │
│ (segment register reads) │ (runtime hooking)           │                │
├──────────────────────────┼─────────────────────────────┼────────────────┤
│ PE header parsing        │ Hook NtReadVirtualMemory    │ Medium         │
│                          │ for self-inspection         │                │
├──────────────────────────┼─────────────────────────────┼────────────────┤
│ Known hash constants     │ Signature-based scanning    │ Easy           │
│ (e.g., 0x9CE0D4A)       │                             │ (if not random)│
├──────────────────────────┼─────────────────────────────┼────────────────┤
│ API use without import   │ Behavioral analysis:        │ Medium         │
│                          │ API called but not in IAT   │                │
└──────────────────────────┴─────────────────────────────┴────────────────┘

YARA Detection Rules

rule IAT_Hiding_PEB_Walk {
    meta:
        description = "Detects PEB walking for module enumeration"

    strings:
        // x64: mov rax, gs:[0x60] (PEB access)
        $peb64 = { 65 48 8B 04 25 60 00 00 00 }

        // x86: mov eax, fs:[0x30] (PEB access)
        $peb32 = { 64 A1 30 00 00 00 }

        // Ldr access offset 0x18 from PEB
        $ldr_access = { 48 8B ?? 18 }

    condition:
        any of ($peb*) and $ldr_access
}

rule API_Hashing_DJB2 {
    meta:
        description = "Detects DJB2 hash algorithm"

    strings:
        // DJB2 initial value 5381 (0x1505)
        $init1 = { C7 ?? 05 15 00 00 }  // mov [mem], 0x1505
        $init2 = { B? 05 15 00 00 }     // mov reg, 0x1505

        // Shift left 5 (the *33 operation)
        $shl5 = { C1 E? 05 }            // shl reg, 5

    condition:
        any of ($init*) and $shl5
}

rule Known_API_Hashes {
    meta:
        description = "Detects known API hash constants"

    strings:
        // Common DJB2 hashes
        $h_virtualalloc = { 4A 0D CE 09 }     // VirtualAlloc
        $h_virtualprotect = { 2F 6F 06 10 }   // VirtualProtect
        $h_createthread = { C3 75 F5 44 }     // CreateThread

    condition:
        any of them
}

Summary: The IAT Hiding Toolkit

IAT hiding represents a fundamental technique for evading static analysis. By understanding how Windows resolves API addresses, we can replicate that process at runtime, eliminating the permanent record that static imports create.

TECHNIQUE COMPARISON
====================

┌────────────────────────┬──────────────┬────────────┬──────────────────┐
│ Technique              │ Effectiveness│ Complexity │ Detection Diff.  │
├────────────────────────┼──────────────┼────────────┼──────────────────┤
│ Custom GetModuleHandle │ High         │ Medium     │ Hard             │
│ (PEB walking)          │              │            │                  │
├────────────────────────┼──────────────┼────────────┼──────────────────┤
│ Custom GetProcAddress  │ High         │ Medium     │ Hard             │
│ (Export parsing)       │              │            │                  │
├────────────────────────┼──────────────┼────────────┼──────────────────┤
│ Runtime API hashing    │ Medium       │ Low        │ Medium           │
│ (Known signatures)     │              │            │ (hash sigs)      │
├────────────────────────┼──────────────┼────────────┼──────────────────┤
│ Compile-time hashing   │ High         │ Medium     │ Hard             │
│ (No strings ever)      │              │            │                  │
├────────────────────────┼──────────────┼────────────┼──────────────────┤
│ Randomized hashing     │ Very High    │ Medium     │ Very Hard        │
│ (Per-build variation)  │              │            │                  │
├────────────────────────┼──────────────┼────────────┼──────────────────┤
│ Pseudo-handle macros   │ Medium       │ Low        │ Easy             │
│ (Constant substitution)│              │            │                  │
└────────────────────────┴──────────────┴────────────┴──────────────────┘

Best Practices:

  1. Combine multiple techniques (PEB walking + hashing + compile-time)
  2. Use randomized hashes to defeat signatures
  3. Resolve APIs lazily on first use, not all at startup
  4. Consider syscalls for the most critical APIs (see Chapter 6)
  5. Include legitimate IAT entries to avoid "empty IAT" detection

References

← Back to Wiki