Windows Process and Thread Internals: Handles, Tokens, and Objects
How the Windows kernel represents processes as containers of handles and a security token, and the tools to inspect both live.
A “process” on Windows is a more structured kernel object than the Unix fork/exec model suggests — it’s a container for a handle table, an access token, a virtual address space, and one or more threads, all managed through the kernel’s general-purpose Object Manager rather than process-specific machinery.
The Object Manager: everything is a handle to an object
Nearly everything a process interacts with in the kernel — files, registry keys, mutexes, other processes, threads — is represented as an object, and a process refers to one via a handle: an opaque, per-process integer index into that process’s private handle table.
HANDLE hFile = CreateFile(L"C:\\data.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
Because handle tables are per-process, the same handle value in two different processes refers to entirely different objects — handles aren’t meaningful without knowing which process’s table they belong to, which is why inter-process handle sharing requires an explicit DuplicateHandle call rather than just passing the integer across.
Get-Process notepad | Select-Object -ExpandProperty HandleCount
Inspecting handles: what does a process actually have open
handle.exe (Sysinternals) and Process Explorer both walk a process’s handle table directly, which is the equivalent of lsof on Unix — invaluable for finding out exactly which process holds a lock on a file you’re trying to delete:
handle.exe -a locked_file.txt
Access tokens: the security context riding along with every process
Every process carries an access token — a kernel object describing the security context it runs under: the user SID, group SIDs, privileges (like SeDebugPrivilege or SeBackupPrivilege), and the process’s integrity level. Every security check the kernel performs — can this process open that file, can this process open that other process — compares the caller’s token against the target object’s security descriptor.
whoami /priv
whoami /groups
Integrity levels: a second axis beyond user permissions
Since Windows Vista, tokens also carry an integrity level (Low, Medium, High, System), enforced independently of standard discretionary permissions — a process running at Low integrity (like a sandboxed browser renderer) is blocked from writing to Medium-or-higher integrity objects even if the user’s own account would otherwise have write access, which is conceptually the same idea as SELinux/AppArmor’s mandatory access control layered on top of discretionary Unix permissions.
[System.Diagnostics.Process]::GetCurrentProcess() | Select-Object Id, ProcessName
Get-Process -Id $PID | ForEach-Object { $_.Handle }
icacls somefile.txt /setintegritylevel Low
UAC and token splitting
User Account Control exists specifically because of token design: an administrator’s logon actually produces two tokens — a full-privilege administrator token and a filtered, standard-user token — and processes run with the filtered token by default. “Running as administrator” via a UAC elevation prompt is the mechanism that swaps in the full token for that one process tree, rather than elevating the entire logon session.
Start-Process powershell -Verb RunAs
Threads: the actual unit of execution
A process itself doesn’t execute anything — every process has at least one thread, the kernel’s actual schedulable unit, each with its own stack, register state, and thread-local storage, all sharing the parent process’s address space and handle table.
Get-Process notepad | Select-Object -ExpandProperty Threads | Select-Object Id, PriorityLevel, ThreadState
Job objects: grouping processes for shared limits
A job object groups multiple processes so they can be managed and limited as a unit — CPU rate limits, memory limits, or “kill all of these when the job is closed” — the mechanism Windows containers and modern browser sandboxing both rely on, and structurally similar in purpose to a Linux cgroup, if simpler in scope.
Get-Process | Where-Object { $_.Id -in (Get-CimInstance Win32_Process | Where-Object ParentProcessId -eq $PID).ProcessId }
The PEB and TEB: user-mode-visible process and thread state
Not everything about a process lives purely in kernel memory inaccessible to its own code — the Process Environment Block (PEB) and Thread Environment Block (TEB) are kernel-populated but user-mode-readable structures, mapped directly into the process’s own address space specifically so user-mode code (and the loader, and language runtimes) can access frequently-needed process and thread state without an expensive kernel transition for every lookup. The PEB holds process-wide information — the loaded module list, the current working directory, command-line arguments, and various process-wide flags the loader and CRT rely on constantly — while each thread gets its own TEB, holding that thread’s last-error value, thread-local storage slots, and (via a pointer inside the TEB) the way to reach back to the shared PEB. On x86-64, the TEB is reachable directly through the GS segment register (and FS on 32-bit x86) without any explicit handle or syscall at all, which is exactly why the PEB/TEB pair shows up constantly in exploit development and malware analysis: reading the PEB pointer off the segment register is one of the oldest, most reliable ways to detect a debugger (PEB.BeingDebugged) or self-locate loaded modules without calling a single monitorable API.
Why the handle table works the way it does
A process’s handle table isn’t a flat array indexed directly by handle value — it’s structured more like a multi-level page table, letting the kernel grow it dynamically as a process opens more objects without needing to pre-allocate a huge, mostly-empty array up front or relocate every existing handle’s underlying storage when it grows. Each entry stores a pointer to the underlying kernel object plus a per-handle access mask (the specific rights that handle was granted, which can be narrower than what the object’s own security descriptor would allow — a process can deliberately hand out a read-only handle to a file it has full read/write access to). This is why Get-Process | Select HandleCount climbing steadily over a process’s lifetime, without ever coming back down, is a meaningful leak signal: it means something is opening handles to objects (files, registry keys, events) and never calling CloseHandle, and the table itself will keep growing to accommodate them rather than erroring out until it hits the per-process handle limit.
Why this model matters for troubleshooting
“Access denied” errors on Windows are almost never a single simple permission check — they’re the result of the token’s user/group SIDs, privileges, and integrity level all being evaluated against the target object’s security descriptor simultaneously. Understanding that a process’s security context is a specific, inspectable kernel object (not just “whichever user double-clicked the icon”) is what turns a vague permissions problem into a concrete, checkable one: what’s this process’s integrity level, what privileges does its token actually hold, and does the target object’s ACL grant access to any of the SIDs in that token.
Related:
- The Windows Security Model: ACLs, Integrity Levels, and UAC
- Windows Registry Internals: Hives, Keys, and How Settings Actually Persist
Sources: