NtGetCurrentProcessorNumber
Liefert den nullbasierten Index des logischen Prozessors, auf dem der aufrufende Thread gerade läuft.
Prototyp
ULONG NtGetCurrentProcessorNumber(VOID);
Argumente
| Name | Type | Dir | Description |
|---|
Syscall-IDs pro Windows-Version
| Windows-Version | Syscall-ID | 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 |
Kernel-Modul
Verwandte APIs
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.
Erkennungsmö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 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()
}MITRE ATT&CK-Mappings
Last verified: 2026-05-20