# 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](https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess), 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](https://rxnveera.blog/the-seclogon-shortcut-spawning-system-processes-via-createprocesswithtokenw), 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) |
| **B1** — `CreateProcessAsUserW` | Direct process spawn with a stolen Primary token |
| **B4** — `CreateProcessWithLogonW` | 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:

```plaintext
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:

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/cf801796-c925-46cf-a1b8-bdf8dd8e1adc.png align="center")

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`](https://github.com/veeramani110400/token_impersonation/blob/main/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.

```plaintext
  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

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/f755f76a-8728-41e9-976a-fed09f22aab3.png align="center")

### The Critical Code

The conversion and application happen in two calls:

```c
/* 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:

```c
/* 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:

```c
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:

```plaintext
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.

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/a2c21a50-402c-401d-a664-4a772544bc86.png align="center")

| 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`](https://github.com/veeramani110400/token_impersonation/blob/main/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.

```plaintext
  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

```c
/* 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

```c
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:

```plaintext
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)
```

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/ad767761-a4d8-48b4-8ebf-96ef8774b334.png align="center")

### 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`](https://github.com/veeramani110400/token_impersonation/blob/main/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:

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/d304df94-4b51-432a-9eba-476eef5dc0d9.png align="center")

### 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

```c
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

```plaintext
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`](https://github.com/veeramani110400/token_impersonation/blob/main/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

```plaintext
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

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/d4f27801-b2e0-406d-8ff2-47a289b1a64b.png align="center")

### 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:

```c
/* 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:

```plaintext
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

```powershell
# 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

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/88d3361d-8d2b-4535-99a7-03fc7e2018e6.png align="center")

* * *

## 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:

```bash
cd Modules/token_impersonation
make all
```

Individual builds:

```bash
# 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](https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess) The most evasive technique. Process creation event is clean. Requires SYSTEM.

**Part 2 (B2):** [The Seclogon Shortcut — CreateProcessWithTokenW](https://rxnveera.blog/the-seclogon-shortcut-spawning-system-processes-via-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:*

*   [*T1134.001 — Token Impersonation/Theft*](https://attack.mitre.org/techniques/T1134/001/) *(A1)*
    
*   [*T1134.002 — Create Process with Token*](https://attack.mitre.org/techniques/T1134/002/) *(B1, B2, B3, B4)*
    
*   [*T1134 — Access Token Manipulation*](https://attack.mitre.org/techniques/T1134/) *(A2)*
    
*   [*T1078 — Valid Accounts*](https://attack.mitre.org/techniques/T1078/) *(B4 — credential usage)*
