Finding Malicious Code Hidden in Windows Memory

12 August 2025 | 5 min read | justruss.tech

Sophisticated attackers avoid writing malicious files to disk where possible. The payload executes entirely in memory, never touches the filesystem, and leaves no binary artifact for file-based scanning tools to find. Memory forensics is how you find them after the fact, and understanding what to look for is what separates a useful investigation from one that concludes “we found nothing” on a system that was fully compromised.

Why memory analysis is non-negotiable for advanced intrusions

A running Windows system holds a complete record of what is executing in RAM. Process memory contains the actual code being run, injected payloads before they encrypt themselves back, decrypted C2 configurations, credential material in LSASS, and encryption keys that are never stored on disk. For intrusions using fileless techniques, reflective loading, or process injection, memory is the primary source of evidence. File system forensics alone will find nothing.

Memory acquisition

// WinPmem: recommended open source tool for memory acquisition
// Download from: https://github.com/Velocidex/WinPmem/releases

winpmem.exe -o C:\evidence\memory.raw

// Verify the image was captured correctly
winpmem.exe --verify C:\evidence\memory.raw

// Alternative: Magnet RAM Capture (free, GUI-based)
// Alternative: Belkasoft RAM Capturer (free, command line and GUI)
// Alternative: DumpIt (commercial, widely used in IR contexts)

// Important: capture memory BEFORE rebooting or running tools
// Memory compression on Windows 10+ means some data is in the paging file
// For complete coverage also capture the pagefile:
copy C:\pagefile.sys C:\evidence\pagefile.sys

Working with Volatility 3

// Install Volatility 3
pip install volatility3

// Verify the image and get OS details
vol -f memory.raw windows.info

// Full process listing with parent relationships
vol -f memory.raw windows.pstree

// Compare with pslist to find unlinked processes (DKOM rootkit technique)
// Processes in pslist but not pstree have had their EPROCESS list entry removed
vol -f memory.raw windows.pslist > /tmp/pslist.txt
vol -f memory.raw windows.pstree > /tmp/pstree.txt

// Check for process name spoofing
// A process named "svchost.exe" running from AppData or Temp is not legitimate
vol -f memory.raw windows.pslist | grep -v "System32\|SysWOW64\|Windows\\\\" | grep "svchost\|lsass\|csrss\|winlogon\|services"

Finding injected code with malfind

Malfind is the primary tool for finding injected shellcode and reflectively loaded DLLs. It scans process memory for regions with execute permissions that are not mapped to a file on disk.

// Scan all processes
vol -f memory.raw windows.malfind

// Scan a specific process
vol -f memory.raw windows.malfind --pid 1234

// Example output indicating reflective DLL injection:
// Process: explorer.exe  Pid: 1234
// Address: 0x400000
// Vad Tag: VadS
// Protection: PAGE_EXECUTE_READWRITE
// Hexdump:
// 4d 5a 90 00 03 00 00 00  MZ......   <-- MZ header: this is a PE file
// 04 00 00 00 ff ff 00 00  ........
// b8 00 00 00 00 00 00 00  ........
// Disassembly:
// 0x400000:  dec ebp      ; 4d
// 0x400001:  pop edx      ; 5a
// This is a PE file in memory with no corresponding file on disk

// Dump the suspicious region for analysis
vol -f memory.raw windows.dumpfiles --virtaddr 0x400000 --pid 1234 -o /tmp/dumped/

// Analyse the dumped PE
file /tmp/dumped/file.0x1234.0x400000.img
strings -a /tmp/dumped/file.0x1234.0x400000.img | grep -i "http\|C2\|beacon\|server"
sha256sum /tmp/dumped/file.0x1234.0x400000.img
# Submit hash to VirusTotal for quick identification

Checking loaded modules for injection indicators

// List all DLLs loaded in a process
vol -f memory.raw windows.dlllist --pid 1234

// A legitimate svchost.exe should load a specific set of service DLLs
// An unexpected DLL, especially one with no path or a path in AppData or Temp,
// indicates DLL injection

// Cross-reference module list with VAD (Virtual Address Descriptor) entries
vol -f memory.raw windows.vadinfo --pid 1234 | grep EXECUTE

// Modules loaded by a process that appear in memory with no disk backing:
// VAD Protection: PAGE_EXECUTE_READWRITE with no FileObject = injected code

Network connections from injected processes

// Find all active and recently closed network connections
vol -f memory.raw windows.netscan

// Example suspicious output:
// TCPv4   192.168.1.101  54321  198.51.100.5  443  ESTABLISHED  1234  explorer.exe
// explorer.exe making an outbound HTTPS connection is suspicious
// explorer.exe does not legitimately connect to external servers

// Cross-reference with process details
vol -f memory.raw windows.pslist | grep 1234
// Then check malfind for PID 1234 to confirm injection

// Full investigation flow:
// 1. netscan: find suspicious outbound connection from unexpected process
// 2. pslist: verify process details
// 3. malfind: confirm injected code in that process
// 4. dumpfiles: extract the injected code
// 5. strings/pe analysis: identify what the injected code is

Extracting LSASS credentials from memory

// If the investigation involves credential theft, extract credentials from the dump
vol -f memory.raw windows.lsadump

// This outputs the same credential material Mimikatz extracts from a live system
// Use this to assess which credentials were at risk during the compromise

// Alternative approach using pypykatz (Python implementation of Mimikatz logic)
pip install pypykatz
pypykatz lsa minidump memory.raw

// Parse the LSASS process memory region specifically
// First find the LSASS process
vol -f memory.raw windows.pslist | grep lsass
// Then dump its address space
vol -f memory.raw windows.memmap --pid [lsass_pid] -o /tmp/lsass_dump/
pypykatz lsa minidump /tmp/lsass_dump/pid.[lsass_pid].dmp

Building a memory forensics workflow

// Automated triage script for memory images
python3 << EOF
import subprocess, json, sys

image = sys.argv[1]

print("[*] Process tree")
subprocess.run(["vol", "-f", image, "windows.pstree"])

print("\n[*] Network connections")
subprocess.run(["vol", "-f", image, "windows.netscan"])

print("\n[*] Malfind scan (all processes)")
result = subprocess.run(["vol", "-f", image, "windows.malfind"],
                         capture_output=True, text=True)
lines = result.stdout.split("\n")
suspicious_pids = set()
for line in lines:
    if "PAGE_EXECUTE_READWRITE" in line or "MZ" in line:
        print(line)
        # Extract PID from context
        # ... parse and collect suspicious PIDs

print(f"\n[*] Suspicious PIDs: {suspicious_pids}")
for pid in suspicious_pids:
    print(f"\n[*] DLL list for PID {pid}")
    subprocess.run(["vol", "-f", image, "windows.dlllist", "--pid", str(pid)])
EOF
python3 triage.py memory.raw