> Windows Syscalls
ntoskrnl.exeT1497.001T1497T1106

NtGetCurrentProcessorNumber

Liefert den nullbasierten Index des logischen Prozessors, auf dem der aufrufende Thread gerade läuft.

Prototyp

ULONG NtGetCurrentProcessorNumber(VOID);

Argumente

NameTypeDirDescription

Syscall-IDs pro 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-Modul

ntoskrnl.exeNtGetCurrentProcessorNumber

Verwandte 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

Undokumentierte Hinweise

Einer der billigsten Syscalls der NT-Tabelle. Der Kernel-Handler liest `KPCR.Prcb.Number` für die aktuelle CPU und gibt ihn als `ULONG` zurück — keine Parameter-Validierung, kein Locking. Das Win32-Pendant `GetCurrentProcessorNumber` (in `kernel32.dll`) geht auf x64 *nicht* durch diesen Syscall; es liest den CPU-Index direkt aus dem GS-basierten `KUSER_SHARED_DATA::XState`-Cache oder via `RDTSCP` / `LSL`-Instruktionen auf Systemen, wo diese zuverlässig sind, und fällt nur auf Architekturen, wo der User-Mode-Shortcut nicht nutzbar ist, auf den Syscall zurück. Folglich ist es schon eine kleine Anomalie, `NtGetCurrentProcessorNumber` *tatsächlich* als Syscall feuern zu sehen — die meisten Apps, die diesen Wert wollen, nutzen den Win32-Wrapper und gehen nie in den Kernel-Mode.

Häufige Malware-Nutzung

Billiger, verlässlicher Baustein für **Sandbox-/VM-Erkennung**. Die Technik: enge Schleife mit `NtGetCurrentProcessorNumber`, optional verschränkt mit `SwitchToThread` oder `Sleep(0)`, und Verfolgen der Menge der beobachteten distinkten CPU-Nummern. Moderne Bare-Metal-Hosts haben 4-32 logische Prozessoren, und ein Thread ohne Affinität wandert binnen Millisekunden über die meisten. Viele Sandboxes (älteres Cuckoo, bestimmte Any.Run-Profile, Default-VirtualBox-Setups, minimale Hyper-V-Sandboxes) zeigen dem analysierten Sample nur 1-2 vCPUs, um Ressourcen zu sparen — die beobachtete Menge sättigt bei 1 oder 2 distinkten Nummern, und das Implant schließt "Sandbox, unterdrücken". Eine raffiniertere Variante nutzt `SetThreadAffinityMask`, um jeden CPU-Index zu *probieren* und meldet, was `NtGetCurrentProcessorNumber` danach zurückgibt; in einer eingeschränkten Sandbox wird das Affinitäts-Set heruntergezwungen. Beobachtet in Loader-Stages von **Emotet**, **IcedID**, **Qakbot**, **Smoke Loader** und einem langen Tail von Commodity-Cryptern. Es ist *einer von mehreren* Checks; allein zu rauschig, um darauf zu handeln.

Erkennungs­möglichkeiten

Per-Call-Telemetrie ist unpraktisch — `NtGetCurrentProcessorNumber` ist als Syscall (gegenüber dem User-Mode-Fast-Path) zu billig und zu selten, als dass ein Event aussagekräftig wäre. Das funktionierende Verhaltenssignal ist *die Kombination*: kurzlebiger Prozess, RDTSC + RDTSCP + CPUID + NtGetCurrentProcessorNumber + NtQuerySystemInformation(SystemBasicInformation) aus demselben Thread innerhalb der ersten paar hundert Millisekunden ergibt einen Sandbox-Probe-Fingerabdruck. Defender for Endpoint scort das auf der Regelfamilie `EvasiveTechnique:Sandbox`. Der ETW-Provider `Microsoft-Windows-Kernel-Audit-API-Calls` legt diesen Syscall nicht offen, daher leisten EDRs mit Kernel-Callbacks die meiste Arbeit über Stack-Walks auf der seltenen Syscall-Seite.

Direkte Syscall-Beispiele

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