Skip to main content

Command Palette

Search for a command to run...

The Complete Token Manipulation Playbook — Every Technique, One Truth About Parent PIDs

Updated
16 min readView as Markdown
The Complete Token Manipulation Playbook — Every Technique, One Truth About Parent PIDs

Four remaining techniques, a debunked myth about SecLogon and svchost.exe, and the definitive comparison of every way to abuse Windows Access Tokens.


Previously — Two Techniques Down

In Part 1, we covered the Suspended Process Token Swap (B3) — the most evasive technique, using NtSetInformationProcess to silently replace a frozen process's identity. In Part 2, we covered CreateProcessWithTokenW (B2) — the seclogon shortcut that trades evasion for simplicity.

This final part covers the remaining four techniques and drops a truth bomb that contradicts most red team blogs you've read: SecLogon does NOT make svchost.exe the parent process on modern Windows.

Here's what we're covering:

Technique What it does
A1 — Steal & Impersonate Thread-level identity theft via SetThreadToken
A2 — Privilege Reduction Strip your own privileges for sandboxing (defensive)
B1CreateProcessAsUserW Direct process spawn with a stolen Primary token
B4CreateProcessWithLogonW Credential-based spawn — no token theft needed

Plus: The Parent PID myth — why every technique in this series produces the same process tree.


The Parent PID Myth — Busted

Before we dive into the techniques, let's address the elephant in the room. You've probably read blog posts claiming:

"CreateProcessWithLogonW and CreateProcessWithTokenW spawn processes through the SecLogon service, so the parent PID will be svchost.exe — useful for PPID spoofing."

This is wrong on modern Windows. We tested every technique, and here's what actually happens:

PS> Get-CimInstance Win32_Process -Filter "ProcessId = 7032" | Select ProcessId, ParentProcessId, Name

ProcessId ParentProcessId Name
--------- --------------- ----
     7032            7864 cmd.exe     ← Parent = B4_logon_spawn.exe, NOT svchost.exe

What Changed — The Timeline

Windows Version SecLogon Parent PID Behavior
Windows XP / Server 2003 svchost.exe (SecLogon) was the parent ✅
Windows Vista / 7 / 10 / 11 The calling process is the parent ❌

Starting from Vista, Microsoft redesigned SecLogon. When CreateProcessWithTokenW or CreateProcessWithLogonW fires an RPC to the SecLogon service, SecLogon internally:

  1. Receives the RPC request

  2. Opens a handle to the caller's process

  3. Creates the child process with the caller's handle as the parent (via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS or equivalent internal mechanism)

The child process's parent is always your tool — not svchost.exe. This applies to all six techniques in this series:

If you want svchost.exe as the parent, you need explicit PPID spoofing via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS — that's a separate technique entirely, not a side effect of SecLogon.

With that settled, let's cover the remaining four techniques.


A1: Steal & Impersonate — Thread-Level Identity Theft

📁 Source: A1_steal_impersonate/steal_impersonate.c

This is the foundational technique. Everything else builds on it. A1 doesn't spawn a new process — it makes your current thread wear someone else's identity.

The Concept

Every thread in Windows can temporarily impersonate a different user by attaching an Impersonation token. The process's Primary token stays unchanged — only the thread's effective identity changes.

  BEFORE:
    Process (Primary: Alice)
      └── Thread (no impersonation token → inherits Alice)

  AFTER SetThreadToken:
    Process (Primary: Alice)       ← unchanged
      └── Thread (Impersonation: SYSTEM) ← wearing SYSTEM's badge

The thread can now access resources as SYSTEM — open SYSTEM-only registry keys, read protected files, access privileged handles. But the process itself still looks like Alice from the outside.

The Flow

The Critical Code

The conversion and application happen in two calls:

/* Primary → Impersonation */
DuplicateTokenEx(
    hStolenPrimary,         /* Source: winlogon's Primary token */
    TOKEN_ALL_ACCESS,
    NULL,
    SecurityImpersonation,  /* Impersonation level */
    TokenImpersonation,     /* Output: Impersonation token */
    &hImpToken
);

/* Apply to current thread */
SetThreadToken(NULL, hImpToken);  /* NULL = current thread */

After this, the thread can perform SYSTEM-only operations:

/* This normally fails for admin users — succeeds under impersonation */
RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SAM\\SAM", 0, KEY_READ, &hKey);
/* → ERROR_SUCCESS — we're SYSTEM on this thread */

Reverting

Impersonation is temporary. One call undoes it:

RevertToSelf();  /* Thread drops the borrowed identity */

Why A1 Matters

A1 is the prerequisite for B1 and B3. Both require SeAssignPrimaryTokenPrivilege — which regular admins don't have, but SYSTEM does. The attack chain is:

Admin shell → A1 (impersonate SYSTEM on thread) → now have SeAssignPrimaryTokenPrivilege
            → B1 or B3 (spawn permanent SYSTEM process)

A1 gives you temporary SYSTEM. B1/B3 give you permanent SYSTEM.

Property Value
Privilege required SeDebugPrivilege (Admin)
Scope Thread only — temporary
Reversible Yes, via RevertToSelf()
Process tree impact None — no new process
MITRE T1134.001

A2: Privilege Reduction — The Defensive Counterpart

📁 Source: A2_privilege_reduction/sandbox_thread.c

A2 is A1 in reverse. Instead of putting on a more-privileged badge, you strip privileges from your own token to create a sandboxed thread. This is a legitimate defensive technique — the same mechanism used by Chrome's sandbox, IIS worker processes, and Windows service isolation.

The Concept

A high-privilege process duplicates its own token, permanently removes dangerous privileges using SE_PRIVILEGE_REMOVED, and applies the restricted token to a worker thread. The worker thread can't re-escalate — the privileges are gone, not just disabled.

  BEFORE:
    Thread: SeDebugPrivilege ✓, SeBackupPrivilege ✓, SeTcbPrivilege ✓
    → Can open winlogon.exe, read SAM, act as part of the OS

  AFTER strip_privileges + SetThreadToken:
    Thread: SeDebugPrivilege ✗, SeBackupPrivilege ✗, SeTcbPrivilege ✗
    → Cannot open protected processes, cannot read SAM

Key Difference: REMOVED vs DISABLED

/* SE_PRIVILEGE_DISABLED — can be re-enabled later */
tp.Privileges[0].Attributes = SE_PRIVILEGE_DISABLED;

/* SE_PRIVILEGE_REMOVED — permanently gone from this token */
tp.Privileges[0].Attributes = SE_PRIVILEGE_REMOVED;

DISABLED is a seatbelt you can unbuckle. REMOVED is cutting the seatbelt out of the car. A2 uses REMOVED — the thread cannot re-escalate.

Privileges Stripped

static const char* DANGEROUS_PRIVILEGES[] = {
    "SeDebugPrivilege",              /* Open SYSTEM processes */
    "SeBackupPrivilege",             /* Read any file regardless of ACL */
    "SeRestorePrivilege",            /* Write any file regardless of ACL */
    "SeTcbPrivilege",                /* Act as part of the OS */
    "SeAssignPrimaryTokenPrivilege", /* Swap process tokens (B1/B3) */
    "SeTakeOwnershipPrivilege",      /* Take ownership of any object */
    "SeLoadDriverPrivilege",         /* Load kernel drivers */
    "SeImpersonatePrivilege",        /* Impersonate other users (B2) */
    NULL
};

Verification

The code verifies the sandbox works by attempting privileged operations before and after:

PRE-SANDBOX:  Can read HKLM\SAM\SAM ✓ (privileged access works)
POST-SANDBOX: SAM access DENIED ✓ (sandbox working correctly!)
POST-SANDBOX: Cannot open winlogon.exe ✓ (SeDebugPrivilege stripped!)
POST-REVERT:  SAM access restored ✓ (original privileges confirmed)

Why Include a Defensive Technique?

Understanding how sandboxing works is critical for attackers and defenders:

  • Red teamers need to know that impersonation tokens with stripped privileges can't be re-escalated — stop trying to abuse them

  • Blue teamers see the same DuplicateTokenEx + SetThreadToken pattern in legitimate sandboxing — helps distinguish malicious from defensive usage

  • The code pattern is identical to A1 — same APIs, opposite intent

Property Value
Privilege required Admin (to have privileges worth stripping)
Scope Thread only — temporary sandbox
Reversible Yes, via RevertToSelf()
Purpose Defensive — least-privilege on worker threads
MITRE T1134 (general)

B1: CreateProcessAsUserW — The Direct Spawn

📁 Source: B1_create_process_as_user/token_to_process.c

B1 is the most direct approach: take a stolen token, convert it to Primary, and spawn a process with it. No services involved, no undocumented APIs. Just CreateProcessAsUserW — a standard Win32 function.

The Flow

B1 is interesting because it demonstrates the full A→B conversion pipeline. It chains A1 (steal & impersonate) with a process spawn:

Why the Round Trip?

Notice the double conversion: Primary → Impersonation → Primary. Why not just use the original Primary token directly?

Two reasons:

  1. Ownership — The original Primary token from OpenProcessToken is a reference to winlogon's token. The kernel won't let you assign someone else's token reference to a new process. DuplicateTokenEx creates an independent copy you own.

  2. Full A1 chain demonstration — B1 demonstrates the complete real-world attack flow: steal, impersonate (to gain SeAssignPrimaryTokenPrivilege), then spawn. In practice, you'd often already be impersonating from a previous step.

The Spawn Call

CreateProcessAsUserW(
    hNewPrimary,       /* hToken — MUST be TokenPrimary */
    NULL,              /* lpApplicationName */
    spawn_cmd,         /* lpCommandLine — "cmd.exe" */
    NULL, NULL,        /* Security attributes */
    FALSE,             /* bInheritHandles */
    CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
    pEnv,              /* Environment block from CreateEnvironmentBlock */
    NULL,              /* Current directory */
    &si, &pi
);

CreateProcessAsUserW is strict — it requires a Primary token. Pass an Impersonation token and it fails. This is the key difference from B2 (CreateProcessWithTokenW), which accepts Impersonation tokens and lets seclogon handle the conversion.

The Privilege Barrier

Error 1314 — ERROR_PRIVILEGE_NOT_HELD
→ Missing SeAssignPrimaryTokenPrivilege
→ Regular admins don't have this. SYSTEM does.

This is B1's biggest limitation. CreateProcessAsUserW requires SeAssignPrimaryTokenPrivilege, which only SYSTEM-level accounts hold. A regular admin running B1 gets error 1314 — you need to chain it with A1 first (impersonate SYSTEM to gain the privilege), then call CreateProcessAsUserW from the impersonated context.

Or just use B2 instead — it only needs SeImpersonatePrivilege, which admins have.

B1 vs B2 — When to Use Which

Situation B1 B2
You're already SYSTEM ✅ Simpler, no service dependency ✅ Also works
You're admin (not SYSTEM) ❌ Error 1314 ✅ Works directly
seclogon is disabled ✅ No dependency ❌ Fails
You want minimal API surface ✅ Direct Win32 call 🟡 RPC to seclogon
Property Value
Privilege required SeAssignPrimaryTokenPrivilege (SYSTEM) + SeDebugPrivilege
Token type Must be Primary
Service dependency None
Parent PID The calling process
MITRE T1134.002

B4: CreateProcessWithLogonW — The Credential-Based Spawn

📁 Source: B4_create_process_with_logon/logon_spawn.c

B4 is fundamentally different from everything else in this series. It doesn't steal tokens at all. It uses plaintext credentials — username, domain, password — and lets SecLogon create a token from scratch.

The Key Difference

B1: Needs a stolen TOKEN handle + SeAssignPrimaryTokenPrivilege
B2: Needs a stolen TOKEN handle + SeImpersonatePrivilege + seclogon
B3: Needs a stolen TOKEN handle + SeDebugPrivilege + NtSetInformationProcess
B4: Needs USERNAME + PASSWORD. That's it. No token handles. No privileges.

B4 is the only API that a standard (non-admin) user can call. You just need valid credentials for the target account and the SecLogon service running.

The Flow

The 0xc0000142 Trap

CreateProcessWithLogonW creates a new logon session. The spawned process runs under a fresh token that may not have DACL permissions on the caller's Window Station (WinSta0) and Desktop (Default). When user32.dll initializes inside the child, it tries to access these objects — and if the DACLs don't include the new SID, initialization fails with STATUS_DLL_INIT_FAILED (0xc0000142).

The fix is granting Everyone access to both objects before spawning:

/* Build Everyone SID (S-1-1-0) */
SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY;
AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID,
                         0, 0, 0, 0, 0, 0, 0, &pEveryoneSID);

/* Grant WINSTA_ALL_ACCESS on Window Station */
ea.grfAccessPermissions = WINSTA_ALL_ACCESS | READ_CONTROL;
ea.Trustee.ptstrName    = (LPTSTR)pEveryoneSID;
SetEntriesInAcl(1, &ea, pOldDacl, &pNewDacl);
SetSecurityInfo(hWinSta, SE_WINDOW_OBJECT, DACL_SECURITY_INFORMATION, ...);

This is a real-world issue — the B4 source code includes the complete grant_winsta_desktop_access() function that handles this transparently.

Error Handling — B4 Has the Most Failure Modes

Because B4 involves credential validation, it encounters errors the other techniques never see:

Error Code Meaning
ERROR_LOGON_FAILURE 1326 Wrong username or password
ERROR_SERVICE_DISABLED 1058 SecLogon is disabled
ERROR_LOGON_TYPE_NOT_GRANTED 1385 Account denied interactive logon (GPO)
ERROR_ACCOUNT_LOCKED_OUT 1909 Account locked after failed attempts
ERROR_ACCOUNT_DISABLED 1331 Account is disabled in AD
ERROR_PASSWORD_MUST_CHANGE 1907 Password expired, must change first

The code handles all of these with actionable guidance. On error 1385, it automatically falls back to LOGON_NETCREDENTIALS_ONLY — the equivalent of runas /netonly, which uses the credentials for network access only.

B4's OPSEC Footprint

B4 generates the most forensic evidence of any technique:

Event 4648 — A logon was attempted using explicit credentials
Event 4624 — An account was successfully logged on (Type 2: Interactive)
Process Creation — Parent PID = your tool (NOT svchost.exe)
Named Pipe — SecLogon RPC communication visible in ETW

The password briefly exists in process memory. Credential Guard won't prevent this — the credentials are supplied by the attacker, not stolen from LSASS.

Usage

# Local account
.\B4_logon_spawn.exe Administrator P@ssw0rd

# Domain account
.\B4_logon_spawn.exe jdoe Spring2024! CORP

# Custom command
.\B4_logon_spawn.exe svc_account Passw0rd! . "powershell.exe"
Property Value
Privilege required NONE — standard user can call this
Input Username + Domain + Password (plaintext)
Service dependency SecLogon (must be running)
Parent PID The calling process (on Vista+)
Forensic events 4648 + 4624 (noisiest technique)
MITRE T1134.002 + T1078

The Complete Family — Side-by-Side Comparison

Now that we've covered all six techniques across three blog posts, here's the definitive comparison:

Privilege Requirements

Technique SeDebug SeImpersonate SeAssignPrimary Available to Admin?
A1 Steal & Impersonate ✅ Yes
A2 Privilege Reduction ✅ Yes
B1 CreateProcessAsUserW ❌ No (SYSTEM)
B2 CreateProcessWithTokenW ✅ Yes
B3 Suspended Token Swap ❌ No (SYSTEM)
B4 CreateProcessWithLogonW ✅ Yes (std user!)

Evasion Properties

Technique Token in creation event? Service dependency Undocumented API? Parent PID
A1 N/A (no process) None No N/A
A2 N/A (no process) None No N/A
B1 🔴 Yes None No Caller
B2 🔴 Yes SecLogon No Caller
B3 🟢 No (original token) None Yes (NtSetInformationProcess) Caller
B4 🔴 Yes SecLogon No Caller

The Decision Matrix — Which Technique to Use

Your Situation Best Technique Why
You have admin, want temporary SYSTEM A1 Fastest. Thread-level. Reversible.
You need a permanent SYSTEM process, have admin B2 Simplest spawn. SecLogon handles conversion.
You need maximum evasion, already SYSTEM B3 Token invisible in creation event.
SecLogon is disabled, you're SYSTEM B1 No service dependency.
You have creds, no admin, no token B4 Only option without privileges.
You need to sandbox a worker thread A2 Defensive. Strip privileges.
You want svchost.exe as parent None Use PROC_THREAD_ATTRIBUTE_PARENT_PROCESS separately.

Token Direction Map


Detection Summary — What Blue Teams Should Monitor

What to Monitor Catches
NtSetInformationProcess with class 9 B3 (the hardest to catch otherwise)
OpenProcessToken on SYSTEM processes by non-SYSTEM callers A1, B1, B2
SecLogon RPC interactions from unexpected processes B2, B4
Event 4648 (Explicit Credential Logon) B4
Process token re-query after creation callbacks B3 (catches the identity mismatch)
Admin process spawning SYSTEM child B1, B2
SetThreadToken from non-service processes A1
AdjustTokenPrivileges with SE_PRIVILEGE_REMOVED A2 (but this is legitimate)

The single most impactful detection: re-query process tokens after creation events. Most EDRs check at creation time and trust forever. A post-creation re-check catches B3's suspended swap — the one technique that evades everything else.


Building Everything

All six techniques compile from one Makefile:

cd Modules/token_impersonation
make all

Individual builds:

# A1: Steal & Impersonate
x86_64-w64-mingw32-gcc -O2 -o A1_steal_impersonate.exe \
    A1_steal_impersonate/steal_impersonate.c -ladvapi32 -lkernel32

# A2: Privilege Reduction
x86_64-w64-mingw32-gcc -O2 -o A2_sandbox_thread.exe \
    A2_privilege_reduction/sandbox_thread.c -ladvapi32 -lkernel32

# B1: CreateProcessAsUserW
x86_64-w64-mingw32-gcc -O2 -o B1_create_process_as_user.exe \
    B1_create_process_as_user/token_to_process.c \
    -ladvapi32 -lkernel32 -luserenv -municode

# B4: CreateProcessWithLogonW
x86_64-w64-mingw32-gcc -O2 -o B4_logon_spawn.exe \
    B4_create_process_with_logon/logon_spawn.c \
    -ladvapi32 -lkernel32 -luserenv -municode

Conclusion — The Series Complete

Across three blog posts and six techniques, we've covered every practical method for Windows Access Token Manipulation:

Part 1 (B3): Born With a Stolen Soul — Suspended Process Token Swap The most evasive technique. Process creation event is clean. Requires SYSTEM.

Part 2 (B2): The Seclogon Shortcut — CreateProcessWithTokenW The pragmatic middle ground. Admin-accessible. Depends on SecLogon.

Part 3 (this post): The Complete Family — A1, A2, B1, B4 + The Parent PID Truth Thread-level operations, the direct spawn, the credential-based approach, and the myth-busting truth that SecLogon does not give you svchost.exe as a parent on modern Windows.

The fundamental lesson: all six techniques produce the same parent-child process tree. The parent is always the calling process. The differences are in privilege requirements, evasion properties, and forensic footprint. Choose based on what you have (admin? SYSTEM? credentials?) and what you need (stealth? simplicity? no dependencies?).


MITRE ATT&CK References: