> Windows Syscalls
ntoskrnl.exeT1497.001T1497T1106

NtGetCurrentProcessorNumber

Returns the zero-based logical-processor index the calling thread is currently executing on.

Prototype

ULONG NtGetCurrentProcessorNumber(VOID);

Arguments

NameTypeDirDescription

Syscall IDs by Windows version

Windows versionSyscall IDBuild
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

Kernel module

ntoskrnl.exeNtGetCurrentProcessorNumber

Related APIs

GetCurrentProcessorNumberGetCurrentProcessorNumberExSetThreadAffinityMaskGetSystemInfoGetLogicalProcessorInformationEx

Syscall stub

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

Undocumented notes

One of the cheapest syscalls in the NT table. The kernel-side handler reads `KPCR.Prcb.Number` for the current CPU and returns it as `ULONG` — no parameter validation, no locking. The Win32 equivalent `GetCurrentProcessorNumber` (in `kernel32.dll`) does *not* even go through this syscall on x64; it reads the CPU index directly from the GS-based `KUSER_SHARED_DATA::XState` cache or via the `RDTSCP` / `LSL` instructions on systems where those are reliable, falling back to the syscall only on architectures where the user-mode shortcut isn't usable. As a result, *seeing* `NtGetCurrentProcessorNumber` actually fire as a syscall is a small anomaly in itself — most apps that want this value use the Win32 wrapper and never cross into kernel mode.

Common malware usage

Cheap, reliable building block for **sandbox / VM detection**. The technique: spin a tight loop that issues `NtGetCurrentProcessorNumber`, optionally interleaved with `SwitchToThread` or `Sleep(0)`, and track the set of distinct CPU numbers observed. Modern bare-metal hosts have 4-32 logical processors and a thread without affinity will float across most of them within milliseconds. Many sandboxes (older Cuckoo, certain Any.Run profiles, default VirtualBox setups, Hyper-V minimal sandboxes) expose only 1-2 vCPUs to the analysed sample to save resources — the observed set saturates at 1 or 2 distinct numbers and the implant concludes "sandbox, suppress". A more sophisticated variant uses `SetThreadAffinityMask` to *try* each CPU index and reports which `NtGetCurrentProcessorNumber` returns afterwards; on a constrained sandbox the affinity set is forced down. Seen in **Emotet** loader stages, **IcedID**, **Qakbot**, **Smoke Loader**, and a long tail of commodity crypters. It is *one of several* checks; alone it is too noisy to act on.

Detection opportunities

Per-call telemetry is impractical — `NtGetCurrentProcessorNumber` is too cheap and too rare via syscall (vs. the user-mode fast path) for an event to be meaningful. The behavioural signal that works is *the combination*: short-lived process, RDTSC + RDTSCP + CPUID + NtGetCurrentProcessorNumber + NtQuerySystemInformation(SystemBasicInformation) issued from the same thread inside the first few hundred milliseconds is a sandbox-probe fingerprint. Defender for Endpoint scores this on the `EvasiveTechnique:Sandbox` family of rules. ETW provider `Microsoft-Windows-Kernel-Audit-API-Calls` does not surface this syscall, so kernel-callback-based EDRs do most of the work via stack-walk on the rare-syscall side.

Direct syscall examples

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

MITRE ATT&CK mappings

Last verified: 2026-05-20