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