<- All posts

Catching Process Hollowing in Memory with Volatility 3

A walkthrough of confirming process hollowing from a memory image: VAD anomalies, hollowed PE headers, and the IOCs that tied it together.

memory-forensicsvolatilityprocess-injectiondfirmalware-analysis

Over the recent holiday break, I came across an exercise for Memory Forensics. It's been awhile! I have had to redact a few tidbits, but mostly documenting the process I took below.

Process hollowing is one of those techniques that looks clean on the surface. A legitimate process name in the task list, a valid parent, the right path on disk. Then you look at what's actually mapped into memory and the story falls apart. This is a writeup of an investigation where a hollowed svchost.exe gave itself away in the VAD tree, and how I worked through it with Volatility 3.

If you're triaging endpoints and you only ever look at disk artifacts, this is the kind of thing you'll miss. The whole point of hollowing is that the malicious code never touches the file the process was launched from.

What process hollowing actually is

The short version: a malware launches a legitimate process in a suspended state, unmaps (hollows out) the original executable image from its address space, writes its own PE into that space, fixes up the entry point, and resumes the thread. The process keeps its legitimate-looking identity but runs attacker code. MITRE tracks this as T1055.012 — Process Hollowing.

The classic API sequence is CreateProcess (with CREATE_SUSPENDED), NtUnmapViewOfSection (or ZwUnmapViewOfSection), VirtualAllocEx, WriteProcessMemory, SetThreadContext, and finally ResumeThread. You won't see those calls in a memory image directly, but you will see their fingerprints in the process memory layout.

The triage that started it

The image came in as a raw acquisition from a Windows 10 workstation flagged by EDR for an outbound beacon to a host with no business reason to exist. I started with the basics:

vol -f memdump.raw windows.pslist
vol -f memdump.raw windows.pstree

pstree is where the first soft signal showed up. There was an svchost.exe whose parent was explorer.exe. On a normal system, svchost.exe is spawned by services.exe. A user shell launching a service host is a red flag on its own — it's a near-guaranteed sign that something is masquerading.

That alone isn't proof. Plenty of things look weird and turn out benign. But it's enough to pull the thread.

Confirming injection via the VAD tree

The Virtual Address Descriptor (VAD) tree is the kernel's record of how a process's user-mode address space is carved up. Each node describes a region of virtual memory, its protection flags, and — crucially — whether it's backed by a file (a mapped image or data file) or is private, unbacked memory.

For a legitimately loaded executable, the main image region is a mapped VAD that points back to the .exe on disk, and it carries the section protections you'd expect for a PE: an executable, read-only code section among others. When a process gets hollowed, the original mapped image is unmapped and the attacker's code lands in a region that is either private memory marked executable, or whose protections don't line up with a normally loaded image.

I ran:

vol -f memdump.raw windows.vadinfo --pid 4812

The smoking gun was a region at the process base address with PAGE_EXECUTE_READWRITE protection that was not backed by any file. A normal image base is a mapped, file-backed region. RWX private memory sitting at the image base is exactly the kind of allocation you get from VirtualAllocEx + WriteProcessMemory. As a heuristic, executable-and-writable memory in a running process is almost always worth a second look — legitimate code rarely needs to be both writable and executable at the same time.

To cross-check, I used the malfind plugin, which is purpose-built to surface this pattern — private, executable regions with no mapped image and PE-like content:

vol -f memdump.raw windows.malfind --pid 4812

malfind lit up on the same PID. The hex dump at the head of the suspect region started with 4d 5a — the MZ magic of a PE header — followed shortly by the PE\x00\x00 signature. So there was a full executable image sitting in private RWX memory at a location that, on disk, claims to be plain old svchost.exe. The on-disk image and the in-memory image did not match. That's the definition of hollowing.

Pulling the dropped payload (the malldrop)

With the region confirmed, I dumped it for static analysis. Volatility 3 will carve the VAD region or the process image out of memory:

vol -f memdump.raw -o ./out windows.vadinfo --pid 4812 --dump
vol -f memdump.raw -o ./out windows.malfind --pid 4812 --dump

The carved region gave me a reconstructable PE. A quick triage:

file ./out/pid.4812.*.dmp
strings -el ./out/pid.4812.*.dmp | sort -u | head
sha256sum ./out/pid.4812.*.dmp

Memory-carved samples are messy — relocations are already applied, the IAT is patched live, and section padding may be off — so don't expect the hash to match a clean on-disk sample. But the strings are gold for IOCs. I pulled out the C2 host and a hardcoded URI path, a mutex name, and a user-agent string that didn't match any browser on the box. Running the dump through capa flagged capabilities consistent with injection and HTTP C2, which lined up with what the EDR telemetry had shown.

A word of caution: treat carved payloads as live malware. Hash them, store them in a contained location, and submit the hash — not necessarily the binary — to threat-intel platforms if disclosure rules apply.

The IOCs

Here's what came out of the investigation, organized so a blue team can actually use it:

Host / behavioral

  • svchost.exe with a parent of explorer.exe (parent-child anomaly)
  • RWX, non-file-backed VAD region at the process image base
  • PE header (MZ / PE\x00\x00) inside private executable memory
  • A mutex used to prevent multiple infections (good for a YARA rule and a host sweep)

Network

  • C2 domain and IP from the carved strings
  • A distinctive URI path and a non-standard user-agent string

File

  • SHA-256 of the carved payload (caveat: memory-modified)

Map these back to detection. The behavioral indicators are the durable ones — a hash changes with every build, but explorer.exe spawning svchost.exe is a detection that survives recompilation.

Turning this into detection

A few takeaways I'd push to any detection engineer:

  1. Alert on parent-child anomalies. System binaries have predictable lineage. svchost.exe should descend from services.exe. The LOLBAS and ATT&CK process trees are good references for what "normal" looks like.
  2. Hunt RWX memory. Sysmon doesn't natively show VAD protections, but EDRs and tools like malfind do. RWX private regions in long-lived system processes are rare and suspicious.
  3. Watch the API sequence. CREATE_SUSPENDED followed by remote memory writes and SetThreadContext is the hollowing recipe. Many EDRs surface this directly.
  4. Capture memory, not just disk. None of the strongest IOCs here existed on disk. If your IR playbook skips volatile acquisition, you're working blind against this class of attack.

Process hollowing isn't exotic anymore — it's a commodity technique baked into off-the-shelf loaders. But it leaves a consistent shape in memory, and once you know to look at the VAD tree and the gap between the disk image and the in-memory image, it's hard to hide.

References