> Windows Syscalls
ntoskrnl.exeT1497.001T1497T1106

NtGetCurrentProcessorNumber

Renvoie l'index zero-based du processeur logique sur lequel le thread appelant s'exécute actuellement.

Prototype

ULONG NtGetCurrentProcessorNumber(VOID);

Arguments

NameTypeDirDescription

IDs de syscalls par version de Windows

Version de WindowsID de syscallBuild
Win10 15070xE4win10-1507
Win10 16070xE7win10-1607
Win10 17030xEAwin10-1703
Win10 17090xEBwin10-1709
Win10 18030xECwin10-1803
Win10 18090xEDwin10-1809
Win10 19030xEEwin10-1903
Win10 19090xEEwin10-1909
Win10 20040xF3win10-2004
Win10 20H20xF3win10-20h2
Win10 21H10xF3win10-21h1
Win10 21H20xF4win10-21h2
Win10 22H20xF4win10-22h2
Win11 21H20xF9win11-21h2
Win11 22H20xFAwin11-22h2
Win11 23H20xFAwin11-23h2
Win11 24H20xFCwin11-24h2
Server 20160xE7winserver-2016
Server 20190xEDwinserver-2019
Server 20220xF8winserver-2022
Server 20250xFCwinserver-2025

Module noyau

ntoskrnl.exeNtGetCurrentProcessorNumber

APIs liées

GetCurrentProcessorNumberGetCurrentProcessorNumberExSetThreadAffinityMaskGetSystemInfoGetLogicalProcessorInformationEx

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 ENDP

cMulti-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