Skip to content

QueryPerformanceCounter Timing

A timing-based anti-analysis check that uses the high-resolution performance counter to detect single-stepping or breakpoint-induced execution delays.

QueryPerformanceCounter (QPC) returns a monotonic, high-resolution timestamp from a hardware or hypervisor-backed counter. Malware brackets a short block of code with two QPC reads and measures the elapsed ticks. Under normal execution the delta is tiny; when an analyst single-steps the block or trips a software breakpoint, the delta explodes, revealing the debugger.

Unlike flag-based checks such as IsDebuggerPresent, a timing check leaves no artifact to patch in memory — it observes a side effect of analysis itself.

How it works

The sample records the counter, runs a few instructions, records it again, and compares the difference against a threshold. A human stepping through, or a debugger servicing exceptions, inflates the measured interval by orders of magnitude.

c
#include <windows.h>

LARGE_INTEGER t1, t2;
QueryPerformanceCounter(&t1);
// ... small block of work the analyst might single-step ...
QueryPerformanceCounter(&t2);

if ((t2.QuadPart - t1.QuadPart) > THRESHOLD) {
    // Execution was too slow — likely being debugged.
    ExitProcess(0);
}

The same idea appears with rdtsc, GetTickCount, and timeGetTime; QPC is favored because it is high-resolution and stable across the cores a hypervisor exposes.

In disassembly the two reads bracket the work, then a subtraction feeds the threshold compare:

asm
lea     rcx, [rbp-0x10]       ; &t1
call    QueryPerformanceCounter
mov     rsi, qword [rbp-0x10] ; t1.QuadPart
; ... small block of work the analyst might single-step ...
lea     rcx, [rbp-0x18]       ; &t2
call    QueryPerformanceCounter
mov     rax, qword [rbp-0x18] ; t2.QuadPart
sub     rax, rsi              ; delta = t2 - t1
cmp     rax, 0x186a0          ; delta > THRESHOLD (e.g. 100000 ticks) ?
jbe     short continue        ; fast enough -> keep running
call    ExitProcess           ; too slow -> debugger present

Detection & analysis

When reverse engineering a sample that uses this check:

  • Static analysis: flag paired imports of QueryPerformanceCounter (and QueryPerformanceFrequency) where the two results feed a subtraction and a comparison branch. Look for the same pattern around decryption or unpacking stubs.
  • Dynamic analysis: hook QueryPerformanceCounter and return monotonically increasing values with small, consistent deltas so timed blocks always appear fast. ScyllaHide's timing patches and time-warp plugins automate this.
  • Detection rule hint: in YARA, match the imports QueryPerformanceCounter plus QueryPerformanceFrequency co-occurring with a conditional ExitProcess or jump; in behavioral logs, alert on two QPC calls separated by very few instructions followed by an early exit.
Votes

Comments(0)