pslist vs psscan vs psxview — finding hidden processes
3 min read
"List the processes" sounds simple. In memory forensics there are
three ways to do it, and the differences between them are exactly
where rootkits get caught. The technique has not changed in fifteen
years; it still works because adversaries who unlink from
ActiveProcessLinks still leave the _EPROCESS object sitting in the
pool.
pslist — the live linked list
pslist walks ActiveProcessLinks, the kernel's doubly-linked list of
_EPROCESS structures (the same list Task Manager reflects).
ramparser does this with exact offsets from the kernel PDB: it locates
the System process (PID 4), takes its DirectoryTableBase as the
kernel CR3, and walks the list through the real page tables.
Accurate, but the technique only sees processes that are still linked
into the list. A DKOM ("Direct Kernel Object Manipulation") rootkit
that unlinks its own _EPROCESS from ActiveProcessLinks becomes
invisible to pslist.
psscan — pool scanning
psscan ignores the list entirely. It scans physical memory for
_EPROCESS allocations by their pool signature (_POOL_HEADER tagged
Proc for processes). It finds:
- Processes that have exited (the memory has not yet been reused).
- Processes that were unlinked from
ActiveProcessLinks. - Processes that the kernel never finished linking in (rare, but real on certain malware that races startup).
The trade-off: pool scanning is more sensitive to build-specific
layout than a symbol-driven list walk. ramparser validates each
candidate _EPROCESS with structural heuristics (plausible PID,
printable image name, page-aligned DirectoryTableBase, canonical
kernel list pointer) to keep false positives low.
psxview — the cross-view
A classic DKOM rootkit unlinks its process from ActiveProcessLinks
so pslist cannot see it. The _EPROCESS object is still in memory,
so psscan can. psxview is the diff:
| Seen by psscan | Seen by pslist | Verdict |
|---|---|---|
| ✔ | ✔ | normal |
| ✔ | ✘ | HIDDEN — unlinked, investigate |
| ✘ | ✔ | usually a transient race; revisit |
A row present in the pool scan but missing from the live list is the signature of a hidden process. It is one of the few places in memory forensics where the absence of evidence in one source is itself the evidence.
Beyond DKOM
Real adversary tradecraft moves beyond simple ActiveProcessLinks
unlinking. Things to add to the cross-view in a serious investigation:
csrss.exehandle table — Windows session manager keeps handles to every process. A row incsrsshandles but absent from both pslist and psscan is very hidden.- Object directory walk under
\BaseNamedObjectsand\KernelObjects. Some hiders forget to scrub the namespace. - Thread / job object scans. A hidden process still has running threads.
Volatility 3's psxview adds more sources by default; ramparser's
current implementation focuses on the pslist/psscan diff. For deeper
hiding scenarios, escalate to Volatility 3.
Practical workflow
- Run pslist for the ground-truth live process tree.
- Run psscan to surface exited / unlinked objects.
- Use psxview to flag the discrepancies automatically.
- For any
HIDDENrow, pivot tocmdline,dlllist, andnetscanfor that PID. Two pivots agreeing on something suspicious is a real finding.
ramparser runs these views together so the diff is one open table,
not three tools. When triage flags hidden processes, the
memory analysis workflow covers
the next stages — malfind, kernel callbacks, YARA — that build the
full kernel-hiding picture.