NtGetCurrentProcessorNumber
Renvoie l'index zero-based du processeur logique sur lequel le thread appelant s'exécute actuellement.
Prototype
ULONG NtGetCurrentProcessorNumber(VOID);
Arguments
| Name | Type | Dir | Description |
|---|
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0xE4 | win10-1507 |
| Win10 1607 | 0xE7 | win10-1607 |
| Win10 1703 | 0xEA | win10-1703 |
| Win10 1709 | 0xEB | win10-1709 |
| Win10 1803 | 0xEC | win10-1803 |
| Win10 1809 | 0xED | win10-1809 |
| Win10 1903 | 0xEE | win10-1903 |
| Win10 1909 | 0xEE | win10-1909 |
| Win10 2004 | 0xF3 | win10-2004 |
| Win10 20H2 | 0xF3 | win10-20h2 |
| Win10 21H1 | 0xF3 | win10-21h1 |
| Win10 21H2 | 0xF4 | win10-21h2 |
| Win10 22H2 | 0xF4 | win10-22h2 |
| Win11 21H2 | 0xF9 | win11-21h2 |
| Win11 22H2 | 0xFA | win11-22h2 |
| Win11 23H2 | 0xFA | win11-23h2 |
| Win11 24H2 | 0xFC | win11-24h2 |
| Server 2016 | 0xE7 | winserver-2016 |
| Server 2019 | 0xED | winserver-2019 |
| Server 2022 | 0xF8 | winserver-2022 |
| Server 2025 | 0xFC | winserver-2025 |
Module noyau
APIs liées
Stub du syscall
4C 8B D1 mov r10, rcx B8 FC 00 00 00 mov eax, 0xFC 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
L'un des syscalls les moins chers de la table NT. Le handler côté noyau lit `KPCR.Prcb.Number` pour le CPU courant et le renvoie en `ULONG` — aucune validation de paramètre, aucun verrou. L'équivalent Win32 `GetCurrentProcessorNumber` (dans `kernel32.dll`) ne passe *pas* par ce syscall sur x64 ; il lit l'index CPU directement depuis le cache GS-based `KUSER_SHARED_DATA::XState` ou via les instructions `RDTSCP` / `LSL` sur les systèmes où elles sont fiables, ne se rabattant sur le syscall que sur les architectures où le raccourci user-mode n'est pas utilisable. En conséquence, *voir* `NtGetCurrentProcessorNumber` réellement déclenché comme syscall est en soi une petite anomalie — la plupart des apps qui veulent cette valeur utilisent le wrapper Win32 et ne traversent jamais en kernel mode.
Usage courant par les malwares
Building block bon marché et fiable pour la **détection sandbox / VM**. La technique : faire tourner une boucle serrée qui émet `NtGetCurrentProcessorNumber`, optionnellement entrelacée avec `SwitchToThread` ou `Sleep(0)`, et suivre l'ensemble des numéros CPU distincts observés. Les hôtes bare-metal modernes ont 4-32 processeurs logiques et un thread sans affinité flottera sur la plupart en quelques millisecondes. Beaucoup de sandboxes (vieux Cuckoo, certains profils Any.Run, setups VirtualBox par défaut, sandboxes Hyper-V minimales) n'exposent que 1-2 vCPU à l'échantillon analysé pour économiser des ressources — l'ensemble observé sature à 1 ou 2 numéros distincts et l'implant conclut "sandbox, supprimer". Une variante plus sophistiquée utilise `SetThreadAffinityMask` pour *essayer* chaque index CPU et rapporte ce que `NtGetCurrentProcessorNumber` renvoie après ; sur une sandbox contrainte l'ensemble d'affinité est forcé vers le bas. Vu dans les stages loader **Emotet**, **IcedID**, **Qakbot**, **Smoke Loader**, et une longue traîne de crypters commodity. C'est *l'un parmi plusieurs* checks ; seul il est trop bruyant pour agir dessus.
Opportunités de détection
La télémétrie par appel est impraticable — `NtGetCurrentProcessorNumber` est trop bon marché et trop rare en tant que syscall (face au fast path user-mode) pour qu'un événement soit significatif. Le signal comportemental qui marche est *la combinaison* : processus de courte durée, RDTSC + RDTSCP + CPUID + NtGetCurrentProcessorNumber + NtQuerySystemInformation(SystemBasicInformation) émis depuis le même thread dans les premières centaines de millisecondes est une empreinte de probe sandbox. Defender for Endpoint score ça sur la famille de règles `EvasiveTechnique:Sandbox`. Le provider ETW `Microsoft-Windows-Kernel-Audit-API-Calls` n'expose pas ce syscall, donc les EDR à callbacks noyau font la plupart du travail via stack-walk côté syscall rare.
Exemples de syscalls directs
asmx64 direct stub (Win11 24H2)
; Direct syscall stub for NtGetCurrentProcessorNumber (SSN 0xFC on Win11 24H2 / Server 2025)
NtGetCurrentProcessorNumber PROC
mov r10, rcx ; syscall convention (no args, but follow ABI)
mov eax, 0FCh ; SSN — drifts; resolve dynamically for portability
syscall
ret
NtGetCurrentProcessorNumber ENDPcMulti-CPU sandbox probe
// Spread across CPUs and count how many distinct numbers we ever see.
// Bare-metal: usually saturates to 4+ within a few ms.
// Sandbox: often stuck at 1 or 2.
#include <intrin.h>
int observe_unique_cpus(int budget_iters) {
unsigned char seen[256] = { 0 };
int distinct = 0;
for (int i = 0; i < budget_iters; ++i) {
ULONG cpu = NtGetCurrentProcessorNumber();
if (cpu < 256 && !seen[cpu]) { seen[cpu] = 1; distinct++; }
SwitchToThread();
}
return distinct;
}
if (observe_unique_cpus(2000) <= 2) {
// Likely sandbox. Bail out silently.
ExitProcess(0);
}rustGetCurrentProcessorNumber wrapper (windows-sys)
// Cargo: windows-sys = "0.59" (Win32_System_SystemInformation)
// The wrapper avoids the syscall on x64 unless the fast path is unavailable.
use windows_sys::Win32::System::SystemInformation::GetCurrentProcessorNumber;
fn current_cpu() -> u32 {
unsafe { GetCurrentProcessorNumber() }
}
// Sandbox probe: sample many times and count distinct values.
fn distinct_cpus(samples: usize) -> usize {
use std::collections::HashSet;
let mut seen: HashSet<u32> = HashSet::new();
for _ in 0..samples {
seen.insert(current_cpu());
std::thread::yield_now();
}
seen.len()
}Mappings MITRE ATT&CK
Last verified: 2026-05-20