Reading width
Wide uses the full column for everything, text, diagrams, code, and exercises. Narrow keeps the standard reading width.
Text size
Scales the body text. Headings and code blocks keep their size.
In this section
Volatility windows.handles: Reading Type, GrantedAccess and What a Handle Proves
Introduction
The previous sub found a single page of unbacked executable memory inside explorer.exe and stopped there, because the plugin that found it can say nothing more. This sub asks a different question about the same process, and it is one of the four that MF0.1 listed as beyond a process list: what does this process have open?
A handle is a process's reference to something the kernel manages on its behalf: a file, a registry key, an event, another process. The kernel keeps a table of them per process, which is exactly the kind of bookkeeping this course has been reading all along. That table is one of the richest sources in a memory image and one of the easiest to over-read, because the column that carries the finding is not the one most people look at.
Scenario
You have a process worth investigating and a colleague who tells you that anything holding a handle to lsass.exe is stealing credentials. That is a rule of thumb which produces false positives on every healthy machine you will ever examine, because dozens of legitimate processes hold handles to lsass for entirely ordinary reasons. The difference between noise and a finding is a single hexadecimal number in one column, and this sub is about reading it.
Running windows.handles Against One Process
Seven columns, and the two that carry the findingThe invocation follows the pattern of everything so far: global options first, then the plugin, then the plugin's own options.
$ vol -f NE-ENG-031-1431.raw windows.handles --pid 1428
PID Process Offset HandleValue Type GrantedAccess
1428 explorer.exe 0xa88e41c2d3d0 0x94 File 0x100020
1428 explorer.exe 0xa88e41b376e0 0x1f8 Event 0x1f0003
1428 explorer.exe 0xa88e41c34690 0x214 Key 0x20019
1428 explorer.exe 0xa88e41bc6080 0x2e0 Process 0x1410
(Name column shown separately below; it runs past the page width)
Four rows from several hundred. A desktop process holds an enormous number of handles and almost all of them are files, registry keys and synchronization objects it needs to do its job, which is why running this plugin without a specific question produces more output than anybody can read.
Read the columns in the order they are printed, as with every plugin so far. Type says what kind of object the handle refers to, and it is the column to filter on first. File, Key, Event and Mutant make up the bulk of any process's handles and are rarely where an investigation starts. Process and Thread are the types worth looking at, because holding a reference to another running program is a more deliberate act than opening a file, and far fewer pieces of software do it.
The Name column, cut above so the rows fit, is where each of those four points. The file handle names a device path, the key handle names a registry path, the event has no name, and the process handle reads lsass.exe Pid 672.
Name identifies the object where the kernel recorded one. For a file it is a device path, for a registry key it is the key path, and for a process it is the target's name and identifier, which is why the fourth row above reads lsass.exe Pid 672 and matches the PID from the very first sub in this module.
Offset and HandleValue identify the handle itself: where the object sits and the number the process uses to refer to it. Both are worth carrying into a report for the same reason an offset was worth carrying in MF0.6, and neither is where the finding lives.
GrantedAccess is the column this sub exists for, and it is the one people scroll past because it looks like an implementation detail. It is not. It is the difference between a handle that proves nothing and a handle that is worth an hour of somebody's time.
Why a Handle to lsass Is Not a Finding
Dozens of processes hold one on every healthy machineWindows opens handles to processes constantly, and overwhelmingly for mundane reasons. Anything that wants to know whether a process is still running, wait for it to exit, ask for its name, or query how much memory it is using opens a handle to do so.
That means handles to lsass.exe are ordinary. Management tooling holds them, security software holds them, parts of Windows itself hold them, and a detection rule that treats their mere existence as evidence will fire on every machine in the estate on the day it is deployed, which is how rules get switched off.
PS> vol -f NE-ENG-031-1431.raw windows.handles | Select-String lsass
PID Process Type GrantedAccess Name
1344 MsMpEng.exe Process 0x1fffff lsass.exe Pid 672
624 services.exe Process 0x1000 lsass.exe Pid 672 ◄ name only
1428 explorer.exe Process 0x1410 lsass.exe Pid 672 ◄ can read memory
Three processes, three handles to the same target, and the target is not the finding. Every row names lsass.exe Pid 672, so a rule written on the target alone fires on all three and on dozens more across the estate.
The fourth column separates them. The service manager holds a handle it cannot read memory through; the shell holds one it can.
That is the whole correction, and it turns an unusable rule into a usable one without discarding the instinct behind it. The question is never whether a handle to a sensitive process exists; it is what that handle was granted permission to do.
The reason this works as evidence is that the rights are fixed when the handle is opened. A caller asks for the access it wants, the kernel decides whether to grant it, and the result is recorded in the handle for as long as it exists. Nothing later can quietly upgrade it, so the number in that column is a record of what was asked for and allowed at a specific moment.
It also means the value answers a question about intent in a limited but real way. Rights are requested, not assigned at random, so a caller that holds wide access to a process asked for wide access to that process. Whether it then used them is a separate question, and the asking is itself a deliberate act rather than an accident of how the software was written.
That property makes GrantedAccess unusually good evidence by the standards of the previous subs. It is kernel-maintained, it is set once, and user-space code cannot rewrite it without the kernel's agreement.
Reading the GrantedAccess Value
A bit field, and the bits that matterGrantedAccess is printed as hexadecimal because it is a set of flags packed into one number rather than a single value. Each bit corresponds to one permission, so reading it means asking which bits are set rather than what the number equals.
You do not need to memorize the encoding. What is worth knowing is the shape of the answers, because a handful of values account for most of what you will see.
0x1fffff everything the object type allows
0x1410 a specific, narrow set of rights
0x1f0003 a common full-access value for synchronization objects
0x100020 a limited file access
# a wide value on a sensitive target is what draws the eye.
0x1fffff is the value to recognize on sight. It is full access to the object, and a process holding full access to another process has been granted everything the kernel offers, including the rights needed to read and write that process's memory.
A narrow value is the opposite signal. A handle granted only the rights required to wait for a process or query its basic information is what ordinary software holds, and it cannot be used to read memory however much the holder might want to.
There is a trap in reading these values by size, and it is worth naming because it looks like a shortcut. A larger number does not mean wider access; the bits mean different things for different object types, so a high-looking value on a File handle and a modest-looking one on a Process handle are not comparable at all. Compare within a type, never across them.
That is why the workflow below filters by Type first and only then looks at access. Doing it the other way round produces a list sorted by an arithmetic property that does not correspond to anything.
The practical approach is to sort by that column rather than read it row by row. Wide values on Process handles float to one end, and on a workstation there will be very few of them, which turns several hundred rows into a short list in one operation.
Finding the Rows That Matter Across an Image
Two filters, and a sortRunning the plugin against a single process is what you do once you already have a suspect. Running it across the whole image is how you find one, and it needs filtering to be usable.
$ vol -q -r csv -f NE-ENG-031-1431.raw windows.handles > handles.csv
$ grep -i "Process" handles.csv | grep -i lsass
$ grep -i "Process" handles.csv | sort -t, -k6 | tail -20
Three commands, and the first is the one that saves the afternoon. The first command produces the whole set once, which matters because this plugin is slow and you do not want to run it repeatedly. Everything after that is filtering a text file rather than re-reading a multi-gigabyte image, which changes the cost of asking a follow-up question from minutes to nothing.
The second narrows to handles whose target is lsass.exe, which on a healthy machine returns a page of rows, all of them boring. The third sorts every Process handle by its access value and shows the widest, which is the query that actually finds things rather than confirming what you already suspected.
The plugin also takes --offset, which points it at a process by its physical offset rather than its identifier. That is the handle that still works when a PID is duplicated or unreliable, which is the situation MF0.6 described, and it is worth knowing the option exists before the day you need it.
sorted descending by GrantedAccess, Process handles only ...
MsMpEng.exe -> several targets, wide access
services.exe -> several targets, wide access
explorer.exe -> lsass.exe Pid 672, 0x1410
That reveal is deliberately uncomfortable, and it is the most useful thing in this sub. Sorting by width alone does not hand you the answer, and an analyst who only ever looks at the top of that list will find security products every time.
So sorting is a filter rather than an answer, and the list it produces still has to be read with knowledge of the estate. The real question combines two things: what access was granted, and whether this program has any business holding that access to that target. explorer.exe holding any kind of reference into lsass.exe is worth a look precisely because the Windows shell has no ordinary reason to be interested in the credential process at all. That is the same judgment the previous sub made about unbacked memory, applied to a different structure.
What a Handle Does Not Establish
Access is not useThis is where findings about handles go wrong, and the error is always the same shape.
A handle records that a process asked for access and the kernel granted it. It does not record whether the holder ever used that access, what it read if it did, whether any read succeeded, or what happened to anything obtained. Those are four separate questions and none of them is answered by this plugin.
The handle establishes four things: that access was requested, that it was granted, by which process, and to which target. It establishes nothing about whether anything was read, what was read if anything was, whether a read succeeded, or what left the machine afterwards. Every one of those needs evidence from somewhere else.
Set out as two boxes, the distinction stops being subtle.
Everything to the right of the dashed arrow leaves no trace in this structure.
The last item on the right is the one that matters most in a report, because it is the claim a reader will supply themselves if you do not rule it out. A handle to the credential process with wide access sounds like credentials were taken, and the handle says nothing whatsoever about that.
There is a fifth thing it does not establish that is worth adding, because it is the one people forget entirely. A handle does not establish that the process which holds it is the thing that opened it. Code running inside a process opens handles as that process, so the holder is where the request came from rather than who made it, which is exactly the situation the previous sub set up.
That is not a reason to distrust the evidence. It is a reason to phrase the finding around the process rather than around an actor, and to let the question of who was driving that process be answered separately.
Writing this honestly is straightforward once the distinction is clear. The finding is that a process held a handle with particular rights to a particular target at a particular moment, and the appropriate next sentence names what would be needed to go further rather than implying it has been established.
That restraint is not caution for its own sake, and it is not modesty either. A report saying credentials were stolen invites the question of how you know, and having to retreat from it damages the findings around it that were solid.
Handles Outlast the Operation
What the timing does and does not tell youThere is a timing subtlety worth understanding because it cuts both ways.
A handle exists from the moment it is opened until the process closes it or exits, and plenty of software opens handles early and keeps them for its whole lifetime. So a handle present in a capture may have been opened seconds before the capture or hours before it, and the plugin does not report when.
A handle present in the image means it was open at capture time and that access was granted at some earlier point. It does not mean the handle was opened recently, that it was opened for the reason you have in mind, or anything at all about ordering relative to other events. The capture bounds it on one side only.
There is a second consequence that works in your favor, though, and it is the reason this plugin is worth running at all. A handle held open is a commitment: the holder has kept a reference to something and has not let go of it. Software that opens, uses and closes tidily leaves nothing, and software that keeps a handle open is telling you it expects to need that access again.
So a wide handle still open at capture time is a stronger observation than a narrow one, not because the width proves use, but because holding it open at all is a deliberate state rather than a leftover.
Drawn against a clock, the one-sided bound is obvious.
A clean handle table is consistent with nothing happening and with something that tidied up.
The useful consequence is that a handle's presence is bounded on one side only. It was opened at some point before the capture, and that is all the plugin can tell you about when.
The other direction matters more for an investigation and is easy to miss. An operation that opened a handle, used it and closed it before the capture leaves nothing at all in this output. Credential access that completed at 14:12 and tidied up after itself is invisible here, and the absence of a suspicious handle is therefore very weak evidence that nothing happened.
That is the same asymmetry as the earlier subs and it is worth carrying: presence in memory is strong, absence is weak, and an analyst who reports a clean handle table as evidence of innocence has misread what the source can do.
Where This Leaves the Investigation
Four subs, four structures, one processStand back from the plugin for a moment and look at what four of them have now produced about process 1428, because the accumulation is the point rather than any single row.
The process list gave a command prompt created at 14:07 whose parent is explorer.exe, with nothing else created for five hours either side. The scan added a short-lived update.exe two minutes later whose own parent cannot be found. malfind found an unbacked executable page inside explorer.exe containing a file header. And this sub adds a handle from the same process into lsass.exe.
Work that sort before reading on, because the split it produces is the shape of every investigation you will run. Two of the four findings are worthless in isolation and neither would survive a challenge on its own, and they become meaningful only because they sit alongside two that do carry weight.
That is also why the order of these subs matters. Running the handle plugin first, with no reason to look at any particular process, would have produced several hundred rows and no way to rank them. Arriving here with a specific process and a specific reason turns the same output into a short, targeted question.
An investigation is mostly a sequence of narrowings like that, and each plugin is cheap only because the one before it removed most of the candidates.
None of this is yet a conclusion, and it is important to see why not. Nothing so far establishes that code ran from that page, that the handle was used, or that anything left the machine. What four plugins have produced is a specific and checkable set of observations about one process, each with an address or a time attached to it, which is exactly what an investigation should look like at this stage and is a great deal better than a suspicion.
Practice
Calibrate on handles you can account for hands on- Run
windows.handlesagainst a whole image with-q -r csvinto a file. Note how long it takes, because this is one of the slower plugins and you will want the results cached rather than regenerated. - Count the total rows, then count the rows whose Type is Process. The ratio is the point: the interesting handles are a tiny fraction of the total, which is why filtering by type is the first move.
- Filter to Process handles whose target is lsass.exe and count them. On a healthy machine this is not zero, and knowing your own number is what stops the first one you ever see looking like an incident.
- Sort those rows by GrantedAccess and identify which processes hold the widest. Look each one up. They will mostly be security products and service management, and learning your estate's set is a one-off investment.
- Pick one row and write the finding sentence for it: which process, which target, which rights, at capture time. Then write the sentence stating what it does not establish. Both sentences belong in a report.
Extend it
These build the calibration that makes handle output a tool rather than an alarm.
- Open a handle to another process yourself using any ordinary administrative tool, capture memory, and find your own handle in the output. Seeing a benign one you created is the fastest way to stop over-reading them.
- Compare the handle counts of a workstation and a server image. The difference in volume explains why a rule tuned on one fires constantly on the other.
- Look up what the wide access value means for a Process object specifically. Knowing which individual rights it contains is what lets you explain a finding to somebody who asks.
- Find a published report that cites a handle to lsass as evidence of credential theft and check whether it quotes the access rights. Many do not, and now you know what that omission costs.
Drill three is the one to do if you expect to explain a finding to somebody senior. Being able to say which specific rights a wide access value contains, rather than that it is wide, is the difference between a claim somebody accepts and a claim somebody queries.
Drill one is worth doing before the others. An analyst who has created a benign handle and seen it in the output has an intuition that no amount of reading provides, and it permanently removes the instinct to treat every handle as sinister.
You can run windows.handles, filter by Type to reach the rows worth reading, explain why a handle to lsass.exe is not a finding and what makes one interesting, read a GrantedAccess value as a set of rights rather than a number, and state the four things a handle does not establish. MF0.5 asks what the machine was talking to, and which process was responsible.