Windows Access Control
- Inherits: An authenticated principal. The SID set an access token carries is only meaningful because the credential chapters authenticated the logon that minted it: Kerberos (Chapter 17, Kerberos) issues the principal's ticket and the PAC group SIDs LSASS stamps into the token, and WebAuthn and Passkeys (Chapter 21, WebAuthn and Passkeys) binds the interactive logon to a hardware credential so those SIDs name a human who actually authenticated. Kernel isolation: on an HVCI-on box the Security Reference Monitor's own code cannot be silently re-coded from VTL0 (Chapter 8, Code Integrity), and the VTL1 secure world (Chapter 6, The Secure Kernel) is the only place a secret can sit that this model's admin-equals-kernel concession cannot reach.
- Promise: Every securable operation on the machine resolves through one kernel routine,
SeAccessCheck, against five fixed inputs (a security descriptor, an access token, a desired-access mask, a per-type generic mapping, and any previously-granted access); for the same inputs the answer to "may this caller do this to this object?" is finite, inspectable, and identical every time. Serviced boundary: the kernel-mode/user-mode separation: the one boundary Microsoft's servicing criteria actually commit to defending [301]. - TCB: The NT kernel's Security Reference Monitor (the code of
SeAccessCheck, the ordered DACL walk, the Mandatory Integrity Control check, and the privilege short-circuit) running unmodified; the integrity of the token a caller holds and the descriptor an object carries; and the Object Manager name resolution that decides which descriptor the check ever sees. Everything running as administrator or in ring 0 is inside this TCB by Microsoft's own boundary definition. Which is the whole problem. - Adversary → Break: Every famous local-privilege-escalation tool of the last twenty-five years attacks one input and leaves the function correct. The Potato lineage forges a SYSTEM-token handle; UACMe's 70+ AutoElevate redirects bend the elevation flow that mints the token before the check; HiveNightmare (CVE-2021-36934) rewrites one descriptor; Object-Manager symlink and NTFS hard-link bugs swap the object out from under the name before resolution completes. The Promise only ever covered a correct decision on the inputs as presented: never that the inputs could not be forged, nor that an attacker who reaches ring 0, where the enforcement code itself lives, is still on the far side of any boundary.
- Residual: The bearer-token property plus service-account
SeImpersonatePrivilege(the structural fuel of the whole Potato lineage) is named here but owned by The SeImpersonate Primitive (Chapter 24); reading credentials out of LSASS once admin-equals-kernel holds → Mimikatz and the Credential-Theft Decade (Chapter 14) and Credential Guard (Chapter 15); the integrity-level lattice this chapter only staples on → The Integrity-Level Stack (Chapter 23). - Bequeaths: One named decision plane (
SeAccessCheck, five inputs, and an ordered DACL walk where canonical DACLs place denies before allows, with a Mandatory Integrity Control check wedged before it) that the next two links specialize: The Integrity-Level Stack (Chapter 23) takes the MIC check and follows it up the AppContainer and browser-sandbox lattice; The SeImpersonate Primitive (Chapter 24) takes the bearer-token property named here and turns it into the Potato lineage's load-bearing privilege. Does NOT provide: any defense once the caller is administrator or kernel (admin equals kernel, by Microsoft's own servicing criteria [301]); the integrity-level mechanics in full; nor any check on how the token's SIDs were authenticated upstream. - Proof: 🔵 documented throughout.
SeAccessCheck's algorithm, the token/descriptor/privilege surfaces, and the twenty-five-year attack record are read from Microsoft Learn, the MSRC servicing criteria, NVD, and named-researcher primaries; the living surfaces are reproducible read-only on any Windows box (whoami /all,icacls,Get-Acl). This chapter ships no captured lab artifact or evidence hash.
The Reasoner's question. If Windows access control is one decision plane, which inputs does it trust, where have attackers spent twenty-five years bending those inputs, and which successor architectures finally move the trust decision somewhere the old model cannot reach?
Foundations. What you need before this chapter.
- Subject and object. A subject is the security identity trying to act: a process or thread carrying an access token. An object is the securable thing being acted on: a file, process, registry key, named pipe, service, or kernel object with a security descriptor.
- SID. A Security Identifier is the never-reused name Windows uses for a user, group, service, package, capability, integrity level, or other trustee. DACL entries point at SIDs; tokens carry SIDs.
- Token. A kernel object attached to a process or thread. It carries SIDs, privileges, integrity level, AppContainer/capability state, and impersonation metadata. A token is a bearer credential: whoever holds the handle gets the rights it names.
- Security descriptor. The policy attached to an object: owner, group, DACL, and SACL. The DACL grants or denies access; the SACL records audit policy and mandatory-integrity labels.
- Privilege. A named authority on the token, such as
SeDebugPrivilege,SeImpersonatePrivilege,SeBackupPrivilege, orSeRestorePrivilege, that can bypass ordinary DACL evaluation for a specific class of operations. - Reasoner stance. Attack families in this chapter are treated as gap analysis, not tutorials. The point is to understand which input to the trust decision each family abuses.
Chapter thesis. Windows answers the question can this code do this? with one kernel function, SeAccessCheck, evaluated against five inputs: a security descriptor, an access token, a desired-access mask, a generic-mapping table, and any previously-granted access. The function and its inputs have not structurally changed since July 27, 1993. Every famous Windows local-privilege-escalation tool of the last twenty-five years (Mimikatz, JuicyPotato and seven other Potatoes, the 70+ AutoElevate-redirect methods cataloged in UACMe) attacks one of those inputs. This chapter tells that story as one system, names the five structural limits Microsoft has publicly conceded, and explains why Adminless, NTLMless, VBS Trustlets, and Credential Guard are the four non-overlapping ways to close them.
One question, billions of times a second
Open a Windows PowerShell window and run whoami /priv. Read the column on the right. SeShutdownPrivilege. SeUndockPrivilege. SeIncreaseWorkingSetPrivilege. SeTimeZonePrivilege. About twenty rows of capabilities, almost all marked Disabled, on a token that lives inside explorer.exe's memory and that the kernel consults billions of times a second.
Now run icacls C:\Windows\System32\drivers\etc\hosts. The output reads BUILTIN\Administrators:(F), NT AUTHORITY\SYSTEM:(F), BUILTIN\Users:(R). Six characters per principal, decoded by something inside the kernel called SeAccessCheck, applied to a data structure called a security descriptor, against a credential called an access token, every time any process anywhere on the machine asks for read access to that single file [952].
This chapter is about the model behind those two outputs. A model that has not structurally changed since July 27, 1993, when Windows NT 3.1 shipped from Redmond [953]. A model that every famous Windows local-privilege-escalation tool of the last twenty-five years (Mimikatz, JuicyPotato, fodhelper.exe, the 70+ methods in the open-source UACMe catalog) exists to attack [261,698,954].
The thesis comes in three convictions:
SeAccessCheckis the answer. Every securable Windows operation that touches a securable object resolves through one decision function with one set of inputs [955].- Every famous Windows escalation tool attacks one of those inputs. JuicyPotato attacks the token. Mimikatz attacks the privilege list. Fodhelper attacks the elevation flow that produces the token. HiveNightmare attacks the DACL on a single file [956]. The vocabulary scales.
- The model has five structural limits its keepers have publicly conceded [301], and Adminless, NTLMless, VBS Trustlets, and Credential Guard are the four non-overlapping ways to close them.
The model's surface is fixed and small: ten primitives (the Security Reference Monitor, security identifiers, tokens, security descriptors, DACLs, SACLs, ACEs, privileges, Mandatory Integrity Control, User Account Control), one canonical oracle (SeAccessCheck), two attack families (the Potato lineage and the UACMe bypass tradition), and four successor architectures (Adminless, NTLMless, VBS Trustlets, Credential Guard).
If the model has not structurally changed since 1993, why has it taken thirty-three years and more than seventy bypasses to map its failure modes, and what does each generation tell us about the next?
Origins: From lampson's matrix to Cutler's kernel (1971-1993)
The vocabulary starts in a paper Butler Lampson presented at Princeton in 1971 and the ACM republished in Operating Systems Review in January 1974. Lampson framed protection as a 2-D matrix: rows index the subjects (users, processes), columns index the objects (files, devices, memory pages), and the cell at the intersection holds the operations the subject is permitted on the object. The matrix is a sparse, mostly-empty table the size of every-process times every-file. No real system has ever stored it that way.
Two implementation strategies fall out of the formalism. Slice the matrix by row and you get capability lists: each subject carries a token that names the objects it can touch. Slice by column and you get access-control lists: each object carries a list of subjects allowed to touch it. Lampson worked through both in the paper. Operating systems built on the second slice came to dominate, partly because hardware in 1971 made unforgeable capabilities expensive, partly because file systems could carry an ACL at the inode without changing every program. Decades later, the gap between the two implementations would still matter; we will reach Norm Hardy's "Confused Deputy" in a moment.
The lever that turned theory into Windows came from procurement, not academia. On December 26, 1985, the U.S. Department of Defense published DoD 5200.28-STD, the Trusted Computer System Evaluation Criteria, known by the color of its cover as the Orange Book [957]. The Orange Book defined four divisions of trusted-system assurance, and its C2 class ("Controlled Access Protection") made discretionary access control plus auditing a federal procurement floor. The September 30, 1987 Neon Orange Book (NCSC-TG-003) and the July 28, 1987 Tan Book (NCSC-TG-001) elaborated DAC and audit respectively [957]. After 1985, no operating system that wanted U.S. federal customers could ship without per-user ACLs and an audit log.
Three years after the Orange Book made DAC a procurement floor, Norm Hardy of the Tymshare / KeyKOS lineage published a three-page paper in Operating Systems Review that named the structural limit of the entire ACL-shaped class: "The Confused Deputy (or why capabilities might have been invented)" [958]. Hardy described a privileged compiler that wrote billing records to a system file. A user could trick the compiler into writing the user's output file over the billing file, because the compiler used its own authority on every write and could not distinguish "authority I have" from "authority the caller asked me to use."
The Wikipedia summary of the field is exact: "Capability systems protect against the confused deputy problem, whereas access-control list-based systems do not" [959]. Hold this paper. It returns when we reach the model's structural limits.
Aside: Cutler, VMS, and the security-by-design culture. The team that built Windows NT was not assembled in Redmond. David Cutler arrived in October 1988 from Digital Equipment Corporation [953], where he had led VMS and the canceled Mica successor, and brought with him a fraction of his old DEC team.
The cultural import mattered: VAX/VMS, announced October 25, 1977 alongside the VAX-11/780 (V1.0 shipped August 1978) [960], introduced UIC-based file protection and a kernel-mode security architecture, and by the mid-1980s the VAX/VMS line had been evaluated at TCSEC Class C2 [960], by which time the system had been hardened with per-object ACLs, audit channels, and an explicit reference monitor. That C2-hardened VMS was the cultural reference Cutler brought with him to Microsoft. G. Pascal Zachary's Showstopper! tells the story of the four-year build of NT 3.1 from that team [961].
The point for this chapter is narrower. NT 3.1's nine access-control primitives (Security Reference Monitor, security identifier, access token, security descriptor, DACL, SACL, ACE, privileges, audit channel) did not arrive piecemeal. They were specified together, before the first line of SeAccessCheck was written, against a procurement standard the team intended to clear.
NT 3.1 released to manufacturing on July 27, 1993 [953]. NT 3.5, released to manufacturing on September 21, 1994 [962], was hardened through Service Pack 3 (June 21, 1995) and was rated by the National Security Agency in July 1995 as complying with TCSEC C2 criteria against the standalone single-user configuration [962]; NT 4.0 Service Pack 6a later received a separate C2/E3 evaluation for a tightly specified configuration rather than merely repeating the NT 3.5 SP3 standalone result. The combination froze the model. Once C2 evaluation was on the books, structural changes to the access-control surface would have required re-evaluation. Federal procurement obligations have kept the structural shape intact in the thirty-one years since that 1995 C2 rating, even after the Department of Defense formally retired TCSEC in favor of the Common Criteria.
Cutler shipped a model that has answered "can this code do this?" the same way for thirty-three years. What is the actual function, and what are its inputs?
The kernel oracle: SeAccessCheck and its inputs
The function has one signature, one return value, and one job. Microsoft's Win32 documentation exposes a user-mode mirror called AccessCheck that lets userland code ask the question without holding a handle, and a kernel routine called SeAccessCheck that the kernel invokes whenever a handle to a securable object is opened or an existing access grant is expanded [952,963]. The shape is the same in both directions:
Three inputs in (a security descriptor, an access token, a requested-access mask), two outputs out (the access mask actually granted, and a STATUS_ACCESS_DENIED flavor if any). Two more hidden inputs make the kernel signature precise: a generic mapping table that translates the four generic rights (GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE, GENERIC_ALL) into object-type-specific bits, and a previously-granted-access mask that the kernel carries forward when an access check happens in two phases. Together: five inputs, one decision, one log entry. The definition below keeps the kernel-signature caveat in one place.
Definition: Security Reference Monitor (SRM).
The kernel-mode component of the Windows NT executive that performs all access checks against securable objects. It owns SeAccessCheck and the audit log generation routines. The SRM is a subsystem, not a feature: every other kernel component that needs to grant or deny access calls into it.
Definition, SeAccessCheck.
The Windows kernel routine that decides whether a thread may perform a requested set of operations on an object. It takes a security descriptor, an access token, a desired-access mask, a per-object-type generic-mapping table, and any previously-granted access. (The documented kernel signature also carries a synchronization flag, an AccessMode discriminator that elides the check for kernel-mode callers, and a privileges out-parameter; the five-input model used here is an explanatory simplification that the user-mode AccessCheck mirror tracks more closely.) It returns the subset of the desired-access mask that the kernel grants and a status code. Every call site that opens a handle to a securable object (or expands an existing grant) eventually reaches this function; once the handle is open, later operations are validated against its cached granted-access mask rather than by re-running the full check [955].
The five inputs are not equally exotic. The desired-access mask and the generic-mapping table are bookkeeping that an object type defines once at registration time. The previously-granted-access input is the kernel handing itself a pencil for two-phase access checks, mostly invisible to userland. The two inputs the rest of the chapter will keep returning to are the security descriptor and the access token.
A security descriptor is the data structure attached to the object. It carries an owner SID, a primary-group SID, a discretionary access-control list (DACL) of allow and deny entries, and a system access-control list (SACL) holding audit and integrity-label entries [964]. The DACL is what icacls prints. The SACL is what writes Event Log entries when something the descriptor's writer wanted to watch happens.
An access token is the data structure attached to the caller. It names the user (one SID), the user's groups (a list of SIDs), the privileges the user holds (a list of named superpowers), the integrity level the kernel will compare against the object's label, and a flag that says whether the token is a primary token (one per process) or an impersonation token (one per thread, used to act on a client's behalf) [963]. It is also a bearer credential: once a process or thread holds a valid token handle, the access check spends the authority named by that token rather than re-authenticating the human or service that originally caused LSASS to mint it. That bearer property is what makes impersonation useful for servers and dangerous when a service can be tricked into holding a more privileged client token.
Microsoft's documentation lists the token's contents almost as bullet points: "The security identifier (SID) for the user's account; SIDs for the groups of which the user is a member; A logon SID that identifies the current logon session; A list of the privileges held by either the user or the user's groups; An owner SID; The SID for the primary group; The default DACL;... Whether the token is a primary or impersonation token; An optional list of restricting SIDs; Current impersonation levels..." [963].
The flow inside SeAccessCheck is mechanical. The kernel maps DesiredAccess from generic to specific bits using the type's mapping table. It checks the integrity label of the object against the integrity level on the token (Mandatory Integrity Control runs before the DACL walk, a point the Mandatory Integrity Control section below expands). It walks the DACL in the order the ACEs appear (which, for a canonically ordered DACL, means explicit deny ACEs come before allows) accumulating bits granted by allow ACEs whose SID is in the token's list. It applies the privilege grants SeAccessCheck itself honors, SeTakeOwnershipPrivilege (which yields WRITE_OWNER) and SeSecurityPrivilege (which yields ACCESS_SYSTEM_SECURITY); the broader DACL-bypass privileges are token-borne but enforced elsewhere, SeBackupPrivilege/SeRestorePrivilege at file open with backup semantics, and SeDebugPrivilege at process open. It returns the accumulated GrantedAccess, or STATUS_ACCESS_DENIED if the requested bits were not all granted.
Decision flow: a single open request through the access-control plane. A user-mode caller asks the Object Manager to open a named object with a desired-access mask and the caller's effective token. The Object Manager resolves the name through the NT namespace, fetches the object's security descriptor from the object header, and calls SeAccessCheck. The Security Reference Monitor maps generic rights to object-specific bits, checks the mandatory-integrity label, applies the privilege grants it honors directly (SeTakeOwnershipPrivilege, SeSecurityPrivilege), walks the DACL in canonical deny-first order, emits a SACL audit record if object-access auditing is enabled and an audit ACE matches, and returns either a handle with the granted-access mask or STATUS_ACCESS_DENIED.
Five inputs. One function. One decision (and, where auditing is configured, one log entry) on the way out. That is the chapter's thesis in miniature: every later section is an attack on one of those inputs, and the model's subject dimension extends the same way. Conditional ACEs and Dynamic Access Control add claims to the token; UAC keeps the token small by default and inflates it only on demand. Every primitive in the chapter maps cleanly onto one of SeAccessCheck's five inputs, and every famous attack tool onto one primitive.
The function is fixed. The inputs are five. So how does the kernel actually walk a DACL, and where does the wrong answer come from?
The DACL algorithm and the SID namespace
"Walk the DACL in order." Six words that have generated hundreds of thousands of misconfigured ACLs since 1993. The Microsoft Learn page that ships the algorithm is short and exact. The system "examines each ACE in sequence until... an access-denied ACE explicitly denies any of the requested access rights to one of the trustees listed in the thread's access token... one or more access-allowed ACEs for trustees listed in the thread's access token explicitly grant all the requested access rights... All ACEs have been checked and there is still at least one requested access right that has not been explicitly allowed, in which case, access is implicitly denied" [955].
Three terminations. Deny terminates with denial. Enough allows terminates with grant. End-of-list with anything left ungranted terminates with denial. Note the asymmetry the algorithm encodes: a single deny anywhere in the DACL kills the request, but an allow has to be paired with explicit coverage of every desired bit. The default is denial. Independently of the walk, the object's owner is always granted READ_CONTROL and WRITE_DAC so an owner can never be permanently locked out of re-permissioning the object, unless an explicit OWNER RIGHTS (S-1-3-4) ACE narrows that default.
Definition: Discretionary Access Control List (DACL). The ordered list of access-control entries (ACEs) attached to a securable object's security descriptor that specifies which trustees are granted or denied which access rights. Discretionary means the object's owner controls the list, in contrast to a mandatory list whose entries the system enforces independent of owner intent.
Definition: Access-Control Entry (ACE).
A single grant, deny, audit, or mandatory-label record inside a DACL or SACL. Each ACE carries an SID identifying the trustee, a 32-bit access mask, a flags byte controlling inheritance, and a type discriminator. Windows defines four primary ACE types (ACCESS_ALLOWED_ACE, ACCESS_DENIED_ACE, SYSTEM_AUDIT_ACE, SYSTEM_MANDATORY_LABEL_ACE) plus callback variants for conditional ACEs [965].
Three subtleties deserve emphasis, because the prose can flatten them. First: the NULL DACL versus empty DACL distinction. A descriptor with no DACL at all (a literal NULL pointer where the list would be) grants full access, on the theory that the writer expressed no policy and the kernel will not invent one. A descriptor with a DACL that exists but contains zero ACEs denies everything, because the writer expressed a policy and that policy has no allows. The single most common high-impact misconfiguration in the Windows codebase is code that meant to write the second and wrote the first, or vice versa.
WARN: NULL DACL grants full access; empty DACL denies all access.
Newly-written code that "creates a file with no protection" almost always wants an empty DACL but ends up with a NULL DACL because of how the SECURITY_DESCRIPTOR initialization defaults work. Verify with Get-Acl or icacls after creation; a NULL DACL surface in icacls looks like Everyone:(F) and is almost always a bug, not a feature [955].
Second: ACE order is the caller's responsibility. The kernel walks the list in the order it finds it. The "canonical" order Windows expects is four-step, quoted verbatim from the Microsoft Learn reference [966]: "1. All explicit ACEs are placed in a group before any inherited ACEs. 2. Within the group of explicit ACEs, access-denied ACEs are placed before access-allowed ACEs. 3. Inherited ACEs are placed in the order in which they are inherited... 4. For each level of inherited ACEs, access-denied ACEs are placed before access-allowed ACEs."
The same page underlines who has to enforce the order: "Functions such as AddAccessAllowedAceEx and AddAccessAllowedObjectAce add an ACE to the end of an ACL. It is the caller's responsibility to ensure that the ACEs are added in the proper order." If the writer of the DACL hands the kernel an out-of-order list with a deny ACE buried after a wide allow ACE, the deny will be unreachable and the descriptor will silently grant more than the writer intended.
Third: there is no special case for "Everyone." The well-known SID S-1-1-0 exists in every token of every process on the machine; an ACE against it applies to every caller. There is no extra logic that says "if this is Everyone, treat it differently from any other group." James Forshaw made the point with characteristic bluntness in 2020: "don't forget S-1-1-0, this is NOT A SECURITY BOUNDARY. Lah lah, I can't hear you!" [967]. The DACL evaluation algorithm does not know "Everyone" is special. It is a SID like any other.
That makes the SID namespace itself worth a tour. Microsoft documents the structure as a revision number, an identifier authority (a six-byte field that says which authority issued the SID), a list of sub-authorities, and a final relative identifier (RID) [968]. Microsoft's own page on SIDs is precise: "A security identifier (SID) is a unique value of variable length used to identify a trustee... When a SID has been used as the unique identifier for a user or group, it cannot ever be used again to identify another user or group."
The well-known SIDs the kernel recognizes by name include SYSTEM (S-1-5-18), LocalService (S-1-5-19), NetworkService (S-1-5-20), Everyone (S-1-1-0), Authenticated Users (S-1-5-11), and the four Mandatory Integrity Control levels (S-1-16-4096 / 8192 / 12288 / 16384) [969,964]. Machine-issued SIDs encode the machine's domain identity in the sub-authorities; domain-issued SIDs encode the domain identity. RID 500 is, by convention, the local Administrator account; RID 501 is the Guest account.
Definition: Security Identifier (SID).
A variable-length, never-reused identifier for a trustee (user, group, machine, service, or capability) inside the Windows security model. SIDs are encoded in canonical form S-R-I-S1-S2-...-RID, where R is a revision number, I is the identifier authority, the Sn are sub-authorities issued by that authority, and RID is the relative identifier. Every ACE references an SID; every token contains a list of them [968].
Sidenote.
James Forshaw documented in 2017 that Windows generates the per-service SID for NT SERVICE\ deterministically: it is the SHA-1 hash of the uppercased service name, formatted into the SID's sub-authority fields. This is why Windows can refer to running services as security principals without an explicit registration step: the kernel derives the SID on demand [970].
Two SID families this chapter will not derive: AppContainer Package SIDs (S-1-15-2-...) and capability SIDs (S-1-15-3-...). Both arrived with Windows 8 in 2012 and extend the matrix's subjects with code identity and capability tokens. A separate article on Windows app identity carries the canonical derivation, including the Crockford-Base32 PublisherId derivation that produces a Package SID from an MSIX package signature (external further reading [19]). The token discussion later in this chapter will mention those SIDs; we will not redefine them here.
The DACL is half the story. What does the thread bring to the access check, and why is the answer "whoever happens to hold the handle"?
Tokens as bearer credentials
A token is not a credential the way a password is. A token is a credential the way cash is: whoever holds it gets the rights. This is the single most important property in the chapter.
Microsoft splits tokens into two flavors by purpose [963]. A primary token hangs off a process and represents the security identity that process runs as. Every process has exactly one. An impersonation token hangs off a thread and lets that thread temporarily act as someone else: typically a network client whose request the thread is servicing. Tokens are kernel objects with handles, and like every other kernel object the kernel does not care how a process obtained the handle. If the handle resolves to a token in the kernel's table, the kernel grants the rights the token names.
Definition, Access Token. A kernel object that names the security identity of a thread or process. It carries the user's SID, the SIDs of the user's groups, the privileges the user holds, an integrity level, an integrity-level mandatory policy, an optional list of restricting SIDs, a flag distinguishing primary from impersonation tokens, and the impersonation level (Anonymous / Identification / Impersonation / Delegation) [963]. The kernel consults a token on every access check for the thread that holds it.
Definition: Primary Token vs Impersonation Token. A primary token is owned by a process and represents the identity the process runs as; every process has exactly one primary token. An impersonation token is owned by a thread and represents an identity the thread is temporarily acting on behalf of: typically a network client. The primary / impersonation distinction is a discriminator inside the token itself, set when the token is created or duplicated [963].
The impersonation flavor acquired its modern shape in Windows 2000. A token's impersonation level takes one of four values, ordered from least to most privileged for the impersonator. Anonymous lets the server know nothing about the client. Identification lets the server learn the client's SIDs but not act as the client. Impersonation lets the server perform local access checks as the client; this is the level a typical RPC server requests. Delegation lets the server forward the client's identity onto another machine, useful for multi-hop scenarios but a frequent source of relay-style bugs. Almost every Potato lineage attack consumes an Impersonation-level token; that is enough to call ImpersonateLoggedOnUser and run as the client [699].
Microsoft documents a third token shape, the restricted token, that is rare in practice but worth understanding because it is the only place in the model where an explicit deny-list lives on the token itself rather than the descriptor. A restricted token combines three knobs: a list of SIDs converted to deny-only (their grants count for no allow ACE but their presence still triggers deny ACEs), a list of restricting SIDs that the access check must independently permit, and a list of privileges removed from the token's privilege set [971].
The kernel runs SeAccessCheck twice and grants only the intersection: "When a restricted process or thread tries to access a securable object, the system performs two access checks: one using the token's enabled SIDs, and another using the list of restricting SIDs. Access is granted only if both access checks allow the requested access rights" [971]. Restricted tokens are operationally niche because the same documentation requires applications using them to "run the restricted application on desktops other than the default desktop. This is necessary to prevent an attack by a restricted application, using SendMessage or PostMessage, to unrestricted applications on the default desktop" [971]. Few applications can spare the desktop overhead.
Sidenote.
whoami /priv shows available privileges, not enabled privileges. The Enabled column is the load-bearing one: an available-but-disabled privilege does not affect any access check until the process explicitly enables it via AdjustTokenPrivileges. The discipline of leaving privileges disabled by default is a defense in depth that depends on the application not having an exploitable bug between disable and use.
A token also carries flags that drive specific runtime behaviors: a split-token indicator points at a linked full-administrator counterpart for the UAC scenario described below; an AppContainer flag plus a Package SID and capability SIDs name an AppContainer-bound process. In every case, the kernel consults the token by handle and trusts the contents. The kernel does not ask how a process obtained the handle. It asks only what the token says.
This is the property that organizes the rest of the chapter.
Key idea.
The Windows access token is a bearer credential. Whichever process holds the handle gets the rights. The kernel does not ask how the handle was obtained; it asks only what the token says. This single property explains the entire Potato lineage, Mimikatz token::elevate, and most of the privilege-abuse canon. Once you see it, every later attack section in the chapter becomes the same bug repeated against a different token-acquisition primitive.
If the token is a bearer credential, anyone with a way to obtain a SYSTEM token's handle is SYSTEM. Every Potato in the lineage is a different way to provoke a SYSTEM-token handle into the attacker's process. But the access check has another input that bypasses the DACL entirely. What is it, and which attackers know about it?
Privileges as a different dimension
Privileges are not access rights. They are pre-checked superpowers. They live on the token, they bypass the DACL for specific operations, and they are baked into the kernel for those operations alone.
Microsoft's framing on Learn is exact: "Privileges differ from access rights in two ways: Privileges control access to system resources and system-related tasks, whereas access rights control access to securable objects" [972]. The same page makes the operational consequence clear: "Most privileges are disabled by default" [972]. A process that holds a privilege but has not enabled it (via AdjustTokenPrivileges) cannot use it. The discipline is "principle of least privilege at the millisecond level". The privilege is on the token, but it does nothing until the program explicitly turns it on for the next system call.
Definition, Privilege.
A named, kernel-recognized authority on the access token that lets the holder perform a specific class of operations the DACL evaluation alone would not permit. Privileges include SeDebugPrivilege (read/write any process), SeImpersonatePrivilege (act on a client's token), SeAssignPrimaryTokenPrivilege (start a process under a token), SeBackupPrivilege (read any file regardless of DACL), SeRestorePrivilege (write any file regardless of DACL), SeTcbPrivilege (act as the operating system), and SeLoadDriverPrivilege (load a kernel driver). Most are disabled by default and must be enabled via AdjustTokenPrivileges before use [972].
The reason privileges deserve a section of their own is that five of them are equivalent to "I am SYSTEM" and the other dozen are housekeeping. The five-versus-housekeeping split is the load-bearing audit decision in any Windows hardening review. Step through them.
SeDebugPrivilege lets the holder open most processes for full read and write, including processes running as SYSTEM (Protected Process Light targets, such as lsass.exe under RunAsPPL, impose additional signer-level restrictions even on SeDebugPrivilege holders). The privilege exists so that Visual Studio and WinDbg can debug code other users have started. The first move in almost every Mimikatz session is privilege::debug, which enables the privilege the local administrator already has on the token [261,461]. Once enabled, the next Mimikatz command opens lsass.exe and reads the credentials.
SeImpersonatePrivilege lets the holder accept any token offered by a client and act as that client. It is present and enabled by default on LOCAL SERVICE and NETWORK SERVICE tokens, and services started through the Service Control Manager receive the built-in Service group that the default user-right assignment covers; custom service accounts can still be narrowed by policy. This is the load-bearing privilege for the Potato lineage [699]. The SeImpersonate Primitive (Chapter 24) is a chapter-long study of what a service account holding SeImpersonate can be tricked into doing; the privilege is the entry condition.
SeAssignPrimaryTokenPrivilege lets the holder launch a new process under any primary token. Combined with SeImpersonate, the pair gives an attacker the entire token-replay attack: get an impersonation handle to a SYSTEM token, then call CreateProcessAsUser to run a command as SYSTEM. itm4n quotes decoder_it on the operational consequence.
Quoted source. if you have SeAssignPrimaryToken or SeImpersonate privilege, you are SYSTEM.: decoder_it, quoted by itm4n in the PrintSpoofer disclosure [699]
SeBackupPrivilege lets the holder read any file regardless of DACL, on the theory that backup software has to. SeRestorePrivilege is the symmetric write privilege. The two together mean that a process holding both can rewrite any file on the machine, including service binaries.
The 2021 HiveNightmare / SeriousSAM vulnerability (CVE-2021-36934) is the worked example of what happens when the model assumes nobody but the backup process has read access to a sensitive file and the assumption breaks. The NVD description is exact: "An elevation of privilege vulnerability exists because of overly permissive Access Control Lists (ACLs) on multiple system files, including the Security Accounts Manager (SAM) database" [956]. The DACL on \Windows\System32\config\SAM was itself overly permissive (it granted BUILTIN\Users read starting with Windows 10 1809). The live file cannot normally be read because the kernel holds it open with an exclusive lock; the Volume Shadow Copy mirror inherits the same permissive DACL but is not locked, so any local user could read the SAM hashes through the shadow-copy device path.
Microsoft's fix required not just a patch but a manual operator step: "After installing this security update, you must manually delete all shadow copies of system files, including the SAM database, to fully mitigate this vulnerability" [956]. A patch alone could not erase the historical shadow copies that already had the wrong DACL.
SeTcbPrivilege lets the holder "act as the operating system". It is the privilege that grants identity to the kernel itself. Held only by LocalSystem services in well-administered environments. A non-system process that somehow acquired SeTcb is, in effect, indistinguishable from the kernel.
SeLoadDriverPrivilege lets the holder load a kernel driver. By itself the privilege is constrained, because the loaded driver still has to be properly signed for HVCI / Driver Signature Enforcement to accept it. Combined with a known-vulnerable signed driver, however, the privilege becomes the entry point for bring-your-own-vulnerable-driver (BYOVD) attacks: load a benign-looking but exploitable signed driver, then use its bug to execute arbitrary kernel code. Two worked examples bracket the class.
The kernel-read/write half of the class is best illustrated by Micro-Star's RTCore64.sys (CVE-2019-16098 [370]), the MSI Afterburner driver that "allows any authenticated user to read and write to arbitrary memory, I/O ports, and MSRs" [370]. In October 2022, the threat actors behind the BlackByte ransomware weaponised the primitive at scale [371]: the dropper loaded RTCore64.sys, then walked the kernel's PspCreateProcessNotifyRoutine callback array and zeroed every entry, blinding every registered process-creation callback before the encryption stage ran.
Sophos's October 4, 2022 disclosure named the technique exactly: "We found a sophisticated technique to bypass security products by abusing a known vulnerability in the legitimate vulnerable driver RTCore64.sys. The evasion technique supports disabling a whopping list of over 1,000 drivers on which security products rely to provide protection" [371].
The kernel-code-execution half of the class is GIGABYTE's gdrv.sys (CVE-2018-19320 [372]). The NVD description states the primitive directly: "The GDrv low-level driver in GIGABYTE APP Center v1.05.21 and earlier... exposes ring0 memcpy-like functionality that could allow a local attacker to take complete control of the affected system" [372]. A signed IOCTL accepts an attacker-supplied source pointer, destination pointer, and length, and copies kernel memory at ring 0: a write-what-where primitive that the attacker can compose with the read-anywhere primitive of RTCore64.sys to mint arbitrary kernel code execution.
CISA added GIGABYTE Multiple Products to the Known Exploited Vulnerabilities Catalog on October 24, 2022 with a remediation due date of November 14, 2022, citing in-the-wild exploitation [372]. The U.S. federal-civilian executive branch had two weeks to remediate; the rest of the install base did not.
Microsoft's structural answer is the Microsoft-recommended driver blocklist, enabled by default on every device since the Windows 11 2022 update [271]. The Learn page is exact about coverage: the blocklist targets drivers with "known security vulnerabilities that an attacker could exploit to elevate privileges in the Windows kernel", and explicitly catches drivers whose behaviors "circumvent the Windows Security Model" [271].
Both RTCore64.sys and gdrv.sys appear on the blocklist; the ride from disclosure to default-on enforcement was four years for RTCore64, four years for gdrv, and the same arc applies to every member of the class. Honorable mention: aswArPot.sys (CVE-2022-26522 / CVE-2022-26523 [973]) shows the same pattern from a security-product driver, with SentinelLabs reporting "two high severity flaws in Avast and AVG... that went undiscovered for years affecting dozens of millions of users" before the silent fix [973].
INSIGHT: Five privileges equal SYSTEM.
On a Windows machine, holding any of SeDebugPrivilege, SeImpersonatePrivilege, SeAssignPrimaryTokenPrivilege, SeBackupPrivilege, or SeRestorePrivilege is operationally indistinguishable from being SYSTEM. The other privileges in the standard token (the long tail of SeShutdown, SeIncreaseWorkingSet, SeTimeZone, SeChangeNotify, SeUndock, SeIncreaseQuota...) are housekeeping. Audit the holders of the five accordingly: any non-LocalSystem-equivalent account that holds them is a target.
The DACL is the load-bearing thing for file access, but exactly one bad DACL on a sensitive file ends the model. HiveNightmare proved that the cost of getting a single security descriptor wrong on a single file is the entire credential database. The general rule the lesson encodes: every primitive in the five-input list has at least one production example where misuse of that primitive alone dropped the security model to zero.
Discretionary access control assumes the principal (the user) is the right unit of authorization. By 2006, exploitable user-mode code had proven the principal was wrong. What did Microsoft do?
Mandatory Integrity Control: Stapling no-write-up onto DAC
November 2006. Vista releases to manufacturing, and for the first time in Windows history, the kernel's access check fires before the DACL walk: on something other than the user's identity. The new layer is Mandatory Integrity Control (MIC), and it adds a four-level lattice to every securable object in the system.
Microsoft Learn frames MIC compactly. "MIC uses integrity levels and mandatory policy to evaluate access. Security principals and securable objects are assigned integrity levels that determine their levels of protection or access. For example, a principal with a low integrity level cannot write to an object with a medium integrity level, even if that object's DACL allows write access to the principal" [964]. The same page enumerates the levels: "Windows defines four integrity levels: low, medium, high, and system" [964]. The levels are encoded as four well-known SIDs: Low (S-1-16-4096), Medium (S-1-16-8192), High (S-1-16-12288), System (S-1-16-16384) [969].
Definition: Mandatory Integrity Control (MIC).
A Windows kernel mechanism, introduced with Vista (released to manufacturing November 8, 2006; consumer general availability January 30, 2007 [561]), that adds an integrity-level check to SeAccessCheck. Each securable object carries an integrity label inside its SACL; each access token carries an integrity level. An access whose direction violates the configured mandatory policy (typically no-write-up) is denied at the integrity check before the DACL walk runs. MIC is the mandatory layer the Windows DAC model historically lacked [964].
Definition, Integrity Level. A linearly-ordered tag (Low / Medium / High / System) carried on every Windows process token and every securable object. The kernel uses the relative ordering to enforce mandatory policy independent of the object's DACL. The four well-known SIDs are S-1-16-4096 (Low), S-1-16-8192 (Medium), S-1-16-12288 (High), and S-1-16-16384 (System) [969].
The default policy is no-write-up. A process at integrity level cannot modify an object at integrity level greater than , regardless of what the DACL says. Microsoft's example is the load-bearing one: a process running at Low IL cannot write to a Medium-IL object even if the DACL says Everyone has full control. The integrity check fires before the DACL walk; if the integrity check denies, the DACL is not consulted [964].
The integrity label is stored as a SYSTEM_MANDATORY_LABEL_ACE inside the SACL, not the DACL [974]. The mask field on the label ACE encodes which directions of access the policy forbids: SYSTEM_MANDATORY_LABEL_NO_WRITE_UP (0x1), SYSTEM_MANDATORY_LABEL_NO_READ_UP (0x2), and SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP (0x4) [974].
Storing the label in the SACL is a deliberate choice with one operational consequence: tools that copy the DACL but not the SACL silently drop the integrity label. The most common consequence is a Low-IL file getting copied to a new location and emerging with no integrity label, which defaults to Medium and unintentionally raises the object's protection. The more dangerous opposite mistake is a high-integrity object losing its label and defaulting to Medium, so a Medium-IL caller can now reach what the explicit label used to protect.
The no-write-up mask is the one to memorise, because it is the policy almost every label uses. When a Low-IL caller tries to act on a Medium-IL object, the kernel denies any access whose mapped result contains write-class bits, including the standard-rights WRITE_DAC (bit 18 of the 32-bit ACCESS_MASK [975]) and WRITE_OWNER (bit 19), the type-generic DELETE (bit 16), and the file-specific FILE_WRITE_DATA (0x2), FILE_APPEND_DATA (0x4), FILE_WRITE_EA (0x10), and FILE_WRITE_ATTRIBUTES (0x100) [976].
The presence of FILE_APPEND_DATA in that list matters operationally: a careless reader of the spec might assume that "append" semantics escape the no-write-up rule because they do not modify existing bytes. They do not. MIC denies both write modes, so log-only append handlers cannot be used as a write-up channel into a higher-IL object.
A second rule completes the model: process integrity inheritance. When a process is created, the kernel assigns it the minimum of the user's integrity level and the file's integrity level [964]. A medium-IL user running a low-IL executable gets a low-IL process. This is the rule that lets Internet Explorer 7 run at Low even when launched from a Medium user session.
The first MIC consumer was IE7 Protected Mode, which shipped with Windows Vista RTM (released to manufacturing November 8, 2006) [977,561]. (IE7 standalone for Windows XP, released October 18, 2006 [978], runs without Protected Mode. The feature depends on Vista's MIC kernel layer.) Skywing's Uninformed Volume 8 Article 6, "Getting Out of Jail: Escaping Internet Explorer Protected Mode," is the first public reverse-engineering of the implementation.
Skywing's framing remains the most-cited primer on the subject: "With the introduction of Windows Vista, Microsoft has added a new form of mandatory access control to the core operating system. Internally known as 'integrity levels', this new addition to the security manager allows security controls to be placed on a per-process basis. This is different from the traditional model of per-user security controls used in all prior versions of Windows NT" [977]. IE7's Protected Mode pattern (a Low-IL worker that does the dangerous parsing, paired with a Medium-IL broker that performs the system-changing operations on the worker's behalf) became the template Windows would later generalize into AppContainer.
Sidenote.
User Interface Privilege Isolation (UIPI) is the window-message gate that uses MIC at the desktop. A Low-IL window cannot send SendMessage or PostMessage traffic to a Medium-IL or higher window. UIPI is the reason you cannot click-jack a UAC consent prompt from a normally-running browser process: the consent prompt runs at High IL, the browser runs at Medium [969].
Windows 8 generalized the MIC pattern into AppContainer. An AppContainer process gets a fresh Low-IL token plus an AppContainer flag, a Package SID that identifies the app, and a list of capability SIDs the app declared in its manifest. Microsoft Learn states the resulting isolation directly: "Windows ensures that processes running with a low integrity level cannot obtain access to a process which is associated with an app container" [964]. The Package SID and capability SID derivations are the subject of a separate article on Windows app identity (external further reading [19]); we will not redefine them here. The full integrity-level and AppContainer lattice is the subject of The Integrity-Level Stack (Chapter 23).
MIC fixed the integrity boundary inside the kernel. But the same year shipped a separate retrofit for a different problem: why was the consumer admin running every clicked-on .exe with full administrative authority? And why did Microsoft refuse to call its answer a security boundary?
UAC, the split-token, and the bypass tradition
Vista's User Account Control is the most famous Windows security retrofit, and the only one whose own keepers explicitly published a document declaring it not a security boundary [301]. The mechanism is precise. The bypass tradition is enormous. The classification is honest.
The mechanism first. Microsoft's documentation on how UAC works gives the verbatim recipe [979]: "When an administrator logs on, two separate access tokens are created for the user: a standard user access token and an administrator access token. The standard user access token... contains the same user-specific information as the administrator access token, but the administrative Windows privileges and SIDs are removed... is used to display the desktop by executing the process explorer.exe. Explorer.exe is the parent process from which all other user-initiated processes inherit their access token. As a result, all apps run as a standard user unless a user provides consent or credentials to approve an app to use a full administrative access token."
Definition: User Account Control (UAC) and Split-Token Elevation.
A Windows mechanism, introduced with Vista (released to manufacturing November 8, 2006 [561]), in which an administrative user receives two linked tokens at logon: a filtered Medium-IL token without administrative privileges or SIDs, used by explorer.exe and every process descended from it, and a full High-IL administrative counterpart that the kernel hands out only after the user clicks through a consent prompt or supplies credentials. The two tokens reference each other through the LinkedToken field. UAC is a UX-and-default-behavior mechanism, not an enforced security boundary [979,301].
The split-token mechanism produces three elevation triggers. First, an executable can declare its required level in its manifest via the requestedExecutionLevel element (asInvoker, highestAvailable, or requireAdministrator). Second, certain Microsoft-signed binaries are marked auto-elevating and appear on an AutoElevate allowlist that the user-mode AppInfo elevation service (not the kernel) consults; processes on the allowlist transparently get the full token without prompting. Third, COM components can be marked elevatable via the COM Elevation Moniker, which lets code instantiate Elevation:Administrator!new:{guid} (or Elevation:Highest!new:{guid}) to obtain a High-IL administrator COM caller: not a SYSTEM caller; the moniker's supported run levels are Administrator and Highest [980]. Method 41 (Oddvar Moe's ICMLuaUtil construction) is the canonical worked example.
The classification next. Microsoft's Security Servicing Criteria for Windows defines a security boundary as the logical separation between security domains with different trust levels, and gives the kernel-mode / user-mode separation as the canonical example [301]. The criteria document then enumerates which boundaries Microsoft commits to servicing. UAC and admin-to-kernel are not on the enumerated list.
Quoted source. A security boundary provides a logical separation between the code and data of security domains with different levels of trust... the separation between kernel mode and user mode is a classic [...] security boundary. Microsoft software depends on multiple security boundaries to isolate devices on the network, virtual machines, and applications on a device.: Microsoft Security Servicing Criteria for Windows [301]
The "outside the enumerated list" classification has a concrete consequence: bypasses of UAC are not eligible for the same security-update treatment a kernel-mode-to-user-mode bypass would get. Mitigations are issued per-redirect, when an attacker's specific path becomes operationally noisy enough to warrant attention. The seventy-plus methods cataloged in UACMe are the empirical consequence.
Key idea. UAC was never a security boundary. The seventy-plus methods cataloged in UACMe are not bugs in UAC. They are the formal consequence of UAC's classification. Once you recognize that UAC is a UX-and-default-behavior mechanism rather than an enforced boundary, the bypass tradition is legible as a feature being used as designed and the structural arc to Adminless makes sense.
The bypass canon. Walk it generation by generation.
Method 1: Leo Davidson, 2009. Davidson's Windows 7 UAC Whitelist writeup is the genealogical root of the UAC bypass tradition [981,954]. He noticed that sysprep.exe is on the AutoElevate whitelist and that, when launched from C:\Windows\System32\sysprep\, it loads several DLLs from the working directory. Use the IFileOperation COM interface (which the elevator treats as AutoApprove, enabling the file copy without prompting) to drop a malicious cryptbase.dll into %SystemRoot%\System32\sysprep\. Then trigger sysprep.exe (which is on the AutoElevate whitelist) and the auto-elevated process loads the attacker's DLL, and the attacker has a High-IL full administrative token.
Davidson's writeup quotes himself bluntly: "This works against the RTM (retail) and RC1 versions of Windows 7" [981]. UACMe Method 1 records the technique with structured metadata: "Author: Leo Davidson / Type: Dll Hijack / Method: IFileOperation / Target(s): \system32\sysprep\sysprep.exe / Component(s): cryptbase.dll / Implementation: ucmStandardAutoElevation / Works from: Windows 7 (7600) / Fixed in: Windows 8.1 (9600)" [954].
Method 25: Matt Nelson (enigma0x3), August 15, 2016. Seven years after Davidson, Nelson published "Fileless UAC Bypass Using eventvwr.exe and Registry Hijacking" [982]. The bypass replaces the file-system DLL hijack with a registry redirect. Nelson noticed that eventvwr.exe, an AutoElevated binary, queries HKCU\Software\Classes\mscfile\shell\open\command before HKCR\mscfile\shell\open\command to find the command to run for the mscfile ProgID. His verbatim observation: "From the output, it appears that 'eventvwr.exe', as a high integrity process, queries both HKCU and HKCR registry hives to start mmc.exe" [982]. HKCU is writable by the standard user; the user writes a malicious command line under that key, runs eventvwr.exe, and the auto-elevated process happily executes the user-supplied command line. The first fileless UAC bypass.
Method 33: winscripting.blog, 2017. Same primitive, different target. The fodhelper.exe binary is on the AutoElevate whitelist and queries HKCU\Software\Classes\ms-settings\shell\open\command to launch the Settings app. UACMe records the credit precisely: "Author: winscripting.blog / Type: Shell API / Method: Registry key manipulation / Target(s): \system32\fodhelper.exe / Component(s): Attacker defined / Implementation: ucmShellRegModMethod / Works from: Windows 10 TH1 (10240) / Fixed in: unfixed" [954]. Fixed in: unfixed. This is what "outside the enumerated list" looks like in practice: nine years after Method 1 and a year after Method 25 demonstrated the underlying class, the registry-redirect template was still being applied to fresh AutoElevate targets and shipping unmitigated.
Method 41: Oddvar Moe. The COM elevation moniker route. UACMe records: "Author: Oddvar Moe / Type: Elevated COM interface / Method: ICMLuaUtil" [954]. Instantiate the CMSTPLUA COM object via the elevation moniker from a Medium-IL process, get back its High-IL ICMLuaUtil interface, and call its ShellExec method to run an attacker command line at High IL. The seam is the COM moniker registry's Elevation\Enabled key, which marks specific CLSIDs as elevation-capable.
Method 31 (sdclt), 29, 34,... The pattern repeats. Matt Nelson's sdclt.exe variants exploit the backup-restore UI's registry lookups. Forshaw's schtasks variant exploits the scheduled-task COM interface. The UACMe README enumerates the lot with the laconic one-liner "Defeating Windows User Account Control by abusing built-in Windows AutoElevate backdoor" [954]. Most of the methods reduce to the same primitive: an AutoElevated Microsoft-signed binary performs a lookup that the standard user can redirect, the standard user supplies an attacker-controlled answer, and the auto-elevated binary executes attacker-controlled work.
AutoElevate redirect template. The recurring UACMe pattern is a Medium-IL process placing attacker-controlled data into a seam it can write (an HKCU registry key, a user-writable file-system path, or scheduled-task metadata) and then triggering a Microsoft-signed AutoElevated binary. The binary receives a High-IL full administrative token, performs a lookup through the writable seam, accepts the attacker-supplied answer, and executes work under the elevated token. The gap is not that SeAccessCheck miscalculates; it is that the elevation flow produced a different token before the check.
Aside: Mark Russinovich on UAC, June 2007. Mark Russinovich's June 2007 TechNet Magazine cover story, "Inside Windows Vista User Account Control," is the canonical practitioner walkthrough of the split-token model and is preserved on the Wayback Machine [983]. Russinovich opens by naming the misunderstanding: "User Account Control (UAC) is an often misunderstood feature in Windows Vista... In this article I discuss the problems UAC solves and describe the architecture and implementation of its component technologies." The framing throughout Russinovich's article is that UAC's purpose is to create the expectation that consumer software would run as a standard user, and to push the developer community to refactor away from gratuitous administrator requirements. That framing (UAC as a UX and migration mechanism) is consistent with the eventual MSRC servicing-criteria position: not a defended boundary, but a behavior gate.
NOTE: UAC bypass mitigations are issued per-redirect, not per-feature.
Microsoft's servicing-criteria position means a UAC bypass that does not also cross a serviced security boundary is not, by policy, eligible for a security update. Mitigations land when the operational footprint of a particular bypass becomes large enough to justify one. Track UAC mitigations by KB number, not by feature description; consult the UACMe README's Fixed in: field as the institutional memory [954,301].
UAC bypasses redirect the elevation flow that produces a token. A different attack family takes the result and steals tokens from already-elevated SYSTEM services. Where do those attacks come from, and why are they all the same bug?
The Object Manager and the lookup surface
SeAccessCheck evaluates a security descriptor it has been handed. Who hands it the descriptor?
The kernel's Object Manager does, after walking a name. Every named kernel object lives somewhere in a hierarchical namespace rooted at \. Devices live under \Device. Synchronisation primitives live under \BaseNamedObjects. Pre-resolved DLL names live under \KnownDlls. Per-session subtrees live under \Sessions. The DOS device prefix \GLOBAL?? (and its session-local sibling \??) holds drive-letter symbolic links into the device tree. When a process calls OpenObject, the Object Manager parses the name, walks the tree, and returns the object whose security descriptor SeAccessCheck will then evaluate.
The kernel performs the lookup before the access check. This sequencing creates a parallel attack surface that bypasses SeAccessCheck entirely. If the attacker can influence the name resolution: redirect a \??\ symbolic link, plant a junction in NTFS that re-targets a directory traversal, hardlink a low-privilege file at a path the kernel will trust because of the parent directory's descriptor: then by the time SeAccessCheck runs, it is being asked about a different object than the original code path intended to open.
The HiveNightmare lookup path is the canonical worked example. The exploit reads the SAM database not via \Windows\System32\config\SAM (which has a tight DACL) but via \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy*\Windows\System32\config\SAM. That path resolves through the Object Manager's \GLOBAL?? symbolic link, into the device tree, into a Volume Shadow Copy mirror of the original volume, into a copy of SAM that inherits the live file's DACL, which (until the August 2021 patch) granted read access to any authenticated local user (BUILTIN\Users); unlike the live file, the shadow copy is not locked open, so the read succeeds [956].
The lookup-phase attack class is wider than file-system shadow copies. Object Manager symbolic links and NTFS hard links both produce the same primitive: the kernel resolves a name through an attacker-influenced redirect and ends up evaluating SeAccessCheck against a different security descriptor than the calling code intended.
CVE-2020-0668, the Service Tracing elevation-of-privilege bug Clément Labro disclosed in February 2020, is the textbook symbolic-link case [984]. The Service Tracing infrastructure under HKLM\SOFTWARE\Microsoft\Tracing is user-writable, and several SYSTEM services (IKEEXT, RasMan, the Update Session Orchestrator service) consult those keys to find a tracing log path. When the log file exceeds the configured MaxFileSize, the service renames it from MODULE.LOG to MODULE.OLD, deleting any existing MODULE.OLD first.
itm4n's exploit is exactly the one his blog post names: "All you need to do is set the target directory as a mountpoint to the \RPC Control object directory and then create two symbolic links: A symbolic link from MODULE.LOG to a file you own; A symbolic link from MODULE.OLD to any file on the file system" [984]. The mountpoint reroutes the kernel's name resolution into an Object Manager directory the attacker controls; the two symlinks reroute the rename operation; the SYSTEM service ends up performing an arbitrary file move with kernel authority. Forshaw's googleprojectzero/symboliclink-testing-tools repository [985] provides the primitive library the exploit consumes (CreateSymlink, CreateMountPoint, BaitAndSwitch) and the repository is, in effect, the institutional library every Object Manager lookup-phase attack of the past decade has linked against.
The NTFS hard-link case predates the symbolic-link case by half a decade. James Forshaw's December 2015 Project Zero post, "Between a Rock and a Hard Link," is the canonical primary source [986]. Forshaw observes that hard links have been a feature of NTFS "since it was originally designed", and that their relevance to local privilege escalation is exactly the lookup-vs-access-check sequencing this section describes: "Why are hard links useful for local privilege escalation? One type of vulnerability is exploited by a file planting attack, where a privilege service tries to write to a file in a known location" [986].
The worked example Forshaw walks is CVE-2015-4481, a Mozilla Maintenance Service hard-link primitive: any user can write a status log to C:\ProgramData\Mozilla\logs\maintenanceservice.log, and during the service's pre-write BackupOldLogs rename a brief window opens in which the attacker can replace the about-to-be-written log path with a hard link to an arbitrary system file. The service's subsequent write (which it performs with its own SYSTEM authority) ends up overwriting the system file. The DACL on the source file (the Mozilla log directory) was correct; the DACL on the destination file (the system binary) was correct; the kernel arrived at the destination by resolving a hard-link name the attacker had planted, and SeAccessCheck saw only the destination DACL, not the planted-link DACL [986]. Microsoft's MS15-115 mitigation tightened the kernel's hard-link semantics for sandboxed callers (the kernel's NtSetInformationFile now requires FILE_WRITE_ATTRIBUTES on the target handle when the caller's token has the sandboxed-token flag, matching what the user-mode CreateHardLink wrapper had always opened the target with). The fix closes the sandboxed-process branch of the bug class but, as Forshaw notes, does nothing for the Maintenance Service vulnerability itself, which is exploited by a non-sandboxed local user; the structural fix is to write the log to a directory the user cannot create files in, not to enforce a hard-link mask: a structural fix to the lookup phase, not the access-check phase.
The two examples generalize the rule. The kernel resolves names before checking the requesting user's authority over the destination. The DACL at target.path and the DACL at attacker.planted.path after a junction or hard-link redirect can be different; SeAccessCheck evaluates the descriptor it arrives at, not the descriptor the original caller intended. Capability systems would resolve names through unforgeable handles instead of strings, and the redirect class would not exist by construction [987]. Windows checks the descriptor on every OpenObject because the name is a forgeable string. The Object Manager namespace is therefore an attack surface whose load-bearing fix is structural rather than per-bug.
Sidenote.
The Object Manager's namespace is not documented as policy in the same way SeAccessCheck's algorithm is. The de facto modern documentation is empirical: practitioners enumerate the namespace with tools that read kernel structures directly. James Forshaw's NtObjectManager PowerShell module, part of the Project Zero sandbox-attacksurface-analysis-tools repository, is the dominant such tool [988]. The repository's banner is exact: "NtObjectManager: A powershell module which uses NtApiDotNet to expose the NT object manager."
The James Forshaw / Project Zero corpus is the systematic reference for the Object Manager attack surface. Forshaw's "Sharing a Logon Session a Little Too Much" (April 2020) names a primitive PrintSpoofer would later consume: when the LSA creates a token for a new logon session, it caches the token for later retrieval. Forshaw's verbatim explanation: "when LSASS creates a Token for a new Logon session it stores that Token for later retrieval. For the most part this isn't that useful, however there is one case where the session Token is repurposed, network authentication" [967]. The cached token plus a named-pipe path-validation bug becomes a non-DCOM SYSTEM-token primitive that no DACL touches.
The lookup surface is half the attack story. The other half is the token surface, and the canonical example of the token surface is a six-year, eight-tool lineage.
The Potato lineage: One bug against the token input (2016-2021)
SeAccessCheck evaluates the token a thread holds and never asks how the thread obtained it. The Potato lineage is the canonical attack on that token input, and its disclosure history is the cleanest public admission that the gap is structural. When Stephen Breen of Foxglove Security published the first of them ("Hot Potato," on January 16, 2016), the post opened with a sentence the rest of the model has to live with: "Microsoft is aware of all of these issues and has been for some time (circa 2000). These are unfortunately hard to fix without breaking backward compatibility and have been [used] by attackers for over 15 years" [696].
The single underlying primitive is one sentence long. A low-privileged service account holding SeImpersonatePrivilege (present by default for LOCAL SERVICE / NETWORK SERVICE and commonly granted through the Service user right for SCM-started workloads unless policy narrows it) coerces SYSTEM into authenticating to a TCP, RPC, or named-pipe endpoint the attacker controls; the endpoint accepts the authentication, ends up with an impersonation handle to a SYSTEM token, and calls ImpersonateLoggedOnUser followed by CreateProcessAsUser to run as SYSTEM. The bearer-token property does the rest: the kernel grants whatever rights the handle names, no questions asked.
Eight tools across six years (Hot Potato, Rotten Potato, Juicy Potato, Rogue Potato, PrintSpoofer, RemotePotato0, PetitPotam, and SharpEfsPotato) are eight different ways to provoke that one SYSTEM-token handle. The full catalog, with each tool's coercion vector (NBNS/WPAD, DCOM activation and OXID resolution, the Print Spooler RPC surface, EFSRPC) and the specific mitigation that did or did not break it, is the subject of The SeImpersonate Primitive (Chapter 24). The access-control lesson is the pattern, not the tooling: the mitigation that broke any one tool was always specific (the loopback-OXID restriction, the cross-session DCOM partial fix, the EFSRPC coercion mitigation in KB5005413), while the mitigation that would break the family is structural. Remove SeImpersonatePrivilege from service accounts, end NTLM-to-self relay, or retire the bearer-credential property of tokens. Microsoft has shipped the first kind of fix repeatedly; the second kind is what the successor architectures are for. Eight tools in six years against the same primitive is not a tooling coincidence. It is the empirical signature of the bearer-credential property and the omnipresent service-account SeImpersonate privilege.
WARN: A service account with SeImpersonate is usually a path to SYSTEM.
A service account holding SeImpersonatePrivilege plus any RPC interface that authenticates to attacker-controllable endpoints can, in common service configurations, be escalated to SYSTEM. Eight Potatoes in six years prove this is structural, not a tooling fad. Audit every server: any non-LocalSystem-equivalent process holding SeImpersonate or SeAssignPrimaryToken should be treated as a Potato target until proven otherwise. Pre-deploy per-service SIDs and Group Managed Service Accounts where possible to constrain the blast radius [989]. The SeImpersonate Primitive (Chapter 24) owns the full lineage and its closure attempts.
Eight Potatoes prove the bearer-token property is unkillable by point fixes. But what does an attacker who is already admin do? The answer is the most-cited offensive Windows tool of the past fifteen years.
Mimikatz, conditional ACEs, and the edges of the model
Benjamin Delpy released the first version of Mimikatz in May 2011, and fifteen years later every offensive Windows engagement still reaches for it. Its module set and the credential-theft decade it defined are the subject of Mimikatz and the Credential-Theft Decade (Chapter 14). Two of those modules, though, sit directly on the access-control surface and are worth naming here, because each is a one-command attack on one of SeAccessCheck's inputs. The repository's own command surface lists them as privilege::debug and token::elevate [261].
privilege::debug is one line of code: the command enables SeDebugPrivilege on the caller's token. Any local administrator on stock Windows holds the privilege on the token by default; the command flips it from Available to Enabled via AdjustTokenPrivileges. With SeDebugPrivilege enabled, the calling process can OpenProcess against most processes on the machine, including SYSTEM-level services such as lsass.exe (Protected Process Light targets are the exception). Every Mimikatz session that wants to read process memory begins with privilege::debug.
token::elevate is three lines of code in spirit. The command opens a SYSTEM-owned process (typically lsass.exe), calls OpenProcessToken to retrieve a handle to the SYSTEM token, calls DuplicateTokenEx to duplicate the handle for impersonation, and calls SetThreadToken to attach the duplicated SYSTEM token to the calling thread. The thread is now SYSTEM. That is the bearer-token property, in three lines of code.
This chapter does not cover sekurlsa::logonpasswords: the command that reads cached credentials out of lsass.exe. Mimikatz's full module set and the credential-theft history belong to Mimikatz and the Credential-Theft Decade (Chapter 14), and the Credential Guard mitigation that finally moves the secret out of the NT kernel's address space is the Credential Guard chapter's subject (Chapter 15) [46]. For the access-control plane, the lesson stops at token::elevate.
The lesson is the structural concession. With administrator rights on the local machine and SeDebugPrivilege enabled, the access-control model has no defense for "I will pretend to be a different process," because admin equals kernel by Microsoft's own boundary definition [301]. The DACL evaluation algorithm does not protect against a caller who can read and write arbitrary kernel memory. The privilege list does not protect against a caller who can rewrite the privilege check. The integrity check does not protect against a caller who can edit the integrity label. Every primitive in the model is, by construction, defenseless against an attacker who has crossed the boundary the model considers itself responsible for defending.
INSIGHT: Admin equals kernel, by design.
With administrator rights and SeDebugPrivilege, the Windows access-control model has no defense for "I will pretend to be a different process," because admin equals kernel by Microsoft's own boundary definition. Mimikatz token::elevate is the canonical demonstration. The structural fix for selected secrets is Credential Guard, which moves the secret out of the NT kernel's address space entirely. See the Credential Guard chapter (Chapter 15) and the Secure Kernel chapter (Chapter 6) for the architecture [46,301].
The model has been extended in only one structural direction since 1993, and that direction is the subject of the access matrix. Conditional ACEs and Dynamic Access Control (DAC) shipped together in Windows Server 2012 and Windows 8 [990]. They are the only extension of the access-matrix subject Microsoft has shipped in thirty-three years.
The mechanism is twofold. First, ACEs gain an expression syntax. The SDDL ACE strings page documents XA, XD, XU, and ZA as conditional callback variants of the basic allow / deny / audit / object-allow ACE types [965]. A conditional ACE carries an expression in addition to a SID and an access mask, and the kernel evaluates the expression against the token's claims at access time. The canonical example is (XA;;FA;;;AU;(@User.Department=="Finance")): an allow-callback ACE that grants FILE_ALL_ACCESS to Authenticated Users if the token carries a Department claim equal to "Finance".
Second, the token gains claims. A claim is a typed key-value pair attached to the token by Active Directory at logon. Claims can be sourced from the user's AD attributes, the device's AD attributes, or resource properties on the object. Microsoft Learn states the role they play: "A central access rule is an expression of authorization rules that can include one or more conditions involving user groups, user claims, device claims, and resource properties. Multiple central access rules can be combined into a central access policy" [990].
A Central Access Policy (CAP) is a set of Central Access Rules (CARs), each of which is a conditional-ACE expression. The CAP is applied to file shares; the file-share metadata says "evaluate this CAP for every access," and the CAP's expressions reference token claims. The DAC scenario guidance enumerates the deployment-side primitives: automatic and manual file classification, central access policies for safety-net authorization, central audit policies for compliance reporting, and Rights Management Service encryption for data-in-use protection [991].
The reason DAC has not displaced classic DAC outside file-server scenarios is in the same Microsoft Learn page: "Dynamic Access Control is not supported in Windows operating systems prior to Windows Server 2012 and Windows 8. When Dynamic Access Control is configured in environments with supported and non-supported versions of Windows, only the supported versions will implement the changes" [990]. Heterogeneous environments fall back to classic DAC. Airgapped environments without a claims-enabled AD DS (a Server 2012+ KDC issuing claims in the Kerberos PAC) have no claims to evaluate. Conditional ACEs are a real extension of the model's subject dimension; they are also a real bet that the AD-and-Kerberos plane is healthy enough to evaluate them on every access.
Sidenote. AppContainer's Package SIDs (Windows 8) and conditional ACEs (Server 2012) shipped the same year. Both extend the subject dimension of the access matrix: one with code identity, one with attribute claims. Neither closes the kernel-equals-admin gap. The two extensions are coordinate, not stacked: a conditional ACE can reference a Package SID; a Package SID can be the subject of a conditional ACE [990,19].
The model has been extended in two coordinate dimensions in thirty-three years. It has not been replaced. So what does the whole thing look like put together, and what does it actually fail at?
The 2026 plane: Ten primitives, one decision
Run a single OpenObject call on a Windows 11 machine and walk the kernel's path. Every primitive the chapter has introduced fires for that one call.
The figure is the chapter in one diagram. Read it left to right. Every box is a primitive named across this chapter, and every famous Windows escalation tool of the last twenty-five years targets one of those boxes:
| # | Primitive | Year shipped | Canonical primary citation | Canonical attack |
|---|---|---|---|---|
| 1 | Security Reference Monitor | 1993 | [952] | (Underlying surface; not directly attacked) |
| 2 | Security Identifier (SID) | 1993 | [968] | Misused well-known SIDs ("Everyone is just a SID") |
| 3 | Access Token | 1993 | [963] | The Potato lineage; Mimikatz token::elevate |
| 4 | Security Descriptor | 1993 | [955] | HiveNightmare (CVE-2021-36934) |
| 5 | DACL + ACE | 1993 | [955,966] | NULL DACL misconfigurations; out-of-order ACEs |
| 6 | SACL + audit | 1993 | [952] | Tools that copy DACL but not SACL silently drop integrity labels |
| 7 | Privilege | 1993 | [972] | Mimikatz privilege::debug; SeBackup abuse |
| 8 | Mandatory Integrity Control | 2007 | [964] | IE7 Protected Mode broker bypasses |
| 9 | UAC split-token | 2007 | [979] | UACMe: 70+ AutoElevate-redirect methods [954] |
| 10 | Conditional ACE / DAC | 2012 | [990,965] | Falls back to classic DAC in heterogeneous environments |
INSIGHT: One decision plane, not a feature catalog.
The Windows access-control model is one decision plane, not a feature catalog. Every securable Windows operation resolves through SeAccessCheck against five fixed inputs. Every famous escalation tool of the last twenty-five years attacks one of those inputs. Recognizing the model as a single plane is the key to using its vocabulary against any specific attack.
The plane is whole. It is also full of structural holes its own keepers have publicly admitted. What are they?
The five structural limits
Microsoft's Security Servicing Criteria for Windows defines a security boundary as "a logical separation between the code and data of security domains with different levels of trust... the separation between kernel mode and user mode is a classic [...] security boundary" [301]. The criteria document then enumerates which boundaries Microsoft commits to servicing. The kernel-mode / user-mode boundary qualifies. UAC and admin-to-kernel are not in the enumerated list. Once that admission is on the public record, the model's structural arc becomes legible. Five derived limits flow from the concession.
Limit 1: Admin equals kernel. Any compromise with administrator rights can rewrite the model's own enforcement code. Consequence: Mimikatz, every kernel-driver-loading attack, every signed-driver bring-your-own-vulnerable-driver path. Successor: VBS Trustlets, which host secrets and policy enforcement in the Virtual Trust Level 1 user-mode environment that the VTL0 NT kernel cannot read or modify. Detailed coverage belongs to the Secure Kernel chapter (Chapter 6) and the VBS Trustlets chapter (Chapter 7) [46].
Limit 2: Tokens are bearer credentials. Whichever process holds the handle gets the rights. The kernel does not ask how the handle was obtained. Consequence: the entire Potato lineage (eight tools, six years), Mimikatz token::elevate, every cross-session token-theft attack. Successor: Adminless / Administrator Protection, which retires the long-lived filtered/full token pair in favor of a fresh, time-limited, just-in-time elevation flow gated by Windows Hello plus a hidden, system-generated, profile-separated user account that issues an isolated admin token [323,992]. Administrator Protection is a shipping Windows 11 feature, documented at [323,992].
Limit 3: broad SeImpersonatePrivilege on service accounts. LOCAL SERVICE / NETWORK SERVICE have it enabled by default, and many SCM-started service workloads receive it through the Service user-right assignment unless policy narrows them. Consequence: every Potato, by construction. Partial successor today: per-service SIDs and Group Managed Service Accounts let administrators constrain the blast radius of a compromised service. Structural successor: Adminless, which removes the privilege from the daily authentication path and demands a fresh elevation per privileged action [323,992].
Limit 4: NTLM relay surface. As long as Windows services accept NTLM and the operating system signs NTLM challenges with the local-machine credential, the local-NTLM-to-self attack is structurally available. Consequence: PetitPotam, RemotePotato0, every cross-protocol relay. Successor: NTLMless, which formally retires NTLM as a default Windows authentication protocol [717]. The on-ramp is the NTLM auditing channel introduced in Windows 11 24H2 and Windows Server 2025 (KB5064479, original publish date July 11, 2025), which records NTLMv1 usage in Microsoft\Windows\NTLM\Operational and gives administrators a per-workload deprecation telemetry [733]. The retirement of NTLM is the subject of The Death of NTLM (Chapter 16).
Limit 5: The DACL is local. Conditional ACEs and Dynamic Access Control claims need a claims-enabled AD DS (a Server 2012+ KDC issuing claims in the Kerberos PAC) to evaluate, with AD FS required only for cross-forest or federated claims; airgapped or heterogeneous environments fall back to user / group SIDs as the only available subject. Consequence: the access-matrix subject is, in practice, still "user and group" for most non-file-server workloads. The 2012 extension to claims and code identity is real but operationally bounded.
The deepest of the five limits is the one Norm Hardy named in 1988. Hardy's framing [959] holds: capability systems close the gap structurally; ACL engineering does not.
seL4 closes it with machine-checked correctness proofs and a capability-based design that makes ambient authority a category error [987]. Windows closes it, when it closes it at all, with VBS Trustlets that move the right to perform the operation into a separate execution domain. The Potato lineage is the textbook confused-deputy instance: a service running with SeImpersonatePrivilege is the privileged compiler; the attacker is the user holding a billing-records-shaped pointer; the service uses its own authority on every authentication it accepts.
INSIGHT: The Potato lineage is the canonical confused-deputy instance. Hardy's 1988 paper [958] and the Wikipedia summary [959] both say the same thing: ACL systems are structurally vulnerable to confused-deputy attacks; capability systems are not. The gap is not asymptotic. ACL engineering does not close it. The Potato lineage is what the gap looks like in the field, repeated against eight different coercion primitives over six years.
Key idea. The next generation of Windows defenses cannot live inside the kernel, because the kernel is on the wrong side of the boundary the model draws. Microsoft's own servicing criteria admit it. Adminless, NTLMless, VBS Trustlets, and Credential Guard are the four non-overlapping ways to fix it. Each successor was scoped to close a specific gap the access-control model could not.
The five limits are named. The successors are shipping. What replaces what?
The successors: Adminless, NTLMless, VBS trustlets, Credential Guard
The previous section named five limits that the 1993 model cannot remove from inside itself: a compromised kernel can rewrite VTL0 memory; a long-lived admin token is a bearer credential; broad service privileges create confused deputies; NTLM supplies relayable local-machine proof; and a DACL is local policy until a directory, claims system, or remote relying party gives it broader meaning. The successor architectures matter because each changes a trust boundary rather than adding another deny ACE.
Adminless / Administrator Protection. The ordinary UAC model begins at logon. If the user is an administrator, Windows creates a filtered Medium-IL token for daily use and keeps a linked full High-IL token available for consent-time elevation [979]. That reduces accidental writes, but it leaves the machine with a long-lived administrative bearer credential attached to the logon session. Administrator Protection changes the flow. Daily work runs without membership in the local Administrators path; when a privileged operation is requested, the system performs an explicit Windows Hello authorization and uses a hidden, system-generated, profile-separated administrative account to mint an isolated admin token for that task [323,992].
The trust boundary therefore moves from "this interactive logon session owns an admin token that may be exposed later" to "this single operation receives a freshly authorized, short-lived administrative identity." The practical consequence for the access-control plane is sharp: malware that steals handles, injects into Medium-IL desktop processes, or waits for a user to consent cannot simply inherit a standing full token. It must either compromise the just-in-time authorization ceremony or the code path that consumes the isolated token. The limitation is equally important. Adminless does not make administrators impossible, does not make every privileged operation safe, and does not turn UAC into a security boundary under MSRC's servicing criteria [301]. It narrows the lifetime and ambient availability of administrator authority. It closes the bearer-token and broad-service-privilege limits by making admin authority event-scoped instead of session-scoped.
NTLMless. The Potato lineage exists because NTLM is a relayable proof of possession. A low-privilege service can coerce a privileged local service to authenticate, catch or relay the NTLM exchange, and transform "the machine or SYSTEM proved itself to me" into a token usable somewhere else. Kerberos does not erase all delegation risk, but it changes the flow: tickets are issued by a KDC for named services, carry service targets, and can be constrained by policy. Microsoft's Windows authentication direction is to reduce NTLM usage until it can be disabled in Windows 11; IAKerb lets Kerberos work when the client cannot directly reach a KDC by proxying the exchange through the server, and the new local KDC covers local-account scenarios that historically fell back to NTLM [717].
The 2025 auditing additions are the migration bridge rather than the fix itself. Windows 11 24H2 and Windows Server 2025 add NTLMv1 audit events in Microsoft\Windows\NTLM\Operational, with client, server, and domain-controller views of who still depends on the protocol [733]. That matters because the trust boundary is organizational: disabling NTLM on a host before discovering a legacy workload breaks production; leaving it enabled everywhere preserves the relay primitive. NTLMless closes limit #4 only when the audit trail has been driven to zero or to explicitly isolated exceptions. Its limitation is compatibility. The transition is not a patch Tuesday switch; it is an inventory, exception, and retirement program.
VBS Trustlets and Isolated User Mode. Classic NT assumes the kernel is the top of the local trust hierarchy. If an attacker reaches ring 0, the access token, security descriptor, DACL, and LSASS memory all live in an address space the attacker can read or modify. Virtualization-Based Security adds a higher local boundary below the Windows kernel: Hyper-V partitions the machine into VTL0, where the normal NT kernel and user processes run, and VTL1, where the secure kernel and trustlets run (developed in the Secure Kernel chapter, Chapter 6, and the VBS Trustlets chapter, Chapter 7). VTL0 can ask VTL1 for services through defined call gates; it cannot directly map or patch VTL1 memory [46].
That changes what "administrator" and even "kernel" mean. A VTL0 kernel compromise may still create tokens, tamper with DACLs, load vulnerable drivers if policy allows, and own ordinary processes. It does not automatically read a secret that has been moved into a trustlet. The boundary is hardware-assisted memory isolation plus a smaller service interface. The limitation is scope. VBS is not a second operating system that reimplements all of Windows access control. It protects selected secrets and policy decisions whose owners explicitly move them across the VTL boundary; everything left in VTL0 remains governed by the old model and by driver integrity.
Credential Guard. Credential Guard is the canonical trustlet deployment because it chooses the single secret store that made Mimikatz famous. In the old flow, lsass.exe in VTL0 received logon material, retained credential blobs or derived secrets, and later answered authentication requests. A process with SeDebugPrivilege, SYSTEM, or a kernel read primitive could inspect LSASS memory and recover material that was never meant to be an access-token right [261]. With Credential Guard, the VTL0 lsass.exe remains the compatibility endpoint, but the sensitive credential operations move to LsaIso in VTL1. VTL0 holds references and marshals requests; VTL1 holds the protected material and performs the operation [46].
The trust boundary is precise: protect credential contents from the normal kernel and from administrators in VTL0, not protect every authentication protocol from every misuse. Credential Guard does not stop pass-the-hash when hashes were already stolen elsewhere, does not fix weak delegation policy, and does not prevent a malicious administrator from creating new accounts or changing DACLs. It does make sekurlsa::logonpasswords fail for the historical reason the command succeeded: the bytes are no longer in LSASS's VTL0 address space.
Pluton-rooted attestation and the hardware foundation. These successors need evidence about the machine they are running on. Secure Boot measures the chain into the TPM; Pluton moves the hardware root of trust into the processor package and supplies patchable, Microsoft-integrated key storage for attestation [993]. The access-control lesson is that local authorization is no longer only "what does this token say?" It is also "which measured platform produced this token, which virtualization boundary is active, and which remote relying party accepts that evidence?" Pluton does not replace SeAccessCheck; it gives Adminless, VBS, and Credential Guard a stronger statement about the host beneath them.
Aside. Why these are the four.
The five limits enumerated above and the four successor families are in one-to-one correspondence. Adminless closes the long-lived-admin-token limit by shrinking when privileged interactive bearer credentials exist; the over-broad-service-privilege limit it only narrows, since Administrator Protection does not remove SeImpersonatePrivilege from service accounts such as IIS, SQL Server, or the Print Spooler. That limit is closed operationally by service isolation, privilege reduction, per-service identities, and NTLM retirement. NTLMless closes the relayable-local-authentication limit by removing the protocol primitive Potatoes keep reusing. VBS Trustlets close the kernel-is-total limit by moving selected secrets behind VTL1. Credential Guard is the first high-value proof of that trustlet pattern. Limit #5 (the DACL is local) is not solved by one kernel feature; it is solved operationally by directory-backed identity, claims, central access policies, and relying-party attestation.
With the gaps named and the successors mapped, what does an administrator actually do today?
🔵 DOCUMENTED command surfaces, not live proof
A Reasoner should be able to verify the model without exploiting anything. The following probes are intentionally read-only and are labeled as documented rather than captured: this chapter does not include a lab transcript or an evidence hash for them.
🔵 DOCUMENTED, access-token surface
Reproduce in a normal PowerShell or Command Prompt session:
whoami /all
whoami /priv
Expected shape: whoami /all prints the user SID, group SIDs, integrity level, and privilege table for the effective token. whoami /priv prints privilege names such as SeShutdownPrivilege, SeChangeNotifyPrivilege, and, on administrative or service tokens, potentially SeDebugPrivilege, SeImpersonatePrivilege, SeAssignPrimaryTokenPrivilege, SeBackupPrivilege, or SeRestorePrivilege, with an Enabled or Disabled state. Available-but-disabled privileges do not affect an access check until a process enables them with AdjustTokenPrivileges [963,972].
🔵 DOCUMENTED: object security descriptor surface
Reproduce on any Windows machine:
icacls C:\Windows\System32\drivers\etc\hosts
Expected shape: the output lists ACEs such as BUILTIN\Administrators:(F), NT AUTHORITY\SYSTEM:(F), and BUILTIN\Users:(R) or their inherited equivalents. Those strings are the DACL rendered in human form: trustee SID aliases plus access masks. SeAccessCheck evaluates those ACEs against the caller's token whenever a process asks to open the file [955,966].
🔵 DOCUMENTED, integrity-label surface
Reproduce with PowerShell:
Get-Acl C:\Windows\System32\drivers\etc\hosts | Format-List *
Expected shape: PowerShell returns the same security descriptor as a structured object. On objects that carry a mandatory integrity label, the label is stored in the SACL as a SYSTEM_MANDATORY_LABEL_ACE, not in the DACL; tools that copy only the DACL can silently drop it [964,953].
These probes do not prove every historical attack discussed below. They prove the living surfaces those attacks were aimed at: tokens, descriptors, privileges, DACLs, and integrity labels. That is enough for the chapter's claim. The trust decision is finite, inspectable, and reproducible.
Practical Guide
Six concrete recommendations for 2026, each tied to a primary Microsoft Learn or named-expert source.
TIP, 1. Use whoami /all to dump the full token surface.
whoami /all prints the SIDs in the calling thread's token, the integrity level, every privilege with its Enabled / Disabled / Default Enabled state, and (on AD-joined machines with claims) the user and device claim set. It is the single most useful diagnostic command for understanding what a session can do. Read the Enabled column carefully: an available-but-disabled privilege does not affect any access check until the process explicitly enables it [963].
TIP, 2. Use icacls and Get-Acl to inspect security descriptors.
icacls <path> prints the DACL on a file or directory; the mass-rights letters are (F) full, (M) modify, (RX) read and execute, (R) read, (W) write, (D) delete, (GA) generic all, (GR) generic read, (GW) generic write [955]. PowerShell's Get-Acl returns the same descriptor as a structured object that can be filtered and audited at scale. Sysinternals accesschk.exe answers the inverted query (which paths grant a given SID a given right) and is the right tool for catching descriptor misconfigurations across a large file system. Treat NULL DACL and empty DACL surfaces as the most-likely misconfiguration vectors.
WARN, 3. Audit SeImpersonate and SeAssignPrimaryToken holders on every server.
On every Windows server, enumerate the principals whose tokens hold SeImpersonatePrivilege or SeAssignPrimaryTokenPrivilege in their Default or Available lists. Treat any non-LocalSystem-or-equivalent holder as a Potato target until proven otherwise. Where a service must hold the privilege (most managed-service workloads do), constrain the blast radius with per-service SIDs and Group Managed Service Accounts so that a compromise of one service does not extend to a compromise of every service that shares the host's identity [989].
NOTE, 4. Track UAC-bypass mitigations by KB number, not by description.
The UACMe README is the institutional memory for the 70+ method bypass canon. Every method's Fixed in: field cites a specific Windows version or unfixed. Before declaring a binary "patched," consult the README; a method with a Fixed in: unfixed annotation is structurally available on every supported Windows version. The institutional position is that UAC bypasses do not, by Microsoft's own servicing-criteria policy, earn CVEs of their own, so the mitigations are issued per-redirect rather than per-feature [954,301].
TIP, 5. Enable Audit Process Creation with command-line auditing on.
Windows Event ID 4688 ("A new process has been created") is the most-cited detection signal for the Potato lineage and the UAC bypass tradition, because almost every member of both families ends in a CreateProcessAsUser or a redirected AutoElevate launch with a command-line argument that does not match the legitimate use of the parent binary. Enable command-line auditing under Audit Process Creation and forward the log; Sysmon Event ID 1 is the equivalent and richer signal in environments that deploy Sysinternals' Sysmon.
TIP, 6. Prefer AppContainer SIDs and conditional ACEs for new code. The access matrix is the part of the model with deliberately extensible subjects. New code that lives behind an AppContainer SID gets a Low-IL token, a Package SID, and a capability list that constrain what it can touch even when the user running it is an administrator. New file shares that need attribute-based authorization should use conditional ACEs and Dynamic Access Control rather than ad-hoc group membership. See the separate article on Windows app identity for Package SID derivation (external further reading [19]) and the Dynamic Access Control overview [990].
The full access-control plane runs in a fixed order: MIC integrity check, then the privilege bypass short-circuit, then the AppContainer capability check, then the DACL walk with conditional-ACE evaluation. Knowing which primitive makes the decision, and why, is the mental model the rest of the chapter has been building toward.
The six tips close the practical loop. With them, the practitioner can reason about any specific access decision the way the kernel does: not by remembering features, but by walking the same plane.
Margin note.
The Sysinternals accesschk and psgetsid utilities have long been first-line investigative tools for ACL audits. Both ship in the Sysinternals Suite today and continue to surface the same descriptors Get-Acl and icacls print, in the form most useful to an administrator working at scale.
Closing: Return to the hook
Open a Windows PowerShell window again. Run whoami /priv. Read the column on the right, this time with the chapter's vocabulary annotated above each line.
SeShutdownPrivilege: a privilege, in the kernel sense of a pre-checked superpower; bookkeeping rather than power.
SeIncreaseWorkingSetPrivilege, the same. Most of the twenty rows are housekeeping that the kernel checks at specific call sites to gate non-security-critical operations.
The five rows that matter are easy to spot once you know what to look for. SeDebugPrivilege: Mimikatz starts here. SeImpersonatePrivilege: the entire Potato lineage starts here. SeAssignPrimaryTokenPrivilege: the second half of every token-replay attack. SeBackupPrivilege: HiveNightmare's privilege class. SeRestorePrivilege, service-binary replacement. The kernel reads the same column on every securable operation, billions of times a second, and the answer to "can this code do this?" is built out of this list every time.
Now run icacls C:\Windows\System32\drivers\etc\hosts again. BUILTIN\Administrators:(F) is an allow ACE granting full control to the well-known SID S-1-5-32-544. NT AUTHORITY\SYSTEM:(F) is an allow ACE granting full control to S-1-5-18. BUILTIN\Users:(R) is an allow ACE granting FILE_GENERIC_READ to S-1-5-32-545. The DACL is in canonical order: explicit entries before inherited entries, deny entries (none here) before allow entries within each group. SeAccessCheck will walk this DACL on every read of hosts from any process on the machine, and the output will be deterministic (the same answer every time, for the same caller) because the model that produces it is closed and finite.
The chapter's payoff is a matter of direction. This book is not arranged in the order the defenses shipped, so it is worth being exact about what runs before this model and what inherits from it. Before it (earlier in the book and earlier at every boot) Secure Boot (Chapter 1) and the TPM (Chapter 2) establish the static-time and boot-time chains that bring the machine to a known-good state before the access-control model loads at all, with Pluton (Chapter 3) rooting the hardware identity they measure [993]. SeAccessCheck inherits that machine; it does not establish it. The architectures that close this model's own limits were likewise presented earlier: the Secure Kernel (Chapter 6) and VBS Trustlets (Chapter 7) host secrets in VTL1 outside the NT kernel's reach, Credential Guard (Chapter 15) spends that isolation to move the long-term secret off the box [46], and The Death of NTLM (Chapter 16) retires the local-relay surface the Potato lineage depends on. Each was scoped to close a specific gap this 1993 model could not close from inside. Which is exactly why this chapter, sitting after them in reading order, is where their motivation finally becomes explicit.
Bequeaths. Two chapters inherit directly from this one. The Integrity-Level Stack (Chapter 23) takes the Mandatory Integrity Control check introduced here (the no-write-up test that fires before the DACL walk) and makes the integrity lattice its entire subject. The SeImpersonate Primitive (Chapter 24) takes the one residual this model cannot retire from inside: the bearer-token property of access tokens, and the service-account SeImpersonatePrivilege that turns it into SYSTEM. What this chapter hands forward, then, is a complete, finite, deterministic decision plane (five inputs, ten primitives) together with the standing concession that the plane is only ever as honest as the kernel that runs it. Admin equals kernel, by Microsoft's own boundary definition. Everything downstream of this chapter is built to survive that concession, not to repeal it.
NT 3.1 froze a model in July 1993 because federal procurement demanded it. That model has not structurally changed in thirty-three years. The accumulated attack surface against it (twenty-five years, eight Potatoes, 70+ UAC bypasses, one Mimikatz) is the empirical proof that "frozen" was always going to mean "attackable from below." The next generation of defenses takes that lesson and stops trying to fix the model from inside. The model is not a feature catalog. It is a decision plane with five inputs, ten primitives, and five publicly conceded structural limits, and the four successor architectures of the next decade are the four non-overlapping ways to close those limits without re-evaluating against TCSEC C2 again.
SeAccessCheck decides every time. The next decade decides what it decides about.