The Windows Object Manager and Handle Tables

Windows Internals
Time it takes to read this article 7 minutes.

If you spend enough time reverse engineering Windows, debugging drivers, or hunting malware, you eventually notice that wildly different resources — a running process, an open file, a named mutex, a registry key, an access token — are all manipulated through the same small set of kernel primitives. That is not a coincidence. Underneath, the Windows kernel represents most shareable resources as objects, and a single executive component called the Object Manager (its routines are prefixed Ob) is responsible for creating them, naming them, securing them, and tracking their lifetime. This uniformity is one of the most elegant design decisions in Windows: instead of a dozen bespoke resource managers, there is one model for naming, one model for security, and one model for lifetime that applies to processes, threads, files, events, sections, keys, tokens, and more. This article dissects that model — the anatomy of an object, the type system, the namespace, and the handle tables that connect user mode to it all.

Object Structure: Header and Body

Every object the kernel manages consists of two conceptual parts: a header that the Object Manager owns, and a body that the owning subsystem owns. When code calls ObReferenceObjectByHandle and receives an object pointer, that pointer actually points at the body. The header sits immediately before it in memory, at a negative offset.

The _OBJECT_HEADER structure contains the fields the Object Manager needs to do its job. The most important are:

  • PointerCount — the number of kernel references to the object (referenced pointers held in the kernel).
  • HandleCount — the number of open user- or kernel-mode handles to the object.
  • TypeIndex — an index identifying the object’s type (Process, File, Event, and so on).
  • SecurityDescriptor — a pointer to the object’s security descriptor, which governs who may open it and with what access.
  • InfoMask — a bitmask describing which optional headers are present.

Objects can carry optional headers that, when present, are laid out in front of the main _OBJECT_HEADER. These include name information (_OBJECT_HEADER_NAME_INFO, which stores the object’s name and a pointer to its containing directory), quota information, process information, handle-revocation information, and creator information. Because they precede the header at variable offsets, the kernel uses InfoMask plus a lookup table to compute where each optional header lives. The body follows the header and is entirely type specific — for a Process object the body is the enormous _EPROCESS structure; for an Event it is a small _KEVENT.

Object Types

Each distinct kind of object is described by an _OBJECT_TYPE structure. The set of types is fixed at boot and includes Process, Thread, File, Event, Mutant (the kernel name for a mutex), Semaphore, Section, Key, Token, Directory, SymbolicLink, and ALPC Port, among others. The _OBJECT_TYPE records the type’s name, its default and valid access rights, its pool type, and — crucially — a set of methods the Object Manager invokes at defined moments in an object’s life.

These methods form a small virtual table: an open procedure, a close procedure, a delete procedure, a parse procedure, a security procedure, and a few others. The parse method is especially important — it is how the Object Manager delegates namespace traversal to a subsystem. When you open \Device\HarddiskVolume2\Windows\notepad.exe, the Object Manager resolves the path only as far as the device object, then calls the I/O manager’s parse method to handle the rest of the path inside the file system. Registry keys work the same way via the Configuration Manager.

The TypeIndex in each object header is an index into a global type array (ObTypeIndexTable). On modern Windows the stored TypeIndex is not the raw index: it is XOR-masked with a per-boot randomization cookie (ObHeaderCookie) and the low byte of the object header’s own address. This hardening makes it harder for an attacker with a kernel write primitive to forge a valid object header or pivot its type, because the correct TypeIndex value differs on every boot and every address.

The Object Namespace

Named objects live in a hierarchical namespace built out of Directory objects, each of which contains other named objects. It looks and behaves much like a file system, and you can browse it live with WinObj from Sysinternals. Some directories you will encounter constantly:

  • \ — the root directory.
  • \Device — device objects created by drivers (for example \Device\HarddiskVolume2).
  • \GLOBAL?? — the DOS device namespace, full of symbolic link objects. The link C: points to \Device\HarddiskVolumeX; PhysicalDrive0, COM ports, and pipe/mailslot prefixes live here too.
  • \BaseNamedObjects — the session-global namespace where user-mode named events, mutexes, semaphores, and sections are created. Per-session equivalents live under \Sessions\<n>\BaseNamedObjects.
  • \Sessions — per-session state, isolating one interactive session’s named objects from another’s.

A SymbolicLink object simply stores a target string; when the Object Manager encounters one during path parsing, it substitutes the target and continues. This indirection is powerful and, as we will see, a rich source of security bugs when a privileged process can be tricked into following an attacker-controlled link.

Handles and the Handle Table

User mode never touches raw object pointers — that would be a catastrophic security and stability hole. Instead, when a process opens or creates an object, the kernel returns a handle: an opaque value that is really an index into that process’s private handle table. The _EPROCESS structure has an ObjectTable field pointing at a _HANDLE_TABLE.

To keep handle lookups fast while supporting processes that hold hundreds of thousands of handles, the handle table is a sparse, multi-level array — up to three levels on x64. Each leaf entry is a _HANDLE_TABLE_ENTRY that packs two things together: an encoded pointer to the target object’s header, and the granted access mask for this particular handle. Because the low bits of each entry are used for flags (such as the inheritance and audit bits), handle values are always multiples of 4 — which is why you never see a handle value of 1, 2, or 3.

The same object can be referenced by many handles across many processes, each with a different granted-access mask. The access you were granted at open time is baked into your handle entry and cannot be silently widened later — a subsequent operation that needs more access will fail even though a different handle to the same object might allow it.

References vs Handles: Lifetime

An object stays alive as long as anything is using it, and the header tracks two independent kinds of usage. HandleCount counts open handles; PointerCount counts kernel references (including one implicit reference per handle, plus any pointers the kernel is actively holding via ObReferenceObject). Every CloseHandle decrements the handle count and drops the associated reference; every ObDereferenceObject drops a pointer reference. When both counts reach zero, the Object Manager calls the type’s delete method and frees the object’s memory.

This two-counter scheme explains a class of bugs and abuses you will meet in practice. A handle leak — code that opens handles but forgets to close them — pins objects in memory indefinitely; if those objects are large (sections, files) or scarce, the process bloats or the system runs out of a resource. From a forensic angle, the counters are why an object (and the credentials or memory it protects) can remain resident long after the code that created it appears to be done.

Access Rights and Security

Because objects are the unit of resource sharing, they are also the unit of access control. Every securable object can carry a security descriptor containing an owner, a group, a DACL (who is allowed or denied which access), and a SACL (what to audit). When a process opens an object, it requests a desired access mask. The Object Manager invokes SeAccessCheck, which compares the caller’s access token against the object’s DACL and computes the granted access mask. That granted mask is what gets stored in the handle table entry.

Two mechanisms let handles cross process boundaries. DuplicateHandle copies a handle from a source process into a target process, optionally narrowing (never widening beyond the source’s rights) the access mask. Handle inheritance lets a child process automatically receive copies of a parent’s handles that were created as inheritable, provided the child was launched with inheritance enabled. Both are legitimate and heavily used — and both, as the next section shows, are of keen interest to attackers.

Inspecting Objects and Handles in WinDbg / Sysinternals

All of this is directly observable. In a kernel debugger, the Object Manager extension commands walk the namespace and the handle tables for you:

# Dump the root object directory
!object \

# Dump the session-global named-object directory
!object \BaseNamedObjects

# Inspect the object header layout and fields
dt nt!_OBJECT_HEADER

# List handles in a process's handle table (flags 7 = show details),
# given the EPROCESS address of the target process
!handle 0 7 <EPROCESS>

# The reverse: find which processes hold a handle to a given object
!object <object-address>

On a live system without a debugger, two Sysinternals tools cover the same ground. WinObj browses the object namespace graphically, showing directories, symbolic links, and their targets. handle.exe (and its GUI cousin Process Explorer) enumerates the handles a process holds, which is invaluable for answering questions like “which process has this file locked?” or “what is this process keeping open?”

Security Relevance

The object and handle model is not just architecture trivia — it is where a surprising amount of offensive and defensive security actually happens:

  • Handle enumeration as a signal. The handles a process holds describe its intentions. A non-security process holding an open handle to lsass.exe with PROCESS_VM_READ is a classic credential-theft indicator; EDRs and hunters watch for exactly this.
  • Leaked privileged handles. If a privileged process holds an inheritable or duplicable handle to a powerful object (a process, a token, a writable section) and a lower-privileged process can obtain it, that handle becomes a privilege-escalation primitive — no memory-corruption bug required.
  • Named-object squatting. Because \BaseNamedObjects is shared, a low-privileged attacker can pre-create a named object (event, mutex, or section) that a privileged service expects to create itself. If the service opens the existing object instead of failing, the attacker may influence its behavior or hijack shared memory — a well-known local attack pattern.
  • Symbolic-link and device abuse. The namespace’s symbolic links are a favorite target: convincing a privileged process to follow an attacker-controlled link under \GLOBAL?? or a per-session directory underlies many local EoP exploits.

Understanding references, handle rights, and the namespace is what lets you reason about all of these precisely rather than by pattern-matching. (Everything here is for defensive understanding and authorized testing.)

Conclusion

The Object Manager gives Windows a single, coherent way to name resources, secure them, and reason about their lifetime, while handle tables provide the fast, sandboxed bridge from user mode to those kernel objects. Once you internalize the header/body split, the type methods, the namespace, and the reference-versus-handle counters, a huge amount of Windows behavior stops being magic. The one piece we deferred repeatedly above is the security descriptor and the token that SeAccessCheck evaluates against it. That is exactly where this series goes next: the Windows security model — access tokens, SIDs, and privileges.

You Might Also Like

This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:

Comments

Copied title and URL