Hunting Unbacked Executable Memory
Overview
Most injection techniques converge on the same end state: code executing from memory that no file on disk accounts for. Hollowing, reflective loading, shellcode stagers and manual mappers differ enormously in how they get there and barely at all in what they leave behind.
That makes unbacked executable memory the most economical thing a defender can hunt for. One check covers a whole category, and the techniques that evade it — module stomping above all — have to give up something else to do so.
This hunt has a real false-positive population: .NET, Java and every JavaScript engine generate private executable memory as a matter of course. Baseline those processes explicitly rather than raising the threshold until the alert goes quiet.
The hunt
- 1enumerate committed regionsWalk the VAD tree for every process and keep MEM_COMMIT regions.
- 2filter for executable protectionKeep PAGE_EXECUTE_*, discard everything read-only or writable-only.
- 3check the backing fileA region with no FILE_OBJECT behind it is private executable memory.
- 4score the survivorsRank by size, entropy, presence of an MZ header and whether a thread starts inside it.
Running it
# offline: score every private executable region in a memory image
vol -f capture.raw windows.malfind --dump
# live triage: same idea, from the running box
Get-Process | ForEach-Object {
# regions with Protect = PAGE_EXECUTE_READWRITE and Type = MEM_PRIVATE
Get-ProcessMemoryRegion -Id $_.Id |
Where-Object { $_.Type -eq 'Private' -and $_.Protect -match 'EXECUTE' }
}# offline: score every private executable region in a memory image
vol -f capture.raw windows.malfind --dump
# live triage: same idea, from the running box
Get-Process | ForEach-Object {
# regions with Protect = PAGE_EXECUTE_READWRITE and Type = MEM_PRIVATE
Get-ProcessMemoryRegion -Id $_.Id |
Where-Object { $_.Type -eq 'Private' -and $_.Protect -match 'EXECUTE' }
}Score, do not alert. A 4 KB private RX region in a browser is noise; a 60 KB one carrying an MZ
header in svchost.exe, with a thread starting inside it, is an incident.