ᛉ Yggdrasil

Malware development

PE Relocations & Import Resolution

15 Oct 2026 · 12,005 words · 55 min

Contents

Legend: [RELOC] = Base Relocation concept | [IAT] = Import Address Table | [EAT] = Export Address Table | [INTERN] = Windows Internals | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [HOOK] = Hooking technique


1. Why Relocations and Imports Matter for Red Teaming

What it means: Every PE file (EXE, DLL, SYS driver) depends on two critical mechanisms to run: relocations (to fix hardcoded addresses when not loaded at the preferred base) and imports (to connect function calls in your binary to function addresses inside other DLLs). These two mechanisms are the primary attack surface for EDRs, the primary mechanism for malware to hide API usage, and the core of every manual PE loader a red teamer builds.

Why this matters operationally:

For the Red Teamer building tools:
  ├─ Manual PE loading REQUIRES processing both relocations and imports manually
  ├─ Reflective loaders (RDI, sRDI, Donut) all do this internally
  ├─ Shellcode must be position-independent (no relocations at all)
  └─ Hiding API usage = avoiding imports entirely (dynamic resolution)

For the Red Teamer evading detection:
  ├─ EDRs read the IAT to know what APIs your binary will call → detects intent
  ├─ IAT hooking is HOW EDRs intercept your API calls at runtime
  ├─ Bypassing IAT hooks = dynamic resolution (D/Invoke, GetProcAddress, syscalls)
  └─ API hashing = no readable function name strings in binary at all

For the Reverse Engineer / Analyst:
  ├─ Import table = highest-signal part of PE for initial triage
  ├─ Suspicious imports: VirtualAlloc, WriteProcessMemory, CreateRemoteThread
  ├─ Missing expected imports = dynamic resolution = evasion technique
  └─ Export table used to find ReflectiveLoader, hooked functions

The full dependency chain:

Binary on disk:
  IAT entries = RVAs to name strings (not yet addresses)
  Hardcoded pointers = absolute VAs using preferred ImageBase
       ↓
Windows Loader (or your manual loader):
  Step 1: Map sections to memory (possibly at a different base)
  Step 2: Process relocations → fix all hardcoded absolute addresses
  Step 3: Process imports → resolve all IAT entries to real function addresses
       ↓
Binary in memory:
  IAT entries = actual function addresses (e.g., 0x7FFAB2340000 = VirtualAlloc)
  Hardcoded pointers = fixed to reflect actual load address
  Binary can now execute correctly

[OPSEC] The red team insight: The IAT is a map of your tool's capabilities visible to every security tool on the system. Dynamic API resolution — calling GetProcAddress at runtime or resolving via PEB walk — removes that visibility. But dynamic resolution itself can be a behavioral signal.


2. PE Memory Layout — RVA, FOA, VA Explained

[INTERN] These three address types appear constantly in PE parsing. Confusing them is the most common bug in PE loaders.

2.1 Definitions

ImageBase:  The preferred load address from OptionalHeader.ImageBase
            Typical values: 0x00400000 (x86 EXE), 0x10000000 (x86 DLL),
                            0x0000000140000000 (x64 EXE/DLL)

RVA (Relative Virtual Address):
  An offset FROM ImageBase when the PE is mapped in memory.
  RVA 0x1000 with ImageBase 0x140000000 → VA 0x140001000

VA (Virtual Address):
  The actual in-memory address. VA = ImageBase + RVA
  (When ASLR moves the base: VA = ActualBase + RVA)

FOA (File Offset Address) / Raw Offset:
  An offset from the START of the PE file on disk.
  Used to locate data in the raw .exe/.dll file bytes.
  NOT the same as RVA — sections are page-aligned in memory
  but packed differently on disk.

Key distinction:
  On disk:   sections start at PointerToRawData (packed, no padding)
  In memory: sections start at VirtualAddress   (page-aligned, gaps zero-filled)

2.2 RVA to VA Conversion

// The most fundamental operation in PE parsing
// Everything in PE headers is expressed as an RVA

// When PE is mapped in memory at base:
VA = (BYTE*)base + RVA;

// Examples:
DWORD entryPointRVA = nt->OptionalHeader.AddressOfEntryPoint;
LPVOID entryPoint   = (BYTE*)imageBase + entryPointRVA;  // VA of DllMain/WinMain

DWORD importRVA = nt->OptionalHeader.DataDirectory[1].VirtualAddress;
PIMAGE_IMPORT_DESCRIPTOR imports = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)imageBase + importRVA);

2.3 RVA to FOA Conversion (when working with raw file bytes)

// When reading a PE file from disk (not yet mapped), RVAs ≠ FOAs
// Must convert via section headers

DWORD rva_to_foa(BYTE* pe_buf, DWORD rva) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)pe_buf;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(pe_buf + dos->e_lfanew);
    PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
    WORD nSec = nt->FileHeader.NumberOfSections;

    for (WORD i = 0; i < nSec; i++) {
        // Is the RVA within this section's virtual range?
        DWORD secStart = sec[i].VirtualAddress;
        DWORD secEnd   = secStart + sec[i].SizeOfRawData;
        if (rva >= secStart && rva < secEnd) {
            // FOA = section's raw offset + (RVA - section's virtual start)
            return sec[i].PointerToRawData + (rva - secStart);
        }
    }
    return 0;  // RVA not in any section (might be in headers)
}

// Usage — find import table in a raw file buffer:
DWORD importRVA = nt->OptionalHeader.DataDirectory[1].VirtualAddress;
DWORD importFOA = rva_to_foa(pe_buf, importRVA);
PIMAGE_IMPORT_DESCRIPTOR imports = (PIMAGE_IMPORT_DESCRIPTOR)(pe_buf + importFOA);

2.4 Quick Reference: Section Alignment

FileAlignment   (from OptionalHeader): typically 0x200 (512 bytes)
  → Sections on disk are aligned to this value
  → SizeOfRawData is always a multiple of FileAlignment

SectionAlignment (from OptionalHeader): typically 0x1000 (4096 bytes = 1 page)
  → Sections in memory are aligned to page size
  → Misc.VirtualSize = actual data bytes, may not be page-aligned

Example: .text section
  On disk:   PointerToRawData = 0x400,  SizeOfRawData = 0x3200
  In memory: VirtualAddress   = 0x1000, VirtualSize   = 0x3180
  Gap between sections on disk: none (packed)
  Gap between sections in memory: zero-filled to next page boundary

3. Base Relocations — Why They Exist

What it means: When a compiler/linker creates a PE, it assumes the binary will load at a specific ImageBase address (e.g., 0x140000000). Any absolute address baked into the code — a pointer to a global variable, a jump table entry, a vtable pointer — is calculated relative to that preferred base. If Windows (or your manual loader) places the PE at a different base due to ASLR or address conflict, every one of those hardcoded addresses is now wrong by exactly (actualBase - preferredBase). The relocation table is the PE's self-repair kit: it lists every absolute address that must be adjusted.

3.1 What Needs Relocation

Code that needs relocation (contains hardcoded absolute addresses):
  mov rax, [0x140001020]     ; Direct access to global variable at VA 0x140001020
  lea rax, [rip+0x...]       ; RIP-relative: does NOT need relocation (self-relative)
  call 0x140003A00           ; Direct call: needs relocation if not RIP-relative
  mov [0x140001028], rax     ; Store to global: needs relocation
  dq  0x140001000            ; Absolute pointer in .rdata vtable: needs relocation

Code that does NOT need relocation (position-independent):
  call [rip + import_thunk]  ; Import call through IAT (IAT RVA, not absolute)
  jmp  short +0x20           ; Relative jump
  lea  rax, [rip + offset]   ; RIP-relative LEA (x64 default)
  mov  eax, [rbp - 8]        ; Stack-relative access

3.2 Position-Independent Code (PIC) vs Relocation-Dependent

PIC (shellcode, modern x64):
  x64 uses RIP-relative addressing by default
  Most accesses like: lea rax, [rip + 0x100] → always correct regardless of load base
  Result: minimal or no .reloc section → shellcode is naturally PIC

Relocation-Dependent (classic x86, old x64 compilers):
  Uses absolute VAs for global access, vtables, jump tables
  Requires .reloc section listing all absolute addresses to patch
  Result: binary CANNOT run at wrong base without relocation fixup

Forced relocation-free:
  DLLs compiled with /FIXED → no .reloc section, cannot be relocated
  EXEs with /FIXED → same
  Risk: if preferred base unavailable, DLL load FAILS
  Malware use: /FIXED + custom loader that guarantees the base → no reloc section
              → smaller binary, harder to analyze (no reloc table to walk)

4. Base Relocation Table Internals

[RELOC] The .reloc section contains a series of IMAGE_BASE_RELOCATION blocks. Each block covers one 4KB page. Each entry in a block specifies one address to patch within that page.

4.1 Data Directory Entry

// Location: OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]
// IMAGE_DIRECTORY_ENTRY_BASERELOC = 5

PIMAGE_DATA_DIRECTORY relocDir =
    &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

// relocDir->VirtualAddress → RVA of first IMAGE_BASE_RELOCATION block
// relocDir->Size           → total size of all relocation data in bytes

// If relocDir->VirtualAddress == 0 or Size == 0:
//   → PE has no .reloc section (PIC or /FIXED build)
//   → Manual loader cannot relocate it (must load at exact preferred base)

4.2 Block Structure

// IMAGE_BASE_RELOCATION structure (8 bytes header per block)
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD VirtualAddress;   // Page RVA: all entries in this block are within
                            // the 4KB page starting at ImageBase + VirtualAddress
    DWORD SizeOfBlock;      // Total size of this block INCLUDING the header
                            // Number of 2-byte entries = (SizeOfBlock - 8) / 2
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;

// After the header: array of WORD (2-byte) entries
// Each WORD entry:
//   bits 15..12 (high 4 bits):  relocation TYPE
//   bits 11..0  (low 12 bits):  OFFSET within the 4KB page
//
// Final address to patch:
//   patchTarget = imageBase + block->VirtualAddress + (entry & 0x0FFF)

4.3 All Relocation Types

// Microsoft PE/COFF Specification: Base Relocation Types
// Source: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#base-relocation-types

#define IMAGE_REL_BASED_ABSOLUTE         0  // Padding entry — skip, no patch
#define IMAGE_REL_BASED_HIGH             1  // Patch high 16 bits of a 32-bit VA
#define IMAGE_REL_BASED_LOW              2  // Patch low 16 bits of a 32-bit VA
#define IMAGE_REL_BASED_HIGHLOW          3  // Patch all 32 bits (x86 standard)
#define IMAGE_REL_BASED_HIGHADJ          4  // High 16 bits + adjustment (RISC)
#define IMAGE_REL_BASED_MIPS_JMPADDR     5  // MIPS jump address
#define IMAGE_REL_BASED_ARM_MOV32        5  // ARM MOVW/MOVT pair (reuses 5)
#define IMAGE_REL_BASED_RISCV_HIGH20     5  // RISC-V upper 20 bits (reuses 5)
#define IMAGE_REL_BASED_THUMB_MOV32      7  // Thumb-2 MOVW/MOVT pair
#define IMAGE_REL_BASED_RISCV_LOW12S     8  // RISC-V S-format lower 12 bits
#define IMAGE_REL_BASED_LOONGARCH_MARK_LA 8 // LoongArch (reuses 8)
#define IMAGE_REL_BASED_MIPS_JMPADDR16   9  // MIPS16 jump address
#define IMAGE_REL_BASED_DIR64           10  // Patch all 64 bits (x64 standard)

// In practice, you will encounter:
//   Type 0  (ABSOLUTE)  → padding/alignment, always skip
//   Type 3  (HIGHLOW)   → x86 32-bit absolute pointer patch
//   Type 10 (DIR64)     → x64 64-bit absolute pointer patch
//
// Types 1, 2, 4, 5, 7, 8, 9 appear only on non-x86/x64 architectures
// When processing x86/x64 PEs: ABSOLUTE + HIGHLOW + DIR64 covers everything

4.4 Block Layout Diagram

.reloc section layout:

Offset 0x00: [IMAGE_BASE_RELOCATION Block #1]
  ┌───────────────────────────────────────────────────┐
  │ VirtualAddress:  0x00001000                       │  ← This block patches 4KB page at RVA 0x1000
  │ SizeOfBlock:     0x00000020                       │  ← 32 bytes total = 8 header + 24 entries
  │                                                   │  ← (32 - 8) / 2 = 12 entries
  ├───────────────────────────────────────────────────┤
  │ Entry[0]:  0xA010  → type=A(DIR64), offset=0x010  │  ← patch VA at RVA 0x1010
  │ Entry[1]:  0xA038  → type=A(DIR64), offset=0x038  │  ← patch VA at RVA 0x1038
  │ Entry[2]:  0xA0C0  → type=A(DIR64), offset=0x0C0  │  ← patch VA at RVA 0x10C0
  │ Entry[3]:  0x0000  → type=0(ABS),   offset=0x000  │  ← padding, skip
  │ ...                                               │
  └───────────────────────────────────────────────────┘

Offset 0x20: [IMAGE_BASE_RELOCATION Block #2]
  ┌──────────────────────────────────────────┐
  │ VirtualAddress:  0x00002000              │  ← Next 4KB page
  │ SizeOfBlock:     0x00000018              │
  │ Entry[0]:  0xA008  → DIR64, offset=0x008 │
  │ ...                                      │
  └──────────────────────────────────────────┘

[NULL terminator block]:
  VirtualAddress = 0, SizeOfBlock = 0

Iteration ends when VirtualAddress == 0

5. Relocation Processing — Full Implementation

5.1 Core Relocation Engine (C)

// reloc_engine.c — Full base relocation processor
// Handles x86 (HIGHLOW) and x64 (DIR64), skips padding (ABSOLUTE)
// Works on both in-memory mapped PEs and raw file buffers (when mapped first)

#include <windows.h>
#include <stdio.h>

// Apply base relocations to a PE mapped at imageBase
// nt: pointer to IMAGE_NT_HEADERS WITHIN the mapped image
// Returns: TRUE on success
BOOL apply_base_relocations(LPVOID imageBase, PIMAGE_NT_HEADERS nt) {

    // ─── Compute delta: how far from preferred base ──────────────
    ULONGLONG preferredBase = nt->OptionalHeader.ImageBase;
    LONGLONG  delta         = (LONGLONG)imageBase - (LONGLONG)preferredBase;

    if (delta == 0) {
        printf("[*] Reloc: loaded at preferred base (0x%llx) — no patches needed\n",
               preferredBase);
        return TRUE;
    }

    printf("[*] Reloc: delta = %+lld (0x%llx → 0x%p)\n",
           delta, preferredBase, imageBase);

    // ─── Locate relocation data directory ─────────────────────────
    PIMAGE_DATA_DIRECTORY relocDD =
        &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

    if (relocDD->VirtualAddress == 0 || relocDD->Size == 0) {
        printf("[-] Reloc: no .reloc section — PE cannot be rebased!\n");
        printf("    Built with /FIXED or no relocations compiled in.\n");
        printf("    Binary will crash or misfunction at relocated base.\n");
        return FALSE;
    }

    // First relocation block
    PIMAGE_BASE_RELOCATION block =
        (PIMAGE_BASE_RELOCATION)((BYTE*)imageBase + relocDD->VirtualAddress);

    // End of relocation data
    BYTE* relocEnd = (BYTE*)block + relocDD->Size;

    DWORD totalPatched = 0;
    DWORD blockCount   = 0;

    // ─── Walk relocation blocks ────────────────────────────────────
    while ((BYTE*)block < relocEnd && block->VirtualAddress != 0) {
        // Base address of the 4KB page this block covers
        BYTE* pageBase = (BYTE*)imageBase + block->VirtualAddress;

        // Number of 2-byte entry words after the 8-byte header
        DWORD entryCount = (block->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION))
                           / sizeof(WORD);

        // First entry word (immediately after the IMAGE_BASE_RELOCATION header)
        WORD* entry = (WORD*)((BYTE*)block + sizeof(IMAGE_BASE_RELOCATION));

        for (DWORD i = 0; i < entryCount; i++, entry++) {
            BYTE type   = (*entry) >> 12;         // High 4 bits
            WORD offset = (*entry) & 0x0FFF;      // Low 12 bits

            // Address of the pointer field to patch
            BYTE* patchTarget = pageBase + offset;

            switch (type) {
                case IMAGE_REL_BASED_ABSOLUTE:
                    // Type 0: padding entry — do nothing
                    break;

                case IMAGE_REL_BASED_DIR64: {
                    // Type 10: 64-bit absolute pointer (x64)
                    ULONGLONG* ptr64 = (ULONGLONG*)patchTarget;
                    *ptr64 += (ULONGLONG)delta;
                    totalPatched++;
                    break;
                }

                case IMAGE_REL_BASED_HIGHLOW: {
                    // Type 3: 32-bit absolute pointer (x86)
                    ULONG* ptr32 = (ULONG*)patchTarget;
                    *ptr32 += (ULONG)(LONG)delta;
                    totalPatched++;
                    break;
                }

                case IMAGE_REL_BASED_HIGH: {
                    // Type 1: only high 16 bits of 32-bit VA
                    USHORT* ptrH = (USHORT*)patchTarget;
                    *ptrH += (USHORT)(HIWORD((ULONG)(LONG)delta));
                    totalPatched++;
                    break;
                }

                case IMAGE_REL_BASED_LOW: {
                    // Type 2: only low 16 bits of 32-bit VA
                    USHORT* ptrL = (USHORT*)patchTarget;
                    *ptrL += (USHORT)(LOWORD((ULONG)(LONG)delta));
                    totalPatched++;
                    break;
                }

                case IMAGE_REL_BASED_HIGHADJ: {
                    // Type 4: High 16-bit adjustment (two-word entry)
                    // The NEXT entry word contains a 16-bit adjustment value
                    // Used on MIPS and some RISC architectures
                    USHORT* ptrAdj = (USHORT*)patchTarget;
                    LONG    adj    = (LONG)*ptrAdj << 16;
                    entry++;    // consume the adjustment word
                    adj += (SHORT)(*entry);
                    adj += (LONG)delta;
                    *ptrAdj = (USHORT)((adj + 0x8000) >> 16);
                    totalPatched++;
                    i++;        // extra increment for adjustment word
                    break;
                }

                default:
                    printf("[!] Reloc: unknown type %d at block RVA 0x%08X offset 0x%03X\n",
                           type, block->VirtualAddress, offset);
                    break;
            }
        }

        blockCount++;

        // Advance to next block
        block = (PIMAGE_BASE_RELOCATION)((BYTE*)block + block->SizeOfBlock);
    }

    printf("[+] Reloc: processed %lu blocks, patched %lu entries\n",
           blockCount, totalPatched);
    return TRUE;
}

5.2 Relocation in Raw File Buffer (Before Mapping)

// Sometimes you want to patch relocations BEFORE writing the PE to remote process
// (easier than patching after WriteProcessMemory)
// Key difference: use FOA (file offsets) instead of in-memory VAs

void patch_relocations_in_buffer(BYTE* peBuf, ULONGLONG actualBase) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)peBuf;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(peBuf + dos->e_lfanew);

    ULONGLONG preferredBase = nt->OptionalHeader.ImageBase;
    LONGLONG  delta         = (LONGLONG)actualBase - (LONGLONG)preferredBase;
    if (delta == 0) return;

    DWORD relocRVA = nt->OptionalHeader.DataDirectory[5].VirtualAddress;
    if (!relocRVA) return;

    // Convert RVA to file offset
    DWORD relocFOA = rva_to_foa(peBuf, relocRVA);
    PIMAGE_BASE_RELOCATION block = (PIMAGE_BASE_RELOCATION)(peBuf + relocFOA);

    while (block->VirtualAddress != 0) {
        DWORD entryCount = (block->SizeOfBlock - 8) / 2;
        WORD* entries = (WORD*)((BYTE*)block + 8);

        for (DWORD i = 0; i < entryCount; i++) {
            BYTE type   = entries[i] >> 12;
            WORD offset = entries[i] & 0x0FFF;

            // Convert the target RVA to file offset
            DWORD targetRVA = block->VirtualAddress + offset;
            DWORD targetFOA = rva_to_foa(peBuf, targetRVA);
            if (!targetFOA) continue;

            BYTE* patchPtr = peBuf + targetFOA;

            if (type == IMAGE_REL_BASED_DIR64) {
                *(ULONGLONG*)patchPtr += delta;
            } else if (type == IMAGE_REL_BASED_HIGHLOW) {
                *(ULONG*)patchPtr += (ULONG)(LONG)delta;
            }
        }
        block = (PIMAGE_BASE_RELOCATION)((BYTE*)block + block->SizeOfBlock);
    }
}

5.3 Python: Parse and Dump Relocation Table

#!/usr/bin/env python3
# parse_reloc.py — Parse and display PE relocation table
# Install: pip install pefile

import pefile
import sys

TYPE_NAMES = {
    0:  "ABSOLUTE (pad)",
    1:  "HIGH",
    2:  "LOW",
    3:  "HIGHLOW (x86)",
    4:  "HIGHADJ",
    5:  "MIPS_JMPADDR / ARM_MOV32",
    7:  "THUMB_MOV32",
    8:  "RISCV_LOW12S",
    9:  "MIPS_JMPADDR16",
    10: "DIR64 (x64)",
}

def dump_relocations(pe_path: str):
    pe = pefile.PE(pe_path)

    if not hasattr(pe, 'DIRECTORY_ENTRY_BASERELOC'):
        print(f"[!] No base relocation table in {pe_path}")
        print(f"    PE has no .reloc section (PIC or /FIXED build)")
        return

    preferred_base = pe.OPTIONAL_HEADER.ImageBase
    print(f"[*] Preferred ImageBase: 0x{preferred_base:016X}")
    print(f"[*] Machine:             {hex(pe.FILE_HEADER.Machine)}")
    print()

    total_entries  = 0
    total_patches  = 0
    total_blocks   = 0

    for reloc_block in pe.DIRECTORY_ENTRY_BASERELOC:
        rva   = reloc_block.struct.VirtualAddress
        size  = reloc_block.struct.SizeOfBlock
        count = len(reloc_block.entries)
        total_blocks += 1

        print(f"  Block: page RVA=0x{rva:08X}, size={size}, entries={count}")

        for entry in reloc_block.entries:
            t      = entry.type
            offset = entry.rva - rva   # offset within the 4KB page
            target_rva = entry.rva
            tname  = TYPE_NAMES.get(t, f"UNKNOWN_{t}")
            total_entries += 1
            if t != 0:
                total_patches += 1
                print(f"    [{tname:24s}] page+0x{offset:03X} → RVA 0x{target_rva:08X}")

    print()
    print(f"[+] Summary: {total_blocks} blocks, {total_entries} total entries, "
          f"{total_patches} actual patches")

    # Simulate relocation to a new base
    new_base = 0x0000000160000000
    delta    = new_base - preferred_base
    print(f"\n[*] If loaded at 0x{new_base:016X}: delta = {delta:+d} (0x{delta & 0xFFFFFFFFFFFFFFFF:X})")

if __name__ == "__main__":
    dump_relocations(sys.argv[1])

# Usage:
# python3 parse_reloc.py payload.dll
# python3 parse_reloc.py C:\Windows\System32\notepad.exe

5.4 Verifying Relocation with WinDbg / x64dbg

# WinDbg — check relocation table at runtime
# Find .reloc section
!dh -f notepad.exe             # Show full PE headers including section list
!dh notepad                    # If loaded in debugger

# Check preferred base vs actual base
lm m notepad                   # List modules — shows actual load address
                               # Compare with ImageBase from headers

# Dump relocation blocks at specific RVA
# (get .reloc section VirtualAddress from dumpbin or PE parser first)
db notepad + 0x<reloc_RVA> L 80   # Dump first 128 bytes of .reloc

# x64dbg — check relocation table
# Memory Map → find module → Right click → Dump section → .reloc
# Or: Plugins → PE → Relocation table
# dumpbin (MSVC tools) — dump relocation table
dumpbin /relocations payload.dll
dumpbin /relocations C:\Windows\System32\ntdll.dll | head -50

# Sample output:
# BASE RELOCATIONS #6
#    2000 RVA,        C size of block
#       8  DIR64
#      10  DIR64

6. ASLR and Relocations

What it means: Address Space Layout Randomization (ASLR) is what forces relocations to matter at runtime. Without ASLR, DLLs could load at fixed preferred bases — relocations would never fire. ASLR randomizes load addresses on every boot (and with High Entropy ASLR, on every load), so every PE with a .reloc section gets relocated every time.

6.1 ASLR Entropy

Standard ASLR (Windows Vista+):
  Image base randomized at boot time
  Same base per boot per module (deterministic after first load)
  Entropy: ~8 bits for x86 (256 possible positions)
            ~17 bits for x64 (131072 possible positions)

High Entropy ASLR (HEASLR, Windows 8+):
  /HIGHENTROPYVA linker flag required
  64-bit ASLR entropy: up to 24 bits (16M possible positions)
  Randomized on every load (not just per boot)

Mandatory ASLR (system-wide):
  All modules rebased even if they have no .reloc section
  /FIXED modules: LOAD FAILS if mandatory ASLR is enabled

Checking ASLR on a binary:
  dumpbin /headers binary.exe | findstr "DLL characteristics"
  # Look for:
  #   IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE      (0x0040) → ASLR aware
  #   IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA   (0x0020) → HEASLR
  #   IMAGE_DLLCHARACTERISTICS_NX_COMPAT         (0x0100) → DEP/NX

6.2 Checking/Setting ASLR Flags

#!/usr/bin/env python3
# check_aslr.py — Check and print ASLR characteristics of a PE

import pefile, sys

DLLCHAR_FLAGS = {
    0x0001: "PROCESS_INIT",
    0x0002: "PROCESS_TERM",
    0x0004: "THREAD_INIT",
    0x0008: "THREAD_TERM",
    0x0020: "HIGH_ENTROPY_VA (HEASLR)",   # ← 64-bit ASLR
    0x0040: "DYNAMIC_BASE (ASLR)",         # ← ASLR aware
    0x0080: "FORCE_INTEGRITY",
    0x0100: "NX_COMPAT (DEP)",
    0x0200: "NO_ISOLATION",
    0x0400: "NO_SEH",
    0x0800: "NO_BIND",
    0x1000: "APPCONTAINER",
    0x2000: "WDM_DRIVER",
    0x4000: "GUARD_CF (CFG)",
    0x8000: "TERMINAL_SERVER_AWARE",
}

pe = pefile.PE(sys.argv[1])
chars = pe.OPTIONAL_HEADER.DllCharacteristics

print(f"DllCharacteristics: 0x{chars:04X}")
for flag, name in DLLCHAR_FLAGS.items():
    status = "✓" if (chars & flag) else "✗"
    print(f"  {status} {name} (0x{flag:04X})")

has_reloc = hasattr(pe, 'DIRECTORY_ENTRY_BASERELOC')
print(f"\n.reloc section present: {'YES' % has_reloc if has_reloc else 'NO → cannot be rebased'}")
# Disable ASLR for testing with editbin (MSVC)
editbin /DYNAMICBASE:NO payload.dll    # Remove ASLR flag
editbin /DYNAMICBASE payload.dll       # Add ASLR flag

# Or use CFF Explorer (GUI) → Optional Header → DllCharacteristics → uncheck DYNAMIC_BASE

7. Import Table Internals (IAT / INT)

[IAT] The import table is a three-level structure: the import directory (one entry per DLL), name table (INT — what to import), and address table (IAT — where resolved addresses go).

7.1 Full Import Data Structure

// ─── Level 1: IMAGE_IMPORT_DESCRIPTOR (one per imported DLL) ───
// Located at: DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] (index 1)
// Array terminated by a null descriptor (all fields zero)

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD Characteristics;     // 0 for terminating null entry
        DWORD OriginalFirstThunk;  // RVA to Import Name Table (INT)
    };
    DWORD TimeDateStamp;           // 0 if not bound; -1 if ALL imports bound
    DWORD ForwarderChain;          // -1 if no forwarders
    DWORD Name;                    // RVA to null-terminated DLL name string
    DWORD FirstThunk;              // RVA to Import Address Table (IAT)
} IMAGE_IMPORT_DESCRIPTOR;

// ─── Level 2: IMAGE_THUNK_DATA (one per imported function) ──────
// Both OriginalFirstThunk (INT) and FirstThunk (IAT) point to arrays of these
// Array terminated by a null thunk (u1 == 0)

typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString; // RVA to forwarder name string
        ULONGLONG Function;        // After loading: actual function VA ← IAT use
        ULONGLONG Ordinal;         // Import by ordinal: MSB set, low 16 = ordinal
        ULONGLONG AddressOfData;   // Before loading: RVA to IMAGE_IMPORT_BY_NAME ← INT use
    } u1;
} IMAGE_THUNK_DATA64;

// ─── Level 3: IMAGE_IMPORT_BY_NAME (name + hint) ────────────────
// Pointed to by each INT thunk when importing by name
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD Hint;      // Ordinal hint: SUGGESTED ordinal value (may be wrong/stale)
                    // Loader tries this first for speed, falls back to name search
    CHAR Name[1];   // Null-terminated function name (variable length)
} IMAGE_IMPORT_BY_NAME;

// ─── Import by Ordinal check ─────────────────────────────────────
// The MSB (bit 63 on x64, bit 31 on x86) of the thunk value signals import-by-ordinal
// IMAGE_SNAP_BY_ORDINAL64(t) → ((t) & 0x8000000000000000) != 0
// IMAGE_ORDINAL64(t)         → ((t) & 0xFFFF)  (low 16 bits = ordinal number)

7.2 IAT vs INT — The Two Parallel Arrays

Before loading (on disk / after mapping, before IAT resolution):

  IMAGE_IMPORT_DESCRIPTOR for "kernel32.dll":
    OriginalFirstThunk (INT): 0x5000  ───────────────────────┐
    FirstThunk         (IAT): 0x6000  ─────────────────────┐ │
                                                           │ │
  INT at RVA 0x5000 (NEVER modified by loader):            │ │
    [0]: 0x7000 ──→ IMAGE_IMPORT_BY_NAME: {Hint:0x01, "VirtualAlloc\0"}
    [1]: 0x700E ──→ IMAGE_IMPORT_BY_NAME: {Hint:0x02, "CreateThread\0"}
    [2]: 0x701C ──→ IMAGE_IMPORT_BY_NAME: {Hint:0x03, "WriteProcessMemory\0"}
    [3]: 0x0000  (null terminator)

  IAT at RVA 0x6000 (same values before loader runs):
    [0]: 0x7000  ← same as INT initially
    [1]: 0x700E
    [2]: 0x701C
    [3]: 0x0000

After loading (loader has resolved all imports):

  IAT at RVA 0x6000 (NOW contains real addresses):
    [0]: 0x00007FFA12340000  ← actual VA of kernel32!VirtualAlloc
    [1]: 0x00007FFA12341234  ← actual VA of kernel32!CreateThread
    [2]: 0x00007FFA12345678  ← actual VA of kernel32!WriteProcessMemory
    [3]: 0x0000000000000000  (null terminator — left alone)

  INT at RVA 0x5000 (UNCHANGED — still has original name RVAs):
    [0]: 0x7000  ← untouched, still RVA to "VirtualAlloc"
    [1]: 0x700E  ← untouched
    [2]: 0x701C  ← untouched

Key insight: INT is a read-only reference; IAT is the live address table.
When implementing IAT hooks: you patch IAT entries, not INT entries.
When implementing IAT resolution in a manual loader: read names from INT, write addresses to IAT.

7.3 Import Table Memory Permissions (Modern Windows)

[INTERN] As of Windows 10 / Server 2019:
  The IAT is write-protected after the loader finishes import resolution.
  The region containing IAT entries is marked PAGE_READONLY by the loader.

  Why? Defense against IAT hooking (attackers patching IAT to intercept calls).

  Impact for manual loaders:
    VirtualProtect(iatRegion, size, PAGE_READWRITE, &old)  // must do this
    patch IAT entries
    VirtualProtect(iatRegion, size, old, &old)             // restore read-only

  Impact for IAT hookers (red team):
    Must call VirtualProtect before patching
    The VirtualProtect call itself is a behavioral signal to EDRs

  Note: Delay-loaded imports are re-written lazily (on first call)
        and use a temporary RW window — this creates timing issues
        with IAT protection restoration (see §12)

8. Import Resolution — Full Implementation

8.1 Complete IAT Resolver (C)

// iat_resolver.c — Full import resolution for manually mapped PE
// Handles: by-name, by-ordinal, nested LoadLibrary for dependencies

#include <windows.h>
#include <stdio.h>

// Optional: custom LoadLibrary + GetProcAddress for when those aren't available
// (e.g., inside a ReflectiveLoader before bootstrapping)
typedef HMODULE(WINAPI* pLoadLibraryA_t) (LPCSTR);
typedef FARPROC(WINAPI* pGetProcAddress_t)(HMODULE, LPCSTR);

BOOL resolve_imports(
    LPVOID          imageBase,
    PIMAGE_NT_HEADERS nt,
    pLoadLibraryA_t   fnLoadLibrary,    // Pass NULL to use default LoadLibraryA
    pGetProcAddress_t fnGetProcAddress  // Pass NULL to use default GetProcAddress
) {
    // Fall back to standard functions if not supplied
    if (!fnLoadLibrary)   fnLoadLibrary   = LoadLibraryA;
    if (!fnGetProcAddress) fnGetProcAddress = GetProcAddress;

    // ─── Locate import data directory ────────────────────────────
    PIMAGE_DATA_DIRECTORY importDD =
        &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

    if (importDD->VirtualAddress == 0) {
        printf("[*] IAT: no import table — PE has no dependencies\n");
        return TRUE;
    }

    // Array of import descriptors (one per DLL, null-terminated)
    PIMAGE_IMPORT_DESCRIPTOR desc =
        (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)imageBase + importDD->VirtualAddress);

    // ─── Walk descriptor array ────────────────────────────────────
    // Terminate on null descriptor (all fields zero)
    while (desc->OriginalFirstThunk != 0 || desc->FirstThunk != 0) {
        // Get the DLL name string
        char* dllName = (char*)((BYTE*)imageBase + desc->Name);

        // Load the dependency DLL (or find already-loaded)
        HMODULE hDep = fnLoadLibrary(dllName);
        if (!hDep) {
            printf("[-] IAT: failed to load dependency: %s (error: %lu)\n",
                   dllName, GetLastError());
            desc++;
            continue;  // Skip — some optional DLLs may legitimately fail
        }

        printf("[+] IAT: resolving imports from '%s' (base: 0x%p)\n",
               dllName, hDep);

        // ─── INT: Import Name Table (original, never modified) ───
        // Use OriginalFirstThunk if available, fall back to FirstThunk
        // (some packers zero out OriginalFirstThunk — bound imports)
        PIMAGE_THUNK_DATA pINT = (desc->OriginalFirstThunk)
            ? (PIMAGE_THUNK_DATA)((BYTE*)imageBase + desc->OriginalFirstThunk)
            : (PIMAGE_THUNK_DATA)((BYTE*)imageBase + desc->FirstThunk);

        // ─── IAT: Import Address Table (where we write addresses) ─
        PIMAGE_THUNK_DATA pIAT =
            (PIMAGE_THUNK_DATA)((BYTE*)imageBase + desc->FirstThunk);

        // ─── Unlock IAT for writing (Windows 10+ protection) ─────
        DWORD  oldProt  = 0;
        SIZE_T iatSize  = 0;
        // Count thunks to know how much to unlock
        PIMAGE_THUNK_DATA tmp = pIAT;
        while (tmp->u1.Function) { iatSize += sizeof(IMAGE_THUNK_DATA); tmp++; }
        VirtualProtect(pIAT, iatSize + sizeof(IMAGE_THUNK_DATA),
                       PAGE_READWRITE, &oldProt);

        // ─── Walk thunks (null-terminated) ───────────────────────
        while (pINT->u1.AddressOfData != 0) {
            FARPROC resolvedAddr = NULL;

            if (IMAGE_SNAP_BY_ORDINAL(pINT->u1.Ordinal)) {
                // ── Import by Ordinal ──────────────────────────────
                WORD ordinal = (WORD)IMAGE_ORDINAL(pINT->u1.Ordinal);
                // GetProcAddress accepts ordinal via MAKEINTRESOURCE trick
                resolvedAddr = fnGetProcAddress(hDep, MAKEINTRESOURCEA(ordinal));

                if (resolvedAddr) {
                    printf("  [>] Ordinal #%-4d → 0x%p\n", ordinal, resolvedAddr);
                } else {
                    printf("  [-] Ordinal #%d: FAILED (error: %lu)\n",
                           ordinal, GetLastError());
                }
            } else {
                // ── Import by Name ─────────────────────────────────
                PIMAGE_IMPORT_BY_NAME ibn =
                    (PIMAGE_IMPORT_BY_NAME)((BYTE*)imageBase + pINT->u1.AddressOfData);

                // ibn->Hint: suggested ordinal (try first for speed)
                // ibn->Name: null-terminated function name string

                // Standard approach: resolve by name (reliable)
                resolvedAddr = fnGetProcAddress(hDep, (LPCSTR)ibn->Name);

                if (resolvedAddr) {
                    printf("  [>] %-40s → 0x%p (hint: %d)\n",
                           ibn->Name, resolvedAddr, ibn->Hint);
                } else {
                    printf("  [-] %s: FAILED (error: %lu)\n",
                           ibn->Name, GetLastError());
                }
            }

            // Write resolved address to IAT entry
            pIAT->u1.Function = (ULONG_PTR)resolvedAddr;

            pINT++;  // Advance INT pointer
            pIAT++;  // Advance IAT pointer (must stay in sync with INT)
        }

        // ─── Restore IAT protection ───────────────────────────────
        DWORD dummy;
        VirtualProtect(pIAT - (iatSize / sizeof(IMAGE_THUNK_DATA)),
                       iatSize + sizeof(IMAGE_THUNK_DATA),
                       oldProt, &dummy);

        desc++;  // Next DLL descriptor
    }

    printf("[+] IAT: import resolution complete\n");
    return TRUE;
}

8.2 Parsing the Import Table with Python

#!/usr/bin/env python3
# parse_imports.py — Dump full import table of a PE file
# pip install pefile

import pefile
import sys

def dump_imports(path: str):
    pe = pefile.PE(path)

    if not hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
        print(f"[!] No import table found in {path}")
        return

    print(f"[*] Import Table for: {path}")
    print(f"[*] Number of imported DLLs: {len(pe.DIRECTORY_ENTRY_IMPORT)}")
    print()

    for dll in pe.DIRECTORY_ENTRY_IMPORT:
        dll_name = dll.dll.decode('utf-8', errors='replace')
        print(f"  DLL: {dll_name}")
        print(f"  OriginalFirstThunk (INT) RVA: 0x{dll.struct.OriginalFirstThunk:08X}")
        print(f"  FirstThunk         (IAT) RVA: 0x{dll.struct.FirstThunk:08X}")
        print(f"  TimeDateStamp:               0x{dll.struct.TimeDateStamp:08X} "
              f"{'(bound)' if dll.struct.TimeDateStamp == 0xFFFFFFFF else ''}")
        print()

        for imp in dll.imports:
            if imp.import_by_ordinal:
                print(f"    [ORD] #{imp.ordinal}")
            else:
                print(f"    [NAME] {imp.name.decode('utf-8', errors='replace')} "
                      f"(hint: {imp.hint})")

        print()

def find_suspicious_imports(path: str):
    """Flag imports commonly associated with injection/evasion."""
    pe = pefile.PE(path)
    if not hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
        return

    suspicious = {
        "VirtualAlloc":          "Memory allocation — shellcode staging",
        "VirtualAllocEx":        "REMOTE memory allocation — process injection",
        "VirtualProtect":        "Permission change — DEP bypass",
        "VirtualProtectEx":      "REMOTE permission change",
        "WriteProcessMemory":    "REMOTE memory write — injection",
        "ReadProcessMemory":     "REMOTE memory read — credential theft",
        "CreateRemoteThread":    "Remote thread creation — injection",
        "CreateRemoteThreadEx":  "Remote thread creation (ex) — injection",
        "NtCreateThreadEx":      "Native remote thread — hook evasion",
        "QueueUserAPC":          "APC injection",
        "NtQueueApcThread":      "Native APC injection",
        "LoadLibraryA":          "Dynamic DLL loading",
        "LoadLibraryExA":        "Dynamic DLL loading (extended)",
        "GetProcAddress":        "Dynamic function resolution",
        "OpenProcess":           "Target process handle",
        "SetWindowsHookEx":      "System-wide hook — keylogging",
        "RegSetValueEx":         "Registry persistence",
        "CreateService":         "Service persistence",
        "WinExec":               "Shell execution",
        "ShellExecuteA":         "Shell execution",
        "InternetOpenA":         "Network — WinInet C2",
        "InternetConnectA":      "Network — WinInet C2",
        "HttpSendRequestA":      "Network — HTTP C2",
        "WSAStartup":            "Network — socket init",
        "socket":                "Network — raw socket",
        "CryptDecrypt":          "Crypto — payload decryption",
        "BCryptDecrypt":         "Crypto (modern) — payload decryption",
        "IsDebuggerPresent":     "Anti-debug check",
        "CheckRemoteDebuggerPresent": "Anti-debug check (remote)",
        "NtQueryInformationProcess":  "Anti-debug / process enumeration",
    }

    print(f"\n[*] Suspicious Import Analysis: {path}")
    found_any = False
    for dll in pe.DIRECTORY_ENTRY_IMPORT:
        for imp in dll.imports:
            if not imp.import_by_ordinal and imp.name:
                name = imp.name.decode('utf-8', errors='replace')
                if name in suspicious:
                    found_any = True
                    dll_name = dll.dll.decode('utf-8', errors='replace')
                    print(f"  [!] {dll_name}!{name}")
                    print(f"      → {suspicious[name]}")
    if not found_any:
        print("  [+] No suspicious imports found in IAT")
        print("      May be using dynamic resolution (GetProcAddress / PEB walk)")

if __name__ == "__main__":
    dump_imports(sys.argv[1])
    find_suspicious_imports(sys.argv[1])

# Usage:
# python3 parse_imports.py meterpreter.dll
# python3 parse_imports.py C:\Windows\System32\notepad.exe

9. Import by Ordinal vs Import by Name

What it means: A PE can import a function by its name (a string like "VirtualAlloc") or by its ordinal (a number like #5). Ordinal imports are faster at load time (skip name string search), are harder to read statically, and are used by internal Windows DLLs that don't advertise all their functions by name.

9.1 Ordinal Import Detection

// Detecting ordinal imports in a thunk entry
PIMAGE_THUNK_DATA thunk = /* ... */;

// Test: is MSB set?
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) {
    // Import by ordinal
    WORD ordinal = (WORD)IMAGE_ORDINAL(thunk->u1.Ordinal);
    // e.g., ordinal = 5 → maps to 5th function in the DLL's export table

    // Resolve:
    FARPROC fn = GetProcAddress(hModule, MAKEINTRESOURCEA(ordinal));
    // MAKEINTRESOURCEA(5) treats the integer 5 as a pseudo-string ordinal
} else {
    // Import by name
    PIMAGE_IMPORT_BY_NAME ibn =
        (PIMAGE_IMPORT_BY_NAME)((BYTE*)imageBase + thunk->u1.AddressOfData);
    FARPROC fn = GetProcAddress(hModule, (LPCSTR)ibn->Name);
}

// Macro definitions from winnt.h:
// IMAGE_SNAP_BY_ORDINAL64(o) → ((o) & IMAGE_ORDINAL_FLAG64) (0x8000000000000000)
// IMAGE_ORDINAL64(o)         → ((o) & 0xffff)
// IMAGE_SNAP_BY_ORDINAL32(o) → ((o) & IMAGE_ORDINAL_FLAG32) (0x80000000)
// IMAGE_ORDINAL32(o)         → ((o) & 0xffff)

9.2 Using Ordinals for Evasion

// Importing by ordinal hides function names from static analysis
// The IAT shows only a number, not a string like "VirtualAlloc"
// Useful for: malware that wants to hide what it imports
//             DLLs that export undocumented/internal functions by ordinal only

// ntdll.dll ordinal examples (undocumented):
// Ordinal #3  → LdrInitializeThunk
// Ordinal #8  → NtTerminateProcess
// These change across Windows versions — ordinals are NOT stable!

// Practical use: NtQuerySystemInformation ordinal in ntdll
// (Most EDRs look for these by name; ordinal import bypasses name-based detection)

// Example: import NtOpenProcess by ordinal (x64)
// Step 1: Find ordinal for NtOpenProcess in ntdll:
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
FARPROC fnNtOpenProcess = GetProcAddress(hNtdll, "NtOpenProcess");
// Step 2: Find the ordinal number for NtOpenProcess in the EAT
//         (use parse_exports tool — see §10)
// Step 3: In your PE, import ntdll.dll!#<ordinal> instead of by name

// WARNING: Ordinals change between Windows versions.
// Much more reliable for well-known DLLs like kernel32.dll than for ntdll.dll.

10. Export Table Internals (EAT)

[EAT] The Export Address Table is the mirror of the IAT — it's how a DLL advertises its functions. Understanding the EAT is essential for: manual import resolution, API hashing, finding undocumented functions, detecting forwarders, and building custom GetProcAddress replacements.

10.1 Export Data Structure

// IMAGE_EXPORT_DIRECTORY — located at DataDirectory[0] (IMAGE_DIRECTORY_ENTRY_EXPORT)
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD Characteristics;        // Reserved, 0
    DWORD TimeDateStamp;          // Creation timestamp
    WORD  MajorVersion;           // Version (rarely used)
    WORD  MinorVersion;
    DWORD Name;                   // RVA → DLL name string (e.g., "KERNEL32.dll")
    DWORD Base;                   // Ordinal base value — ordinals are 0-based
                                  // internally but exposed as Base+index externally
    DWORD NumberOfFunctions;      // Total entries in AddressOfFunctions array
                                  // (named AND unnamed/ordinal-only exports)
    DWORD NumberOfNames;          // Number of NAMED exports (≤ NumberOfFunctions)
    DWORD AddressOfFunctions;     // RVA → DWORD array[NumberOfFunctions]
                                  //         Each element: RVA to function code
                                  //         OR: RVA to forwarder string (see §11)
    DWORD AddressOfNames;         // RVA → DWORD array[NumberOfNames]
                                  //         Each element: RVA to function name string
    DWORD AddressOfNameOrdinals;  // RVA → WORD array[NumberOfNames]
                                  //         Each element: index into AddressOfFunctions
} IMAGE_EXPORT_DIRECTORY;

10.2 The Three-Table Export Lookup

Export Table: Three parallel arrays
────────────────────────────────────────────────────────────────
AddressOfNames        (DWORD array, size: NumberOfNames):
  [0] RVA → "AddAtom"
  [1] RVA → "CreateFileA"
  [2] RVA → "VirtualAlloc"
  ...
  Names are SORTED ALPHABETICALLY → binary search possible

AddressOfNameOrdinals (WORD  array, size: NumberOfNames):
  [0] 12    ← "AddAtom" maps to AddressOfFunctions[12]
  [1] 45    ← "CreateFileA" maps to AddressOfFunctions[45]
  [2] 198   ← "VirtualAlloc" maps to AddressOfFunctions[198]
  ...

AddressOfFunctions    (DWORD array, size: NumberOfFunctions):
  [0]   RVA to function #0 (may be ordinal-only, no name)
  [1]   RVA to function #1
  ...
  [12]  0x00012345  ← code RVA of AddAtom
  ...
  [45]  0x00034567  ← code RVA of CreateFileA
  ...
  [198] 0x00056789  ← code RVA of VirtualAlloc

External ordinal = OrdinalBase + internal_index
  If OrdinalBase = 1:
    External ordinal of VirtualAlloc = 1 + 198 = 199

10.3 Manual GetProcAddress (C — Complete Implementation)

// manual_getprocaddress.c
// Full EAT walk to find a function — no GetProcAddress dependency
// Used in: ReflectiveLoader, shellcode, API hashing resolvers

#include <windows.h>
#include <string.h>

// Resolve a function by name from a module's EAT
// Handles: by-name, by-ordinal, forwarder strings
FARPROC manual_get_proc_address(HMODULE hModule, LPCSTR funcName) {
    if (!hModule || !funcName) return NULL;

    BYTE* base = (BYTE*)hModule;
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    if (dos->e_magic != IMAGE_DOS_SIGNATURE) return NULL;

    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);
    if (nt->Signature != IMAGE_NT_SIGNATURE) return NULL;

    DWORD exportRVA = nt->OptionalHeader
                        .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                        .VirtualAddress;
    DWORD exportSize = nt->OptionalHeader
                         .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                         .Size;

    if (!exportRVA) return NULL;

    PIMAGE_EXPORT_DIRECTORY expDir =
        (PIMAGE_EXPORT_DIRECTORY)(base + exportRVA);

    // The three arrays
    DWORD* funcRVAs    = (DWORD*)(base + expDir->AddressOfFunctions);
    DWORD* nameRVAs    = (DWORD*)(base + expDir->AddressOfNames);
    WORD*  nameOrdinals = (WORD*)(base + expDir->AddressOfNameOrdinals);

    // ─── Import by ordinal ─────────────────────────────────────────
    if ((ULONG_PTR)funcName <= 0xFFFF) {
        // funcName is actually an ordinal (from MAKEINTRESOURCE)
        WORD externalOrdinal = (WORD)(ULONG_PTR)funcName;
        DWORD internalIndex  = externalOrdinal - (WORD)expDir->Base;

        if (internalIndex >= expDir->NumberOfFunctions)
            return NULL;

        DWORD funcRVA = funcRVAs[internalIndex];
        if (!funcRVA) return NULL;

        // Check if it's a forwarder
        if (funcRVA >= exportRVA && funcRVA < exportRVA + exportSize) {
            // It's a forwarder (see §11)
            return NULL;  // Simplified: forwarder handling not shown here
        }

        return (FARPROC)(base + funcRVA);
    }

    // ─── Import by name — binary search (names are sorted) ─────────
    DWORD lo = 0, hi = expDir->NumberOfNames;
    while (lo < hi) {
        DWORD mid = (lo + hi) / 2;
        char* name = (char*)(base + nameRVAs[mid]);
        int cmp = strcmp(funcName, name);

        if (cmp == 0) {
            // Found the name — get the internal ordinal index
            WORD  ordIdx = nameOrdinals[mid];
            DWORD funcRVA = funcRVAs[ordIdx];

            if (!funcRVA) return NULL;

            // ─── Forwarder check ─────────────────────────────────────
            // If funcRVA points WITHIN the export directory, it's a forwarder
            if (funcRVA >= exportRVA && funcRVA < exportRVA + exportSize) {
                // Forwarder string: "DllName.FunctionName"
                char* forwarder = (char*)(base + funcRVA);
                // Parse: split at '.'
                char dllPart[128] = {0};
                char funcPart[128] = {0};
                char* dot = strchr(forwarder, '.');
                if (!dot) return NULL;

                strncpy(dllPart, forwarder, dot - forwarder);
                strcat(dllPart, ".dll");
                strcpy(funcPart, dot + 1);

                // Recursively resolve via the forwarded DLL
                HMODULE hFwdDll = LoadLibraryA(dllPart);
                if (!hFwdDll) return NULL;
                return manual_get_proc_address(hFwdDll, funcPart);
            }

            return (FARPROC)(base + funcRVA);
        }

        if (cmp < 0) hi = mid;
        else         lo = mid + 1;
    }

    return NULL;  // Not found
}

// ─── Linear search variant (for when binary search is overkill) ─
FARPROC linear_get_proc(HMODULE hModule, const char* funcName) {
    BYTE* base = (BYTE*)hModule;
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);

    DWORD expRVA = nt->OptionalHeader.DataDirectory[0].VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(base + expRVA);

    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++) {
        if (strcmp((char*)(base + names[i]), funcName) == 0) {
            return (FARPROC)(base + funcs[ords[i]]);
        }
    }
    return NULL;
}

10.4 Parsing EAT with Python

#!/usr/bin/env python3
# parse_exports.py — Dump EAT with ordinal mapping
# pip install pefile

import pefile
import sys

def dump_exports(path: str):
    pe = pefile.PE(path)

    if not hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
        print(f"[!] No export table in {path}")
        return

    exp = pe.DIRECTORY_ENTRY_EXPORT
    dll_name = exp.name.decode('utf-8', errors='replace')
    base = pe.OPTIONAL_HEADER.ImageBase

    print(f"[*] Export Table: {dll_name}")
    print(f"[*] Ordinal Base:         {exp.struct.Base}")
    print(f"[*] NumberOfFunctions:    {exp.struct.NumberOfFunctions}")
    print(f"[*] NumberOfNames:        {exp.struct.NumberOfNames}")
    print()

    for sym in exp.symbols:
        ext_ordinal = exp.struct.Base + sym.ordinal  # external ordinal
        if sym.name:
            name = sym.name.decode('utf-8', errors='replace')
            if sym.forwarder_offset:
                fwd = sym.forwarder_offset.decode('utf-8', errors='replace')
                print(f"  #{ext_ordinal:4d} [FORWARDER] {name} → {fwd}")
            else:
                print(f"  #{ext_ordinal:4d} [NAME] {name} (RVA: 0x{sym.address:08X})")
        else:
            print(f"  #{ext_ordinal:4d} [ORDINAL ONLY] (RVA: 0x{sym.address:08X})")

# Usage:
# python3 parse_exports.py C:\Windows\System32\kernel32.dll
# python3 parse_exports.py C:\Windows\System32\ntdll.dll | grep "Nt"

11. Export Forwarders

What it means: An export forwarder is an export entry that doesn't point to code in the DLL itself — instead, it contains a string like "KERNELBASE.VirtualAlloc" redirecting the caller to a function in another DLL. This is how Windows implements DLL architecture (e.g., kernel32.dll forwards many functions to kernelbase.dll). Forwarders must be handled correctly in manual resolvers, or function resolution silently fails.

11.1 How Forwarders Work

Forwarder detection:
  An AddressOfFunctions[i] entry is a FORWARDER if its RVA falls
  WITHIN the export directory's own VA range:

  exportDir VA = DataDirectory[0].VirtualAddress
  exportDir size = DataDirectory[0].Size

  If:  exportDir.VA <= funcRVA < exportDir.VA + exportDir.Size
  Then: funcRVA points to a forwarder string, NOT code

Forwarder string format:
  "KERNELBASE.VirtualAlloc"    → forward to KERNELBASE.dll!VirtualAlloc (by name)
  "NTDLL.#182"                 → forward to ntdll.dll ordinal #182

Real-world examples (kernel32.dll → kernelbase.dll):
  kernel32!VirtualAlloc       → KERNELBASE.VirtualAlloc
  kernel32!HeapAlloc          → KERNELBASE.HeapAlloc
  kernel32!GetLastError       → KERNELBASE.GetLastError
  kernel32!WriteFile          → KERNELBASE.WriteFile
  (most kernel32 functions are now forwarders to kernelbase)

11.2 Forwarder Resolution Code (C)

// Handle forwarders in EAT lookup
FARPROC resolve_with_forwarder(HMODULE hModule, const char* funcName) {
    BYTE* base   = (BYTE*)hModule;
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);

    DWORD expRVA  = nt->OptionalHeader.DataDirectory[0].VirtualAddress;
    DWORD expSize = nt->OptionalHeader.DataDirectory[0].Size;
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(base + expRVA);

    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++) {
        if (strcmp((char*)(base + names[i]), funcName) == 0) {
            DWORD funcRVA = funcs[ords[i]];

            // ─── Check for forwarder ──────────────────────────────
            if (funcRVA >= expRVA && funcRVA < expRVA + expSize) {
                char* fwdStr = (char*)(base + funcRVA);
                // fwdStr = "KERNELBASE.VirtualAlloc" or "NTDLL.#182"
                printf("[*] Forwarder: %s!%s → %s\n", funcName, funcName, fwdStr);

                // Split at '.'
                char dllName[256] = {0};
                const char* dot = strchr(fwdStr, '.');
                if (!dot) return NULL;

                strncpy(dllName, fwdStr, dot - fwdStr);
                strcat(dllName, ".dll");
                const char* fwdFunc = dot + 1;

                HMODULE hFwd = LoadLibraryA(dllName);
                if (!hFwd) return NULL;

                // Check if forwarding by ordinal (#N)
                if (*fwdFunc == '#') {
                    WORD ordinal = (WORD)atoi(fwdFunc + 1);
                    return GetProcAddress(hFwd, MAKEINTRESOURCEA(ordinal));
                } else {
                    return GetProcAddress(hFwd, fwdFunc);
                }
            }

            return (FARPROC)(base + funcRVA);
        }
    }
    return NULL;
}

12. Delay Import Table

What it means: Delay imports (also called delay-load imports) are DLLs that are not loaded at process startup. Instead, the first time a function from a delay-loaded DLL is called, the delay-load helper stub intercepts the call, loads the DLL, resolves the function, patches the IAT with the real address, and then calls through. This exists for performance (don't load rarely-used DLLs at startup) and for optional dependencies.

12.1 Delay Import Descriptor

// Located at: DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT] (index 13)
typedef struct ImgDelayDescr {
    DWORD   grAttrs;           // Attributes (0 = using RVAs, 1 = using VAs)
    DWORD   rvaDLLName;        // RVA to DLL name string
    DWORD   rvaHmod;           // RVA to HMODULE field (filled on first call)
    DWORD   rvaIAT;            // RVA to delay-load IAT
    DWORD   rvaINT;            // RVA to delay-load INT (names/ordinals)
    DWORD   rvaBoundIAT;       // RVA to optional bound IAT
    DWORD   rvaUnloadIAT;      // RVA to optional unload IAT copy
    DWORD   dwTimeStamp;       // Timestamp (0 = not bound)
} ImgDelayDescr, *PImgDelayDescr;

// Flow on first call to delay-loaded function:
//   CALL [delay_IAT_entry]          → initially points to delay-load stub
//       ↓
//   Delay-load stub fires:
//       LoadLibrary(dllName)
//       GetProcAddress(hDll, funcName)
//       patch IAT entry with real address  ← VirtualProtect RW needed
//       call the real function
//   Subsequent calls: CALL [delay_IAT_entry] → real address directly

12.2 Delay Import and IAT Protection

[INTERN] IAT Write-Protection Interaction:

  Problem: The delay-load helper needs to write to the delay IAT
           but the IAT is now write-protected (Windows 10+).

  Microsoft's solution: The delay-load helper calls VirtualProtect(RW)
           on the IAT entry before patching, then restores READ-ONLY.

  Red team implication: If you're patching a normal IAT entry at the same
           time a delay-load is happening in another thread, the VirtualProtect
           sequence can race. The race condition is usually benign but can cause
           access violations in poorly written loaders.

  Best practice: In your manual loader, handle delay imports AFTER setting
           full read-only IAT permissions — the delay stub will handle its own
           VirtualProtect calls on first use.

13. Bound Imports

What it means: Before ASLR, Windows could pre-resolve all IAT entries at compile/link time if the target DLLs were known to load at specific addresses. This is called a "bound" import — the IAT already has absolute addresses baked in. ASLR made this mostly obsolete, but you still encounter bound imports in old binaries. A manual loader must handle them correctly.

13.1 Detecting Bound Imports

// Bound import check: IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp
// If TimeDateStamp != 0 && TimeDateStamp != -1:
//   The IAT entries are PRE-FILLED with absolute addresses from bind time
//   These addresses may be wrong if ASLR changed the DLL bases
//   → Your manual loader must RE-RESOLVE these imports

// If TimeDateStamp == -1 (0xFFFFFFFF):
//   ALL imports are bound (in the new-style bound import format)
//   → Check DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT] for the table

// Modern approach: Always re-resolve all imports regardless of bound state
// (safer, correct, ASLR-compatible)
void resolve_ignoring_bounds(LPVOID imageBase, PIMAGE_NT_HEADERS nt) {
    PIMAGE_IMPORT_DESCRIPTOR desc =
        (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)imageBase +
            nt->OptionalHeader.DataDirectory[1].VirtualAddress);

    while (desc->Name) {
        if (desc->TimeDateStamp != 0) {
            printf("[*] Bound import detected for %s — will re-resolve\n",
                   (char*)((BYTE*)imageBase + desc->Name));
        }
        // Proceed with normal resolution regardless
        // ... (standard IAT resolution code)
        desc++;
    }
}

14. TLS (Thread Local Storage) Callbacks

What it means: TLS callbacks are functions in a PE that execute before DllMain (and even before WinMain for EXEs). They fire when: a process attaches, a thread attaches/detaches, a process detaches. Malware uses TLS callbacks for anti-debug checks and early execution. A correct manual loader must invoke them.

14.1 TLS Data Directory and Callback Structure

// Located at: DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS] (index 9)
// IMAGE_TLS_DIRECTORY64 (for x64 PEs)
typedef struct _IMAGE_TLS_DIRECTORY64 {
    ULONGLONG StartAddressOfRawData;    // VA of TLS data template
    ULONGLONG EndAddressOfRawData;      // End VA of TLS data template
    ULONGLONG AddressOfIndex;           // VA of TLS index variable
    ULONGLONG AddressOfCallBacks;       // VA of null-terminated callback array
    DWORD     SizeOfZeroFill;
    DWORD     Characteristics;
} IMAGE_TLS_DIRECTORY64;

// AddressOfCallBacks: VA of an array of function pointers
// Each pointer: a PIMAGE_TLS_CALLBACK function
// Array terminated by NULL pointer

typedef void (NTAPI* PIMAGE_TLS_CALLBACK)(
    PVOID DllHandle,    // Base of the module (same as HMODULE)
    DWORD Reason,       // DLL_PROCESS_ATTACH / DLL_THREAD_ATTACH / etc.
    PVOID Reserved
);

14.2 Invoking TLS Callbacks in a Manual Loader

// tls_invoke.c — Call TLS callbacks before DllMain

void invoke_tls_callbacks(LPVOID imageBase, PIMAGE_NT_HEADERS nt, DWORD reason) {
    PIMAGE_DATA_DIRECTORY tlsDD =
        &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];

    if (!tlsDD->VirtualAddress) {
        return;  // No TLS directory — nothing to do
    }

    PIMAGE_TLS_DIRECTORY64 tlsDir =
        (PIMAGE_TLS_DIRECTORY64)((BYTE*)imageBase + tlsDD->VirtualAddress);

    // AddressOfCallBacks is a VA (absolute address), not an RVA
    if (!tlsDir->AddressOfCallBacks) return;

    PIMAGE_TLS_CALLBACK* callbacks =
        (PIMAGE_TLS_CALLBACK*)(tlsDir->AddressOfCallBacks);

    printf("[*] TLS: invoking callbacks (reason: %lu)\n", reason);

    // Walk null-terminated callback pointer array
    for (int i = 0; callbacks[i] != NULL; i++) {
        printf("  [>] TLS callback[%d]: 0x%p\n", i, callbacks[i]);
        callbacks[i]((LPVOID)imageBase, reason, NULL);
    }
}

// Call order in manual loader:
//   1. invoke_tls_callbacks(base, nt, DLL_PROCESS_ATTACH)  ← BEFORE DllMain
//   2. DllMain(base, DLL_PROCESS_ATTACH, NULL)
//
// Anti-debug in TLS example (fires before any user code or debugger attach):
// void NTAPI tls_callback(PVOID dll, DWORD reason, PVOID) {
//     if (reason == DLL_PROCESS_ATTACH) {
//         if (IsDebuggerPresent()) ExitProcess(1);  // kills before main() runs
//     }
// }

14.3 Detecting TLS Callbacks in Malware

# dumpbin: show TLS info
dumpbin /TLS malware.exe

# Expected output if TLS exists:
# TLS directory at 00001000:
#     ...
#     Addresses of callbacks: 0x000000001400030A0
#         0x000000001400022B0  ← actual callback function address

# pefile (Python):
import pefile
pe = pefile.PE("malware.exe")
if hasattr(pe, 'DIRECTORY_ENTRY_TLS'):
    for cb in pe.DIRECTORY_ENTRY_TLS.struct.AddressOfCallBacks_list:
        print(f"  TLS Callback: 0x{cb:016X}")

# IDA Pro / Ghidra: 
# TLS callbacks show as "TlsCallback_0", "TlsCallback_1" etc.
# In x64dbg: Options → Preferences → Events → TLS Callbacks → checked
#   → debugger will break at each TLS callback before entry point

15. IAT Hooking Techniques

[HOOK] IAT hooking replaces IAT entries with addresses of attacker-controlled functions. Used by EDRs to intercept API calls, by red teamers to intercept calls from a specific module, and by malware for persistence and evasion.

15.1 IAT Hooking Concept

Normal execution:
  Application code → CALL [IAT_VirtualAlloc] → 0x7FFA1234ABCD → kernel32!VirtualAlloc

After IAT hook by EDR:
  Application code → CALL [IAT_VirtualAlloc] → 0x7FFA9999DEAD → EDR!HookedVirtualAlloc
  EDR checks parameters → logs event → optionally blocks → calls real VirtualAlloc

After IAT hook by red team (intercept calls from a target module):
  Application code → CALL [IAT_WriteProcessMemory] → 0x<attacker_stub>
  Attacker stub: logs call, modifies args, or blocks entirely

15.2 IAT Hook Installation (C)

// iat_hook.c — Install an IAT hook in a specific module
// Replace a named function's IAT entry with our hook function

#include <windows.h>
#include <stdio.h>

typedef struct _IAT_HOOK {
    void* originalAddress;   // Save original so we can call through / unhook
    void* hookAddress;
    char  dllName[64];
    char  funcName[64];
} IAT_HOOK;

IAT_HOOK g_hook = {0};

// Install IAT hook in targetModule for dllName!funcName
BOOL install_iat_hook(
    HMODULE     targetModule,   // Module whose IAT to hook
    const char* dllName,        // DLL containing the function
    const char* funcName,       // Function to hook
    void*       hookFunc        // Our replacement function
) {
    BYTE* base = (BYTE*)targetModule;
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);

    PIMAGE_IMPORT_DESCRIPTOR desc =
        (PIMAGE_IMPORT_DESCRIPTOR)(base +
            nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
                               .VirtualAddress);

    while (desc->Name) {
        char* thisDll = (char*)(base + desc->Name);

        // Case-insensitive DLL name match
        if (_stricmp(thisDll, dllName) != 0) {
            desc++;
            continue;
        }

        // Found the right DLL — walk its thunks
        PIMAGE_THUNK_DATA pINT =
            (PIMAGE_THUNK_DATA)(base + desc->OriginalFirstThunk);
        PIMAGE_THUNK_DATA pIAT =
            (PIMAGE_THUNK_DATA)(base + desc->FirstThunk);

        while (pINT->u1.AddressOfData) {
            if (!IMAGE_SNAP_BY_ORDINAL(pINT->u1.Ordinal)) {
                PIMAGE_IMPORT_BY_NAME ibn =
                    (PIMAGE_IMPORT_BY_NAME)(base + pINT->u1.AddressOfData);

                if (strcmp((char*)ibn->Name, funcName) == 0) {
                    // Found target entry in IAT

                    // Save original address
                    g_hook.originalAddress = (void*)pIAT->u1.Function;
                    g_hook.hookAddress     = hookFunc;
                    strncpy(g_hook.dllName,  dllName,  63);
                    strncpy(g_hook.funcName, funcName, 63);

                    printf("[+] IAT Hook: %s!%s\n", dllName, funcName);
                    printf("    Original address: 0x%p\n", g_hook.originalAddress);
                    printf("    Hook address:     0x%p\n", hookFunc);

                    // Make IAT entry writable
                    DWORD oldProt;
                    VirtualProtect(&pIAT->u1.Function,
                                   sizeof(ULONG_PTR),
                                   PAGE_READWRITE, &oldProt);

                    // Overwrite with hook address
                    pIAT->u1.Function = (ULONG_PTR)hookFunc;

                    // Restore original protection
                    VirtualProtect(&pIAT->u1.Function,
                                   sizeof(ULONG_PTR),
                                   oldProt, &oldProt);

                    return TRUE;
                }
            }
            pINT++;
            pIAT++;
        }
        desc++;
    }

    printf("[-] IAT Hook: %s!%s not found in module\n", dllName, funcName);
    return FALSE;
}

// ─── Example Hook: intercept MessageBoxA ────────────────────────
typedef int (WINAPI* pMessageBoxA_t)(HWND, LPCSTR, LPCSTR, UINT);

int WINAPI HookedMessageBoxA(HWND hWnd, LPCSTR text, LPCSTR caption, UINT type) {
    printf("[HOOK] MessageBoxA intercepted!\n");
    printf("       Text: %s\n", text);
    printf("       Caption: %s\n", caption);

    // Call original (call-through)
    pMessageBoxA_t orig = (pMessageBoxA_t)g_hook.originalAddress;
    return orig(hWnd, text, caption, type);

    // Or: block the call entirely → return 0;
    // Or: modify parameters → return orig(hWnd, "HOOKED!", "HOOKED!", type);
}

// Installation:
// install_iat_hook(GetModuleHandle(NULL), "user32.dll",
//                  "MessageBoxA", HookedMessageBoxA);

15.3 IAT Hook Detection (Blue Team / Self-Check)

#!/usr/bin/env python3
# detect_iat_hooks.py — Compare IAT addresses against GetProcAddress resolution
# Run IN the process being checked

import ctypes
import pefile
import sys

def check_iat_hooks(module_path: str):
    """
    For each IAT entry, compare:
      - The address currently in the IAT
      - The address from GetProcAddress(dll, funcName)
    If they differ → IAT has been hooked
    """
    k32 = ctypes.windll.kernel32

    pe = pefile.PE(module_path)
    if not hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
        return

    base = pe.OPTIONAL_HEADER.ImageBase
    print(f"[*] Checking IAT hooks in: {module_path}")
    print(f"[*] Loaded at base:         0x{base:016X}")
    print()

    for dll_entry in pe.DIRECTORY_ENTRY_IMPORT:
        dll_name = dll_entry.dll.decode('utf-8', errors='replace')
        hDll = k32.GetModuleHandleA(dll_name.encode())
        if not hDll:
            hDll = k32.LoadLibraryA(dll_name.encode())

        for imp in dll_entry.imports:
            if imp.import_by_ordinal:
                continue  # Skip ordinal-only for simplicity

            func_name = imp.name.decode('utf-8', errors='replace')

            # Address currently in IAT (what the binary will actually call)
            iat_address = imp.address  # pefile gives the resolved address

            # Expected address from GetProcAddress
            expected = k32.GetProcAddress(hDll, func_name.encode())

            if iat_address and expected and iat_address != expected:
                print(f"  [!] HOOK DETECTED: {dll_name}!{func_name}")
                print(f"      IAT value:         0x{iat_address:016X}")
                print(f"      GetProcAddress:    0x{expected:016X}")
                print(f"      Delta:             {iat_address - expected:+d}")

# Usage: detect_iat_hooks.py C:\path\to\target.exe

16. EAT Hooking Techniques

[HOOK] EAT hooking replaces addresses in a DLL's export table — redirecting calls that go through that DLL's export to an attacker function. Less common than IAT hooks because most programs have the IAT already filled at load time, but useful for redirecting calls in a library before processes load it.

16.1 EAT Hook Installation (C)

// eat_hook.c — Replace EAT entry with a hook (affects ALL processes loading this DLL)
// Requires: VirtualProtect on the EAT region

#include <windows.h>
#include <stdio.h>

DWORD g_originalRVA = 0;

BOOL install_eat_hook(HMODULE hModule, const char* funcName, void* hookFunc) {
    BYTE* base = (BYTE*)hModule;
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);

    DWORD expRVA  = nt->OptionalHeader.DataDirectory[0].VirtualAddress;
    DWORD expSize = nt->OptionalHeader.DataDirectory[0].Size;
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(base + expRVA);

    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++) {
        if (strcmp((char*)(base + names[i]), funcName) == 0) {
            DWORD* eatEntry = &funcs[ords[i]];

            // Save original RVA
            g_originalRVA = *eatEntry;
            printf("[+] EAT Hook: %s  original RVA: 0x%08X\n",
                   funcName, g_originalRVA);

            // Compute the RVA of our hook function
            DWORD hookRVA = (DWORD)((BYTE*)hookFunc - base);

            // Unlock EAT for writing
            DWORD oldProt;
            VirtualProtect(eatEntry, sizeof(DWORD), PAGE_READWRITE, &oldProt);

            // Overwrite the function RVA with hook RVA
            *eatEntry = hookRVA;

            // Restore protection
            VirtualProtect(eatEntry, sizeof(DWORD), oldProt, &oldProt);

            printf("[+] EAT Hook installed: %s → 0x%p (RVA: 0x%08X)\n",
                   funcName, hookFunc, hookRVA);
            return TRUE;
        }
    }
    return FALSE;
}

// Note: EAT hooks affect only future IAT resolutions in NEW processes.
// Processes already loaded with the IAT filled will not be affected.
// EAT hooks are therefore most useful for:
//   1. Hooking before any process uses the DLL (very early)
//   2. Hooking undocumented exports called by other DLLs dynamically
//   3. Forwarder-based hooks (safer, preserved across process loads)

17. Dynamic Import Resolution (No IAT)

What it means: Instead of listing imported functions in the IAT (which EDRs, AV, and analysts can easily read), dynamically resolve function addresses at runtime using GetProcAddress or a manual PEB walk. The binary's IAT shows no suspicious imports — all sensitive API calls are found and used at runtime.

17.1 GetProcAddress-Based Dynamic Resolution

// dynamic_resolve.c — Resolve APIs at runtime without IAT entries
// Advantage: No suspicious function names in IAT
// Disadvantage: GetProcAddress call + LoadLibrary call are themselves visible

#include <windows.h>

// Pattern 1: Simple runtime resolution
void method_getprocaddress() {
    // Load the DLL (may already be loaded — GetModuleHandle avoids re-loading)
    HMODULE hK32 = GetModuleHandleA("kernel32.dll");
    if (!hK32) hK32 = LoadLibraryA("kernel32.dll");

    // Resolve function addresses at runtime
    typedef LPVOID(WINAPI* pVA_t)(LPVOID, SIZE_T, DWORD, DWORD);
    typedef HANDLE(WINAPI* pCT_t)(LPVOID, DWORD, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);

    pVA_t  VirtualAlloc_fn  = (pVA_t)GetProcAddress(hK32, "VirtualAlloc");
    pCT_t  CreateThread_fn  = (pCT_t)GetProcAddress(hK32, "CreateThread");

    // Use resolved pointers — IAT has no "VirtualAlloc" or "CreateThread" entries
    LPVOID mem = VirtualAlloc_fn(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE,
                                  PAGE_EXECUTE_READWRITE);
    // ...
}

// Pattern 2: Function pointer struct (cleaner for many APIs)
typedef struct {
    HMODULE  hK32;
    HMODULE  hNtdll;
    LPVOID  (WINAPI* VirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
    BOOL    (WINAPI* VirtualFree)(LPVOID, SIZE_T, DWORD);
    BOOL    (WINAPI* VirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
    HANDLE  (WINAPI* CreateThread)(LPVOID, SIZE_T, LPTHREAD_START_ROUTINE,
                                    LPVOID, DWORD, LPDWORD);
    BOOL    (WINAPI* WriteProcessMemory)(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*);
    HANDLE  (WINAPI* OpenProcess)(DWORD, BOOL, DWORD);
    LPVOID  (WINAPI* VirtualAllocEx)(HANDLE, LPVOID, SIZE_T, DWORD, DWORD);
} WIN32_API;

WIN32_API g_api = {0};

void init_api_table(void) {
    g_api.hK32 = GetModuleHandleA("kernel32.dll");

    #define RESOLVE(dll, name) g_api.name = (void*)GetProcAddress(dll, #name)
    RESOLVE(g_api.hK32, VirtualAlloc);
    RESOLVE(g_api.hK32, VirtualFree);
    RESOLVE(g_api.hK32, VirtualProtect);
    RESOLVE(g_api.hK32, CreateThread);
    RESOLVE(g_api.hK32, WriteProcessMemory);
    RESOLVE(g_api.hK32, OpenProcess);
    RESOLVE(g_api.hK32, VirtualAllocEx);
    #undef RESOLVE

    // Use later:
    // g_api.VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
}

17.2 D/Invoke Pattern (C# — Bypass IAT Hooks + No IAT Entry)

// dinvoke_example.cs — Dynamic P/Invoke in C#
// Import: https://github.com/TheWover/DInvoke
// Advantage over regular P/Invoke:
//   - No IAT entry (function not in managed module's import table)
//   - Bypasses IAT hooks placed by EDRs
//   - Can call NT-level functions without managed wrappers

using System;
using System.Runtime.InteropServices;
using DInvoke.DynamicInvoke;

class DInvokeExample {
    // P/Invoke (standard — visible in IAT, hookable):
    // [DllImport("kernel32.dll")] 
    // static extern IntPtr VirtualAlloc(...);

    // D/Invoke (dynamic — NOT in IAT, bypasses IAT hooks):
    static IntPtr DynVirtualAlloc(IntPtr lpAddress, uint dwSize, uint flType, uint flProtect) {
        // Define the delegate type matching the function signature
        object[] args = { lpAddress, dwSize, flType, flProtect };

        // Resolve and invoke dynamically
        var result = Generic.DynamicApiInvoke(
            "kernel32.dll",          // DLL name
            "VirtualAlloc",          // Function name
            typeof(VirtualAllocDelegate),
            ref args
        );
        return (IntPtr)result;
    }

    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    delegate IntPtr VirtualAllocDelegate(IntPtr lpAddr, uint sz, uint allocType, uint protect);

    // Alternatively: use GetProcAddress directly
    static T GetExport<T>(string dll, string func) {
        IntPtr hDll  = Win32.GetModuleHandle(dll);
        IntPtr pFunc = Win32.GetProcAddress(hDll, func);
        return Marshal.GetDelegateForFunctionPointer<T>(pFunc);
    }
}

18. API Hashing for Import Obfuscation

What it means: Instead of storing function name strings like "VirtualAlloc" anywhere in your binary (where YARA rules and AV can find them), store only a 32-bit (or 64-bit) hash of the name. At runtime, walk the export table, hash each function name, compare to stored hash, and resolve on match. No strings, no static signatures.

18.1 Common Hash Algorithms Used in Malware

// ─── djb2 hash (simple, fast) ───────────────────────────────────
DWORD hash_djb2(const char* str) {
    DWORD hash = 5381;
    while (*str)
        hash = ((hash << 5) + hash) + *str++;  // hash * 33 + c
    return hash;
}

// ─── FNV-1a (used in many C2 loaders) ───────────────────────────
DWORD hash_fnv1a(const char* str) {
    DWORD hash = 0x811C9DC5;  // FNV offset basis
    while (*str) {
        hash ^= (BYTE)*str++;
        hash *= 0x01000193;   // FNV prime
    }
    return hash;
}

// ─── ROR-13 hash (used in Metasploit / Cobalt Strike shellcode) ──
DWORD hash_ror13(const char* str) {
    DWORD hash = 0;
    while (*str) {
        hash = ((hash >> 13) | (hash << 19));  // ROR 13
        hash += (BYTE)*str++;
    }
    return hash;
}

// Cobalt Strike's implementation also hashes the DLL name:
// hash = DLL_name_hash(uppercase) + function_name_ror13_hash
// This allows collision-free lookup across all loaded DLLs

// ─── Pre-compute hashes offline ──────────────────────────────────
// python3 -c "
// s='VirtualAlloc'
// h=0
// for c in s: h = ((h>>13)|(h<<19))&0xFFFFFFFF; h=(h+ord(c))&0xFFFFFFFF
// print(hex(h))
// "
// → 0x91AFCA54 (example — actual value depends on hash algorithm)

18.2 Full Hash-Based API Resolver (C)

// hash_resolve.c — Resolve any function by hash, no plaintext strings
// Used in: shellcode, position-independent reflective loaders

#include <windows.h>
#include <stdint.h>

// FNV-1a hash (case-sensitive)
static inline uint32_t fnv1a(const char* s) {
    uint32_t h = 0x811C9DC5;
    while (*s) { h ^= (uint8_t)*s++; h *= 0x01000193; }
    return h;
}

// FNV-1a hash (case-insensitive) — for DLL names
static inline uint32_t fnv1a_ci(const char* s) {
    uint32_t h = 0x811C9DC5;
    while (*s) {
        char c = *s++;
        if (c >= 'A' && c <= 'Z') c |= 0x20;  // tolower
        h ^= (uint8_t)c;
        h *= 0x01000193;
    }
    return h;
}

// Pre-computed hashes (no strings in binary!)
#define HASH_VirtualAlloc         0xCEC8AC2E
#define HASH_VirtualFree          0x30B5AD3E
#define HASH_CreateThread         0x1BC9BCB0
#define HASH_WriteProcessMemory   0xD83D6AA1
#define HASH_OpenProcess          0x8C5AC7EC
#define HASH_LoadLibraryA         0x5FBFF0FB
#define HASH_GetProcAddress       0x1FC0EAEE
#define HASH_VirtualAllocEx       0x9EF6A5B6

// Walk EAT of a module, resolve function by hash
FARPROC resolve_by_hash(HMODULE hMod, uint32_t targetHash) {
    BYTE* base = (BYTE*)hMod;
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);

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

    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(base + expRVA);
    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++) {
        char* name    = (char*)(base + names[i]);
        uint32_t hash = fnv1a(name);

        if (hash == targetHash) {
            DWORD funcRVA = funcs[ords[i]];

            // Handle forwarder
            if (funcRVA >= expRVA && funcRVA < expRVA + expSize) {
                char* fwd = (char*)(base + funcRVA);
                // Parse forwarder and recurse (simplified)
                return NULL;
            }

            return (FARPROC)(base + funcRVA);
        }
    }
    return NULL;
}

// ─── Usage (no plaintext function names anywhere) ─────────────────
void demo_hash_resolve(void) {
    // Find kernel32.dll via PEB (no LoadLibrary!)
    HMODULE hK32 = peb_find_module(L"kernel32.dll");  // from reflective_loading_internals.md

    // Resolve by hash — no strings visible in binary
    typedef LPVOID(WINAPI* pVA)(LPVOID, SIZE_T, DWORD, DWORD);
    pVA fn_VirtualAlloc = (pVA)resolve_by_hash(hK32, HASH_VirtualAlloc);

    LPVOID mem = fn_VirtualAlloc(NULL, 0x1000,
                                  MEM_COMMIT | MEM_RESERVE,
                                  PAGE_EXECUTE_READWRITE);
    // No "VirtualAlloc" string anywhere in the binary
    // YARA string rules targeting "VirtualAlloc" find nothing
}

18.3 Python: Pre-Compute Hashes for All Exports

#!/usr/bin/env python3
# precompute_hashes.py — Generate hash table for all exports of a DLL
# Use to build your hash constants before compiling

import pefile
import sys

def fnv1a(s: str) -> int:
    h = 0x811C9DC5
    for c in s.encode('ascii'):
        h ^= c
        h = (h * 0x01000193) & 0xFFFFFFFF
    return h

def ror13(s: str) -> int:
    h = 0
    for c in s:
        h = ((h >> 13) | (h << 19)) & 0xFFFFFFFF
        h = (h + ord(c)) & 0xFFFFFFFF
    return h

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

def generate_hash_table(dll_path: str, funcs_filter: list = None):
    pe = pefile.PE(dll_path)
    if not hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
        print(f"[!] No exports in {dll_path}")
        return

    print(f"// Hash table generated from: {dll_path}")
    print(f"// Algorithms: FNV-1a, ROR-13, DJB2")
    print()

    for sym in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        if not sym.name: continue
        name = sym.name.decode('utf-8', errors='replace')

        if funcs_filter and name not in funcs_filter:
            continue

        fnv = fnv1a(name)
        r13 = ror13(name)
        d2  = djb2(name)

        print(f"// {name}")
        print(f"#define HASH_{name:<40s}_FNV  0x{fnv:08X}u")
        print(f"#define HASH_{name:<40s}_ROR  0x{r13:08X}u")
        print(f"#define HASH_{name:<40s}_DJB  0x{d2:08X}u")
        print()

if __name__ == "__main__":
    dll_path = sys.argv[1]
    # Filter to common red team APIs
    interesting = [
        "VirtualAlloc", "VirtualAllocEx", "VirtualFree", "VirtualProtect",
        "VirtualProtectEx", "CreateThread", "CreateRemoteThread",
        "WriteProcessMemory", "ReadProcessMemory", "OpenProcess",
        "LoadLibraryA", "LoadLibraryExA", "GetProcAddress",
        "NtAllocateVirtualMemory", "NtWriteVirtualMemory",
        "NtCreateThreadEx", "NtQueueApcThread",
    ]
    generate_hash_table(dll_path, interesting)

# Usage:
# python3 precompute_hashes.py C:\Windows\System32\kernel32.dll
# python3 precompute_hashes.py C:\Windows\System32\ntdll.dll

19. Tools Reference

19.1 PE Analysis Tools

# ─── dumpbin (MSVC / Visual Studio) ──────────────────────────────
# Ships with Visual Studio — most comprehensive PE analysis

dumpbin /all payload.dll              # Everything
dumpbin /imports payload.exe          # Full import table
dumpbin /exports kernel32.dll         # Full export table
dumpbin /relocations payload.dll      # Relocation table
dumpbin /headers payload.exe          # All PE headers
dumpbin /TLS payload.exe              # TLS callbacks
dumpbin /DEPENDENTS payload.exe       # DLL dependencies

# ─── pefile (Python) ────────────────────────────────────────────
pip install pefile
python3 -c "
import pefile
pe = pefile.PE('payload.dll')
for d in pe.DIRECTORY_ENTRY_IMPORT:
    print(d.dll)
    for i in d.imports:
        print('  ', i.name)
"

# ─── PE-bear (GUI) ──────────────────────────────────────────────
# https://github.com/hasherezade/pe-bear
# Best GUI PE editor — shows all tables, can edit headers/sections
# Great for: relocation table visualization, IAT inspection

# ─── CFF Explorer (GUI) ─────────────────────────────────────────
# http://www.ntcore.com/exsuite.php
# Classic PE explorer — good for ASLR flag editing, section editing

# ─── pesieve (CLI) ──────────────────────────────────────────────
# https://github.com/hasherezade/pe-sieve
pe-sieve.exe /pid 1234          # Scan running process for PE anomalies
pe-sieve.exe /pid 1234 /imp 3  # Also dump IAT state

# ─── pestudio ───────────────────────────────────────────────────
# https://www.winitor.com/
# Static analysis with import risk scoring, YARA scanning

19.2 Dynamic Analysis + Hook Detection

# ─── x64dbg ─────────────────────────────────────────────────────
# IAT inspection: Right-click in disassembler → Follow in Imports
# Relocation: View → Memory Map → find module → right-click → PE Header
# TLS: Options → Preferences → Events → System Breakpoint + TLS Callbacks

# ─── Process Hacker / System Informer ───────────────────────────
# Show IAT of a process: Process → Properties → Modules → DLL → Imports
# Check for hooks: Module → Verify (compares in-memory vs disk)

# ─── API Monitor ────────────────────────────────────────────────
# http://www.rohitab.com/apimonitor
# Hook all API calls in a target process (DLL injection)
# Great for: watching import resolution in action

# ─── Frida ──────────────────────────────────────────────────────
# Dynamic instrumentation — attach, hook APIs, read IAT at runtime
frida-trace -p <PID> -i "VirtualAlloc*"    # Trace VirtualAlloc family
frida-trace -p <PID> -i "CreateThread"

# Python script to dump IAT of a running process via Frida:
# frida -p <PID> -l dump_iat.js

19.3 IDA Pro / Ghidra — Reloc + IAT Analysis

IDA Pro:
  View → Open Subviews → Imports          → IAT contents
  View → Open Subviews → Exports          → EAT contents
  View → Open Subviews → Segments         → section list
  Edit → Segments → Rebase Program        → simulate relocation delta

  Relocation entries shown inline in disassembly as "r offset" annotations
  Pressing 'X' on an import → cross-references (call sites)

Ghidra:
  Window → Symbol Table → Filter: type=Function → imports visible
  Window → Relocation Table → shows all relocation entries
  Window → References → finds all call sites to an import

  For manual PE analysis scripts:
    script PEHeaderScript.java → dumps all PE headers to console
    script ImportExportSymbolScript.java → dumps imports/exports

19.4 WinDbg — Runtime IAT/Reloc Inspection

# Find module base and loaded modules
lm                                        # List all loaded modules
lm m kernel32                             # Specific module info

# Dump IAT of a loaded module (e.g., notepad.exe)
# First find base of notepad
lm m notepad
# Then parse PE headers
.process /r /p <process_handle>
!dh notepad                               # Dump all PE headers
!dh -f notepad                            # Full header dump

# Inspect IAT at specific offset
# DataDirectory[1] gives import table RVA — then:
dd notepad+<import_rva>                   # Dump import descriptors
dps notepad+<iat_rva>                     # Dump IAT as pointer array
u poi(notepad+<iat_rva>)                  # Disassemble first IAT entry target

# Check for hooks (expected vs actual IAT values)
# Compare IAT value with what GetProcAddress returns
? kernel32!VirtualAlloc                   # Get expected address
dq notepad+<VirtualAlloc_IAT_offset> L1  # Get actual IAT value

20. Detection & Evasion

20.1 What EDRs and Analysts Look For

[DETECT] STATIC ANALYSIS — IAT SIGNALS:
  ┌─ High-risk imports: VirtualAllocEx + WriteProcessMemory + CreateRemoteThread
  │    → Classic injection trifecta — immediate flag
  │
  ├─ Sensitive imports with no cover story: CryptDecrypt, BCryptDecrypt, IsDebuggerPresent
  │    → Suspicious without legitimate-looking other imports
  │
  ├─ Minimal IAT with few/no imports → dynamic resolution is happening
  │    → GetProcAddress alone in IAT = suspicious
  │    → LoadLibraryA alone in IAT = suspicious
  │
  └─ No import table at all (pure shellcode / sRDI blob)
       → File has no IAT section = likely shellcode/reflective loader

[DETECT] STATIC ANALYSIS — RELOCATION SIGNALS:
  ┌─ No .reloc section in a DLL → compiled /FIXED or PIC shellcode
  │    → May fail ASLR checks on hardened systems
  │
  └─ .reloc section size vs binary size ratio unusually high
       → May indicate packed/modified PE

[DETECT] RUNTIME/BEHAVIORAL:
  ┌─ GetProcAddress("VirtualAlloc") at runtime when VirtualAlloc not in IAT
  │    → Common dynamic resolution pattern (Sysmon EventID 7: DLL load)
  │
  ├─ LoadLibrary followed immediately by GetProcAddress
  │    → Dynamic loader pattern — EDR behavioral engine signature
  │
  ├─ IAT entry value ≠ GetProcAddress result for same function
  │    → IAT hook detected (EDR's own hook, or attacker hook)
  │
  └─ API called without corresponding IAT entry
       → PEB walk / hash resolution — very suspicious if confirmed

[DETECT] HOOKS SET BY EDR (what we're trying to bypass):
  ┌─ IAT hooks: EDR replaces IAT entries with EDR intercept addresses
  │    → All calls to hooked function go through EDR before reaching target
  │
  └─ Inline hooks (JMP patch): EDR patches first bytes of function
       → More aggressive, harder to bypass than IAT hooks
       → Bypass: load fresh ntdll.dll from disk (unhooking)

20.2 Evasion Strategies

1. Blind the IAT — No Sensitive Imports

// Before: IAT contains "VirtualAlloc", "WriteProcessMemory" etc.
// After: IAT is empty (or only benign imports), all resolved dynamically
// Trade-off: GetProcAddress itself becomes the signal

// Better: Resolve via PEB walk (no GetProcAddress call at all)
// See §17 + Reflective Loading Internals §11

// Better still: Use indirect syscalls (bypass user-mode hooks entirely)
// Direct syscall: emit syscall instruction directly (no ntdll function call)
// Indirect syscall: use the syscall instruction from ntdll's own code
//                   (call through ntdll's syscall stub without tripping hooks)

2. API Hashing + PEB Walk (No Strings, No GetProcAddress)

// Binary contains: no function name strings, no GetProcAddress import
// Runtime: PEB walk → find kernel32/ntdll → hash EAT names → resolve
// YARA coverage: near zero (no strings to match)
// Behavioral: suspicious PEB walk pattern (see Reflective Loading §11)
// Combined with sleep encryption: very hard to catch dynamically

// See §18 for full implementation

3. Import Table Flooding (Camouflage)

// Add many legitimate-looking imports alongside sensitive ones
// Makes sensitive imports look like part of a normal application
// Example: add imports from shell32.dll, comctl32.dll, gdiplus.dll
// Technique: "import table stuffing" — dilutes signal in import list
// Tool: can be done with PE editor or by #include-ing unnecessary headers

4. Delay Import for Deferred Resolution

// Mark sensitive DLLs as delay-load
// /DELAYLOAD:ntdll.dll in linker flags (unusual but works)
// IAT shows delay-import stub, not direct function address
// Some automated tools miss delay imports
// Note: delay imports ARE processed by the delay-load helper — still visible

5. Obfuscated Relocation (Remove .reloc for Shellcode)

// Shellcode by definition: no relocations, fully PIC
// Custom loaders for shellcode: just copy and execute — no reloc processing
// For PIC code in Visual Studio:
//   Use RIP-relative addressing (x64 default for most cases)
//   Avoid: global variable direct access, absolute jumps, vtables
//   The linker /FIXED flag removes .reloc section (dangerous for ASLR)

// Testing PIC compliance:
// Load your DLL at a different base than preferred and see if it works
// Or: check dumpbin /relocations — if empty, the code is PIC

21. Comparison Matrix

Technique IAT Visible YARA Strings Hooks Bypass Complexity OPSEC
Standard IAT (imports) Full list All names No None None
GetProcAddress + LoadLibrary Only these 2 "VirtualAlloc" etc. at runtime Partial Low Low
PEB Walk + manual resolve None None if hashed Yes (no GetProc) Medium High
API Hashing (FNV/ROR13) None None Yes Medium High
D/Invoke (.NET) None None Yes (IAT) Low (library) High
Import by Ordinal Ordinal #s only No names No Low Medium
Import Flooding All + noise Lost in noise No Low Medium
Indirect Syscalls None (ntdll stub) None Full (kernel) Very High Highest

22. Cheat Sheet

Key PE Data Directory Indices

// DataDirectory[] indices:
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0  // EAT (export table)
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1  // IAT (import table)
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2  // .rsrc
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3  // .pdata (x64 unwind info)
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4  // Authenticode signature
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5  // .reloc (relocations)
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6  // Debug info
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7  // (reserved)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8  // MIPS GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9  // TLS callbacks
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10  // Security cookie, CFG
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11  // Bound imports
#define IMAGE_DIRECTORY_ENTRY_IAT            12  // IAT (raw, not import desc)
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13  // Delay load imports
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14  // .NET CLR header

PE Navigation One-Liners

// From any base address — the full navigation chain:
PIMAGE_DOS_HEADER dos     = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS nt      = (PIMAGE_NT_HEADERS)((BYTE*)base + dos->e_lfanew);
PIMAGE_FILE_HEADER fh     = &nt->FileHeader;
PIMAGE_OPTIONAL_HEADER oh = &nt->OptionalHeader;
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);

// Jump to any table via DataDirectory:
#define DATA_DIR(base, nt, idx) \
    ((BYTE*)(base) + (nt)->OptionalHeader.DataDirectory[(idx)].VirtualAddress)

PIMAGE_IMPORT_DESCRIPTOR imports  = (PIMAGE_IMPORT_DESCRIPTOR)DATA_DIR(base, nt, 1);
PIMAGE_EXPORT_DIRECTORY  exports  = (PIMAGE_EXPORT_DIRECTORY) DATA_DIR(base, nt, 0);
PIMAGE_BASE_RELOCATION   relocs   = (PIMAGE_BASE_RELOCATION)  DATA_DIR(base, nt, 5);
PIMAGE_TLS_DIRECTORY64   tls      = (PIMAGE_TLS_DIRECTORY64)  DATA_DIR(base, nt, 9);

Relocation Quick Reference

Entry format (2 bytes / WORD):
  [15..12] = TYPE   (4 bits)
  [11..0]  = OFFSET (12 bits, within 4KB page)

Critical types:
  0  = ABSOLUTE → skip (padding)
  3  = HIGHLOW  → patch 32-bit VA (x86)
  10 = DIR64    → patch 64-bit VA (x64)

Delta = actualLoadBase - preferredImageBase
Apply:  *patchTarget += delta

Block terminator: VirtualAddress == 0, SizeOfBlock == 0
Entries per block: (SizeOfBlock - 8) / 2
Page covered: 4KB starting at imageBase + block.VirtualAddress

Import Resolution Quick Reference

Level 1: IMAGE_IMPORT_DESCRIPTOR (array, null-terminated)
  .OriginalFirstThunk → INT (read-only names, never modified)
  .FirstThunk         → IAT (write resolved addresses here)
  .Name               → RVA to DLL name string

Level 2: IMAGE_THUNK_DATA (arrays, null-terminated)
  If MSB set → import by ordinal (low 16 bits = ordinal)
  Else       → .AddressOfData = RVA to IMAGE_IMPORT_BY_NAME

Level 3: IMAGE_IMPORT_BY_NAME
  .Hint = suggested ordinal (optional speed hint)
  .Name = null-terminated function name string

Resolution: GetProcAddress(LoadLibraryA(dllName), funcName)
Write to: pIAT->u1.Function = (ULONG_PTR)resolvedAddr
Lock: VirtualProtect(pIAT, size, PAGE_READWRITE, &old) FIRST

Export Lookup Quick Reference

IMAGE_EXPORT_DIRECTORY:
  .AddressOfFunctions    → DWORD[] of function RVAs (size: NumberOfFunctions)
  .AddressOfNames        → DWORD[] of name string RVAs (size: NumberOfNames)
  .AddressOfNameOrdinals → WORD[]  of index into Functions[] (size: NumberOfNames)
  .Base                  → External ordinal base (add to internal index)

Lookup by name:
  1. Find name in AddressOfNames[] (binary search — names are sorted)
  2. Use same index into AddressOfNameOrdinals[] → get internal index
  3. Use internal index into AddressOfFunctions[] → get function RVA
  4. VA = imageBase + funcRVA

Lookup by ordinal:
  internal_index = external_ordinal - Base
  VA = imageBase + AddressOfFunctions[internal_index]

Forwarder check:
  if funcRVA >= exportDir.RVA && funcRVA < exportDir.RVA + exportDir.Size:
      → it's a forwarder string ("DLL.FunctionName"), recurse

Common Tools Summary

# Parse full PE (Python):
pip install pefile && python3 -c "import pefile; pe=pefile.PE('x.dll'); print(pe.dump_info())"

# Import table:
dumpbin /imports x.exe
pefile → pe.DIRECTORY_ENTRY_IMPORT

# Export table:
dumpbin /exports x.dll
pefile → pe.DIRECTORY_ENTRY_EXPORT

# Relocation table:
dumpbin /relocations x.dll
pefile → pe.DIRECTORY_ENTRY_BASERELOC

# ASLR/DEP flags:
dumpbin /headers x.exe | findstr "DLL characteristics"
pefile → pe.OPTIONAL_HEADER.DllCharacteristics

# Live IAT dump (WinDbg):
!dh module_name     # Show PE headers including IAT RVA
dps module+<IAT_RVA> L<count>   # Dump IAT entries as pointers

# Detect IAT hooks (runtime):
compare: actual_IAT_value vs GetProcAddress(dll, func)
mismatch → hook present

# Hash pre-computation:
python3 -c "
def fnv1a(s):
    h=0x811C9DC5
    for c in s: h=(h^ord(c))*0x01000193 & 0xFFFFFFFF
    return h
print(hex(fnv1a('VirtualAlloc')))
"

References

Microsoft Documentation: - PE/COFF Specification → https://learn.microsoft.com/en-us/windows/win32/debug/pe-format - Base Relocation Types → https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#base-relocation-types - Import Directory Table → https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table - Export Directory Table → https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#export-directory-table - IAT Write Protection (2022) → https://devblogs.microsoft.com/oldnewthing/20221006-07/

Research & Papers: - HookChain (2024) → https://arxiv.org/abs/2404.16856 — IAT hook + indirect syscall EDR bypass - Matt Pietrek — "Peering Inside the PE" (1994) — foundational PE internals - Matt Pietrek — "An In-Depth Look into the Win32 Portable Executable Format" (2002)

Open Source Tools: - PE-bear → https://github.com/hasherezade/pe-bear (best GUI PE analyzer) - pefile (Python) → https://github.com/erocarrera/pefile - LIEF → https://github.com/lief-project/LIEF (cross-language PE parsing) - pe-sieve → https://github.com/hasherezade/pe-sieve (runtime PE anomaly detection) - DInvoke → https://github.com/TheWover/DInvoke (C# dynamic API invocation) - HookChain → https://github.com/helviojunior/hookchain (IAT hook + indirect syscall) - saferwall/pe → https://github.com/saferwall/pe (Go PE library, malware-resistant)

MITRE ATT&CK: - T1055.001 — Dynamic-link Library Injection (IAT abuse) - T1574.002 — DLL Side-Loading (EAT/IAT manipulation) - T1056.004 — Input Capture: API Hooking - T1027.007 — Dynamic API Resolution (API hashing)

← Malware development top ↑