In this section

Volatility malfind: Private Executable Memory, PAGE_EXECUTE_READWRITE and False Positives

Module 0

Introduction

Three subs have now established what exists on this machine and what used to. None of them can tell you anything about what is inside any of it. A process list reports that a process exists; it has no column for what that process is carrying, and an injected process prints exactly the same line as a clean one. That gap is what this plugin was written for.

malfind looks for memory that a process holds which is executable, private to that process, and has no file behind it. That combination is what code delivered into a running process looks like. It is also what several entirely legitimate pieces of software look like, which is why the output of this plugin is a set of questions rather than a set of answers, and why most of this sub is about telling the two apart.

Scenario

Two subs ago you found a command prompt at 14:07 whose parent was explorer.exe, and one sub ago a short-lived update.exe two minutes later. Both pointed at process 1428. The process list says explorer.exe started at 08:52 and has done nothing unusual since, and it would say exactly the same thing if somebody had put their own code inside it at 14:06. This is the command that asks.

01

Running malfind, and a Plugin That Moved

The path changed in 2026, and the old one is gone

Start with the invocation, because this plugin is a live example of tooling changing underneath the documentation written about it.

$ vol -f NE-ENG-031-1431.raw windows.malware.malfind --pid 1428
PID   Process       Start VPN   End VPN     Tag   Protection
1428  explorer.exe  0x2a10000   0x2a10fff   VadS  PAGE_EXECUTE_READWRITE

One row, and it is the answer to the question the last three subs kept arriving at. Before reading it, the invocation itself needs a note that will save you an afternoon.

The plugin used to be windows.malfind. Volatility moved it under windows.malware, ran a deprecation warning on the old path for a period, and removed that path after June 2026. Run the old name on a current install and you get an error rather than output. Every older write-up, cheat sheet and conference slide uses the short name, and on a current installation all of them fail with an unhelpful error about an unknown plugin. The plugin did not disappear; it moved.

The lesson generalizes and it is worth more than the command itself. Plugin paths, option names and output columns all move between releases. A tutorial that worked perfectly two years ago may not run at all today, and its author is not at fault. The fix is the same every time: ask the tool what it has rather than asking the internet what it used to have.

Two commands do that, and they are worth learning before any specific plugin.

$ vol -h | grep -i malfind
$ vol -f NE-ENG-031-1431.raw windows.malware.malfind -h

The first lists every plugin this installation actually carries, filtered to the one you want. The second shows the options this version of that plugin actually accepts, which is where you find out whether --pid and --dump exist here and what else does. Between them they settle every question about syntax in about ten seconds, and they are authoritative in a way no web page can be: they describe the software on your disk rather than the software somebody had two years ago. Make that reflex early and the tooling stops being a source of friction.

02

What the Plugin Is Actually Looking For

Three properties, and why all three matter

Windows does not hand a process memory in one lump. It tracks each process's memory as a set of described regions, each with a size, a protection setting and a note of where its contents came from, and malfind examines those descriptions rather than the memory itself. It reports the regions that satisfy three conditions at once, and all three matter.

The region must be executable, so the processor will run instructions from it. It must be private to the process rather than shared with others. And it must have no file behind it, meaning the region was not produced by loading a program or a library from disk in the ordinary way.

Why is "executable" alone not worth reporting?pick one, then the tell

Notice what the plugin is not doing. It is not looking at the contents of memory for anything resembling malicious code, it is not matching signatures, and it has no opinion about what any program is for. It reads the operating system's own descriptions of each region and reports the ones whose properties fit a shape.

That is why the results are cheap to produce and why they need judging afterwards. A property test finds everything with that property, including every legitimate thing that shares it.

The Protection column is where the first condition shows up. PAGE_EXECUTE_READWRITE means the region can be both written to and executed from. A compiled program almost never needs that combination, because its code is loaded once and then only read; code that writes itself into memory and then runs needs exactly it.

PrivateMemory reports the second condition. The third, having no file behind it, is the absence of a mapped file name on the region. Read together, those three columns make a single claim: this process is carrying something executable that did not come from any file on this machine. Code normally arrives by being loaded from disk, so a region holding instructions with no file behind it was written there while the process was running.

03

Reading Start VPN, End VPN and Tag

A small region at a round address

The two address columns bound the region at either end, and on the row above they are 0x2a10000 to 0x2a10fff: four kilobytes, which is one page.

Both details are worth noticing. A single page is small, and a great deal of delivered code starts out as exactly one page, because the first thing to arrive is often a loader whose only job is to fetch or unpack the rest of it. The start address is also conspicuously round, which is the shape an allocation request produces rather than anything the Windows loader would have chosen for a program it was placing.

0x2a10000  to  0x2a10fff     one page, 4 KB
           ^^^^^^^^^^^
           allocated, not loaded: a program's own code
           lands where the loader puts it, not on a
           round boundary a request produced

Size is worth calibrating on rather than reacting to. A single page is small enough that it cannot hold much, which is informative in both directions: too small for a substantial program, and exactly the size of a stub that fetches one. Regions of several hundred kilobytes appear too, and they are a different kind of finding because the code is all there.

Tag reports the kind of allocation Windows recorded for this region. VadS is the tag Windows uses for a private allocation with no file behind it. It restates what the other columns already say, and it is worth recognizing because it is the value you will see on almost every row this plugin returns.

The VPN in those column names is the virtual page number, so the values are addresses in the process's own address space rather than positions in the image. That means they are the numbers to hand to any other plugin asking about this process, and MF0.6's point applies: they only mean anything alongside the process that owns them.

None of that amounts to proof of anything on its own. It is a description of one region, precise enough that somebody handed the same image can go and look at exactly the same bytes, which is the standard this whole course works to and the reason addresses belong in findings.

04

The Hexdump and Disasm Columns

The plugin shows you the bytes, and what they decode to

malfind does not stop at describing the region, which is what makes it unusual. It prints the opening bytes of the region as hex and disassembles them, so one command takes you from "this process holds odd memory" all the way to looking at what is actually in that memory. Most plugins describe what they found; this one shows it to you.

$ vol -f NE-ENG-031-1431.raw windows.malware.malfind --pid 1428
...
Hexdump:
4d 5a 90 00 03 00 00 00  MZ......
04 00 00 00 ff ff 00 00  ........
Disasm:
0x2a10000:  dec ebp
0x2a10001:  pop edx

The output is wide, so this is one of the plugins where -r pretty or piping to a file earns its keep. What matters is that both columns are describing the same bytes in two ways.

Read the hexdump first and the disassembly second, every time. 4d 5a at the start of a region is the characters MZ, which are the first two bytes of every Windows executable file, and finding an executable file header sitting in private memory that no file on disk produced is a considerably stronger observation than the region's properties on their own. The properties say something was written here at run time; the header says what was written was a whole program.

The disassembly beneath it is the same bytes decoded as instructions, and here it is meaningless: dec ebp and pop edx are what MZ happens to decode to. That is worth recognizing early, because a disassembly that reads as nonsense usually means you are looking at data rather than at code, and reading it as a failed decode rather than as strange instructions is the correct interpretation.

The disassembly is nonsense. Does that weaken the finding?predict, then reveal
0x2a10000: dec ebp
0x2a10001: pop edx
no. it strengthens it.
the bytes are a PE header, not instructions,
so the region holds a whole executable image
rather than a fragment of shellcode.
The disassembler decodes whatever it is given. Meaningless output where a file header sits tells you the region's first bytes are a file, and a complete executable living in memory that no file on disk produced is exactly what reflective loading leaves behind.

Set the two readings of the same bytes beside each other and the point is hard to miss.

4d 5a 90 00 03 00 00 00
04 00 00 00 ff ff 00 00
WHAT THOSE BYTES ARE
4d 5a          "MZ", the signature on every
               Windows executable file
90 00 03 00    the DOS header that follows it

The second pane is not the output of running anything on the first. It is the same sixteen bytes read as a structure rather than as hex, and that re-reading is the entire finding: a file header, in memory, that no file on disk produced.

So the two columns are doing genuinely different jobs and should be read in a fixed order. The hexdump tells you what the region is; the disassembly tells you what it would do if the processor started executing at the first byte, which is only a meaningful question once you know those bytes are instructions rather than a header.

05

Why malfind Produces False Positives

Legitimate software does this constantly

That said, the plugin's judgment is broad by design. Run malfind across a whole image rather than a single process and you will get rows from software nobody would ever call malicious. That is not a defect in the plugin, and understanding why it happens is the difference between a tool you can use and a tool that alarms you every time you run it.

Anything that generates code while it runs produces exactly this pattern, and a surprising amount of ordinary software does. A runtime that compiles as it executes writes instructions into memory it allocated, marks that memory executable, and runs it, which is three for three on the conditions. Browsers do it constantly, several language runtimes do it, and some security software does it as part of its own instrumentation.

Sort each malfind row by whether it needs investigatingtap a row, then a bucket
Expected for this programWorth the next hour

There is a second source of noise worth knowing about, which is that a region can satisfy all three conditions and contain nothing at all. Memory gets allocated and marked executable before anything is written into it, and a page of zeroes ticks every box the plugin checks. Current versions filter the obviously empty ones, and the general point stands: the conditions describe a shape rather than a payload.

The sorting rule is the whole of the skill here: the question is never whether the region exists, because plenty do. It is whether this particular program has any business generating code while it runs. That question is about what the program does for a living, which is knowledge about ordinary software rather than about attackers, and it is the recurring shape of this whole module.

None of this makes the plugin unreliable. It makes it a filter rather than a detector, and a filter that reduces a few hundred processes to a handful of regions is doing exactly the job an early-stage tool should do.

So the useful workflow is a sweep and a filter. Run malfind against the whole image, discount every process whose job explains the region, and look hard at whatever survives. On most machines that leaves very few rows, and on this one it leaves a single page inside a process that has no business holding it.

06

Extracting the Region for Examination

--dump, and what you get back

Once a region is worth more than a glance, the plugin will write it out to disk for you rather than making you extract it another way.

$ vol -o ./out -f NE-ENG-031-1431.raw windows.malware.malfind --pid 1428 --dump
$ ls ./out
pid.1428.vad.0x2a10000.dmp

Note the argument order again, because it is the one thing about this tool that people get wrong repeatedly. -o is a global option and goes before the plugin name; --dump belongs to the plugin and goes after it. That is the rule from MF0.1 and it applies to every plugin that writes files.

The filename is worth reading rather than ignoring. It carries the process identifier, the kind of structure, and the region's start address, so the file names its own provenance and you can tell six months later exactly which region of which process it came from without consulting notes.

That is a small thing, and it is exactly the kind of small thing that makes a report defensible months later when nobody remembers anything. A file called dump1.bin on somebody's desktop proves nothing about anything.

What comes out is the bytes as they existed in memory, which is not the same as the file they came from even when a file exists. Anything that loads code adjusts it while doing so, patching addresses so the code works where it actually landed, so what you extract is the loaded form rather than the on-disk form.

That distinction matters the moment somebody tries to identify what you found. Comparing the extracted bytes against a known file will not match exactly, and concluding from that mismatch that the bytes are unknown is wrong. It is very likely the same program, in the state the loader left it rather than the state the compiler produced. Tools that analyze extracted regions expect this and handle it; an analyst comparing hashes by hand does not, and reaches the wrong conclusion confidently.

07

What This Row Does and Does Not Establish

A region is not an action

Take the single row from the top of this sub and write down exactly what it supports and nothing more, because this is where findings usually overreach.

It establishes that process 1428, at the time of capture, held one page of memory that is executable, private to it, unbacked by any file on disk, and whose first bytes are an executable file header. Every clause of that is a structural claim, and anybody handed the same image can check every one of them independently. Nothing in it requires trusting your judgment.

The row supports four claims: that the region exists, what its properties are, what its first bytes contain, and which process holds it. It supports nothing about whether anything ran from it, who put it there, what it did, or how it arrived on the host. Each of those four needs evidence from somewhere else, and two of them need a source other than this image.

What would it take to establish that something actually ran from this region?pick one, then the tell

The two halves are worth seeing side by side, because the right one is longer.

the malfind row the region exists its properties its first bytes which process holds it answered here did anything run from it? the thread list, a later module who put it there? another process, not this row what did it do? execution evidence how did it arrive on the host? disk or endpoint logs, not memory

Four answers on the left. Each arrow leaves this plugin and lands somewhere else.

The right-hand column is longer than the left and that is the normal shape of an honest finding. A single plugin answers one question well and is silent on the rest, and a report that reflects that silence is describing its evidence accurately rather than being tentative.

The first item on the right is the one that catches people. A region containing instructions is not a region that executed instructions; establishing that something ran from it means finding a thread whose start address falls inside that address range, which is a different plugin and a later module in this course. Until then the region is a container, not an execution.

The second item is worth a moment too. This row says nothing about who created the region, and the process holding it is not necessarily the process responsible: one of the standard ways code gets into a process is another process putting it there. So the owner of the memory is where the code IS, not where it came from.

So the honest finding from this sub is narrow and useful: explorer.exe, which has no business generating code, holds an unbacked executable page containing a file header, at the address 0x2a10000. The next question is whether anything is actually running from that region, and the value of the work so far is that the question now has an address attached to it rather than being a general worry about a machine.

08

Practice

Run it wide, then explain every row
hands on
Do this Calibrate on a machine you believe is clean
  1. Run vol -h and find what malfind is called on your installation. If you have an older Volatility it may still be windows.malfind; on a current one it is under windows.malware. Never copy this path from a blog post.
  2. Run it against a whole image with no --pid and count the rows. On an ordinary workstation you should get several, and every one of them is a false positive until you have a reason otherwise.
  3. For each row, name the process and decide whether generating code at run time is part of that program's job. Look up anything you cannot answer, because that lookup is the actual skill.
  4. Look at the hexdump on each row. Note which begin with a recognizable file header and which look like arbitrary bytes, and note that both happen on clean machines.
  5. Pick one row and dump it with -o and --dump. Confirm the file appears, then write one sentence about what its existence does and does not establish. This is the step that separates a description from a conclusion.
What you should end up with: the correct plugin path for your installation, a count of malfind rows on a machine you trust, an explanation for every one of them, and one extracted region with a written statement of what it proves. The count is the important number: knowing that a clean machine produces several is what stops the first one alarming you.

Extend it

These build the calibration that makes malfind output readable rather than alarming.

  1. Run malfind against images of several different machines and compare how many rows each produces. A developer workstation and a file server give very different baselines.
  2. Find the plugin source and read the conditions it applies. They are a short list, and seeing them is what turns false positives from mysterious into predictable.
  3. Take a hexdump that begins with a file header and work out what kind of file it is from the first bytes alone. Recognizing headers on sight is worth the hour it takes to learn.
  4. Compare a dumped region against the file on disk it appears to correspond to. The differences are the loader's work, and seeing them explains why hash comparison fails here.

Drill one is the one to do first. A count from a machine you trust is the only baseline that means anything, and an analyst who has never seen malfind output from a clean workstation has no way to judge output from a compromised one.

Drill two pays for itself immediately. The plugin's conditions are the whole of its judgment, and an analyst who has read them knows in advance which rows it will produce and which it will miss.

You can run malfind on a current Volatility, explain what the three conditions are and why all three are needed, read Protection, Tag and the hexdump, tell an expected row from one worth an hour, extract a region, and state precisely what a row does not establish. MF0.4 asks what a process has open rather than what it contains.