NtQueryPerformanceCounter
Liefert den aktuellen Wert des hochauflösenden Performance-Counters und optional dessen Frequenz.
Prototyp
NTSTATUS NtQueryPerformanceCounter( PLARGE_INTEGER PerformanceCounter, PLARGE_INTEGER PerformanceFrequency );
Argumente
| Name | Type | Dir | Description |
|---|---|---|---|
| PerformanceCounter | PLARGE_INTEGER | out | Empfängt den aktuellen 64-Bit-Performance-Counter-Wert. |
| PerformanceFrequency | PLARGE_INTEGER | out | Optional. Empfängt die Tick-Frequenz des Counters in Hz. NULL übergeben, falls nicht benötigt. |
Syscall-IDs pro Windows-Version
| Windows-Version | Syscall-ID | Build |
|---|---|---|
| Win10 1507 | 0x31 | win10-1507 |
| Win10 1607 | 0x31 | win10-1607 |
| Win10 1703 | 0x31 | win10-1703 |
| Win10 1709 | 0x31 | win10-1709 |
| Win10 1803 | 0x31 | win10-1803 |
| Win10 1809 | 0x31 | win10-1809 |
| Win10 1903 | 0x31 | win10-1903 |
| Win10 1909 | 0x31 | win10-1909 |
| Win10 2004 | 0x31 | win10-2004 |
| Win10 20H2 | 0x31 | win10-20h2 |
| Win10 21H1 | 0x31 | win10-21h1 |
| Win10 21H2 | 0x31 | win10-21h2 |
| Win10 22H2 | 0x31 | win10-22h2 |
| Win11 21H2 | 0x31 | win11-21h2 |
| Win11 22H2 | 0x31 | win11-22h2 |
| Win11 23H2 | 0x31 | win11-23h2 |
| Win11 24H2 | 0x31 | win11-24h2 |
| Server 2016 | 0x31 | winserver-2016 |
| Server 2019 | 0x31 | winserver-2019 |
| Server 2022 | 0x31 | winserver-2022 |
| Server 2025 | 0x31 | winserver-2025 |
Kernel-Modul
Verwandte APIs
Syscall-Stub
4C 8B D1 mov r10, rcx B8 31 00 00 00 mov eax, 0x31 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
Bemerkenswert: Die SSN für NtQueryPerformanceCounter ist **seit Windows 10 1507 0x31 und bleibt unter Windows 11 24H2 / Server 2025 weiterhin 0x31** — einer der ganz wenigen Syscalls, die Microsoft über neun Jahre Builds perfekt stabil gehalten hat. Sein Win32-Wrapper `QueryPerformanceCounter` wird fast immer komplett aus KUSER_SHARED_DATA bedient (der `QpcData`-Block bei `0x7FFE0000`), ohne je in den Kernel zu wechseln; der Syscall wird nur als Fallback aufgerufen, wenn der User-Mode-Fast-Path eine Taktquellenänderung, ein Hypervisor-Enlightenment-Update erkennt oder wenn der Aufrufer in einem Prozess läuft, dessen KUSER_SHARED_DATA-Mapping absichtlich gestört wurde (selten). Auf moderner Hardware ist die Counter-Quelle der invariante TSC, skaliert auf eine Nominalfrequenz von 10 MHz.
Häufige Malware-Nutzung
Das ist das **Arbeitstier der Sandbox-Evasion**. Das Muster ist universell: QPC lesen, eine enge Schleife oder bekannt-teure Sequenz ausführen (z. B. eine Serie CPUID-Instruktionen, einen festen Iterationszahl-CryptoAPI-Hash oder eine kalibrierte Busy-Loop), QPC erneut lesen, durch die Frequenz teilen. Sandboxes, die Zeit beschleunigen, single-steppen oder den ausführenden Thread instrumentieren, zeigen Wall-Clock-Werte, die entweder viel zu schnell (zeitspringend) oder viel zu langsam (instrumentiert) sind. VMProtect und Themida verbacken aufwendige QPC-Leitern in ihre Stubs; GuLoader, AsyncRAT-Dropper, IcedID und Dutzende Commodity-Loader nutzen einfachere Zwei-Sample-QPC-Delta-Checks vor dem Entpacken. Der Vorteil gegenüber `rdtsc` ist, dass QPC in Hyper-V- / VMware- / KVM-Gästen funktioniert, in denen der Hypervisor den TSC virtualisiert — QPC liest die vom Hypervisor *exponierte* Uhr, die immer noch Anomalien zeigt, wenn Gastzeit zu Analysezwecken vom Host-Wall-Clock entkoppelt wird. NtDelayExecution + QPC ist die klassische Sleep-Skip-Erkennung (60 Sekunden anfordern, tatsächlich vergangenes QPC messen, abbrechen, wenn die Sandbox in Millisekunden zurückkehrt).
Erkennungsmöglichkeiten
QPC ist *zu* häufig, um direkt darauf zu alarmieren — jeder UI-Thread, jedes Spiel, jeder Browser-Tab ruft es ständig auf. Erkennung muss verhaltensbasiert sein: nach einem engen Muster `QPC → billige Berechnung → QPC → bedingter Branch auf Delta` suchen, besonders wenn der bedingte Branch der Unterschied zwischen Ausführung der nächsten Stage und sauberem Abbruch ist. ETW Microsoft-Windows-Threat-Intelligence exponiert das nicht. Memory-Scan-basierte Erkennung von VMProtect-/Themida-Stubs fängt das *Resultat* statt der QPC-Nutzung selbst. Der beste Verhaltensmarker ist die *Delay-Skip*-Signatur: NtDelayExecution(60000ms) unmittelbar gefolgt von NtQueryPerformanceCounter, mit Frühabbruch bei verstrichener Zeit < Schwellwert. Sandboxes, die das durch Einhalten der Delays beheben, fallen dann ins enge Schleifenmuster.
Direkte Syscall-Beispiele
cSleep-skip sandbox detection
// Request a long sleep; if the sandbox skips it, QPC reveals the lie.
#include <windows.h>
BOOL looks_like_sandbox(void) {
LARGE_INTEGER freq, before, after;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&before);
Sleep(60000); // request 60 seconds
QueryPerformanceCounter(&after);
double elapsed_ms = (double)(after.QuadPart - before.QuadPart) * 1000.0 / (double)freq.QuadPart;
// Real wall-clock 60s ± a few ms. Sandboxes that skip Sleep return in < 5s.
return elapsed_ms < 50000.0;
}cTight-loop instrumentation detector
// CPUID has fixed retired-instruction cost; a single-step or trace-driven
// sandbox blows past the expected wall-clock by orders of magnitude.
#include <windows.h>
#include <intrin.h>
BOOL stepped_under_debugger(void) {
LARGE_INTEGER freq, t0, t1;
QueryPerformanceFrequency(&freq);
int regs[4];
QueryPerformanceCounter(&t0);
for (int i = 0; i < 100000; ++i) __cpuid(regs, 0);
QueryPerformanceCounter(&t1);
double us = (double)(t1.QuadPart - t0.QuadPart) * 1e6 / (double)freq.QuadPart;
// Native: ~5-50 ms. Hypervisor-traced or single-stepped: seconds.
return us > 1000000.0; // > 1s for 100k cpuid is wildly anomalous
}asmx64 direct stub (SSN 0x31, all builds)
; NtQueryPerformanceCounter has used SSN 0x31 from Win10 1507 through Win11 24H2.
; The Win32 wrapper QueryPerformanceCounter usually services the request from
; KUSER_SHARED_DATA without invoking this syscall at all.
NtQueryPerformanceCounter PROC
mov r10, rcx
mov eax, 31h
syscall
ret
NtQueryPerformanceCounter ENDPMITRE ATT&CK-Mappings
Last verified: 2026-05-20