25 Detection Vectors and How I Beat Them
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.