daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

handle-enumeration.md (12384B)


      1 ---
      2 title: "Handle Enumeration and Kernel Object Addresses"
      3 description: "NtQuerySystemInformation with SystemHandleInformation to list every handle on the host, and confirming the kernel object behind one in WinDBG with !object and !process."
      4 category: dfir
      5 subcategory: "Windows Internals"
      6 tags: [windows, detection-engineering, memory-forensics, kernel, threat-hunting]
      7 tools: [windbg, process-hacker, handle, visual-studio]
      8 difficulty: advanced
      9 updated: 2026-10-04
     10 upstreamName: "ired.team"
     11 upstreamUrl: "https://ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/get-all-open-handles-and-kernel-object-address-from-userland"
     12 upstreamAuthor: "Mantvydas Baranauskas"
     13 upstreamLicense: none
     14 upstreamRelation: derived
     15 references:
     16   - name: "SYSTEM_HANDLE_INFORMATION — Process Hacker"
     17     url: "https://processhacker.sourceforge.io/doc/struct___s_y_s_t_e_m___h_a_n_d_l_e___i_n_f_o_r_m_a_t_i_o_n.html"
     18     relation: inspired
     19     note: "Field layout of the undocumented structures the query returns."
     20   - name: "SystemHandleInformation — Geoff Chappell"
     21     url: "https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/handle.htm"
     22     author: "Geoff Chappell"
     23     relation: inspired
     24     note: "Per-build behaviour of the information class, including which builds truncate the PID field."
     25   - name: "NtQuerySystemInformation — Microsoft Learn"
     26     url: "https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation"
     27     relation: inspired
     28     note: "The documented call and its STATUS_INFO_LENGTH_MISMATCH contract."
     29 ---
     30 
     31 ## What this is
     32 
     33 A single call enumerates every open handle on the machine — processes, threads, files,
     34 registry keys, sections, mutants, tokens — and for each one gives you the kernel virtual
     35 address of the object it refers to. No driver, and on most builds no elevation needed to get
     36 the list itself. That makes it the cheapest way to answer "what does this process have hold
     37 of", which is a question that distinguishes injection and credential access from normal
     38 behaviour better than almost anything else in user mode.
     39 
     40 `NtQuerySystemInformation` with information class `SystemHandleInformation` (`0x10`) returns a
     41 `SYSTEM_HANDLE_INFORMATION`: a count, followed by that many
     42 `SYSTEM_HANDLE_TABLE_ENTRY_INFO` records.
     43 
     44 ```cpp
     45 typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO {
     46     USHORT UniqueProcessId;          // owner PID — truncated to 16 bits
     47     USHORT CreatorBackTraceIndex;
     48     UCHAR  ObjectTypeIndex;          // index into the object type table
     49     UCHAR  HandleAttributes;         // OBJ_INHERIT, OBJ_PROTECT_CLOSE
     50     USHORT HandleValue;              // the handle as the owner sees it
     51     PVOID  Object;                   // kernel address of the object
     52     ULONG  GrantedAccess;            // the access mask the handle was opened with
     53 } SYSTEM_HANDLE_TABLE_ENTRY_INFO, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO;
     54 
     55 typedef struct _SYSTEM_HANDLE_INFORMATION {
     56     ULONG NumberOfHandles;
     57     SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
     58 } SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
     59 ```
     60 
     61 Neither structure is documented by Microsoft. Both are stable enough to rely on and have
     62 been for twenty years, but `UniqueProcessId` being a `USHORT` is a real limitation: PIDs
     63 above 65535 wrap. Use `SystemExtendedHandleInformation` (`0x40`) instead when you need the
     64 full-width PID and object-type handling.
     65 
     66 ## Prerequisites
     67 
     68 - `ntdll.dll` — resolve the export at runtime with `GetProcAddress`; there is no import
     69   library.
     70 - A buffer sized by retry, not by guess. The call returns `STATUS_INFO_LENGTH_MISMATCH`
     71   (`0xC0000004`) when the buffer is too small, and the handle count changes between calls, so
     72   the only correct pattern is a loop that grows the buffer until the status is not that.
     73 - Kernel debugging for the confirmation half — see
     74   [the kernel debugging lab](/sheets/dfir/kernel-debugging-lab) for the transport setup.
     75 - Process Hacker for cross-checking without a debugger.
     76 
     77 ## Walkthrough
     78 
     79 ### The call
     80 
     81 ```cpp
     82 #define SystemHandleInformation 0x10
     83 
     84 using fNtQuerySystemInformation = NTSTATUS(WINAPI*)(
     85     ULONG SystemInformationClass, PVOID SystemInformation,
     86     ULONG SystemInformationLength, PULONG ReturnLength);
     87 
     88 auto NtQuerySystemInformation = (fNtQuerySystemInformation)GetProcAddress(
     89     GetModuleHandleW(L"ntdll"), "NtQuerySystemInformation");
     90 
     91 ULONG size = 0x10000, needed = 0;
     92 PSYSTEM_HANDLE_INFORMATION info = nullptr;
     93 NTSTATUS status;
     94 
     95 do {
     96     if (info) HeapFree(GetProcessHeap(), 0, info);
     97     info = (PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, size);
     98     status = NtQuerySystemInformation(SystemHandleInformation, info, size, &needed);
     99     size *= 2;
    100 } while (status == 0xC0000004);
    101 
    102 for (ULONG i = 0; i < info->NumberOfHandles; i++) {
    103     auto& h = info->Handles[i];
    104     if (h.UniqueProcessId != targetPid) continue;
    105     printf("handle 0x%04x  object 0x%p  type %u  access 0x%08x\n",
    106            h.HandleValue, h.Object, h.ObjectTypeIndex, h.GrantedAccess);
    107 }
    108 ```
    109 
    110 Two things worth saying about the loop. The records are not grouped by PID in any guaranteed
    111 order, so filter rather than break out on the first non-match. And `ObjectTypeIndex` is an
    112 index into the object-type table, whose numbering changes between builds — resolve it against
    113 `NtQueryObject(ObjectTypeInformation)` or a `SystemExtendedHandleInformation` pass rather than
    114 hard-coding "8 means File".
    115 
    116 Run it against PID 4 and you get the `System` process's handles, which is a useful smoke
    117 test because that set is large and stable.
    118 
    119 <figure class="shot">
    120   <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28600%29.png"
    121        alt="Console output listing handle values, kernel object addresses and owning PID for every handle held by the System process"
    122        loading="lazy" referrerpolicy="no-referrer">
    123   <figcaption>Handle value, kernel object address and owner PID for each handle held by PID 4.
    124     <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
    125   </figcaption>
    126 </figure>
    127 
    128 ### Cross-check against Process Hacker
    129 
    130 Process Hacker's Handles tab for the same process shows the same handle values, and its
    131 Properties pane shows the same kernel object address. Confirming one handle both ways is
    132 worth doing before you trust a parser you just wrote, because a buffer-sizing bug produces
    133 plausible-looking garbage rather than an error.
    134 
    135 <figure class="shot">
    136   <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28601%29.png"
    137        alt="Process Hacker handles view for the System process with handle 0x4 selected, showing its object address in kernel memory"
    138        loading="lazy" referrerpolicy="no-referrer">
    139   <figcaption>The same handle in Process Hacker: handle value, owning PID and object address.
    140     <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
    141   </figcaption>
    142 </figure>
    143 
    144 ### Confirm the object in the kernel
    145 
    146 The `Object` field is a kernel virtual address. In a kernel debugging session you can ask
    147 what lives there:
    148 
    149 ```erlang
    150 !object 0xffff8f077c882300
    151 ```
    152 
    153 This resolves the object header and names its type, which is the authoritative answer to
    154 "what is handle 0x4 actually a handle to".
    155 
    156 <figure class="shot">
    157   <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28602%29.png"
    158        alt="WinDBG !object output for a kernel address reporting a valid object header of type Process"
    159        loading="lazy" referrerpolicy="no-referrer">
    160   <figcaption><code>!object</code> confirms the address is a valid object and names its type.
    161     <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
    162   </figcaption>
    163 </figure>
    164 
    165 For a process object, go further and overlay `_EPROCESS` to read the identity fields:
    166 
    167 ```erlang
    168 !process 0xffff8f077c882300 0
    169 dt _eprocess 0xffff8f077c882300 UniqueProcessId ImageFileName
    170 ```
    171 
    172 `UniqueProcessId` and `ImageFileName` from `_EPROCESS` are the kernel's own record of which
    173 process this is — not the PEB's claim, the kernel's.
    174 
    175 <figure class="shot">
    176   <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28605%29.png"
    177        alt="WinDBG dt _eprocess output printing UniqueProcessId 4 and ImageFileName System for the object address"
    178        loading="lazy" referrerpolicy="no-referrer">
    179   <figcaption><code>_EPROCESS</code> overlaid on the object address: PID and image name straight from the kernel.
    180     <span class="shot-credit">ired.team · Mantvydas Baranauskas</span>
    181   </figcaption>
    182 </figure>
    183 
    184 ## What it tells you
    185 
    186 **`GrantedAccess` on process handles is the highest-signal field in the whole table.** A
    187 handle is opened with a specific access mask, and the mask tells you what the opener intended
    188 to do. The combinations that matter:
    189 
    190 | Mask bits | Right | Why it is interesting |
    191 |---|---|---|
    192 | `0x0008` | `PROCESS_VM_OPERATION` | Needed to allocate or change protection in another process. |
    193 | `0x0010` | `PROCESS_VM_READ` | Reading another process's memory — the credential-dumping precondition. |
    194 | `0x0020` | `PROCESS_VM_WRITE` | Writing another process's memory. |
    195 | `0x0002` | `PROCESS_CREATE_THREAD` | Needed for `CreateRemoteThread`. |
    196 | `0x0040` | `PROCESS_DUP_HANDLE` | Stealing a handle someone else already holds. |
    197 | `0x1F0FFF` | `PROCESS_ALL_ACCESS` | The lazy injector's mask. Rarely legitimate across process boundaries. |
    198 
    199 `0x0008 | 0x0020` together on a handle to a process the holder has no relationship with is
    200 the handle-table shape of [classic DLL injection](/sheets/exploitation/dll-injection) and
    201 [process hollowing](/sheets/exploitation/process-hollowing). `0x0010` against `lsass.exe` is
    202 the shape of credential dumping. Enumerating handles gives you this *while the handle is
    203 still open*, which a process-access event log gives you only at open time.
    204 
    205 **Who holds a handle to what is a graph, and it is a small one.** Build the owner-to-target
    206 map for process-type handles across the host and the legitimate edges are few and
    207 predictable: service hosts to their children, the debugger to its debuggee, antimalware to
    208 everything. An edge from an unsigned binary in a user-writable directory to a system process
    209 is an anomaly you can find without any signature.
    210 
    211 **Handles outlive the API call that created them.** This is the practical advantage over
    212 telemetry. If a process opened `lsass` with `VM_READ` an hour ago and kept the handle, the
    213 event may have rolled out of your log but the handle is still in the table. Sweeping handles
    214 is therefore a good complement to event-based collection, not a substitute for it.
    215 
    216 **The same enumeration is an attack primitive, which is why it is worth watching.** Kernel
    217 object addresses are exactly what an exploit against a vulnerable signed driver needs: given
    218 an arbitrary kernel read or write, locating the `_EPROCESS` of a privileged process is the
    219 step between "I can write kernel memory" and "I am SYSTEM". The artefact to look for is not
    220 the enumeration itself — it is too common — but a non-administrative process that enumerates
    221 handles and then opens a handle to a driver object it has no reason to touch.
    222 
    223 **Prefer the extended class in anything you keep.** `SystemExtendedHandleInformation` returns
    224 a full-width `UniqueProcessId` and a wider record. The classic class is fine for a lab and
    225 quietly wrong on a busy host with high PIDs.
    226 
    227 ## References
    228 
    229 - [NtQuerySystemInformation — Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation)
    230 - [SystemHandleInformation — Geoff Chappell](https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/handle.htm)
    231 - [SYSTEM_HANDLE_INFORMATION — Process Hacker docs](https://processhacker.sourceforge.io/doc/struct___s_y_s_t_e_m___h_a_n_d_l_e___i_n_f_o_r_m_a_t_i_o_n.html)
    232 - [Process access rights](https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights)
    233 - [The kernel debugging lab](/sheets/dfir/kernel-debugging-lab) — getting a session in which
    234   `!object` works