Game of Hide and Seek: Detecting Dynamic API Resolution at Runtime With Aether
10 min read
research

Game of Hide and Seek: Detecting Dynamic API Resolution at Runtime With Aether

Game of Hide and Seek: Detecting Dynamic API Resolution at Runtime With Aether
10 min read 1,987 words

Introduction

Welcome to the second blog post exploring the new capabilities of the Aether v0.9 stable release. Following the beta launch(v0.8), the community provided valuable feedback and test cases involving packed malware, custom builds, and unique evasions. Offensive security consistently resembles a game of hide and seek; whenever defenders build a wall, adversaries inevitably find a way underneath.

While testing the previous iteration of Aether against the Adaptix C2 agent (both EXE and DLL payloads), the structural scan failed to trigger any detections. despite the static rules scan and beaconing detection system detected it successfully. I decided to perform deep dive analysis to explore the reasons behind not detecting it with Structural IOC scanning, my subsequent analysis identified the specific mechanics behind this evasion:

  • Memory Allocation: Aether primarily targets MEM_PRIVATE allocations. The Adaptix agent, however, executes entirely from its own MEM_IMAGE region and stores its C2 data, functions in the heap.
  • Lack of Injection: The agent did not rely on Shellcode injection, and no Shellcode artifacts were present in the private memory spaces.
  • Module Integrity: All MEM_IMAGE regions mapped directly to legitimate modules listed in the Process Environment Block (PEB), effectively ruling out process hollowing.
  • Low Entropy: The payload maintained a low profile, with an entropy score measuring below 4.0.
  • Execution Flow: There were no runtime code modifications. Additionally, all threads initiated directly from verified, legitimate modules.

Concealing the IAT: The Mechanics of Dynamic API Resolution

The majority of the malware loaders I came a cross or even ones I wrote share a common evasion capability: obfuscating or hiding the Import Address Table (IAT). The objective is to deceive static scanners and PE parsers by presenting an IAT filled exclusively with benign API calls, while actively concealing the malicious functions required for execution.

The question is how they achieve this. There are several established methods for dynamically resolving API functions at runtime without leaving a footprint in the static executable headers.

Traversing the Process Environment Block (PEB)

The most reliable method for dynamic resolution relies on the Process Environment Block(PEB). Instead of waiting for the OS loader to map dependencies and populate the IAT, the loader manually locates the base addresses of core libraries like kernel32.dll or ntdll.dll, which are automatically loaded into every Windows process.

The execution flow typically follows a strict path:

  • Access the Thread Environment Block (TEB) via the fs register for 32-bit architectures or the gs register for 64-bit architectures.
  • Read the pointer to the PEB.
  • Navigate to the PEB_LDR_DATA structure, which contains the InLoadOrderModuleList.
  • Traverse this linked list until the target DLL base address is identified.
  • Parse the Export Address Table (EAT) of that DLL to calculate the exact memory addresses of the required functions.

Once the payload dynamically resolves fundamental functions like GetProcAddress or LoadLibraryA, it gains the ability to map any other necessary capability into memory at runtime.

API Hashing

If a defender examines a binary and finds strings like VirtualAllocEx or CreateRemoteThread in the .rdata section, the evasion fails. To prevent this, developers pair PEB traversal with API hashing.

Instead of storing target function names as plaintext strings, the loader stores pre-calculated hashes of those names using algorithms like CRC32, MurmurHash, or custom bitwise operations like ROR13. When the malware iterates through the Export Directory of a loaded DLL, it hashes every exported function name it encounters on the fly. It then compares the calculated hash against its internal list. Upon a match, it extracts the function pointer. This strips the binary of suspicious strings and neutralizes static signature scanning.

Direct System Calls

Modern Endpoint Detection and Response (EDR) solutions monitor user-land APIs using inline hooking. Even if a loader hides the IAT and dynamically resolves NtAllocateVirtualMemory, the EDR will intercept the call upon execution. To bypass these hooks, advanced loaders implement direct system calls.

Techniques like Hell’s Gate or Halo’s Gate parse ntdll.dll directly from memory or disk to extract the System Service Number (SSN) for the desired function(READ MY BLOGPOST). The malware then populates the CPU registers and executes the syscall assembly instruction directly. This transitions execution straight into the kernel, completely evading user-land hooks.

By stacking these techniques, a loader can present an entirely hollowed-out, benign IAT to static analysis tools while maintaining a highly lethal capability in memory.

Case Study: Unmasking the Adaptix C2 Agent

During initial testing against both EXE and DLL variants of the Adaptix agent, the beta version of Aether v0.8 yielded zero alerts. A deep dive into the memory space of the target process revealed why the agent successfully bypassed the structural IOC scanning:

Evasion of Private Memory Scans: Traditional scanners focus heavily on detecting execution from unmapped or private memory regions (MEM_PRIVATE with RX or RWX protections) which typically indicate running shellcode. The Adaptix agent executes from its own legitimate, mapped MEM_IMAGE space.

Standard Execution Flow: The threads start in legitimate modules listed in the Process Environment Block (PEB), and no process hollowing or runtime code modifications are present.

Low Entropy profile: The memory regions occupied by the agent maintain a normal entropy score below 4.0, triggering no signature-based anomaly flags.

However, while the agent resides quietly within a mapped module, the implant adopts what the majority of malware does. The agent uses system APIs at runtime. It stores these resolved pointers inside a heap-allocated configuration block.

The following table shows how Adaptix C2 agent uses API resolution and hashing:

Function FilePurpose
Djb2A / Djb2WProcLoader.cppHash function (seed 1572, case-insensitive)
GetModuleAddress ProcLoader.cppFind DLL base via PEB walk, no GetModuleHandle
GetSymbolAddressProcLoader.cppFind export by hash via PE export table walk
ApiLoadApiLoader.cppPopulate ApiWin/ApiNt function pointer tables
HdChrA / HdChrWApiLoader.cppPrevent DLL name strings from appearing as literals
HASH_LIB_* / HASH_FUNC_*ApiDefines.hPre-computed hash constants (198 entries)
hashes.pyfiles/hashes.pyBuild-time hash generator that produces ApiDefines.h
crt_djb2 + resolveHeapFunctionscrt.cppIndependent early-bootstrap heap resolution from ntdll

You might ask why the Adaptix agent uses this approach. Like many malware families, it avoids the standard Import Address Table (IAT) because imported APIs can reveal functionality to static analysis tools and malware scanners. Instead, the agent resolves the required functions dynamically by performing the following steps:

  1. It accesses the Thread Environment Block (TEB) to locate the Process Environment Block (PEB).
  2. It traverses the InMemoryOrderModuleList to find where critical DLLs like kernel32.dll and ntdll.dll are loaded in memory.
  3. It parses the Export Address Table (EAT) of those DLLs, hashes the exported function names, and matches them against its internal hash list.

The following code shows how the agent resolve the module function address / pointers by walking through the PEB directly via segment registers and walks InMemoryOrderModuleList :

// ProcLoader.cpp:36-56 
// Adaptix C2 Agent. 

HMODULE GetModuleAddress(ULONG modHash)  
{  
#ifdef _M_IX86  
    PEB* ProcEnvBlk = (PEB*)__readfsdword(0x30);   // x86: fs:[0x30]  
#else  
    PEB* ProcEnvBlk = (PEB*)__readgsqword(0x60);   // x64: gs:[0x60]  
#endif  
  
    PEB_LDR_DATA* Ldr = ProcEnvBlk->Ldr;  
    LIST_ENTRY* ModuleList = &Ldr->InMemoryOrderModuleList;  
    LIST_ENTRY* pStartListEntry = ModuleList->Flink;  
  
    for (LIST_ENTRY* pListEntry = pStartListEntry; pListEntry != ModuleList; pListEntry = pListEntry->Flink) {  
        LDR_DATA_TABLE_ENTRY* pEntry = (LDR_DATA_TABLE_ENTRY*)((BYTE*)pListEntry - sizeof(LIST_ENTRY));  
        if ( Djb2W((PWCHAR)pEntry->BaseDllName.Buffer) == modHash )  
            return (HMODULE)pEntry->DllBase;  
    }  
    return NULL;  
}

Building a detection capabilities and filtration pipeline

Coming up with detection mechanism from the user-mode is not an easy task because of the mount of false positive reports that you might get while scanning memory region of a process. As Aether is designed at this moment to work under user-mode level, I need to design a detection system with confidence level and adoptive filtration to reduce the amount of false positive as the following:

Phase 1 – build the map : at scan startup, Aether enumerates every loaded module in the target process and records it’s base address and size. for a curated set of capability modules, system DLLS that malware depend on such as (Kernel32.dll, ntdll.dll, ws2_32, crypt32, etc). it parses each DLL’s PE export directory to extract and sort every exported function (RVA). This export set is stored for binary search-lookup later.

Phase 2 – Scan the heap: Aether walks and scan each private, non-executable heap region in 8-byte steps. For every 8-byte value, it asks: does this fall inside a loaded module’s address range, and is the offset past the PE headers? if yes, the pointer is recorded and a sliding-window run begins. non-module values (NULLs, freed memory, HMODULE handles with RVA Guard) are tolerated up to a gap threshold of two slots before the run is considered broken.

Phase 3 – Correlate: each candidate pointer run pass through a five-filter ladder to separate genuine API-resolution tables from the false positive:

FilterRule FP class
F1 – minimum pointer density count > 5 or = 5 random pointer-shaped data, NULL, HMODULES
F2 – reject C++ vtablesreject run that point only into the host EXE Application-class C++ vtables
F3 – distinct module require >2 or =2 distinct module Single DLL framework vtables (Qt,MFC,wxWidgets or COM dispatch tables)
F4 – distinct capability module require >2 = 2 capability module, the table must reference DLLs a malware would actually resolve. browser / CRT vtables that touch a single OS DLL (e.g. iertutil + ucrtbase + shlwapi)
F5 – export verification > 80% of checkable pointers land on exported RVAs, not internal methods. gold standard that kills vtable false positive

Even though the Adaptix agent executes with perfect OPSEC from legitimate mapped image space, it cannot hide its operational toolbox. Aether identifies the heap-resident table, looking for potential indicators of API resolution or hashing, and emits a high-severity HEAP_API_TABLE alert.

The following video shows how Aether detects API resolution a technique used by Adaptix C2 Agent:

Additionally, now Aether added new feature allows you to read the memory region directly on terminal:

Does this detect any API resolution?

During evaluation and testing, this newly added detection pipeline successfully identified every known runtime resolution method that I am aware of. While minor gaps or edge cases may emerge as evasion techniques adapt, the core architecture of this engine is designed to be highly scalable. This flexibility ensures we can easily deploy updated detection rules or integrate additional correlation filters in the future. you can check the study cases section at Aether official documentation portal to download sample and give it a try.

Executive Summary: Heap API Table Detection

For readers looking for a high-level technical overview of this research, the core concepts of the Heap API Table Detection framework are summarized below:

Operational Impact: By analyzing resident pointer data rather than tracking active code execution, the framework shifts the defensive paradigm. It successfully unmasks advanced, stealthy malware behaviour (such as the Adaptix agent) with near-zero false positives.

The Security Blind Spot: advanced implants evade traditional memory-scanning tools by executing entirely within mapped memory image regions (MEM_IMAGE) and bypassing the Import Address Table (IAT) using manual PEB-walking and dynamic API resolution.

The Unavoidable Artifact: To maintain stable performance and avoid noisy, repetitive PEB lookups, implants must cache their resolved Win32 API function pointers in a contiguous struct allocated on the private heap (MEM_PRIVATE). This data footprint cannot be easily spoofed or hidden.

The Detection Engine: Aether uses a three-phase forensics pipeline:

Build: Pre-maps loaded modules and indexes sorted export relative virtual addresses (RVAs) for key system DLLs.

Scan: Sweeps private, non-executable heap space in 8-byte increments. It tracks sequences of valid module pointers, using a sliding-window state machine that tolerates small gaps (up to two slots) for stored HMODULE handles or alignment padding.

Correlate: Routes candidate pointer sequences through a five-stage verification ladder (F1 to F5). This ladder checks pointer density, excludes host executable virtual tables, requires multi-module diversity, ensures capability relevance, and verifies that at least 80% of the addresses map to official exported RVAs.

if this detection system is not yet clear, you can visit the following simulator to run a test.