NtCreateThread
Syscall historique de création de thread nécessitant une INITIAL_TEB construite à la main ; remplacé par NtCreateThreadEx.
Prototype
NTSTATUS NtCreateThread( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, HANDLE ProcessHandle, PCLIENT_ID ClientId, PCONTEXT ThreadContext, PINITIAL_TEB InitialTeb, BOOLEAN CreateSuspended );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ThreadHandle | PHANDLE | out | Reçoit le handle du thread nouvellement créé en cas de succès. |
| DesiredAccess | ACCESS_MASK | in | Droits d'accès pour le handle de thread retourné, typiquement THREAD_ALL_ACCESS. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Attributs d'objet optionnels ; presque toujours NULL pour la création de thread. |
| ProcessHandle | HANDLE | in | Handle vers le processus cible qui possédera le thread (PROCESS_CREATE_THREAD requis). |
| ClientId | PCLIENT_ID | out | Reçoit la paire (PID, TID) identifiant le thread nouvellement créé. |
| ThreadContext | PCONTEXT | in | Structure CONTEXT entièrement remplie avec RIP/RSP pour le nouveau thread — état initial des registres fourni par l'appelant. |
| InitialTeb | PINITIAL_TEB | in | INITIAL_TEB construite à la main décrivant la pile utilisateur (base, limite, base d'allocation). La raison pour laquelle cette API est pénible. |
| CreateSuspended | BOOLEAN | in | Si TRUE, le thread est créé en état suspendu et doit être repris avec NtResumeThread. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x4E | win10-1507 |
| Win10 1607 | 0x4E | win10-1607 |
| Win10 1703 | 0x4E | win10-1703 |
| Win10 1709 | 0x4E | win10-1709 |
| Win10 1803 | 0x4E | win10-1803 |
| Win10 1809 | 0x4E | win10-1809 |
| Win10 1903 | 0x4E | win10-1903 |
| Win10 1909 | 0x4E | win10-1909 |
| Win10 2004 | 0x4E | win10-2004 |
| Win10 20H2 | 0x4E | win10-20h2 |
| Win10 21H1 | 0x4E | win10-21h1 |
| Win10 21H2 | 0x4E | win10-21h2 |
| Win10 22H2 | 0x4E | win10-22h2 |
| Win11 21H2 | 0x4E | win11-21h2 |
| Win11 22H2 | 0x4E | win11-22h2 |
| Win11 23H2 | 0x4E | win11-23h2 |
| Win11 24H2 | 0x4E | win11-24h2 |
| Server 2016 | 0x4E | winserver-2016 |
| Server 2019 | 0x4E | winserver-2019 |
| Server 2022 | 0x4E | winserver-2022 |
| Server 2025 | 0x4E | winserver-2025 |
Module noyau
APIs liées
Stub du syscall
4C 8B D1 mov r10, rcx B8 4E 00 00 00 mov eax, 0x4E 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
NtCreateThread est la primitive originelle de création de thread, antérieure à Vista. Contrairement à son successeur NtCreateThreadEx, elle est non documentée dans les en-têtes publics et oblige l'appelant à construire à la main une INITIAL_TEB — décrivant la base, la limite et la base d'allocation de la pile — plus un CONTEXT entièrement rempli avec RIP/RSP. Le SSN `0x4E` est resté stable sur toutes les builds Windows 10 et Windows 11 supportées, ce qui est inhabituel et reflète le fait que Microsoft ne touche plus à cette surface. Le noyau l'implémente toujours pour compatibilité ascendante mais Win32 CreateThread, RtlCreateUserThread et NtCreateThreadEx passent tous aujourd'hui par NtCreateThreadEx.
Usage courant par les malwares
Utilisée par d'anciens outils d'injection antérieurs aux listes d'attributs de NtCreateThreadEx — certaines variantes de PassTheHash, les premières primitives de vol de token de Mimikatz et des alternatives historiques à CreateRemoteThread. Une petite portion de l'outillage offensif moderne choisit NtCreateThread précisément parce que peu d'EDR la hookent : l'API est si rarement utilisée par du code légitime que la couverture des éditeurs est inégale, en faisant un contournement de niche contre des piles de hooks userland naïves. La contrepartie est que l'appelant doit allouer et assembler une pile dans le processus cible avant d'émettre l'appel.
Opportunités de détection
ETW Microsoft-Windows-Threat-Intelligence émet des événements ThreadCreate depuis PspInsertThread, peu importe que le point d'entrée userland soit NtCreateThread ou NtCreateThreadEx — la télémétrie côté noyau est donc identique. Sysmon Event ID 8 (CreateRemoteThread) se déclenche également pour toute injection de thread cross-process. Le seul discriminant intéressant est en userland : un EDR qui ne hooke que ntdll!NtCreateThreadEx manquera les chemins passant par NtCreateThread. Les défenseurs doivent veiller à instrumenter les deux stubs et surveiller la rare combinaison NtCreateThread + ProcessHandle distant.
Exemples de syscalls directs
asmx64 direct stub
; Direct syscall stub for NtCreateThread (SSN 0x4E, Win10 1507+ through Win11 24H2)
NtCreateThread PROC
mov r10, rcx ; syscall convention
mov eax, 4Eh ; SSN
syscall
ret
NtCreateThread ENDPcLegacy remote thread with hand-built INITIAL_TEB
// Caller must allocate stack in the target process and populate both CONTEXT and INITIAL_TEB.
typedef struct _INITIAL_TEB {
PVOID PreviousStackBase;
PVOID PreviousStackLimit;
PVOID StackBase;
PVOID StackLimit;
PVOID AllocatedStackBase;
} INITIAL_TEB, *PINITIAL_TEB;
SIZE_T stackSize = 0x10000;
PVOID remoteStack = NULL;
NtAllocateVirtualMemory(hProcess, &remoteStack, 0, &stackSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
INITIAL_TEB teb = { 0 };
teb.StackBase = (PBYTE)remoteStack + stackSize;
teb.StackLimit = remoteStack;
teb.AllocatedStackBase = remoteStack;
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_FULL;
ctx.Rip = (DWORD64)pShellcode;
ctx.Rsp = (DWORD64)teb.StackBase - 0x28;
HANDLE hThread = NULL;
CLIENT_ID cid = { 0 };
NtCreateThread(&hThread, THREAD_ALL_ACCESS, NULL, hProcess,
&cid, &ctx, &teb, FALSE);rustwindows-sys + naked syscall stub
// Cargo: windows-sys = "0.59"
use std::arch::asm;
#[unsafe(naked)]
unsafe extern "system" fn nt_create_thread_stub() {
asm!(
"mov r10, rcx",
"mov eax, 0x4E",
"syscall",
"ret",
options(noreturn),
);
}Mappings MITRE ATT&CK
Last verified: 2026-05-20