Skip to content
λmaldev wiki/
pagesTiming-Based Evasion
T1497.003WindowsLinuxC / C++ASMRDTSC

Timing-Based Evasion

updated 2026-08-047 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

Timing checks exploit the fact that analysis tools — debuggers, emulators, and sandbox accelerators — change the relationship between elapsed time and CPU cycles. A debugger single-steps through instructions, dramatically increasing the cycle count per code path. A sandbox may accelerate Sleep calls to avoid waiting, causing the measured wall time to diverge from what GetTickCount returns.

None of these timing signals are conclusive on their own. Hypervisors can virtualise RDTSC. Sandboxes can patch GetTickCount to lie. But combining multiple orthogonal time sources makes comprehensive neutralisation expensive.

Note.

On bare-metal hardware without a hypervisor, RDTSC is the most reliable timing primitive. Inside any virtualised environment, the hypervisor can intercept RDTSC and return a synthetic value. The check still catches many sandboxes that do not implement RDTSC emulation.

The call chain

  1. 1
    RDTSC before a block
    Read the time-stamp counter to establish a baseline cycle count.
  2. 2
    execute the measured block
    The block can be a NOP sled, a CPUID, or any short sequence.
  3. 3
    RDTSC after the block
    Compute the delta. Normal execution is tens to hundreds of cycles; a debugger single-stepping inflates this to millions.
  4. 4
    GetTickCount before Sleep(n)
    Measure wall time to detect sleep acceleration — sandbox skips the delay but returns the wrong tick count.
  5. 5
    QueryPerformanceCounter (QPC) delta
    High-resolution wall clock; compare against RDTSC to detect cycle-count virtualisation.

Reference implementation

timing_checks.cC
#include <windows.h>
#include <intrin.h>

// 1. RDTSC single-step detection
// Normal execution: delta < 500 cycles
// Debugger single-step: delta > 100,000 cycles
static BOOL rdtsc_single_step(void) {
  UINT64 t1 = __rdtsc();
  // force serialisation so the CPU doesn't reorder
  int cpuid_regs[4];
  __cpuid(cpuid_regs, 0);
  UINT64 t2 = __rdtsc();
  return (t2 - t1) > 500;
}

// 2. Sleep acceleration detection
// Ask for 2 seconds; sandboxes often skip to milliseconds
static BOOL sleep_accelerated(void) {
  DWORD t1 = GetTickCount();
  Sleep(2000);
  DWORD elapsed = GetTickCount() - t1;
  // If elapsed < 1500ms, the sandbox compressed the sleep
  return elapsed < 1500;
}

// 3. QPC vs RDTSC divergence
// In a virtualised environment, these may advance at different rates
static BOOL qpc_rdtsc_diverge(void) {
  LARGE_INTEGER freq, q1, q2;
  QueryPerformanceFrequency(&freq);
  QueryPerformanceCounter(&q1);
  UINT64 r1 = __rdtsc();
  Sleep(100);
  QueryPerformanceCounter(&q2);
  UINT64 r2 = __rdtsc();

  // expected TSC ticks = QPC_delta * TSC_freq / QPC_freq
  // large divergence suggests emulated RDTSC
  double qpc_sec = (double)(q2.QuadPart - q1.QuadPart) / freq.QuadPart;
  double rdtsc_cycles = (double)(r2 - r1);
  // on a 3GHz CPU, 100ms ~ 300M cycles
  // a sandbox returning 0 or very small RDTSC delta is suspicious
  return rdtsc_cycles < 1000000;   // < 1M cycles for 100ms = suspicious
}

// 4. GetTickCount vs FILETIME comparison
static BOOL tickcount_filetime_diverge(void) {
  DWORD  tc  = GetTickCount();
  FILETIME ft;
  GetSystemTimeAsFileTime(&ft);
  ULARGE_INTEGER ul;
  ul.LowPart  = ft.dwLowDateTime;
  ul.HighPart = ft.dwHighDateTime;
  // FILETIME is 100-nanosecond intervals since 1601; convert to ms since boot
  // This is a rough check — the key is that both should advance together
  // A sandbox patching GetTickCount but not GetSystemTimeAsFileTime will diverge
  (void)tc; (void)ul;
  return FALSE;   // left as exercise; compare over two samples
}

BOOL timing_evasion_detected(void) {
  return rdtsc_single_step()   ||
         sleep_accelerated()   ||
         qpc_rdtsc_diverge();
}

Comparing timing sources

Source Win API Resolution Virtualised by sandbox?
TSC RDTSC instruction ~1 cycle Sometimes (KVM, VMware)
QPC QueryPerformanceCounter ~100ns Usually no
GetTickCount GetTickCount ~15ms Often patched
System time GetSystemTimeAsFileTime 100ns intervals Rarely patched
HPET / PM timer indirect via QPC ~1µs Rarely virtualised

A sandbox that patches GetTickCount to return accelerated values but does not also patch GetSystemTimeAsFileTime and QueryPerformanceCounter will diverge when compared. That divergence is the detection signal.

Inline assembly variant (MASM / GAS)

rdtsc_check.asmASM
; Inline RDTSC timing check — position-independent, no imports
;
; Returns ZF=1 (JZ taken) if analysis environment detected
check_timing PROC
  cpuid                          ; serialise before first rdtsc
  rdtsc
  mov   ecx, eax                 ; save low 32 bits
  cpuid                          ; serialise between readings
  rdtsc
  sub   eax, ecx                 ; delta in eax
  cmp   eax, 500                 ; normal < 500 cycles
  jg    analysis_detected        ; if > 500 → debugger / emulator
  xor   eax, eax                 ; clean exit
  ret
analysis_detected:
  mov   eax, 1
  ret
check_timing ENDP

Sandbox bypass for timing checks

Modern hardened sandboxes address timing in several ways:

Sandbox technique Effectiveness
RDTSC emulation returning real elapsed time Defeats most RDTSC checks
Patching Sleep to skip but fix GetTickCount Defeats simple sleep checks
Accurate QPC emulation Defeats QPC checks
High-CPU-load emulation (runs at full speed) Defeats cycle-count checks
Running on real hardware (bare-metal sandbox) Defeats all virtualisation-based checks

Bare-metal sandboxes (Cuckoo running on physical iron) defeat virtually all timing checks. They are expensive to operate at scale, so most automated detonation still uses virtualisation.

Detection

YARA
RDTSC (0F 31) instructions followed by a CMP/JMP — classic timing check pattern.
SANDBOX
Instrument RDTSC to return a monotonically advancing counter that matches real elapsed time; patch CMP thresholds to always pass.
BEHAVIOURAL
Process that exits cleanly without performing any network or file activity — timing check decided to abort.

Timing checks leave no file artefact and generate no network traffic. The detectable signal is the behaviour they cause: a process that performs environment checks and then exits cleanly is more suspicious than one that simply crashes. Log all process exits with a clean code (0) and no subsequent parent activity.

Was this page useful?edit this page ↗