> Windows Syscalls
ntoskrnl.exeT1497.003T1497T1106

NtQueryPerformanceCounter

Returns the current value of the high-resolution performance counter and optionally its frequency.

Prototype

NTSTATUS NtQueryPerformanceCounter(
  PLARGE_INTEGER PerformanceCounter,
  PLARGE_INTEGER PerformanceFrequency
);

Arguments

NameTypeDirDescription
PerformanceCounterPLARGE_INTEGERoutReceives the current 64-bit performance counter value.
PerformanceFrequencyPLARGE_INTEGERoutOptional. Receives the counter's tick frequency in Hz. Pass NULL if not needed.

Syscall IDs by Windows version

Windows versionSyscall IDBuild
Win10 15070x31win10-1507
Win10 16070x31win10-1607
Win10 17030x31win10-1703
Win10 17090x31win10-1709
Win10 18030x31win10-1803
Win10 18090x31win10-1809
Win10 19030x31win10-1903
Win10 19090x31win10-1909
Win10 20040x31win10-2004
Win10 20H20x31win10-20h2
Win10 21H10x31win10-21h1
Win10 21H20x31win10-21h2
Win10 22H20x31win10-22h2
Win11 21H20x31win11-21h2
Win11 22H20x31win11-22h2
Win11 23H20x31win11-23h2
Win11 24H20x31win11-24h2
Server 20160x31winserver-2016
Server 20190x31winserver-2019
Server 20220x31winserver-2022
Server 20250x31winserver-2025

Kernel module

ntoskrnl.exeNtQueryPerformanceCounter

Related APIs

QueryPerformanceCounterQueryPerformanceFrequencyNtQuerySystemTimeGetTickCount64RtlQueryPerformanceCounter

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

Undocumented notes

Notably, the SSN for NtQueryPerformanceCounter has been **0x31 since Windows 10 1507 and remains 0x31 on Windows 11 24H2 / Server 2025** — one of the very few syscalls Microsoft has held perfectly stable across nine years of builds. Its `QueryPerformanceCounter` Win32 wrapper is almost always satisfied entirely by KUSER_SHARED_DATA (the `QpcData` block at `0x7FFE0000`) without ever entering the kernel; the syscall is only invoked as a fallback when the user-mode fast path detects a clock-source change, a hypervisor enlightenment update, or when the caller is inside a process whose KUSER_SHARED_DATA mapping has been intentionally disturbed (rare). On modern hardware the counter source is invariant TSC, scaled to a 10 MHz nominal frequency.

Common malware usage

This is the **sandbox-evasion workhorse**. The pattern is universal: read QPC, execute a tight loop or known-cost sequence (e.g. a series of CPUID instructions, a fixed-iteration CryptoAPI hash, or a calibrated busy-loop), read QPC again, divide by frequency. Sandboxes that time-accelerate, single-step, or instrument the executing thread show wall-clock readings that are either way too fast (time-jumped) or way too slow (instrumented). VMProtect and Themida bake elaborate QPC ladders into their stubs; GuLoader, AsyncRAT droppers, IcedID, and dozens of commodity loaders use simpler two-sample QPC delta checks before unpacking. The advantage over `rdtsc` is that QPC works inside Hyper-V / VMware / KVM guests where the hypervisor virtualises TSC — QPC reads the hypervisor's *exposed* clock, which still shows anomalies when guest time is decoupled from host wall-clock for analysis purposes. NtDelayExecution + QPC is the classic sleep-skip detection (request 60 seconds, measure actual elapsed QPC, bail if the sandbox returned in milliseconds).

Detection opportunities

QPC is *too* common to alert on directly — every UI thread, every game, every browser tab calls it constantly. Detection has to be behavioural: look for a tight `QPC → cheap computation → QPC → conditional branch on delta` pattern, especially when the conditional branch is the difference between executing the next stage and bailing out clean. ETW Microsoft-Windows-Threat-Intelligence does not surface this. Memory-scan-based detection of VMProtect/Themida stubs catches the *outcome* rather than the QPC use itself. The best behavioural marker is the *delay-skip* signature: NtDelayExecution(60000ms) immediately followed by NtQueryPerformanceCounter, with an early exit if elapsed time < threshold. Sandboxes that fix this by honouring delays then get caught by the tight-loop pattern instead.

Direct syscall examples

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 ENDP

MITRE ATT&CK mappings

Last verified: 2026-05-20