> Windows Syscalls
ntoskrnl.exeT1497.001T1497T1106

NtGetCurrentProcessorNumber

Devuelve el índice zero-based del procesador lógico en el que el hilo llamante se está ejecutando actualmente.

Prototipo

ULONG NtGetCurrentProcessorNumber(VOID);

Argumentos

NameTypeDirDescription

IDs de syscalls por versión de Windows

Versión 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

Módulo del kernel

ntoskrnl.exeNtGetCurrentProcessorNumber

APIs relacionadas

GetCurrentProcessorNumberGetCurrentProcessorNumberExSetThreadAffinityMaskGetSystemInfoGetLogicalProcessorInformationEx

Stub del 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

Notas no documentadas

Uno de los syscalls más baratos de la tabla NT. El handler del lado kernel lee `KPCR.Prcb.Number` para el CPU actual y lo devuelve como `ULONG` — sin validación de parámetros, sin locking. El equivalente Win32 `GetCurrentProcessorNumber` (en `kernel32.dll`) *no* pasa por este syscall en x64; lee el índice CPU directamente del cache GS-based `KUSER_SHARED_DATA::XState` o vía las instrucciones `RDTSCP` / `LSL` en sistemas donde son fiables, recurriendo al syscall solo en arquitecturas donde el atajo user-mode no es usable. Como resultado, *ver* `NtGetCurrentProcessorNumber` realmente dispararse como syscall es en sí una pequeña anomalía — la mayoría de apps que quieren este valor usan el wrapper Win32 y nunca cruzan a kernel mode.

Uso común por malware

Building block barato y fiable para **detección sandbox / VM**. La técnica: girar un bucle apretado que emita `NtGetCurrentProcessorNumber`, opcionalmente intercalado con `SwitchToThread` o `Sleep(0)`, y trackear el conjunto de números CPU distintos observados. Los hosts bare-metal modernos tienen 4-32 procesadores lógicos y un hilo sin afinidad flotará por la mayoría en milisegundos. Muchas sandboxes (Cuckoo viejo, ciertos perfiles Any.Run, setups VirtualBox por defecto, sandboxes Hyper-V mínimas) exponen solo 1-2 vCPUs al sample analizado para ahorrar recursos — el conjunto observado satura a 1 o 2 números distintos y el implante concluye "sandbox, suprimir". Una variante más sofisticada usa `SetThreadAffinityMask` para *intentar* cada índice CPU y reporta lo que `NtGetCurrentProcessorNumber` devuelve después; en una sandbox restringida el conjunto de afinidad se fuerza hacia abajo. Visto en stages loader **Emotet**, **IcedID**, **Qakbot**, **Smoke Loader**, y una larga cola de crypters commodity. Es *uno entre varios* checks; solo es demasiado ruidoso para accionar.

Oportunidades de detección

La telemetría por llamada es impráctica — `NtGetCurrentProcessorNumber` es demasiado barato y demasiado raro como syscall (vs. el fast path user-mode) para que un evento sea significativo. La señal conductual que funciona es *la combinación*: proceso de corta duración, RDTSC + RDTSCP + CPUID + NtGetCurrentProcessorNumber + NtQuerySystemInformation(SystemBasicInformation) emitidos desde el mismo hilo dentro de los primeros cientos de milisegundos es una huella de probe de sandbox. Defender for Endpoint puntúa esto en la familia de reglas `EvasiveTechnique:Sandbox`. El provider ETW `Microsoft-Windows-Kernel-Audit-API-Calls` no expone este syscall, así que los EDR con callbacks kernel hacen la mayoría del trabajo vía stack-walk del lado syscall raro.

Ejemplos de syscalls directos

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()
}

Mapeos MITRE ATT&CK

Last verified: 2026-05-20