.. /blog
2026-02-10 // 16 min

25 Detection Vectors and How I Beat Them

stealthanti-detectiontiming

I cataloged every known method to detect an AMD SVM hypervisor. Twenty-five vectors across six categories -- timing, CPUID, MSR, memory, hardware, and behavioral. Then I built countermeasures for all of them. This is the full breakdown.

The goal is not theoretical perfection. Some of these vectors are only exploitable with kernel-mode access. Some require statistical analysis over thousands of samples. Some are purely academic. But anti-cheat engines like Vanguard have kernel drivers, they do run statistics, and they do read papers. So I handle everything.

Tier 1: Timing

Timing is the hardest category because physics is against me. Every VMRUN/#VMEXIT round-trip burns CPU cycles, and those cycles are measurable.

// TSC compensation -- hardware offset field
vmcb.control.tsc_offset = -(AVG_EXIT_COST as i64);

// per-exit adjustment for variable-cost exits
fn adjust_tsc(vmcb: &mut VMCB, exit_start: u64) {
    let cost = rdtsc() - exit_start;
    vmcb.control.tsc_offset -= cost as i64;
}

1. RDTSC around CPUID -- the classic. Measure cycles before and after a CPUID that I intercept. Without compensation, the gap is ~300 cycles instead of ~30. My tsc_offset subtracts the exit cost. Detection goes from "one measurement" to "statistical distribution analysis."

2. RDTSC around RDMSR -- same idea, different instruction. I use the MSR permission bitmap to passthrough most MSRs, so only the ones I actually intercept show timing anomalies.

3. PM Timer (ACPI) -- an independent hardware timer at I/O port 0x1808. It doesn't care about TSC offsets. I intercept the I/O port read and compensate the returned value.

4. HPET -- High Precision Event Timer, MMIO at 0xFED00000. Same approach -- intercept MMIO reads to the HPET main counter register and adjust.

5. TSC_AUX mismatch -- RDTSCP returns the TSC and the IA32_TSC_AUX value (usually the processor ID). If the value changes between two reads without an obvious context switch, something is wrong. I keep TSC_AUX consistent.

Tier 2: CPUID

6. Leaf 0x40000000 -- hypervisor vendor string. I return "Microsoft Hv" to blend in with Hyper-V.

7. Leaf 0x40000001-0x4000000F -- Hyper-V enlightenment leaves. I emulate the full set so anything querying Hyper-V capabilities gets plausible responses.

8. Leaf 0x1 ECX bit 31 -- the hypervisor present bit. Has to be set, otherwise CPUID 0x40000000 is undefined behavior.

9. Leaf 0x0 max function -- some detectors check if the max CPUID leaf is suspiciously low or high. I pass through the real hardware value.

10. SVM-specific leaves -- CPUID leaf 0x8000000A returns SVM feature bits. I mask out bits that would reveal my intercept configuration.

Tier 3: MSR

// MSR permission bitmap -- 8KB, 2 bits per MSR
// bit 0 = intercept read, bit 1 = intercept write
fn setup_msr_bitmap(bitmap: &mut [u8; 8192]) {
    // default: passthrough everything (all zeros)
    bitmap.fill(0);

    // intercept only what I need
    set_intercept(bitmap, 0x40000000, READ);  // HV_MSR
    set_intercept(bitmap, 0xC0000080, WRITE); // EFER
    set_intercept(bitmap, 0x0000001B, READ);  // APIC_BASE
}

// hypercall page: VMMCALL; RET stub
let hv_page: [u8; 4096] = {
    let mut p = [0xCC; 4096]; // fill with INT3
    p[0] = 0x0F; p[1] = 0x01; p[2] = 0xD9; // VMMCALL
    p[3] = 0xC3;                           // RET
    p
};

11. EFER.SVME -- if SVM is enabled in EFER, something is virtualizing. I intercept EFER reads and clear the SVME bit in the returned value.

12. VM_CR MSR -- MSR 0xC0010114 can reveal SVM lock state. Passthrough with filtered bits.

13. Hyper-V MSRs (0x40000000-0x400000FF) -- I emulate the full range. The hypercall page MSR points to my VMMCALL; RET stub page.

14. Unknown MSR #GP -- reading an MSR that doesn't exist causes #GP. Some detectors try reading VM-related MSRs to see if the #GP is intercepted versus native. I passthrough unknown MSRs to let the real #GP fire.

Tier 4: Memory

15. Physical memory scan -- scanning all of RAM looking for VMCB structures or hypervisor signatures. My pages are NPT-unmapped -- they don't exist in the guest's physical address space.

16. NPT artifact detection -- checking if certain physical addresses cause unexpected #NPF behavior. Hard to detect from inside the guest since the fault is transparent.

17. SMRAM detection -- for SmmInfect, the memory controller hides SMRAM in hardware. No software check can read it.

Tier 5: Hardware

18. LBRV (Last Branch Record Virtualization) -- without LBRV, the host's LBR MSRs leak through to the guest. Hardware detectors can read LBR and see branches into hypervisor code. I enable LBRV in the VMCB -- hardware automatically swaps LBR MSRs on VMRUN/#VMEXIT.

19. IBPB on exit -- Indirect Branch Prediction Barrier. Without it, branch predictor state from the hypervisor leaks to the guest. Spectre-style attacks could extract hypervisor code addresses. I issue IBPB on every #VMEXIT.

20. vmcb_clean bits -- properly setting these tells the CPU which VMCB fields haven't changed, avoiding unnecessary reloads. But it also affects timing -- wrong clean bits cause extra microarchitectural work that's measurable. I set them correctly.

21. TLB_CONTROL -- not cached by hardware, read fresh on every VMRUN. I set flush-on-entry for guest VMCBs to prevent TLB side channels between host and guest.

22. Virtual APIC ID -- guest sees APIC ID 0/1, not the real 14/15. Without this, any code reading APIC ID sees impossible values.

Tier 6: Behavioral

23. Interrupt delivery latency -- injecting virtual interrupts adds latency that's measurable with high-precision timers. Minimized by using V_IRQ (virtual interrupt injection) instead of manual injection.

24. Exception behavior -- some exceptions behave differently when intercepted versus native. I passthrough all exceptions I don't need to handle.

25. NMI window -- NMI delivery timing changes subtly under virtualization. I use the NMI interception bit only when necessary and passthrough otherwise.

No single countermeasure is enough. Detection is an arms race. TSC compensation defeats casual timing checks, but a determined attacker with kernel access can still find statistical anomalies over millions of samples. The goal is to raise the cost of detection high enough that it's not worth the engineering effort -- or at least force them to use techniques that are fragile and hardware-specific.