The Windows security model rests on a simple but powerful idea: every action is a subject trying to access an object, and the kernel decides whether to allow it. The subject’s identity and rights are carried in an access token; the object’s protection is described by a security descriptor. Every thread runs under a token, and every securable object — a file, registry key, process, mutex, or named pipe — carries a security descriptor. When code calls CreateFile or OpenProcess, the Security Reference Monitor in ntoskrnl.exe compares the caller’s token against the target’s descriptor and returns a granted-access mask. This article dissects the pieces that make that decision: SIDs, tokens, privileges, integrity levels, and the ACLs inside a security descriptor.
Security Identifiers (SIDs)
Every security principal — a user, group, computer, or logon session — is named not by its display name but by a Security Identifier (SID). A SID is a variable-length binary structure containing a revision number, a 48-bit identifier authority, and a series of 32-bit sub-authorities, the last of which is usually the Relative Identifier (RID). Its canonical string form is written as S-1-5-21-<domain>-<RID>, where 1 is the revision, 5 is the NT Authority, 21 marks a domain/machine-issued SID, the domain portion is three 32-bit values unique to the machine or domain, and the final RID identifies the specific principal.
Many SIDs are well-known and identical on every Windows system, which is why they are worth memorizing:
S-1-5-18— LocalSystem, the most privileged local account.S-1-5-19andS-1-5-20— LocalService and NetworkService.S-1-5-32-544— the built-in Administrators group (544 is the RID for that domain-independent alias).S-1-1-0— Everyone.- Domain RIDs appended to the domain SID:
500(the built-in Administrator account),512(Domain Admins),513(Domain Users).
Because the machine/domain portion is unique but the RID is predictable, the RID 500 account is always the true local Administrator even if it has been renamed — a detail attackers rely on when enumerating targets.
Access Tokens
An access token is a kernel object (nt!_TOKEN) that captures the complete security context of a subject. When you inspect one, the fields that matter for access decisions are:
- User SID — the primary identity the token represents.
- Groups — a list of group SIDs, each with attributes such as enabled, enabled-by-default, or use-for-deny-only. A deny-only group SID can be used to refuse access but never to grant it.
- Privileges — an array of privileges, each identified by a LUID (locally unique identifier) plus attributes marking it enabled or disabled.
- Integrity level SID — the mandatory integrity level of the token (see below).
- DefaultDacl — the DACL automatically applied to objects the subject creates without specifying one.
- Owner and primary group SIDs used when the subject creates new objects.
- Token type — primary or impersonation.
- Authentication ID — a LUID that links the token back to the logon session created by LSASS at authentication time.
The kernel ties a token to a process through the executive process block: EPROCESS.Token points to the process’s primary token. Every thread in that process inherits it unless the thread is actively impersonating.
Primary vs Impersonation Tokens
A primary token is the one attached to a process and used by default for all of its threads. An impersonation token allows a single thread to temporarily assume a different security context — typically so that a server can act on behalf of a client that connected to it. Impersonation has four levels that determine how far the borrowed identity reaches:
- Anonymous — the server cannot identify the client at all.
- Identification — the server can check the client’s identity and privileges but cannot act as it.
- Impersonation — the server can act as the client on the local machine.
- Delegation — the server can act as the client on remote machines too.
Impersonation is where a great deal of Windows privilege escalation lives. A process that holds SeImpersonatePrivilege can capture and impersonate a token handed to it — and if it can coax a SYSTEM service into authenticating to it, it can impersonate SYSTEM. This is the mechanism behind the long-running “Potato” family of exploits (RottenPotato, JuicyPotato, PrintSpoofer, and relatives), which trick a privileged service into connecting and then impersonate the resulting token.
Privileges
Beyond group membership, a token carries an array of privileges — rights to perform system-wide operations that are not tied to any particular object’s DACL. Each is stored as a LUID with an enabled/disabled attribute, and critically, most privileges are disabled by default and must be explicitly enabled by the process before use. Several are effectively administrative super-powers:
- SeDebugPrivilege — open any process, including protected ones, bypassing its DACL; a direct path to reading LSASS memory or injecting into system processes.
- SeImpersonatePrivilege — impersonate a token the process obtains, the basis of the Potato attacks.
- SeBackupPrivilege and SeRestorePrivilege — read or write any file regardless of its DACL, ostensibly for backup software.
- SeTcbPrivilege — act as part of the Trusted Computing Base, allowing a process to construct arbitrary tokens.
- SeLoadDriverPrivilege — load kernel drivers, a route to ring-0 code execution.
From a defender’s standpoint, granting any of these to a non-administrative service account is functionally equivalent to granting full control of the machine.
Integrity Levels and UAC
Layered on top of the discretionary model is Mandatory Integrity Control (MIC). Every token carries an integrity level expressed as a SID — Low (S-1-16-4096), Medium (S-1-16-8192), High (S-1-16-12288), and System (S-1-16-16384). Objects may carry a mandatory label in their SACL that enforces a no-write-up policy: a subject at a lower integrity level cannot modify an object labeled at a higher level, even if the DACL would otherwise allow it. This is what sandboxes a Low-integrity browser tab from tampering with Medium-integrity user files.
User Account Control (UAC) builds directly on this. When an administrator logs on, LSASS creates two tokens: a filtered token at Medium integrity with administrative group SIDs marked deny-only and powerful privileges stripped, and a linked elevated token at High integrity with full administrative rights. The user’s shell runs with the filtered token; an elevation prompt swaps in the linked elevated token for the specific process being launched. This is why an administrator does not automatically wield administrative power until something elevates.
Security Descriptors: DACL and SACL
If the token is the subject’s side of the equation, the security descriptor is the object’s. It contains an Owner SID, a primary Group SID, a DACL (Discretionary Access Control List), and a SACL (System Access Control List). The DACL determines who may access the object and how; the SACL holds audit entries and the mandatory integrity label.
Each list is an ordered set of Access Control Entries (ACEs). An ACE combines an ACE type (access-allowed or access-denied), a SID, an access mask (the bitfield of rights such as read, write, delete, or object-specific rights), and flags controlling inheritance and auditing. Ordering matters: Windows places explicit deny ACEs before explicit allow ACEs, and explicit ACEs before inherited ones, so that a deny is evaluated before a matching allow. The owner is special — it always retains the implicit right to rewrite the DACL, which is why taking ownership of an object is a classic escalation step.
The Access Check
When a subject requests access, the Security Reference Monitor performs an access check — exposed to drivers as SeAccessCheck. It takes the subject’s token, the object’s security descriptor, and the desired access mask, and produces the granted access mask (or a denial). The evaluation proceeds roughly as follows:
- First, the integrity level is compared. If the object’s mandatory label forbids write-up and the subject is lower, write access is removed before the DACL is even consulted.
- Certain privileges short-circuit the check — SeBackupPrivilege/SeRestorePrivilege grant file access outright, and an owner is always granted the right to read and write the DACL.
- The DACL is walked in order, accumulating granted rights from allow ACEs that match the token’s user or group SIDs, and immediately failing on a matching deny ACE, until every requested right is satisfied or the list is exhausted.
If a security descriptor has a NULL DACL, everyone is granted full access — a dangerous misconfiguration — whereas an empty DACL (present but with zero ACEs) denies everyone.
Inspecting Tokens
These structures are not abstract — every one is visible on a live system. From user mode, whoami decodes your current token; from a kernel debugger, you can read the raw structures:
# User mode: dump the full token - user SID, groups, privileges, integrity
whoami /all
# Just the privilege array and their enabled/disabled state
whoami /priv
# Just the group SIDs
whoami /groups
# Kernel debugger (WinDbg): find a process and its token
!process 0 0 explorer.exe
# Decode the token pointed to by EPROCESS.Token
!token <token-address>
# Inspect the raw token structure and a security descriptor
dt nt!_TOKEN <token-address>
!sd <security-descriptor-address>
whoami /all is the fastest triage on a compromised or audited host: it reveals whether the current context holds SeImpersonatePrivilege or SeDebugPrivilege, what integrity level it runs at, and which groups are enabled. In WinDbg, !token resolves the SIDs and privilege LUIDs into human-readable names, while dt nt!_TOKEN exposes the underlying fields for deeper study.
Security Relevance
Nearly every Windows local privilege escalation reduces to manipulating one of these structures. Token theft and impersonation — duplicating a SYSTEM process’s token and attaching it to an attacker thread — grant full local control without ever touching a password. Enabling dangerous privileges already present in a token (SeDebug, SeBackup, SeImpersonate) turns a limited service account into an administrator. Integrity-level bypasses defeat the UAC and sandbox boundaries. And DACL misconfigurations — a weak ACL on a service, a writable registry key, or a NULL DACL on a named pipe — let an unprivileged user reconfigure privileged components.
For defenders, the takeaways are concrete: minimize privilege assignment, audit which non-admin accounts hold impersonation or backup rights, watch for token-manipulation API sequences (OpenProcessToken → DuplicateTokenEx → ImpersonateLoggedOnUser), and tighten the DACLs on services, pipes, and scheduled tasks. Understanding the access check is what lets you tell a benign impersonation from a malicious one.
Conclusion
Access tokens, SIDs, privileges, integrity levels, and security descriptors together form the vocabulary of the Windows security model. A token answers “who am I and what may I do,” a security descriptor answers “who may touch me and how,” and the access check is the kernel arbitrating between them on every single object operation. With this foundation in place, the next question is how a user-mode request even reaches the kernel to be checked in the first place. In the next article in the series we cross that boundary and examine the user-to-kernel transition — system calls, the SSDT, and the syscall instruction.
You Might Also Like
This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:



Comments