All posts

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 psscanSeen by pslistVerdict
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.exe handle table — Windows session manager keeps handles to every process. A row in csrss handles but absent from both pslist and psscan is very hidden.
  • Object directory walk under \BaseNamedObjects and \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

  1. Run pslist for the ground-truth live process tree.
  2. Run psscan to surface exited / unlinked objects.
  3. Use psxview to flag the discrepancies automatically.
  4. For any HIDDEN row, pivot to cmdline, dlllist, and netscan for 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.

Further reading