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 }
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.