In this section

Volatility 3 windows.pslist: the Commands, the Options and Reading the Output

Module 0

Introduction

Search for how to find injected code in a memory image and the first result will tell you to run vol -f image windows.malfind. That command was removed from Volatility in June 2026. It now lives at windows.malware.malfind, and every cheat sheet, conference slide and blog post still showing the short form fails on a current install with an unhelpful error about an unknown plugin.

That is the gap this course exists in. The tooling moves, the free material does not, and the difference between the two is an afternoon lost to an error message that does not say what is wrong.

This sub is the commands a practitioner actually types in the first ten minutes of looking at a Windows memory image, in the order they type them, with the output each one returns. It uses two tools rather than one: Volatility, which runs a plugin and prints rows, and MemProcFS, which mounts the same image as a drive letter so you can browse it. Both are free, both are current, and they answer the same questions in ways that suit different moments.

Nothing here needs you to have an image. There is one below, and a listing from it you will read three times before the sub is finished.

Scenario

NE-ENG-031 is an engineering workstation that started behaving oddly this afternoon. Memory was captured at 14:31 and the file is NE-ENG-031-1431.raw. You have the file and nothing else: no alert, no ticket, no idea what happened. The six commands below are what a practitioner actually types in the first ten minutes, in the order they type them.

Before any of the commands, read the listing they produce. Three questions, all answerable from the columns, and none of them needs a process name you recognize.

If the third question took you somewhere other than the row at 14:07, reread the creation times rather than the names. The rest of this sub is the commands that produced that listing and the ones you run once it has given you a row to chase.

01

windows.info, the Command You Run First

Establish what the image is before asking it anything

Never start with windows.pslist, however tempting it is to go straight to the process list. Start by confirming that Volatility can read the image at all, and that it believes it is looking at the operating system you think it is. Both of those can be false, and finding out later wastes an hour.

$ vol -f NE-ENG-031-1431.raw windows.info
Volatility 3 Framework 2.28.2
Progress:  100.00		PDB scanning finished

Variable	Value

Kernel Base	0xf80342a00000
DTB	0x1ad000
Symbols	file:///.../windows/ntkrnlmp.pdb/B857959AD591F829F5D6CDAEDBEA975C-1.json.xz
Is64Bit	True
IsPAE	False
layer_name	0 WindowsIntel32e
memory_layer	1 FileLayer
KdVersionBlock	0xf8034392c400
Major/Minor	15.26100
MachineType	34404
KeNumberProcessors	2
SystemTime	2026-03-15 14:31:06+00:00
NtSystemRoot	C:\Windows
NtProductType	NtProductWinNt
NtMajorVersion	10
NtMinorVersion	0
PE MajorOperatingSystemVersion	10
PE MinorOperatingSystemVersion	0
PE Machine	34404
PE TimeDateStamp	Thu Jan  9 06:42:18 2025

That is a lot of fields and most of them you will never use. Four matter on a first run. Is64Bit and layer_name say Volatility worked out the architecture and built a layer on it, which means it found the kernel. DTB is the directory table base, the root of the page tables, and having it is what makes every later plugin possible. SystemTime is when the capture was taken according to the machine itself, which is not necessarily when it was taken according to reality.

Major/Minor is the build, which is the value that has to match a symbol table exactly, and Kernel Base is where Volatility found the kernel in memory. Between them they are the evidence that the tool has correctly identified what it is reading rather than guessing.

If this command fails or returns nothing, no other plugin will work, and the reason is almost always missing symbols rather than a corrupt image, which is a fixable problem with a specific fix. That is a whole sub later in this module.

You captured at 14:31 by your watch and SystemTime says 14:31:06. What have you just learned?pick one, then the tell

One more reason to run it first, beyond the checking. windows.info is what forces Volatility to work out the memory layout, and that work is the slow part of any run. The framework can save the result with --save-config and reuse it with --config, which turns every subsequent plugin on the same image from slow to immediate. On a large image that is the difference between a comfortable afternoon and an irritating one.

Run this every time, on every image, before anything else. It takes a few seconds and it establishes both that the tooling works and what the machine believed the time was.

02

windows.pslist, and the Listing It Returns

The second command, and the one you will run most

windows.pslist walks the list of process objects the kernel maintains to track what is running, and prints one line per process.

$ vol --hide-columns Threads Handles Wow64 File Exit -f NE-ENG-031-1431.raw \
    windows.pslist | head -14
Progress:  100.00		PDB scanning finished
Volatility 3 Framework 2.28.2

PID	PPID	ImageFileName	Offset(V)	SessionId	CreateTime

4	0	System	0xd187c2060580	N/A	2026-03-15 07:41:02.000000 UTC
336	4	smss.exe	0xd187c0a4e600	N/A	2026-03-15 07:41:04.000000 UTC
120	4	Registry	0xd187c3761f40	N/A	2026-03-15 07:41:06.000000 UTC
2188	4	MemCompression	0xd187c22204c0	N/A	2026-03-15 07:41:09.000000 UTC
468	456	csrss.exe	0xd187c14a3300	0	2026-03-15 07:41:11.000000 UTC
548	456	wininit.exe	0xd187c1c5e640	0	2026-03-15 07:41:14.000000 UTC
5800	548	fontdrvhost.ex	0xd187c0508700	0	2026-03-15 07:41:14.000000 UTC
624	548	services.exe	0xd187c32b6980	0	2026-03-15 07:41:17.000000 UTC
2412	624	spoolsv.exe	0xd187c20c5980	0	2026-03-15 07:41:17.000000 UTC
3392	624	svchost.exe	0xd187c07cacc0	0	2026-03-15 07:41:17.000000 UTC
$ vol --hide-columns Threads Handles Wow64 File Exit -f NE-ENG-031-1431.raw \
    windows.pslist | tail -6
Progress:  100.00		PDB scanning finished
3312	1428	cmd.exe	0xd187c3dd24c0	1	2026-03-15 14:07:33.000000 UTC
5128	3312	conhost.exe	0xd187c3ec0300	1	2026-03-15 14:07:34.000000 UTC
6940	1428	cmd.exe	0xd187c0789ec0	1	2026-03-15 14:30:41.000000 UTC
6952	6940	conhost.exe	0xd187c3bbf900	1	2026-03-15 14:30:42.000000 UTC
4120	6940	winpmem.exe	0xd187c2db7d40	1	2026-03-15 14:30:58.000000 UTC
4128	4120	conhost.exe	0xd187c278ad80	1	2026-03-15 14:30:58.000000 UTC

One thing to be clear about before reading any of the columns: this plugin finds processes by walking a linked list that the kernel maintains for its own purposes. It is not searching memory, it is following pointers from one process object to the next, exactly as the operating system does when it needs to enumerate what is running.

the kernel's list of process objects, as pslist walks it System PID 4 smss.exe PID 336 lsass.exe PID 672 explorer.exe PID 1428 ... a process whose links were removed never visited, and leaves no gap in the output the walk steps straight past it

Following pointers is fast and honest, and it can only reach what is still linked.

That mechanism is why the output is fast and why it is trustworthy: you are reading the same records the operating system relies on to function, not a reconstruction. It is also the source of the one blind spot this plugin has, because a list can only be walked while its links are intact. Hold that thought; the next sub is entirely about it.

The block shows the first rows and the last rows of the listing, with head and tail. --hide-columns drops five columns so each row fits the page: Threads, Handles and Wow64, which a first read does not use, ExitTime, which is N/A for every process still running, and File output, which says Disabled unless you asked the plugin to dump. It is a Volatility option, so it goes before the plugin name, and in practice before -f as well: it takes every word after it as a column name until the next option, so written straight before the plugin it swallows the plugin name and Volatility stops with Please select a plugin to run. It matches column names by their start, which is why File and Exit are enough. The remaining columns are as Volatility prints them, in its own order, separated by tabs.

The whole listing is 101 processes, which is ordinary for a Windows 11 workstation, and its shape is easier to see than its length suggests: a large block of system processes from boot, a smaller block belonging to the user's session, and whatever sits outside both.

Now the columns. PID identifies the process to the operating system while it runs. PPID is the identifier of whatever created it, written once at creation and never updated. Offset(V) is the virtual address of the process object itself, so every other value on the line was read out of the structure sitting at that address. It is the column people skip and the one that lets a reader verify your finding, because a PID is ambiguous across two captures and an offset is not. SessionId is which logon session it belongs to: session 0 is non-interactive and is where services run, session 1 is the user's own, and N/A means the process belongs to no session at all, which is correct for System and would be nonsense as a zero. CreateTime is when the kernel created the object, written by the kernel into kernel memory, which is why it outranks almost every other timestamp you will meet.

03

Making pslist Output Usable: -r and grep

Renderers, and the pipe you will use constantly

The terminal is the wrong place to read a hundred and one lines of tabular data and there is no prize for persevering. Volatility gives you two ways out of that, and both are worth having in your fingers rather than looking up.

A hundred and one lines of default output is hard to read, and Volatility's global -r option changes the renderer. The default is quick, which prints rows as it finds them at the cost of ragged columns. pretty waits until the run finishes and aligns everything, which is better to read and slower to start. csv and json are for feeding the output into something else rather than reading it yourself.

$ vol -r pretty -f NE-ENG-031-1431.raw windows.pslist
$ vol -q -r csv -f NE-ENG-031-1431.raw windows.pslist > pslist.csv

The -q on the second line turns off the progress line. Progress goes to standard error, and so does the version banner when the renderer is csv or json, so neither would reach the file; -q keeps the terminal clear while it is written. -r and -q are both global options and go before the plugin name, not after it. That trips people up constantly: plugin options like --pid go after the plugin, Volatility's own options go before it.

$ vol --hide-columns Threads Handles Wow64 File Exit -f NE-ENG-031-1431.raw \
    windows.pslist | grep -i explorer
Progress:  100.00		PDB scanning finished
1428	1204	explorer.exe	0xd187c0564200	1	2026-03-15 08:52:11.000000 UTC

The grep line is the pipe you will type more than any other. Volatility writes plain text to standard output, so every filtering tool you already know works on it unchanged: grep for a name, grep -c to count, sort and awk if you prefer them to a spreadsheet. There is no special query syntax to learn.

The csv line is the one worth adopting as a habit. Getting the process list into a spreadsheet lets you sort by creation time in one click, which is the single most useful thing you can do with a process list and is genuinely awkward at a terminal. The json renderer is the one to reach for if you ever automate any of this, since it gives you the column names alongside the rows.

You sort that CSV by CreateTime. What does the machine's day look like?predict, then reveal
sorted ascending by CreateTime ...
07:41:02 - 07:42:47   75 processes   boot and services
08:51:54 - 09:02:17   20 processes   logon and the user's apps
14:07:33              2 processes    cmd.exe and its console host, five hours later
14:30:41 - 14:30:58   4 processes    a second cmd.exe, winpmem.exe, two console hosts
Four groups. The last is yours: the prompt you opened at 14:30 and the capture tool you started from it, each with the console host Windows starts beside a console program. That leaves one group nobody has explained: cmd.exe at 14:07, with its console host. That grouping took no expertise and no threat intelligence, only sorting one column, and it has already reduced a hundred and one lines to one worth asking about.

Plotted against a clock rather than listed, the same hundred and one rows look like this.

07:41 08:52 14:07 14:31 your capture boot 75 processes logon and apps 20 processes cmd.exe with its console host five hours in which the machine created nothing at all

The line is ordinary. The empty space either side of it is the finding.

Four groups, and the last is yours: the prompt and the capture tool you started. Of the other three, boot and logon are what every workstation shows, which leaves cmd.exe at 14:07. Nothing about that grouping required knowing anything about this machine, and it is why the CSV line is worth typing even when the terminal output looks readable.

04

windows.pstree, the Same Data Shaped as Descent

Where a missing parent becomes visible

windows.pstree enumerates processes by exactly the same mechanism pslist uses, walking the same kernel list, and then indents each process under its parent. Same data, same blind spots, different shape. It will not show you anything pslist missed, and the shape makes two things obvious that a flat list hides: what the ordinary service chain looks like, and which processes have no parent at all.

$ vol --hide-columns Threads Handles Wow64 Exit Audit Cmd Path -f \
    NE-ENG-031-1431.raw windows.pstree \
    | grep -wE "System|smss|wininit|services|lsass|explorer|cmd|winpmem"
Progress:  100.00		PDB scanning finished
4	0	System	0xd187c2060580	N/A	2026-03-15 07:41:02.000000 UTC
* 336	4	smss.exe	0xd187c0a4e600	N/A	2026-03-15 07:41:04.000000 UTC
548	456	wininit.exe	0xd187c1c5e640	0	2026-03-15 07:41:14.000000 UTC
* 624	548	services.exe	0xd187c32b6980	0	2026-03-15 07:41:17.000000 UTC
* 672	548	lsass.exe	0xd187c0684180	0	2026-03-15 07:41:19.000000 UTC
1428	1204	explorer.exe	0xd187c0564200	1	2026-03-15 08:52:11.000000 UTC
* 3312	1428	cmd.exe	0xd187c3dd24c0	1	2026-03-15 14:07:33.000000 UTC
* 6940	1428	cmd.exe	0xd187c0789ec0	1	2026-03-15 14:30:41.000000 UTC
** 4120	6940	winpmem.exe	0xd187c2db7d40	1	2026-03-15 14:30:58.000000 UTC

Asterisks mark depth, one per level, and grep keeps them because they are part of each line. The full tree has dozens of svchost.exe processes under services.exe, which sits under wininit.exe: the ordinary service chain on every Windows machine, and what you want to be able to recognize at a glance. wininit.exe itself is at the left margin, because the session manager instance that started it exits once it has done so, which Windows does on every boot.

It is worth running pstree on every image even though it shows you nothing pslist did not already contain. Descent is hard to see in a flat list and obvious in an indented one, and the whole value of this plugin is that it does that one transformation for you.

Now look at explorer.exe. It is at the left margin, not indented under anything, even though its PPID says 1204. The tree puts it at the root because there is no process 1204 anywhere in the image to nest it under. In the flat listing that fact was a number you would have had to check by hand; here the layout does the checking for you.

explorer.exe has no parent in the tree. What does that most likely mean?pick one, then the tell

Notice what answering that required: knowledge of how Windows starts a desktop, not knowledge about attackers or indicators. That is the shape of most memory forensics. Anomalies are departures from ordinary system behavior, so ordinary system behavior is the thing worth studying, and an analyst who knows what a clean Windows process tree looks like needs no list of indicators to notice one that is wrong.

That also explains why pstree is worth running on machines you believe are clean. Building the picture of normal is the work; spotting the departure is comparatively easy once you have it.

05

Narrowing With --pid, and What Comes Next

Plugin options, and the three commands you run after pslist

Up to this point every command has been run against the whole image, which is correct while you are surveying and wasteful once you are not. Once the list has pointed at a process, most plugins take --pid to restrict their output to it, which turns thousands of lines into a few dozen. The option goes after the plugin name.

$ vol -f NE-ENG-031-1431.raw windows.dlllist --pid 1428
$ vol -f NE-ENG-031-1431.raw windows.handles --pid 1428
$ vol -f NE-ENG-031-1431.raw windows.netscan
$ vol -o ./dump/ -f NE-ENG-031-1431.raw windows.pslist --pid 3312 --dump

Each of those is a command you will run in anger before this module is finished, so it is worth knowing now what each of them returns and why you would reach for it. dlllist shows which modules are loaded inside a process, handles shows the objects it has open and the access it was granted to each, netscan shows connections and which process owns each, and --dump with an output directory writes the process executable out to disk so it can be examined with tools that know nothing about memory images.

windows.netscan is the odd one out in that list because it takes no --pid. It scans for network structures across the whole image and prints the owning process for each connection it finds, so you run it whole and read the process column. That is worth knowing because it is the only source on the machine that ties a connection to the program responsible; a firewall sees the addresses and never learns which program opened them.

vol -o ./dump/ -f IMAGE windows.pslist --pid 3312 --dump how Volatility runs always BEFORE the plugin the plugin what the plugin does always AFTER the plugin

Get this wrong and you get an argument error, not a wrong answer. It is the only syntax rule the tool has.

Note -o ./dump/ on the last one: it is a global option so it goes before the plugin, while --pid and --dump belong to the plugin and go after. Getting that wrong produces an argument error rather than a wrong answer, which is the kind of mistake you only make twice. The rule is worth saying once more because it is the entire syntax of the tool: anything about how Volatility runs goes before the plugin, anything about what the plugin does goes after.

06

What pslist Cannot Tell You

Four gaps, and which command closes each

A process list is an inventory of what exists. That is genuinely useful and it is a narrower thing than people assume, because several of the questions an investigation actually cares about sit outside it no matter how carefully the columns are read.

Work the sort below before reading on, because getting one wrong is more informative than getting them all right.

Sort each question by whether pslist answers ittap a question, then a bucket
pslist answers itanother plugin does

Two of the five are answered by columns you have already read and three are not answered at all, and the pattern behind that split is worth naming. pslist reports the process OBJECT: its identity, its descent, its timing, where it sits in memory. Everything about what the process is doing, holding or talking to lives in different structures entirely, which is why those questions need different plugins rather than more careful reading of this one.

The third question is the one to sit with. The listing you have read on this page would look exactly the same if explorer.exe had been running attacker code the entire time, because pslist reports that a process exists and never what is inside it.

That is not a criticism of the plugin. It is doing exactly what it claims, and the error is entirely on the analyst who treats a clean process list as evidence that a machine is clean. A process list showing nothing unusual is a statement about process objects and about nothing else.

There is a fourth gap that announces itself less than the others. A process that exited before the capture has been unlinked from the list this plugin walks, so it leaves no blank line and no marker: the output looks exactly as complete as it would if nothing were missing. There is no gap in the numbering, no placeholder, no warning. That is what the next sub is about, and it is the reason this one kept mentioning that pslist walks a list.

07

The Six Commands as a Sequence

What you type, in order, on any image

Everything above collapses into a short sequence you can run on any Windows image without thinking about it, which is the point: the thinking should go on the output rather than on what to type.

$ vol -f IMAGE windows.info                          # does it read? what time?
$ vol -q -r csv -f IMAGE windows.pslist > ps.csv     # list, into a spreadsheet
$ vol -f IMAGE windows.pstree                        # descent, and orphans
$ vol -f IMAGE windows.netscan                       # connections, with owners
$ vol -f IMAGE windows.handles --pid TARGET          # what it has open
$ vol -f IMAGE windows.dlllist --pid TARGET          # what is loaded inside

Three things about that sequence are worth noticing. The order is not arbitrary: windows.info proves the tooling works before you invest time, the process list gives you targets, and the last two commands need a target to point at. The global options sit before the plugin name in every line. And four of the six take no options at all, which is the general case with Volatility: most plugins do one thing and need only the image.

The first four are run against every image and cost a few minutes between them. The last two are aimed at a process the first four told you to look at, which is why TARGET is a placeholder rather than a number.

Ten minutes is a realistic estimate for the first four on a workstation image, most of it spent waiting rather than typing. On this image TARGET is arguably 1428, because explorer.exe is the parent of the one process that arrived five hours after everything else. That is a question rather than a finding, and the difference matters for what you do next. A question sends you to another command; a finding goes in a report. An analyst who writes "cmd.exe launched at 14:07, indicating compromise" has skipped every command in this module and produced a sentence nobody can check and nobody can act on.

The rest of the module is the sequence that turns that question into something defensible, one plugin at a time. Each one narrows what is possible, and the report is written from what is left rather than from what looked suspicious at the start.

08

The Same Image as a Drive Letter

MemProcFS, and when browsing beats a plugin

Volatility answers a question you already know how to ask. MemProcFS answers the ones you do not, by mounting the image as a filesystem and letting you look around.

PS> MemProcFS.exe -device NE-ENG-031-1431.raw -mount M
PS> dir M:\name

    Directory: M:\name

explorer.exe-1428
cmd.exe-3312
lsass.exe-672

Every process is a directory, named and numbered, and inside each one are files holding what the plugins return: the command line, the loaded modules, the handles, the memory regions. Reading a process becomes dir and type rather than remembering which plugin carries which field.

That difference matters more than it sounds when you do not yet know what you are looking for. A plugin returns what you asked for; a filesystem lets you notice something you were not looking for, which is most of what early triage actually is.

There is a second advantage that only shows up under pressure. Everything in that mount is readable by tools that know nothing about memory forensics: findstr, a text editor, a script somebody else wrote, your own grep habits. An image stops being a format requiring a specialist tool and becomes a directory tree, which is the difference between handing a colleague a file and handing them something they can actually open.

The trade is that a filesystem has no history. You cannot diff two mounts the way MF0.2 diffs two listings, and you cannot pipe a directory into a counting command as easily as a CSV. Exploration and analysis are different activities and these two tools are good at different ones.

It takes -pagefile0 pagefile.sys where you have the page file, which recovers memory Windows had written out to disk, and -device pmem against a live machine with the WinPMEM driver rather than against a file at all.

Neither tool replaces the other. Volatility scripts and produces text you can filter, count and diff, which is every workflow in this module. MemProcFS is faster to explore and far easier to hand to somebody who has never used either. Run whichever fits the question, and know that both read the same structures underneath.

09

Practice

Install Volatility and run the sequence
hands on
Do this Get the tool working and run all six commands
  1. Install both tools: Volatility 3 from the project's repository, and MemProcFS from its releases page. Confirm each runs before going further.
  2. Confirm Volatility's own help works: vol -h should list the global options this sub used, and vol -f x windows.info -h should show the plugin's own. Knowing which options belong where is most of using the tool.
  3. Get an image to work on. Capture your own from a virtual machine you control, or use one of the public sample images the Volatility project and several universities publish for teaching. Either works for this exercise.
  4. Run windows.info first, every time. Note the architecture, the DTB and the system time, and compare that time against when you took the capture.
  5. Run the pslist-to-CSV command, open it in a spreadsheet and sort by CreateTime. Identify the boot cluster, the logon cluster, and anything that sits apart from both.
  6. Run windows.pstree and find every process sitting at the left margin with no parent above it. Explain each one. On a clean machine you should be able to account for all of them.
  7. Pick one process and run windows.handles --pid and windows.dlllist --pid against it. You will not understand most of the output yet, and the point is to see what the commands return before the module explains them.
What you should end up with: Volatility 3 installed and running, all six commands executed against a real image, a process list sorted by creation time with its clusters identified, and every unparented process in the tree explained. That is the working loop for the rest of the course.

Extend it

Once the six commands run, these are the variations worth trying.

  1. Run the same plugin with -r quick, -r pretty and -r json and see what each is good for. The json output is what you would parse if you were automating any of this.
  2. Run windows.pslist --dump --pid with -o pointing at an empty directory, and look at what it writes. Knowing the tool can extract an executable changes what you do when you find a suspicious one.
  3. Time windows.info on a large image. It is slower than you expect because Volatility is scanning to find the kernel, which is why the --save-config option exists.
  4. Deliberately run a plugin option before the plugin name and read the error. Making the mistake once on purpose is cheaper than making it at three in the morning.

Drill two is the one with the most in it. An executable pulled out of memory is not the same bytes as the file on disk, because the loader has already done its work on it, and seeing that for yourself explains a great deal about why memory analysis and file analysis answer different questions.

Drill four is worth the thirty seconds. Volatility's argument order is the commonest source of friction for people new to it, and the error message is clear once you have seen it.

You can run the six commands that start any Windows memory investigation, mount the same image with MemProcFS and browse it, know which options go before the plugin and which after, read every column windows.pslist prints, and name which plugin closes each gap the process list leaves. MF0.2 runs a seventh command that returns a different answer from pslist against the same image.