Windows Memory Forensics: Detecting Code Injection

 


A recurrent analytical question in memory-forensics practice is: “If code injection leaves comparatively conspicuous artifacts in process address space, why does it remain a dominant post-compromise technique?” The operational answer is that reliable detection is largely confined to deep, offline, or high-fidelity memory analysis—examination of Virtual Address Descriptors (VADs), PEB/LDR module lists, anomalous executable regions lacking corresponding file-backed image sections, mismatched PE headers, and cross-process memory-write primitives—capabilities that most host-based sensors cannot sustain continuously without prohibitive performance overhead. Concurrently, successive generations of injection variants (notably refined process-hollowing implementations that omit NtUnmapViewOfSection, reflective loaders, and APC/queue-user-APC delivery) progressively attenuate these artifacts, elevating the forensic threshold required for confident attribution.


Code injection confers robust operational camouflage: rather than instantiating a discrete, baseline-anomalous process that would surface under process-creation telemetry or administrative scrutiny, the adversary coerces an already-resident, trusted process to execute the payload within its own address space. This confers automatic inheritance of the host process’s virtual-memory mappings, handle table, and primary access token—privileges that credential-access tooling routinely exploits by targeting LSASS (or its protected variants) to harvest NTLM hashes, Kerberos tickets, and residual plaintext credentials. Emotet exemplifies the pattern: its core and modular components predominantly inject into explorer.exe and selected system processes for persistence and stealth, while credential-harvesting modules (frequently wrappers around legitimate NirSoft utilities such as WebBrowserPassView) are themselves subject to process-hollowing or remote-thread injection, thereby obtaining indirect access to browser credential stores without necessarily residing inside the browser process itself.


Process migration represents a related, temporally subsequent tactic. Following successful exploitation that initially maps shellcode into an ephemeral or high-visibility process (commonly a browser renderer), the implant frequently migrates into a longer-lived system process to survive user-initiated termination. Both Metasploit’s Meterpreter migrate facility and Cobalt Strike’s Beacon implement this as first-class tradecraft, reflecting its entrenched utility for session stability.


The multiplicity of injection primitives further complicates host-based detection. Techniques that rely exclusively on documented Win32/Native APIs (OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread, LoadLibrary, NtMapViewOfSection, etc.) blend into legitimate system activity; exhaustive policing of every such call generates prohibitive false-positive rates. More sophisticated loaders deliberately avoid disk-resident PE/DLL images, employing reflective or memory-only mapping that bypasses application-control policies predicated on file-path or image-hash allow-listing. Finally, code injection frequently serves as an intermediate stage within multi-phase campaigns; classic user-mode (ring-3) rootkits, for instance, depend upon injection followed by inline or IAT/EAT hooking to achieve concealment of processes, files, and network artifacts without requiring kernel-mode privileges.


Code injection represents a foundational tradecraft within contemporary malware ecosystems. High-profile families routinely weaponize it as a primary means of runtime concealment. While exceptionally effective at blending malicious execution into the address space of a live system, the technique remains comparatively amenable to detection under rigorous memory-forensic examination—particularly through inspection of Virtual Address Descriptors (VADs), PEB/LDR module lists, unbacked executable regions, mismatched image paths, and anomalous thread start addresses.

Two dominant categories predominate: DLL injection and process hollowing. Windows architecture renders classical DLL injection relatively low-friction once the injector possesses adequate privileges (SeDebugPrivilege or equivalent administrative rights). Multiple implementation variants exist. In the remote-thread model, the adversary allocates memory within a target process via VirtualAllocEx, writes the payload (or path to a malicious DLL), and triggers execution with CreateRemoteThread (commonly invoking LoadLibrary). Alternative vectors include filter-function hooking via SetWindowsHookEx, Atom Bombing (which abuses the global atom table together with APC delivery for write-what-where and subsequent execution), and reflective DLL injection—long employed by Metasploit—wherein the payload implements its own PE loader, thereby circumventing many monitored loader APIs and remaining unregistered in the host process’s module lists. PowerShell further extends these capabilities, supporting both classic and reflective injection, and is increasingly observed performing such operations in operational environments. Irrespective of the specific primitive, every publicly documented injection technique leaves residual forensic artifacts recoverable through systematic memory analysis.

Process hollowing follows a related but distinct sequence and is likewise detectable by analogous memory-forensic methods. A legitimate system process is instantiated in the suspended state (CREATE_SUSPENDED). The original image is subsequently unmapped (NtUnmapViewOfSection / ZwUnmapViewOfSection), the thread context is redirected to a newly allocated region containing the malicious payload, and the process is resumed. Consequently, the process image name, path, and command-line arguments remain those of the legitimate binary, furnishing effective camouflage. Stuxnet is widely credited with early operationalization of the technique; DarkComet and Kronos (the latter often employing entry-point-patching variants against suspended processes such as svchost.exe) likewise rely on process-hollowing or closely allied methods.


Memory analysis provides a robust framework for detecting code injection by exploiting the invariant that all techniques—regardless of sophistication—must ultimately map and execute malicious code within a target process’s address space. Three complementary forensic approaches systematically exploit residual artifacts left across Windows memory structures.

1. Enumeration of loader-registered modules

Techniques that rely on documented Windows loader APIs (or that abuse legitimate loading paths such as DLL search-order hijacking and side-loading) register the injected image in the canonical Process Environment Block (PEB) structures—most notably the InLoadOrderModuleList, InMemoryOrderModuleList, and InInitializationOrderModuleList of the PEB_LDR_DATA. Cross-referencing these lists against expected module inventories, file-backed image sections, and handle tables already performed during routine process enumeration will surface anomalous or unexpected DLLs.

2. Identification of anomalous executable memory regions

Reflective and other loader-independent injection methods deliberately avoid PEB registration, yet they still allocate or map pages with execute permissions. Because code execution is possible only from pages marked executable (PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, etc.), systematic inspection of the Virtual Address Descriptor (VAD) tree for private, non-image-backed regions that possess execute rights—especially those containing PE headers, shellcode patterns, or mismatched protection attributes—reliably exposes these artifacts.

3. Detection of cross-structure inconsistencies

The most evasive implementations attempt to sanitize the indicators targeted by the first two methods: they may alter page protections after allocation, redirect thread entry points (as in process hollowing), or patch already-loaded code. Because the same memory objects are simultaneously tracked by multiple independent kernel and user-mode structures (VAD tree, PEB module lists, section objects, working-set lists, and PTE attributes), these manipulations inevitably produce discrepancies when the structures are compared. Correlating these disparate views—exactly as performed at earlier stages of a memory-forensic workflow—exposes the residual inconsistencies that even advanced injection techniques cannot fully eliminate.

In every documented case, the act of injecting and executing foreign code leaves recoverable markers; the forensic task is therefore to interrogate the available memory data through multiple orthogonal lenses until those markers surface.


Code Injection Detection Tools

This section surveys the principal tooling ecosystems purpose-built for the detection of code injection through memory analysis.

The Volatility framework has long provided foundational support via a suite of specialized plugins. ldrmodules enumerates PEB-linked and unlinked modules, exposing both anomalous memory locations and deliberately unlinked DLLs—artifacts frequently produced by advanced injection. malfind functions as a dedicated scanner that surfaces the characteristic signatures of multiple injection classes, including reflective loading and process hollowing (most commonly private, non-image-backed regions possessing execute permissions and containing PE headers or shellcode).

The widespread adoption of these plugins has driven the development of countermeasures, prompting community contributions that maintain forensic parity. hollowfind (Volatility 2) performs targeted detection of process-hollowing indicators by cross-comparing Process Environment Block (PEB) data against the process’s Virtual Address Descriptor (VAD) tree and by examining atypical section permissions. threadmap (also Volatility 2) leverages thread objects—structures that are substantially more difficult to manipulate or conceal—as its primary evidence source. A more recent port from the Rekall framework introduces ptemalfind to Volatility 3; this plugin derives executable-page “ground truth” directly from kernel page-table entries, thereby improving resilience against both classic hollowing and a range of memory-forensic countermeasures.

MemProcFS, a highly capable memory-analysis suite, maintains an expanding set of injection detections consolidated within its findevil plugin, offering an alternative high-fidelity workflow.

State-of-the-art detection is increasingly shifting toward live-memory analysis. Sophisticated evasion techniques—exemplified by Gargoyle, which places malicious code into a sleeping, pageable state so that it is only transiently present during brief execution windows—render purely offline RAM-image examination insufficient. Live scanners such as Moneta (Forrest Orr) and Hollows Hunter/pe-sieve (hasherezade) incorporate advanced capabilities that include comparison of in-memory images against their on-disk counterparts and validation of digital signatures. Although detailed examination of this live-analysis category lies beyond the present scope, practitioners should remain aware that these same detection primitives are progressively being absorbed into commercial endpoint detection and response (EDR) platforms.


Consider the canonical sequence underlying one of the simplest forms of DLL injection.


The injector (attacker process) first acquires a handle to the target (victim) process, typically via the documented OpenProcess() API. Successful attachment requires the presence of SeDebugPrivilege within the injector’s token—an attribute granted by default to accounts holding administrative rights.

Next, a modest region of memory is allocated inside the victim’s address space with VirtualAllocEx(). The fully qualified path of the malicious DLL is then written into that region by means of the cross-process write primitive WriteProcessMemory().


Finally, execution is triggered by creating a remote thread inside the target with CreateRemoteThread(). The previously written path and the address of LoadLibraryA() are supplied as arguments; the remote thread therefore invokes the Windows loader, which maps the DLL from disk, performs relocation and import resolution, registers the module in the PEB loader lists, and transfers control to the DLL’s entry point.

A critical constraint follows from the legitimate Windows loader's design: every API that ultimately calls LoadLibrary (or its variants) expects the image to reside on disk. No documented interface exists for loading arbitrary code solely from memory; techniques that circumvent this restriction are examined later.

The sequence just described is sufficient to coerce an arbitrary process into executing foreign code. While the illustration employs a DLL, the same remote-allocation and remote-thread primitives can deliver raw shellcode with only minor adaptation. Note that, beginning with Windows Vista, CreateRemoteThread() is restricted to processes sharing the same session; system processes now execute in an isolated session. Contemporary implants such as Mimikatz and Meterpreter therefore substitute the undocumented native APIs NtCreateThreadEx() or RtlCreateUserThread(), leaving the remainder of the injection workflow essentially unchanged.



This example illustrates a straightforward DLL injection into svchost.exe (PID 1000) performed with the Windows APIs CreateRemoteThread and LoadLibraryA. The displayed output is produced by the Volatility plugin ldrmodules. While superficially similar to dlllist, the two plugins differ in data sources. dlllist reports solely from the Process Environment Block (PEB) loader lists; ldrmodules correlates those PEB lists (InLoad, InInit, InMem) with the Virtual Address Descriptor (VAD) tree, additionally recovering the mapped file path and base address.


Because the injection utilized the legitimate Windows loader, the malicious module appears in the PEB lists and therefore does not stand out on the basis of registration alone. The anomaly is instead visible in the MappedPath column: one DLL originates from a temporary directory and bears the atypical name winsrv.dll. Legitimate system modules are normally loaded from \Windows\System32, \Program Files, or \Windows\WinSxS. Although a non-standard path is not conclusive proof of malice, it constitutes a high-value starting point for investigation.


Note that the process image itself (svchost.exe) is marked False in the InInit column. This is expected behavior; the initialization-order list never includes the main executable. Every ordinary process exhibits one such entry. Subsequent examples will demonstrate more evasive techniques that require the additional VAD correlation provided by ldrmodules for detection. Equivalent PEB-versus-VAD discrepancy analysis is also available in MemProcFS through its findevil module.




The displayed output originates from the Volatility plugin ldrmodules. While superficially similar to dlllist, the two plugins differ in data sources. dlllist reports solely from the Process Environment Block (PEB) loader lists. ldrmodules correlates those PEB lists (InLoad, InInit, InMem) against the Virtual Address Descriptor (VAD) tree, additionally recovering the backing file path (MappedPath) and base address for each region.


Legitimate resource-only modules (.mui, .mun, and certain satellite DLLs) frequently appear as False/False/False because they are loaded outside the normal PEB registration path (via resource-loading routines). These entries still resolve to a valid file-backed VAD and therefore display a MappedPath. The critical distinguishing artifact is an entry whose MappedPath is reported as N/A. This indicates that the corresponding VAD has no associated _FILE_OBJECT. The memory region is therefore either:


  • a privately allocated, executable page containing a manually mapped or reflectively loaded image (never passed through the Windows loader and consequently absent from every PEB list), or
  • an anonymous private allocation used to stage and execute code.


Combined with the explorer.exe → 192.168.135.57:8070 ESTABLISHED connection (a destination msedge.exe also talked to) [discovered during network connections review but not shown in the above figure], this gives a coherent, evidence-backed narrative: code was injected into explorer.exe via a private, non-file-backed memory region, and that process subsequently established an outbound connection to an internal peer that a browser process also contacted. That's a strong working hypothesis for process hollowing/injection with C2 or lateral-movement behavior. The MemProcFS project also has the capability to list loaded DLLs per process and identify anomalies in PEB and VAD data sources via its findevil module.


Reflective code injection, originally developed by Stephen Fewer, was engineered to substantially reduce the observable footprint of remote code execution. The core innovation is the ability to map and execute code inside a target process without invoking the Windows loader (specifically LoadLibrary or its variants). By bypassing the loader, the injected image is never registered in the PEB module lists (InLoadOrderModuleList, InMemoryOrderModuleList, or InInitializationOrderModuleList) and therefore remains invisible to tools that rely solely on those structures.

In addition, the technique eliminates the disk-resident requirement imposed by the legitimate loader, permitting purely memory-resident payloads—including code retrieved directly over the network—and thereby shrinking the set of artifacts available to host-based scanners.

Although numerous refinements and variants have appeared since the original publication, any self-loading technique that deliberately avoids the Windows PE loader is classified here as reflective. Metasploit was among the earliest frameworks to operationalize the method; Cobalt Strike subsequently adopted essentially the same loader for Beacon delivery. The major PowerShell post-exploitation frameworks PowerSploit and Empire likewise treat reflective injection as a primary execution vector. Further enhancements appeared in the DoublePulsar implant, which has been observed in the wild on multiple occasions. Nation-state tooling attributed to Iranian and North Korean actors has also incorporated reflective loaders.

Memory forensics remains particularly well-suited to the detection of these techniques: the resulting private, non-file-backed executable regions, anomalous VAD permissions, and absence of corresponding PEB entries produce reliable, recoverable artifacts that subsequent analysis plugins are designed to surface.


In its most elementary form, code injection coerces a target process to load an additional DLL. Such modules are frequently recoverable from the Process Environment Block via plugins such as Volatility’s dlllist or ldrmodules. Because these loader-registered artifacts are relatively straightforward for defensive tools to surface, adversaries have migrated toward more sophisticated techniques that deliberately bypass the standard Windows PE loader. Circumventing legitimate loading mechanisms, however, generates a new class of residual artifacts that can be recovered through alternative forensic methods. This illustrates a recurring principle in memory forensics: every attempt at concealment leaves corresponding traces that can demonstrate the activity’s existence.

The detection workflow illustrated here is a canonical three-stage process applicable to both classical and advanced injection variants, including reflective loading. It leverages supplementary kernel memory structures—most importantly the Virtual Address Descriptor (VAD) tree that enumerates every memory region belonging to a process. Analysis tools walk the VAD tree of each process and subject every region to the following checks:

  1. Protection attributes—Regions marked with atypical permissions, particularly PAGE_EXECUTE_READWRITE, are flagged. The simultaneous presence of write and execute rights is inherently hazardous and constitutes a classic residual signature of numerous injection techniques.
  2. File mapping—Each candidate region is examined for the presence of a backing _FILE_OBJECT. Legitimate executable images (DLLs and EXEs) must originate from disk; consequently, a mapped path recorded in the VAD is expected. A private, non-file-backed executable region is therefore highly suspicious.
  3. Content validation—Because the preceding checks can generate false positives (for example, legitimate just-in-time compilers or .NET runtime allocations), a final content inspection is performed to determine whether the region actually contains a Portable Executable image or identifiable shellcode. Regions that exhibit anomalous permissions yet contain no executable code are discarded.

Although the procedure may appear intricate, purpose-built memory-analysis plugins—Volatility’s malfind and MemProcFS’s findevil—automate the entire sequence and present the resulting candidate regions for analyst review.


The windows.malfind.Malfind plugin has long been a foundational component of the Volatility framework. Its original author, Michael Hale Ligh, recognized that injected code characteristically produces memory regions that are both executable and lack an associated file-backed mapping on disk. For every process, the plugin enumerates the process’s Virtual Address Descriptor (VAD) tree and subjects each region to these two criteria. Matching regions are reported to standard output; when the --dump option is supplied, the corresponding memory is written to individual files named according to the process identifier and virtual base address.

A third, essential validation—confirmation that the region actually contains executable content (a Portable Executable image or recognizable shellcode)—is deliberately left to the analyst. This step is necessary because many legitimate runtime environments (JIT compilers, .NET, etc.) allocate private executable pages that would otherwise generate false positives. For each candidate region, the plugin reports the following fields (Volatility 3 column names; Volatility 2 differs slightly):

  • PID
  • Process name
  • Start VPN / End VPN (virtual address range)
  • Tag (pool tag identifying the memory object type)
  • Protection (page permissions)
  • CommitCharge (number of committed pages)
  • PrivateMemory (0 = mapped, 1 = private)

malfind retains high efficacy against classical injection techniques and against many reflective-loading variants. Its widespread adoption, however, has motivated more recent injection methods to deliberately sanitize the very markers it examines (page protections, VAD flags, etc.). Consequently, the plugin remains valuable as an initial triage pass, while complementary detection methods are required for the most evasive implants. False positives are common; the plugin therefore displays the beginning of each region in both hexadecimal and disassembled form. The presence of an “MZ” header or recognizable shellcode patterns can be assessed at a glance. Regions of interest should be dumped with --dump and subsequently examined in a disassembler, subjected to signature scanning, or reviewed for embedded strings.

The Volatility 2 equivalent is simply malfind (using --dump-dir). The Volatility 3 port is not a direct one-to-one translation; it incorporates improved disassembly support, particularly for 64-bit address spaces.


Truncated and filtered output of Volatility's malfind plugin


In the above figure, the Print Spooler process contains multiple private (PrivateMemory = 1), PAGE_EXECUTE_READWRITE regions that display classic injection artifacts.


Start VPN

Notes / Content

Assessment

0x4afbf20000

Shellcode-style prologue (fc 48 89 ce 48 81 ec...) with PEB walking patterns

Highly suspicious; looks like reflective/shellcode loader.

0x4afc1f0000

Clear MZ header (4d 5a 90 00...)

Injected PE image

0x4afc070000

MZ header followed immediately by code (4d 5a 41 52 55 48 89 e5...)

Injected PE / reflective DLL with embedded loader stub

0x4afc260000

Another clean MZ header

Additional injected PE image


These regions are:


  • Private (not file-backed).
  • Marked RWX.
  • Contain either full PE headers or recognizable shellcode.


This combination is a textbook signature of reflective DLL injection or process hollowing into the spooler service—a common target because it runs as SYSTEM and is frequently allowed through application control policies.


Metasploit’s Meterpreter was among the earliest operational implementations of reflective DLL injection and continues to present detection challenges for many contemporary security products. By mapping and executing its payload entirely in memory without invoking the Windows loader (LoadLibrary or equivalent), Meterpreter avoids registration in the PEB module lists and leaves no disk-resident image. This design still defeats a significant subset of host-based tools that rely primarily on loader events, module enumeration, or file-based scanning, although modern memory-forensic and behavioral detections (VAD inspection, private executable region analysis, etc.) are considerably more effective against it.



Spotting patterns is a critical threat-hunting skill. You don't need to be a reverse engineer who reads assembly fluently—with a little study and practice, you can usually identify enough structure to make a defensible triage decision on the data available. When you're staring at the Volatility malfind output, the question you're actually answering is simple: does the data in this suspicious memory region look like code, or does it look like something else (data, packed/encrypted bytes, padding)?


Executable code is built from functions—discrete blocks that perform a subtask and then hand control back to the caller. Before a function's body executes, the CPU has already pushed a return address onto the stack via CALL; the function itself is responsible for setting up its own stack frame—establishing a frame pointer and reserving local stack space so it can address its parameters and locals reliably, and so the frame can be torn down cleanly on exit. This setup sequence is the function prologue, and because compilers emit it constantly, it produces one of the most reliable and recognizable byte patterns in disassembly. On x86, the classic prologue is:


push ebp
mov  ebp, esp


On x64, the same logical operation is performed using the 64-bit register set — rbp and rsp are the 64-bit forms of ebp/esp (not simply renamed registers; they're accessed via REX-prefixed opcodes that widen the operation to 64 bits):


push rbp
mov  rbp, rsp


Caveat worth flagging for anyone triaging x64 samples: this frame-pointer setup is not guaranteed. Under frame-pointer omission (FPO), which the x64 calling convention permits and compilers frequently use, rbp is left free for general use, and the function relies on rsp-relative addressing plus separate unwind metadata (.pdata/.xdata) instead. So absence of this prologue pattern does not mean "not code"—it means the compiler chose not to establish a traditional frame. Treat the pattern as a strong positive indicator, not a required one. A convenient way to recognize this pattern quickly in a hex/ASCII pane is by sight rather than by decoding opcodes byte-by-byte. Here's a real example pulled from a malfind hit on spoolsv.exe:


4d 5a 41 52 55 48 89 e5 48 83 ec 20 48 83 e4 f0


Disassembled:


BytesInstruction
4d 5a(DOS header MZ — precedes the code, not part of the prologue)
41 52push r10
55push rbp
48 89 e5mov rbp, rsp
48 83 ec 20sub rsp, 0x20
48 83 e4 f0and rsp, 0xfffffffffffffff0


Rendered as ASCII, the prologue bytes (excluding the MZ header) read as ARUH..H.. H... — the space in the middle isn't a formatting artifact; it's literally the byte 0x20 from sub rsp, 0x20. Once you've seen this shape once, you'll start noticing it constantly in x64 memory dumps.


Note that this isn't the textbook two-instruction prologue—it's a slightly more elaborate variant: push r10 ahead of push rbp (commonly a spilled register, or padding to keep the push count even for 16-byte alignment going into a subsequent call), followed by an explicit and rsp, 0xfffffffffffffff0 to force 16-byte stack alignment. This alignment step shows up often in compiler-emitted code that uses SSE/AVX locals and in hand-written or injected shellcode that can't assume the incoming stack is already aligned. It's a good reminder that prologues come in families, not one fixed byte sequence—train your eye on the shape (register pushes, frame-pointer setup, stack adjustment), not a single memorized string.


Below, the malfind plugin has flagged a private region within the OneDrive.exe process that carries PAGE_EXECUTE_READWRITE permissions and lacks a file-backed mapping. A short hexdump and corresponding disassembly of the region’s initial bytes are presented for triage.




Inspection of the disassembly reveals a highly repetitive sequence consisting solely of the instruction add [rax], al. This artifact arises because the leading portion of the region is filled with null bytes (00 00); the disassembler correctly interprets the opcode 00 00 as that instruction and simply continues to do so for every subsequent null pair. The resulting output therefore constitutes noise rather than legitimate executable content. Consequently, the region fails the third of the three canonical injection criteria—presence of actual code—and does not, on the evidence of the examined prologue, indicate injection.


It must be noted, however, that only the first 64 bytes are visible. Executable content could still reside at a later offset within the larger allocation. A complete review of the dumped pages remains necessary before you can confidently dismiss the region as benign.


Attackers and defenders remain locked in a continuous contest for operational superiority. As defensive instrumentation increasingly threatens an adversary’s ability to maintain persistence and achieve objectives, corresponding countermeasures inevitably emerge. Memory forensics, once a specialized capability, is now routinely employed both by trained analysts and by enterprise EDR platforms; consequently, sophisticated actors have invested in techniques designed to degrade its efficacy.


Code injection furnishes a clear illustration of this adaptive cycle. Early implementations relied on the Windows loader (LoadLibrary), a pathway that security products rapidly learned to monitor. In response, frameworks such as Metasploit and implants such as DoublePulsar adopted reflective loading, thereby avoiding loader registration entirely. Once reflective injection itself became detectable through VAD-based heuristics of the type embodied in Volatility’s malfind plugin, the subsequent generation of malware began to sanitize residual artifacts after mapping. CoreFlood was among the first observed families to zero the PE header (specifically the first 4 096-byte page) of its injected image. Comparable header-erasure behavior later appeared in Cobalt Strike payloads employed by APT29 and APT32, in the Winnti RAT associated with Chinese operators, and in numerous shellcode loaders that deliberately pad executable content deeper into an allocation so that the 64-byte preview displayed by malfind appears inert.


Contemporary injection methods have grown still more refined. Memory is frequently allocated with PAGE_READWRITE permissions and only later re-protected to PAGE_EXECUTE_READ; because the VAD records the original protection attributes, the classic EXECUTE_READWRITE signature is never present. The Process Environment Block, residing in user-mode address space, has likewise become a target of manipulation: loader-list entries are linked or unlinked at will, and PEB fields are rewritten (commonly via PowerShell) to masquerade process name and image path. Additional camouflage techniques include in-place patching of existing legitimate code and module stomping—overwriting the contents of a legitimately loaded DLL while preserving its PEB registration.


Collectively, these evolutions demonstrate that every successful detection method eventually elicits a corresponding evasion, underscoring the necessity of layered, multi-structure forensic analysis rather than reliance on any single indicator.


Detection of these more evasive injection variants requires examination beyond the limited preview furnished by malfind. Dumping the full contents of candidate regions neutralizes several common obfuscations, including PE-header erasure, deliberate padding, and certain forms of in-memory patching. The --dump option of the malfind plugin writes each flagged region to a discrete file on disk. In Volatility 2, the resulting files are named according to the virtual address of the injected process; in Volatility 3, they incorporate the process identifier together with the base address of the suspicious page. These artifacts may subsequently be subjected to a range of analytical techniques:


  • extraction of embedded strings,
  • signature-based scanning with antivirus engines or YARA rules tuned for known indicators of compromise,
  • or, when resources permit, comprehensive static reverse engineering.


Full disassembly and code review remain the most definitive method, yet it is also the most resource-intensive—an asymmetry that sophisticated adversaries deliberately exploit. Consequently, automated triage of the dumped regions followed by selective deeper analysis constitutes the practical balance between coverage and operational cost.



Detection of the most sophisticated injection techniques necessitates supplementary tooling and analytic methods. It is important to recognize, however, that only a minority of malware samples currently implement these advanced approaches. Many of the techniques demand elevated privileges or introduce instability, frequently resulting in process crashes or broader system disruption. The transition from proof-of-concept code to a reliable, production-grade implant remains substantial.

Consequently, the majority of observed injection attacks continue to rely on comparatively straightforward methods. These techniques persist because they are simple to implement, operationally stable, thoroughly tested, and—remarkably—still successful against a large proportion of contemporary security controls. Complementary behavioral indicators, such as anomalous parent–child process relationships and the presence of orphaned files, retain detection value even against many of the more advanced variants.

The analytic methods under discussion therefore serve a dual purpose: they address present-day threats while simultaneously preparing defenders for the eventual mainstream adoption of more evasive techniques. Continued refinement of memory-resident detection capabilities remains essential if defensive efficacy is to keep pace with adversarial innovation.


Understanding Process Memory

A more granular understanding of process address-space organization materially improves the detection of anomalies introduced by advanced injection techniques. Process memory is partitioned into three principal categories: private, shareable, and image-mapped.



Private memory is exclusive to the owning process and is never mapped outside its address space. It is typically obtained via VirtualAlloc and encompasses application data, the process heap, and the process stack. Executable images (PE files or DLLs) are not expected to reside here. The predominant page protection in this region is PAGE_READWRITE, consistent with the read/write requirements of stack, heap, and data structures.

Shareable (mapped) memory is reserved for mapping all or part of files that may be shared among processes (although sharing is not mandatory). Typical occupants include data files such as .dat and .mui resources loaded from disk. The dominant protection is PAGE_READONLY.

Image-mapped memory constitutes a specialized subset of shareable memory that is explicitly tagged for legitimate executable images—EXE, DLL, and driver binaries. This is the sole region in which execute permissions are routinely observed. The great majority of image mappings employ PAGE_EXECUTE_WRITECOPY (the copy-on-write mechanism that protects shared code) or PAGE_EXECUTE_READ; PAGE_EXECUTE_READWRITE remains anomalous even here.

Additional structural details of forensic relevance include the location of the Process Environment Block (PEB) within user-mode process memory (rendering it readily susceptible to manipulation) and the presence of kernel memory structures—non-paged and paged pools (exploited by Volatility’s scanning plugins), the VAD tree, and page tables—that will be leveraged in subsequent analysis.

These well-defined partitions supply strong priors for detection. Reflective injection and classical process hollowing characteristically leave PAGE_EXECUTE_READWRITE permissions, which are not normal in any part of process memory; statistical measurements performed by Forrest Orr on Windows 10 systems demonstrated that such RWX pages occur in only 0.24% of private memory, 0.014% of shareable memory, and 0.01% of image memory—exceedingly rare under normal conditions. Although adversaries have begun allocating memory as PAGE_READWRITE and later re-protecting it to PAGE_EXECUTE_READ, the same research shows that executable pages outside image-mapped memory remain uncommon (0.62% private, 0.036% shareable). Consequently, any executable permission observed outside the image-mapped region warrants investigation.

Most process-hollowing variants (excluding doppelgänging) place the malicious image in private memory—an area that should never contain executable code. Even process doppelgänging, among the most sophisticated techniques, deposits executable content in shareable rather than image-mapped memory. Effective detection therefore rests on a precise model of what is statistically normal versus rare within each memory class.


MemProcFS FindEvil Detections

A refined model of process-memory organization enables more precise interpretation of the detections implemented in MemProcFS. All current detections are consolidated within the single plugin findevil. The plugin is not enabled by default; it must be activated either by supplying the -forensic command-line switch at mount time or by writing the value 1 to M:\forensic\forensic_enable.txt after the file system is already mounted. Because of an explicit effort to constrain false positives, the feature is supported only on Windows 10 and later. Once enabled, MemProcFS produces the report M:\forensic\findevil.txt, annotating each suspicious locus with one or more of the flags described below.

Process-level irregularities (previously examined in earlier stages of the analysis workflow):

  • PROC_NOLINK – The process object is absent from the active EPROCESS doubly-linked list. Possible explanations include ordinary termination, corruption of the memory image, or deliberate unlinking by malware.
  • PROC_PARENT – The parent process deviates from the expected parentage of known system components.
  • PROC_USER – A process whose name matches a system binary is executing under an unexpected security token.

Memory-page anomalies (interpreted against the private/shareable/image-mapped taxonomy):

  • PE_INJECT – A Portable Executable header resides outside image-mapped memory. Functionally analogous to filtering the Volatility malfind output for “MZ” signatures.
  • NOIMAGE_RWX – A region carrying PAGE_EXECUTE_READWRITE permissions lies outside image-mapped memory.
  • NOIMAGE_RX – A region carrying PAGE_EXECUTE_READ permissions lies outside image-mapped memory.
  • PRIVATE_RWX – A private-memory region carries PAGE_EXECUTE_READWRITE permissions.
  • PRIVATE_RX – A private-memory region carries PAGE_EXECUTE_READ permissions.

These detections are especially potent because the act of injecting code is comparatively trivial, whereas placing that code in a statistically normal location with statistically normal attributes is decidedly non-trivial. The checks are intentionally layered; a single malicious region commonly raises multiple flags. For example, an MZ header located in an RX page outside image-mapped memory simultaneously triggers both PE_INJECT and NOIMAGE_RX. The private-memory flags are particularly discriminative: executable permissions are rarer still in private address space, yet certain injection techniques deliberately target the stack or heap. Real-world families such as NetTraveler and many Cobalt Strike beacons exhibit precisely this private-RX signature. Collectively, the findevil detections represent a substantial advance over the heuristics embodied in Volatility malfind. Residual false-positive volume, however, remains an operational consideration that still requires analyst triage.

Supplementary page-state annotations occasionally appear in findevil and related MemProcFS output. While not primary indicators of malice, they supply useful context about residency:

  • A – Active (resident) page
  • T – Transient page (still in physical memory but no longer active)
  • Z – Zero page
  • C – Compressed page (may or may not be backed by RAM or the pagefile)
  • Pf – Pagefile-backed page


Uncovering Kernel and Userland Process Inconsistencies



Detection of the most advanced injection techniques requires identification of inconsistencies across the multiple kernel and user-mode structures that track process memory. Contemporary implants actively manipulate these structures in order to blend with legitimate activity and to defeat earlier generations of detection logic. Typical manipulations include post-allocation permission changes, redirection of execution pointers (as seen in process hollowing), and direct patching of already-loaded legitimate code. Although such alterations increase stealth, they remain recoverable because the same memory objects are recorded in several independent data structures; cross-comparison of those structures exposes the discrepancies.

A frequent target is the Process Environment Block (PEB). Because the PEB resides in user-mode address space, an adversary already possessing injection privileges can modify it with relative ease. Common PEB-based techniques include:

  • unlinking modules from the three loader lists (InLoadOrderModuleList, InMemoryOrderModuleList, InInitializationOrderModuleList) so that the injected image is invisible to tools that enumerate only those lists;
  • illicitly inserting entries into the same lists in an attempt to appear loader-registered;
  • PEB masquerading—overwriting the command-line and image-path fields to spoof a different process name or on-disk location.

In contrast, the Virtual Address Descriptor (VAD) tree and the page-table entries (PTEs) reside in kernel memory and are correspondingly harder to alter. Direct Kernel Object Manipulation (DKOM) attacks that attempt to unlink VAD nodes have been theorized, yet they are inherently unstable and frequently precipitate system crashes. Even if a VAD node were successfully hidden, the page tables would still reflect the true allocation, allowing the discrepancy to be observed.

A more practical and widely observed PTE-level technique leverages the legitimate API NtProtectVirtualMemory: memory is first allocated with PAGE_READWRITE permissions, malicious code is written, and the protection is subsequently changed to PAGE_EXECUTE_READ. Because the VAD records only the original protection attributes, a comparison between the VAD-reported protection and the current PTE protection readily reveals the alteration.

The forensic value of these structures therefore lies in their partial redundancy. Systematic comparison of the duplicated information maintained in the PEB, the VAD tree, and the page tables supplies a robust means of detecting the sophisticated manipulations employed by modern injection frameworks.


The ldrmodules plugin exemplifies Volatility’s capacity to correlate multiple independent data sources for forensic analysis. Its purpose is the identification of anomalously loaded modules within a process by cross-referencing several distinct tracking structures. Each process maintains a Process Environment Block (PEB) that contains three doubly-linked lists enumerating loaded DLLs:

  • InLoadOrderModuleList
  • InInitializationOrderModuleList
  • InMemoryOrderModuleList

Under normal conditions, these lists hold identical sets of modules, merely ordered differently. The plugin further reconciles the PEB lists against the image-mapped regions recorded in the Virtual Address Descriptor (VAD) tree, thereby exposing inconsistencies. Output columns comprise:

  • Process identifier (PID)
  • Process name
  • Base address recovered from the VAD
  • Presence in the PEB InLoadOrderModuleList (InLoad)
  • Presence in the PEB InInitializationOrderModuleList (InInit)
  • Presence in the PEB InMemoryOrderModuleList (InMem)
  • Mapped file path recovered from the VAD (MappedPath)

A legitimately loaded DLL is expected to appear in all three PEB lists (all flags True) and to possess a corresponding on-disk path in the VAD. Any deviation from this pattern warrants investigation. False positives nevertheless occur with regularity. The main process image itself is customarily absent from the InInitializationOrderModuleList, because executables are not initialized in the same manner as DLLs. Resource and satellite files (.fon, .mui, .winmd, .msstyles, etc.) are frequently mapped into image memory yet never registered in the PEB lists. In addition, a DLL may be mapped by the VAD but not yet loaded, or may have been unloaded after earlier use; such modules appear only in the MappedPath column. Volatility 3 has also been observed to report certain SysWOW64 modules as absent from the PEB lists—an artifact believed to stem from differing tracking of 32-bit libraries—prompting many analysts to cross-check results with the Volatility 2 implementation.

Absence of a value in the MappedPath column is particularly significant: it indicates that the module was never loaded from disk via the legitimate Windows loader and therefore lacks a file-backed path. Even when such a module appears in one or more PEB lists, the missing mapped path remains strong evidence of injection. We saw this earlier in this post. The Volatility 2 equivalent is ldrmodules; the -v switch supplies full path information.




Stuxnet employs multiple injection techniques, among them the specialized form known as process hollowing. Application of the ldrmodules plugin to a candidate process (lsass.exe, PID 868) yields two entries that lack a MappedPath. Absence of a mapped path ordinarily indicates that the corresponding memory region was never loaded through the Windows loader—whose contract requires every image to originate from disk—and was therefore introduced directly into the address space. By remaining entirely memory-resident, the implant reduces both on-disk antivirus signatures and conventional file-system forensics.

The same entries also exhibit incomplete registration in the PEB loader lists. The region at base 0x80000 appears in none of the three lists and is characteristic of a purely injected module. The region at base 0x1000000 is present in the InLoad and InMem lists yet absent from InInit. While every process image is expected to be missing from the initialization-order list, the simultaneous lack of a disk path is anomalous; the legitimate path for this process should be \WINDOWS\system32\lsass.exe. The discrepancy is an artifact of process hollowing: the original lsass.exe image was unmapped and replaced in situ by malicious code. Because the substitution occurred wholly in memory, no corresponding file exists on disk—an intentional design choice that further reduces exposure to host-based controls.

Cross-referencing the base addresses reported by ldrmodules with the output of malfind confirms that both plugins independently flag the same regions as suspicious. ldrmodules therefore supplies an independent detection path focused on loader-list and VAD inconsistencies.



An additional analytic technique compares the base address recovered by ldrmodules (sourced from the kernel-resident VAD tree) with the address reported by dlllist (sourced from the user-mode PEB). Divergences between these two values are indicative of several process-hollowing variants. The comparison has been automated in the Volatility 2 plugin HollowFind, authored by Monnappa K A.


The MemProcFS findevil plugin incorporates an additional suite of detections oriented toward the identification of manipulated memory structures.

  • PEB_MASQ detects Process Environment Block masquerading attacks in which an adversary alters the PEB to falsify the name or file path of loaded code. Detection is performed by direct comparison of the corresponding fields in the PEB and the VAD tree for the same image-mapped region.
  • PE_NOLINK operates on principles analogous to those of Volatility’s ldrmodules. A positive result indicates that a memory region tracked by the VAD contains a Portable Executable header yet is absent from the PEB loader lists. While effective against numerous injection techniques, the check inherits the same classes of false positives previously discussed for ldrmodules. MemProcFS documentation notes that many such false positives arise from paged-out memory or corrupted PEB structures; inclusion of a pagefile in the analysis can therefore reduce noise.
  • PE_PATCHED seeks executable pages that have been modified after initial loading. Advanced process-hollowing variants increasingly adopt in-place patching because it permits malicious code to reside inside an otherwise legitimate image. Comparable modifications are also performed for AMSI bypasses and other runtime evasions. Page-table entries record such changes through prototype PTEs. findevil therefore enumerates every active image-mapped page in the VAD and flags any that possess a corresponding prototype PTE indicating modification. Although powerful, the check generates substantial false-positive volume on many systems—particularly those hosting SysWOW64 32-bit modules or just-in-time compilers such as the .NET runtime (including PowerShell). Consequently, a high baseline of noise must be anticipated and filtered.

These three detections rely heavily on cross-referencing of independent memory structures, yet they are not the only findevil checks that do so. PE_INJECT, PRIVATE_RWX/PRIVATE_RX, and NOIMAGE_RWX/NOIMAGE_RX likewise incorporate PTE data, treating the page tables as the closest available ground truth. Collectively, these capabilities render findevil substantially more effective at locating injected code than the stock Volatility framework.

The relative limitations of Volatility in this domain have not gone unaddressed. Among the more notable third-party contributions for Volatility 3 is ptemalfind by Frank Block, which similarly leverages page-table information to surface advanced injection techniques. Block’s research almost certainly informed several of the detection strategies now implemented in MemProcFS.



This is a memory-forensics triage combining MemProcFS's built-in YARA-driven findevil scan with a targeted Volatility 3 ldrmodules follow-up. Here's what the output is telling you: Two processes are flagged:

  • PID 2684 (msedge.exe) and PID 5832 (explorer.exe) both hit YR_TROJAN against the signature Windows_Trojan_Nimplant_44ff3211, and YR_SHELLCODE against Windows_Shellcode_Rdi_edc62a10 (RDI = Reflective DLL Injection loader shellcode).
  • Both processes also show a PE_INJECT entry for Module: [NimPlant.dll] at the identical virtual address 0x692c0000.

That combination—a Nimplant YARA hit, an RDI-shellcode YARA hit, and a PE_INJECT classification at the same base in two unrelated processes—is MemProcFS telling you it found a PE image in memory that isn't backed by a normal LoadLibrary/section-mapped load, consistent with a reflectively-loaded NimPlant beacon (NimPlant is the open-source Nim-based C2 implant) manually mapped into both a browser process and explorer.exe. The other entries are lower-signal noise you should mentally bucket separately:

  • PE_NOLINK on MpDefenderCore.exe and RuntimeBroker.exe (npmproxy.dll, netprofm.dll, AppResolver.dll, Bcp47Langs.dll)—PE_NOLINK just means the module isn't in the loader-list linkage MemProcFS expects, which is a common benign artifact for late-bound COM/network-list-manager DLLs. Don't overweight this without further corroboration.
  • PE_PATCHED (PID 8712 msedge.exe) and PRIVATE_RWX (PID 3088 msedge.exe)—worth a look (RWX private regions are a classic injection/shellcode staging indicator), but they're in different msedge.exe instances (different PIDs) than the flagged 2684, so treat as separate leads, not automatically part of the same chain.

ldrmodules cross-references the VAD-enumerated mapped regions against the three PEB linked lists (InLoad/InInit/InMem order). The highlighted row is the money shot:

0x692c0000 | False | False | False | MappedPath: N/A

Same base address as the PE_INJECT hit above. All three PEB lists are False, and there's no backing mapped file path. That's the textbook signature of a manually-mapped/reflectively-injected module—it exists as executable memory with a valid PE structure, but the OS loader never linked it into any of the standard module-tracking structures, and it has no on-disk file association (so !vad/dlllist-style enumeration alone would miss it entirely; only VAD-vs-PEB cross-referencing catches it).


Column labels within MemProcFS findevil output can be difficult to interpret because the plugin aggregates data from multiple independent memory structures. The Description field may contain anything from a simple module name to a dense, multi-field record describing a memory region. When the latter format appears, the following fields (documented in the MemProcFS wiki) are present:


  • Record number
  • PID
  • Process name
  • Detection type
  • Virtual address
  • Offset : size (in bytes)
  • Physical address
  • PTE flags
  • VAD virtual address
  • VAD physical address
  • VAD PTE
  • VAD flags/type
  • VAD name


Of these, the most operationally useful values for triage are PID, Process, Type, Virtual Address, and VadName. Comparison of the PTE flags against the VAD flags/type can reveal post-allocation permission changes—an important indicator of certain advanced injection techniques. Analysts should nevertheless remain aware that legitimate permission transitions also occur under normal system operation, so such discrepancies require contextual evaluation rather than automatic classification as malicious.




One of the most significant recent enhancements to MemProcFS is the integration of YARA signature scanning. Combining YARA’s pattern-matching power with detailed memory-resident analysis substantially improves the detection of even highly stealthy implants. When activated, YARA matches are elevated to the top of the FindEvil report—an appropriate placement, as these hits frequently serve as the initial triage signals that direct deeper examination of specific processes.


YARA scanning has been extended beyond process memory to include file objects, with the explicit goal of improving identification of malicious or vulnerable drivers. Enabling the capability is straightforward: Ulf Frisk has pre-loaded the open-source Elastic Security YARA rule repository containing more than one thousand rules, each capable of detecting multiple variants of prevalent malware families. Acceptance of the Elastic license is required and is performed by adding the argument:


-license-accept-elastic-license-2.0

Activation introduces a measurable delay while the rules are evaluated against the memory image; larger images may require several minutes before results appear under M:\forensic. Built-in YARA hits are merged into the standard FindEvil output (M:\forensic\csv\findevil.csv) and are categorized under multiple YR_* prefixes. The description string for each hit is taken directly from the originating YARA rule and can be used as a search term to locate the precise rule text—useful both for validating true positives and for investigating false positives or poorly written rules. A companion file, M:\forensic\yara.csv, supplies additional detail and is cross-referenced by the bracketed index values (e.g., [3]) shown in the FindEvil report.


The example illustrated on the image above demonstrates a high volume of hits against two processes. In this instance, the processes are explorer.exe and msedge.exe.


Analysts who prefer a custom rule set may supply either an individual YARA rule or a rule-index file via the -forensic-yara-rules parameter. Matches from custom rules are written to a dedicated directory (M:\forensic\yara). Within that directory, match-count.txt records the total number of hits, and result.txt contains the detailed findings, including associated process metadata when available.


Once candidate injected regions have been identified, the natural next question is how to proceed. Because false positives remain common, additional validation of the implicated memory sections is frequently required before a definitive determination of compromise can be reached. In certain cases, however, the aggregate evidence already assembled is sufficient. Discovery of a svchost.exe instance executing from an anomalous path, possessing an unexpected parent process, and displaying clear indicators of process hollowing, for example, ordinarily constitutes conclusive proof of malicious activity.


When deeper examination is warranted, memory-analysis platforms afford a degree of visibility that most host-based security products cannot match. During the investigative process, it is essential to maintain precise notes—recording process names, the virtual addresses of suspect regions, and any path information recovered from the VAD tree (all of which are supplied by MemProcFS findevil output).


Recovery of the flagged memory sections then becomes the immediate objective. Although this activity borders on the final phase of the overall memory-analysis workflow (“Dump suspicious processes and drivers”), a brief discussion is warranted here. MemProcFS automatically attempts to reconstruct in-memory files by drawing upon multiple independent data sources. The recommended starting point is the per-process files directory, which contains sub-folders populated from handle information, module information (DLLs, executables, and drivers), and extracted VAD pages.


The vmemd sub-folder for each process presents files and memory segments derived from the process memory map. This location is particularly useful when the region of interest is not file-backed (for example, injected shellcode). Such regions appear as .vvmem files that may be copied for offline analysis or examined directly in a hex editor. The analyst’s goal is to locate an entry whose virtual address or object name matches the previously documented indicator.


Successful recovery is not guaranteed; memory corruption and pages that were paged out at the moment of acquisition are the principal obstacles. MemProcFS mitigates this risk by supporting simultaneous analysis of a pagefile, an option that materially improves recovery rates. Once a region has been successfully extracted, conventional reverse-engineering techniques—strings extraction, YARA or antivirus scanning, inspection with tools such as PEStudio, and, when necessary, full disassembly—may be applied to reach a final determination.


Memory forensics remains an inherently demanding discipline. Although contemporary tools generate substantial volumes of data and frequently direct attention toward relevant loci, they cannot replace the judgment of a trained analyst. Process memory is complex: activity that appears anomalous may prove benign, while initially unremarkable behavior may conceal malice. This intrinsic ambiguity is further compounded by the deliberate efforts of malware authors to blend with legitimate activity and to subvert detection logic.


False-positive management, previously examined in the context of Volatility malfind, applies with equal force to the MemProcFS findevil plugin. At the granularity of individual memory pages, numerous irregular but non-malicious conditions exist. The findevil output illustrated below originates from a system infected with SolarMarker; every listed detection, however, is a false positive.




Effective triage begins by cross-referencing the reported process names and identifiers against any anomalous processes identified in earlier stages of the analysis. In the present case, the majority of entries correspond to ordinary Windows components. Next, the detection category itself should be evaluated for its known false-positive propensity—information that MemProcFS documents with reasonable clarity. PE_PATCHED is a particularly prolific source of noise. Legitimate runtime modification of executable pages occurs under several well-understood circumstances. The .NET runtime, for example, performs just-in-time compilation, producing frequent page modifications that are reflected in path names containing Microsoft.NET and in the loading of native images such as System.Core.ni.dll and clr.dll. Consequently, any process that hosts a .NET runtime—including PowerShell and applications such as Microsoft Outlook—tends to generate numerous PE_PATCHED alerts. Modern web browsers likewise employ JIT techniques and contribute similar noise. An additional common source of false positives arises during the loading of 32-bit code through the SysWOW64 subsystem.


Although the illustrated SolarMarker output consists solely of false positives, it nevertheless supplies useful investigative leads. The presence of PowerShell—especially the 32-bit SysWOW64 variant—warrants scrutiny in any advanced-attack investigation. The concurrent execution of the 32-bit msiexec.exe is likewise noteworthy; while not inherently malicious, the binary is frequently abused for code execution and therefore merits examination.


While the absolute number of potential false positives is large, many recurring patterns are predictable. A recommended practice is to execute both malfind and findevil against a known-clean baseline image of the same operating-system version. Familiarity with the resulting noise profile materially accelerates the subsequent identification of genuine anomalies.

Post a Comment

Previous Post Next Post