Hypervisors were designed to partition hardware: run multiple operating systems side by side, each believing it owns the machine. But the same mechanism that enables cloud computing also enables a class of malware called virtualization-based rootkits (VMBRs) - also known as hyperrootkits. Instead of running above the OS, these rootkits install beneath it, in VMX-root mode (Intel) or SVM-host mode (AMD). The operating system becomes a guest, and the rootkit becomes the hypervisor.

The enabler of this stealth is Second Level Address Translation (SLAT) - Intel's Extended Page Tables (EPT) and AMD's Nested Page Tables (NPT). SLAT was created to accelerate memory virtualization, but it also gives a hypervisor total control over what a guest OS sees. A hypervisor can present a clean, unmodified view of memory to integrity scanners while secretly executing modified code from a hidden page. The guest cannot tell the difference because every read, write, and execute goes through the hypervisor's translation tables.

I am going to stay close to mechanics here: how the second translation layer is built, where split-view memory tricks fit, which probes survive common spoofing, and what the existing tools actually check.

How SLAT Works

EPT Page Table Walk Architecture
Figure 1: The EPT page table walk - from guest virtual address through guest page tables, then through the 4-level EPT structure to the system physical address.

The Two-Level Translation Problem

In a non-virtualized environment, the OS manages page tables that map virtual addresses (VAs) to physical addresses (PAs). In a virtualized environment, there are two layers:

  1. Guest page tables - map guest VAs to guest physical addresses (GPAs), managed by the guest OS via CR3.
  2. SLAT tables - map GPAs to system physical addresses (SPAs), managed by the hypervisor.

Every memory access goes through both walks. The hardware does this automatically - no hypervisor intervention needed for normal accesses. The result is a two-dimensional page walk: the CPU traverses guest PML4 → PDPT → PD → PT, producing a GPA from each level, then traverses EPT PML4 → PDPT → PD → PT to resolve each GPA to an SPA.

Intel EPT Structure

EPT uses a 4-level (or 5-level with 5-level paging) radix tree identical in structure to x86-64 page tables. Each EPT entry contains:

FieldBitsDescription
R (read)0Read access allowed
W (write)1Write access allowed
X (execute)2Execute access allowed
Page Frame Number12:51Physical address (4K-aligned)
Memory Type3:5Cacheability (UC, WC, WT, WB, WP)
Ignore PAT6Override PAT
Accessed8Set by hardware on access
Dirty9Set by hardware on write
Execute-only2Intel-specific: X=1, R=0, W=0

The critical architectural feature for stealth is execute-only pages: setting X=1 and R=W=0 allows code execution while making the page unreadable. Any attempt to read the page triggers an EPT violation, giving the hypervisor control.

AMD NPT Structure

AMD's NPT is functionally equivalent but with subtle differences:

  • Permission granularity: R, W, X are not fully independent on all models - on some AMD CPUs, X implies R, making execute-only pages impossible.
  • Fault mechanism: Nested page faults raise #VMEXIT(NPF) with EXITINFO1 distinguishing whether the fault occurred during a guest page-table walk or at the final GPA.
  • Context tagging: Uses Address Space Identifiers (ASIDs) instead of Intel's VPIDs.

VPID, ASID, and Tagged TLBs

To avoid flushing the TLB on every VM entry/exit, Intel uses Virtual-Processor Identifiers (VPIDs) and AMD uses ASIDs. These tags let the CPU cache translations for multiple address spaces simultaneously. A stealthy hypervisor:

  • Assigns a non-zero VPID to the guest so guest and host TLB entries coexist.
  • Uses selective invalidation (INVVPID type 3 for CR3 writes, INVEPT for specific EPT entries) instead of global flushes.
  • Avoids unnecessary TLB maintenance that would create measurable performance dips.
INVVPID type 3: selective invalidation on CR3 change
; type 3 = individual-address with global entries preserved
moveax, 3; INVVPID type (individual-address)
invvpid[desc], eax; invalidate single entry, keep globals

EPT Violation Path

When an EPT violation occurs (e.g., guest tries to read an execute-only page), the CPU:

  1. Saves the guest state to the VMCS.
  2. Loads the host state from the VMCS.
  3. Delivers control to the hypervisor's VM-exit handler.
  4. Records the exit reason (48 for EPT violation) and qualification in the VMCS.

The hypervisor can then inspect the qualification, determine which GPA caused the fault, and respond - by swapping EPT entries, injecting a fake value, or terminating the guest.

EPT Entry Bit-Level Breakdown

Each 64-bit EPT entry encodes more than just permissions. The full layout matters for both functionality and detection engineering:

BitsNameRole in Stealth
0Read accessCleared to deny reads, triggering EPT violations for integrity scanners
1Write accessCleared to prevent guest modification of hooked pages
2Execute accessSet alone (with R=W=0) for execute-only split-view hooking
3:5Memory typePAT-like cacheability: UC=0, WC=1, WT=4, WP=5, WB=6. Mismatched types between views cause timing side channels
6Ignore PATOverrides guest PAT; set to 1 for predictable cache behavior in hooks
7ReservedMust be 0
8AccessedSet by hardware on any access; can be monitored as a covert channel by SMM
9DirtySet by hardware on write access
10EPT exec-onlyIntel-specific: enables execute-only pages. Cleared by AMD NPT
12:51SPA page frame number44-bit physical address of the backing page. Swapping this field implements view switching
52:63Reserved / protection keyMust be 0 for valid entries
The Accessed bit (bit 8) is a subtle detection vector. A hypervisor that swaps EPT entries on every read/execute transition will cause the Accessed bit to be set far more frequently than bare metal, where it's set once and never cleared. An SMM monitor sampling EPT structures can count Accessed-bit transitions across sampling intervals and flag abnormal rates.

VMCS Fields: The Hypervisor's Control Surface

The Virtual Machine Control Structure (VMCS) is a 4KB-aligned memory region that controls every aspect of VMX operation. Understanding the critical fields reveals what a hypervisor can and cannot hide:

VMCS FieldEncodingPurposeDetection Relevance
EPTP0x0000201AEPT pointer: 12:51 = physical base of PML4, 0:2 = memory type, 3:5 = page walk lengthMultiple EPTPs enable view switching; SMM can enumerate all valid EPTPs in the VMCS
EPT_POINTER0x0000201ASame as EPTP, read-only aliasComparing EPTP readback vs the active page tables reveals altp2m usage
VM_EXIT_CTLS0x00002036Which events cause VM-exits. Bits for CPUID, RDMSR, WRMSR, RDTSC, CR3, INVLPG, MOV-DR, I/OA hypervisor that clears the RDTSC exit bit is not intercepting TSC - timing probes will be accurate
VM_ENTRY_CTLS0x0000203AControls for VM entry: load debug controls, load MSRs, inject eventsLoad IA32_EFER controls whether guest NX enforcement matches host expectations
TSC_OFFSET0x00002010Signed 64-bit offset added to guest RDTSC valuesPrimary mechanism for hiding VM-exit overhead; negative values = time stolen from guest
TSC_MULTIPLIER0x00002032Fixed-point multiplier for guest TSC (scaling)Can accelerate or decelerate guest's perception of time; creates mismatch with HPET/PMTIMER
CR0_READ_SHADOW0x00006004Shadow value returned when guest reads CR0If CR0 doesn't match actual control register state, SMSW/LMSW emulation becomes inconsistent
CR4_READ_SHADOW0x00006006Shadow value returned when guest reads CR4Mismatch between shadow and actual CR4 causes detection via instruction behavior divergence
VMCS_LINK_POINTER0x00002800Links to shadow VMCS for nested virtualizationNon-null value indicates nested virtualization is in use (Blue Pill / DroidStone technique)

The VMCS is itself a detection surface. A hypervisor that virtualizes MSRs must also virtualize the VMX capability MSRs (0x480-0x493) to maintain consistency. If IA32_VMX_BASIC (0x480) reports VMCS revision ID X but the actual VMCS in use has revision Y, the discrepancy exposes the hypervisor.

The Hypervisor Attack Surface

EPT Memory Cloaking Flow
Figure 2: EPT memory cloaking - an integrity scanner reads the original page while execution follows the hook.

What a Hypervisor Can Intercept

A hypervisor running beneath the OS can intercept:

ResourceInterception MethodEvasion
CPUID leavesVM-exit on CPUIDReturn spoofed values
MSR accessVM-exit on RDMSR/WRMSRMask virtualization MSRs
Memory reads/writesEPT permissionsExecute-only pages
CR3 writesVM-exit on MOV CR3Track page tables
InterruptsAPIC virtualizationSynthesize guest APIC state
RDTSC/TSCTSC offsetting/scalingHide VM-exit overhead
I/O portsVM-exit on IN/OUTEmulate or passthrough

Memory Cloaking via EPT

The split-view technique exploits execute-only EPT entries. The hypervisor:

  1. Allocates a shadow page containing its hook code.
  2. Creates an EPT mapping for the target GPA that points to the shadow page, marked X-only.
  3. Keeps the original page mapped elsewhere for read/write access.
  4. When the guest executes the address → CPU succeeds (X bit set).
  5. When an integrity scanner reads the address → EPT violation → hypervisor swaps the EPT entry to the original page, returns the clean content, then re-applies the hook.
AMD NPT historically didn't support pure execute-only permissions (X implied R on many models). That forced attackers to use more complex transient hooking or view-switching logic, but the fundamental concept remains the same - the hypervisor interposes on the translation.

View Switching via altp2m / Multiple EPTPs

Intel supports multiple EPT pointers (EPTPs) in the VMCS. Technologies like Xen's altp2m allow up to 512 memory views per guest. Switching views is near-instantaneous - just change the EPTP index. This avoids the overhead of EPT entry modifications and TLB invalidations.

VMFUNC Leaf 0 - EPTP switching (no VM-exit)
; ECX = EPTP index (0-511)
; New memory view is active immediately after this instruction
movecx, 3; select view #3 (hooked view)
vmfunc; activate EPT pointer #3, zero VM-exit cost

A stealthy rootkit can maintain separate views: one for normal operation, one for intercepted syscalls, and one for hiding its own kernel module.

Historical Context: The Evolution of Hypervisor Rootkits

The progression from theory to weaponized hypervisor stealth tracks the evolution of hardware virtualization itself:

YearProjectSignificance
2005Intel VT-x / AMD-V siliconHardware virtualization becomes available on consumer CPUs, eliminating the need for binary translation
2006SubVirt (King et al., Microsoft/UMich)First academic proof-of-concept: installs a VMM beneath a running OS, hides malware from the guest. Demonstrated that the guest OS cannot detect ring -1 compromise through in-guest tools
2006Blue Pill (Rutkowska, COSEINC)Demonstrated on-the-fly migration of a live Windows OS into a VM without reboot. Used AMD SVM. Introduced the concept that ring -1 changes the evidentiary model - guest-visible bytes are not trustworthy
2009Vitriol (Rutkowska)Intel VT-x version of Blue Pill. Demonstrated EPT-based page remapping for stealth. First practical use of EPT execute-only pages for memory hiding
2011Stoned Bootkit / MebromiReal-world BIOS-level bootkit that installed a hypervisor. Combined MBR infection with VMX rootkit. Marked the transition from research to in-the-wild malware
2015DroidStone (Peking University)Nested hypervisor hiding: a hypervisor that virtualizes itself, making detection by a higher-privilege layer harder. Demonstrated on ARM/Android
2017kAFL (USENIX Security)Hardware-assisted kernel fuzzing using Intel PT. Though defensive, it proved that hypervisors can be instrumented for security monitoring - closing the loop
2019Ophion stealth hypervisorModern VT-x rootkit: EPT large-page identity mapping, firmware-locked MSR masks, TSC trapping, and VMFUNC view switching. Represents the state of the art in stealth
2020HyperLeech (RAID)DMA-based hypervisor injection via PCIe. Bypasses IOMMU on misconfigured systems. Introduced the threat of external-attacker hypervisor installation without kernel code execution
2023+Intel TDX / AMD SEV-SNPConfidential computing: the hypervisor is removed from the trust boundary. Guest memory is encrypted and integrity-protected. Shifts the threat model entirely
SubVirt's useful lesson still holds: the better checks sit below or outside the VMBR. SMM, firmware attestation, and external DMA give the defender a cleaner vantage point. Guest-side checks still help, but they are evidence, not proof.

Stealth Techniques

CPUID and MSR Masking

The most basic detection vector is the CPUID hypervisor bit (ECX bit 31 of leaf 1). A legitimate hypervisor sets this to 1, but a stealthy one clears it:

// Hypervisor's CPUID handler for leaf 1
void handle_cpuid_leaf1(guest_state* g) {
    g->eax = read_native_cpuid(1, 0);  // pass through
    g->ebx = read_native_cpuid(1, 0);
    g->ecx = read_native_cpuid(1, 0) & ~BIT31;  // clear hypervisor bit
    g->edx = read_native_cpuid(1, 0);
}

For MSRs, the hypervisor must:

  • IA32_FEATURE_CONTROL (0x3A): Report VMX locked/disabled by firmware.
  • VMX capability MSRs (0x480-0x493): Pass through real hardware values for coherence.
  • Synthetic MSR range (0x40000000+): Inject #GP to avoid identification as Hyper-V, KVM, or Xen.
void handle_rdmsr(guest_state* g) {
    switch (g->rcx) {
    case IA32_FEATURE_CONTROL:
        g->rax = IA32_FEATURE_CONTROL_LOCK_BIT;  // locked, no VMX
        break;
    case 0x40000000 ... 0x4000FFFF:  // synthetic range
        inject_gp(g, 0);  // pretend MSR doesn't exist
        break;
    default:
        g->rax = native_rdmsr(g->rcx);  // pass through
        break;
    }
}

TSC Compensation

VM exits cost hundreds to thousands of cycles. A bare-metal CPUID takes ~80 cycles; in a VM, the exit/entry penalty adds measurable overhead. TSC offsetting and scaling can hide this:

TSC offset in VMCS to hide VM-exit overhead
; VMCS.TSC_OFFSET = -overhead_cycles
; When guest executes RDTSC, result = hardware TSC + offset
; Net effect: RDTSC appears to take bare-metal time
; Set TSC offset field in VMCS
moveax, -2000; offset = negative overhead
vmwriteTSC_OFFSET, eax; write offset to VMCS field
; Guest-side: RDTSC returns hardware_TSC + (-2000)
; CPUID timing appears to match bare metal (~80 cycles)
rdtsc; guest reads compensated timestamp

Advanced rootkits like Ophion use a "trap next RDTSC" strategy: on a CPUID exit, the handler arms a trap for the next RDTSC instruction. When the guest measures elapsed time, the hypervisor returns a spoofed value matching bare-metal expectations.

Nested Virtualization (Blue Pill, DroidStone)

A hypervisor can install another hypervisor beneath itself, creating a nesting loop. Blue Pill (2006) demonstrated on-the-fly migration of a live OS into a VM without rebooting. DroidStone showed a hypervisor that virtualizes itself, making detection even harder:

┌─────────────────────────┐
│      Guest OS           │ ← Can detect the inner hypervisor
├─────────────────────────┤
│   Inner Hypervisor      │ ← May be detected by nested checks
├─────────────────────────┤
│   Outer Hypervisor      │ ← Undetectable from guest
├─────────────────────────┤
│      Hardware           │
└─────────────────────────┘

Intel Processor Trace (PT) Evasion

Intel PT records control-flow metadata. A hypervisor can:

  1. Disable PT on VM-entry by clearing IA32_RTIT_CTL.TraceEn.
  2. Use PT's FilterEnable and FilterEn to exclude hypervisor code from tracing.
  3. Configure RTIT_CTL to stop tracing on VM-exit.
// Disable PT before VM-entry
vmcs_write(VM_ENTRY_CTLS, vmcs_read(VM_ENTRY_CTLS) | VM_ENTRY_LOAD_RTIT_CTL);
wrmsr(IA32_RTIT_CTL, 0);  // clear TraceEn before entering guest

APIC Virtualization (APICv / x2APIC)

APICv reduces VM exits for interrupt operations, which both improves performance and reduces detection surface. With APICv:

  • Guest writes to the APIC page do not cause VM exits (they update a shadow page).
  • The hypervisor can defer interrupt injection.
  • Fewer exits = less timing variance = harder to detect.

Detection Methods

Detection Pipeline
Figure 3: The detection pipeline - multiple probe streams feed into a correlation engine that produces a VM probability score.

Taxonomy of Detection Techniques

CategoryMethodWhat It DetectsReliability
ArchitecturalCPUID bit checkHypervisor presenceLow (trivially spoofable)
ArchitecturalCPUID vendor stringHyper-V, VMware, KVM, XenMedium
MSR-basedSynthetic MSR probeHyper-V/KVM enlightenmentsMedium
TimingRDTSC delta around CPUIDVM exit overheadMedium
TimingStatistical IET analysisVariance/kurtosis anomaliesHigh
MemoryEPT hook scanExecute-only page anomaliesHigh
MemoryTLB profilingNested page walk overheadMedium
MicroarchitecturalRSB (Return Stack Buffer)Hypervisor stack pollutionHigh
MicroarchitecturalCache contentionHypervisor code footprintHigh
Performance monitorPMU/HPC countersStolen cycles, ITLB flushesMedium
SMM / out-of-bandExternal DMA readGround-truth memory viewVery high
SoftwareDriver name scanningVM guest driversHigh
SoftwareShared page checkNtQuerySystemInfo(0xC5)High

CPUID Discrepancies

The simplest check: query CPUID leaf 1 and check ECX bit 31.

int cpuinfo[4] = {0};
__cpuid(cpuinfo, 1);
int hypervisor_bit = (cpuinfo[2] >> 31) & 1;

Limitation: a stealthy hypervisor simply clears this bit. More useful: check for inconsistencies between leaves. For example, if CPUID leaf 0xD subleaf 0 and subleaf 1 return identical values, the hypervisor is ignoring ECX - a sign of lazy emulation.

Timing Analysis

RDTSC delta: Measure the cycles for a CPUID instruction (which causes a VM exit).

uint64_t t1 = __rdtsc();
__cpuid(cpuinfo, 0);
uint64_t t2 = __rdtsc();
uint64_t delta = t2 - t1;
// Bare metal: ~80-200 cycles
// Virtualized: ~1000-5000 cycles (even with TSC compensation)

Statistical Instruction Execution Time (IET): Even if the hypervisor spoofs the mean, it cannot hide the variance. VMM dispatcher logic adds randomness:

MetricBare MetalVirtualized (stealthy)
Mean CPUID~80 cyclesSpoofed to ~80
VarianceExtremely lowHigher (dispatcher noise)
Spectral widthNarrowBroad/layered
Outlier patternRandom (interrupts)Periodic (VMM dispatcher)

The Wilcoxon rank-sum test can reject the null hypothesis ("this is bare metal") with high confidence from a few thousand samples.

TLB and Translation Profiling

Nested paging multiplies page-table walk latency. Bare-metal TLB miss penalty is ~5x the hit latency; with EPT/NPT, it exceeds 10x:

// Measure TLB-friendly (sequential, 64B stride) vs TLB-unfriendly (sparse 64KB stride)
for (int i = 0; i < 10000; i += 64) {
    t1 = __rdtsc();
    *(volatile char*)(region + i) = 0x42;
    t2 = __rdtsc();
    hit_latencies.push_back(t2 - t1);
}
for (size_t i = 0; i < alloc_size; i += 4096 * 16) {
    t1 = __rdtsc();
    *(volatile char*)(region + i) = 0x42;
    t2 = __rdtsc();
    walk_latencies.push_back(t2 - t1);
}
double ratio = mean_walk / mean_hit;
bool is_vm = (ratio > 10.0);  // threshold

RSB (Return Stack Buffer) Probing

The Hyper-channel technique: populate the RSB with known return addresses, trigger a VM-exit (e.g., CPUID), then time a sequence of RET instructions. If the hypervisor's exit handler used the stack, it polluted the RSB, causing misses:

void hyper_channel_probe() {
    // Train RSB with known addresses
    asm volatile("call train; ret; train: ...");
    // Trigger VM exit
    __cpuid(cpuinfo, 0);
    // Measure RET timing - miss = ~15 cycles penalty
    t1 = __rdtsc();
    asm volatile("ret" : ...);
    t2 = __rdtsc();
    // High delta → RSB was polluted → hypervisor present
}

Hardware Performance Counter Monitoring

The PMU gives another view of the machine. The counters I would start with are:

  • ITLB misses: View switching causes ITLB flushes.
  • L3 cache misses: Hypervisor code thrashes shared cache.
  • Instructions retired vs cycles: Ratio reveals stolen cycles.
// Configure PMU to count ITLB misses
wrmsr(IA32_PERFEVTSEL0, EVENT_ITLB_MISS);
// Run probe
uint64_t before = rdpmc(0);
probe_function();
uint64_t after = rdpmc(0);
uint64_t misses = after - before;
if (misses > threshold) { /* likely virtualized */ }

SMM and External Introspection

System Management Mode (SMM) runs below the hypervisor. An SMM monitor can:

  • Periodically trigger an SMI to freeze the system.
  • Read the hypervisor's code and EPT structures from physical memory.
  • Compare a guest's self-reported state against the SMM view.

External DMA (PCIe forensic cards) reads physical memory without CPU involvement, immune to hypervisor spoofing.

Cache Contention Probing

Caches are shared between the hypervisor and the guest. A hypervisor's VM-exit handler occupies cache lines during dispatch. A detector can:

  1. Prime: fill a cache set with known data from the guest.
  2. Trigger: cause a VM-exit (CPUID, RDMSR to synthetic range).
  3. Probe: measure access latency to the cached data. If latency increased, the hypervisor evicted those lines.
// Prime+Probe cache contention across VM-exit boundary
uint64_t probe_cache_contention() {
    static char buffer[64 * 256];  // one cache set worth

    // Prime: fill target cache set
    for (int i = 0; i < 256; i++) {
        *(volatile char*)(buffer + i * 64) = 0xAA;
    }

    // Trigger VM-exit
    __cpuid(cpuinfo, 0);

    // Probe: measure access latency
    uint64_t t1 = __rdtsc();
    for (int i = 0; i < 256; i++) {
        *(volatile char*)(buffer + i * 64) = 0xBB;
    }
    uint64_t t2 = __rdtsc();

    return (t2 - t1) / 256;  // average cycles per access
    // Bare metal: ~3-4 cycles (L1 hit)
    // Post-VM-exit: >10 cycles (L1 miss, L2/L3 hit)
}

The technique is probabilistic - one sample is useless. Statistical significance requires hundreds of iterations. The noise floor from interrupts and context switches must be accounted for. But when tuned correctly, cache contention is one of the hardest signals for a hypervisor to mask because the hypervisor must use the cache to run its exit handler at acceptable speed.

PMU Counter Virtualization Detection

Modern hypervisors can virtualize PMU counters via the VMCS's PMU_VIRTUALIZATION controls. A detector can probe for counter virtualization by:

// Write known value to PMC0, read back, check if it stuck
wrmsr(IA32_PMC0, 0xDEADBEEF);
uint64_t val = rdpmc(0);
if (val != 0xDEADBEEF) {
    // Counter virtualization intercepted the write
    return DETECTED;
}

// Write to counter config, check if enable bit retained
wrmsr(IA32_PERFEVTSEL0, COUNTER_ENABLED | EVENT_INSTRUCTIONS_RETIRED);
uint64_t cfg = rdmsr(IA32_PERFEVTSEL0);
if (!(cfg & COUNTER_ENABLED)) {
    // Hypervisor stripped the enable bit - counter is dead
    return DETECTED;
}

hvdetecc expands on this by checking whether multiple counter access paths agree: MSR readback, RDPMC instruction, and actual counter behavior (enabled counter should increment). Disagreement between any two of these reveals counter virtualization.

Case Study: Multi-Pass Driver Scan & SharedPage Check

Multi-Pass Driver Name Scanning
Figure 4: Multi-pass driver name scanning. Each pass compares every loaded module against a blacklist of VM driver names.

Driver Name Scanning

Some commercial integrity scanners implement a brute-force approach to virtual machine detection. At load time, the scanner makes 10+ full passes over the kernel's loaded module list. Each pass searches for a specific blacklisted driver name using RtlCompareString. Across all passes, this amounts to approximately 4,674 string comparisons.

Blacklisted VM platform drivers:

DriverVendorPlatform
VBoxGuest.sysOracleVirtualBox guest additions
VBoxVideo.sysOracleVirtualBox virtual display
vm3dmp.sysVMwareVMware 3D display miniport
prl_kmdd.sysParallelsParallels kernel-mode display
HyperVideo.sysMicrosoftHyper-V synthetic video
vrd.sysMicrosoftVirtual Remote Desktop display
viostor.sysRed HatVirtIO/KVM/QEMU storage
vioscsi.sysRed HatVirtIO/KVM/QEMU SCSI

Blacklisted debugging and inspection drivers:

DriverTarget
Dbgv.sysKernel debugger (DbgView/SoftICE)
PROCMON23.sysProcess Monitor
dbk64.sysLow-level memory inspection driver

Detection logic (conceptual):

// Simplified module scanning - 10 passes, one driver per pass
typedef struct _SYSTEM_MODULE_INFO {
    HANDLE  Section;
    PVOID   MappedBase;
    // ...
    WCHAR   ImageName[256];
} SYSTEM_MODULE_INFO, *PSYSTEM_MODULE_INFO;

const wchar_t* blacklist[] = {
    L"VBoxGuest.sys", L"VBoxVideo.sys", L"vm3dmp.sys",
    L"prl_kmdd.sys", L"HyperVideo.sys", L"vrd.sys",
    L"viostor.sys",  L"vioscsi.sys",
    L"Dbgv.sys",     L"PROCMON23.sys", L"dbk64.sys"
};

bool scanner_vm_detect() {
    for (int pass = 0; pass < 10; pass++) {
        // Query loaded module list
        PSYSTEM_MODULE_INFORMATION mods = query_modules();

        for (ULONG i = 0; i < mods->Count; i++) {
            if (RtlCompareString(
                    mods->Modules[i].ImageName,
                    blacklist[pass],
                    TRUE) == 0) {
                return TRUE;  // VM DETECTED
            }
        }
    }
    return FALSE;
}

Strengths:

  • Detects VMs by identifying drivers that the VM installs as guest additions.
  • Multiple passes ensure the driver hasn't been unloaded between checks.
  • Simple and effective against standard VM configurations.

Weaknesses:

  • A stealth hypervisor can hide the driver from the loaded module list by intercepting NtQuerySystemInformation(SystemModuleInformation) and filtering out blacklisted names.
  • The driver list is static - renaming a driver defeats this check.
  • Performance cost: ~500ms for the full 10-pass scan.

Evasion by a rootkit hypervisor:

A hypervisor-level rootkit can intercept the NtQuerySystemInformation system call (class 0x0B, SystemModuleInformation) via an EPT hook on ntoskrnl.exe's system service dispatch. When the class matches module information:

// Hypervisor-side interception of module info query
void hv_intercept_module_query(guest_regs* regs) {
    // Let the query proceed normally
    // Then walk the returned module list and remove entries
    // matching blacklisted driver names

    SYSTEM_MODULE_INFORMATION* info = (SYSTEM_MODULE_INFORMATION*)regs->rsi;
    for (int i = 0; i < info->Count; i++) {
        if (is_blacklisted(info->Modules[i].ImageName)) {
            // Remove this entry by compacting the list
            memmove_s(&info->Modules[i],
                      &info->Modules[i+1],
                      (info->Count - i - 1) * sizeof(info->Modules[0]));
            info->Count--;
            i--;
        }
    }
}

SystemHypervisorSharedPageInformation (NtQuerySystemInformation Class 0xC5)

Windows 10 and later expose the hypervisor's shared page through a user-mode-accessible system information class. The call:

// Query class 0xC5: SystemHypervisorSharedPageInformation
SYSTEM_HYPERVISOR_SHARED_PAGE_INFORMATION info = {0};
NTSTATUS status = NtQuerySystemInformation(
    (SYSTEM_INFORMATION_CLASS)0xC5,  // SystemHypervisorSharedPageInformation
    &info,
    sizeof(info),
    NULL
);

Returned structure:

typedef struct _SYSTEM_HYPERVISOR_SHARED_PAGE_INFORMATION {
    PSYSTEM_HYPERVISOR_USER_SHARED_DATA HypervisorSharedUserVa;
} SYSTEM_HYPERVISOR_SHARED_PAGE_INFORMATION, *PSYSTEM_HYPERVISOR_SHARED_PAGE_INFORMATION;

Detection logic:

bool is_hypervisor_exposed() {
    SYSTEM_HYPERVISOR_SHARED_PAGE_INFORMATION info = {0};
    NTSTATUS status = NtQuerySystemInformation(
        (SYSTEM_INFORMATION_CLASS)0xC5, &info, sizeof(info), NULL);

    if (NT_SUCCESS(status) && info.HypervisorSharedUserVa != NULL) {
        // A hypervisor has mapped its shared page into user space
        // This indicates a type-1 hypervisor (Hyper-V, etc.)
        return TRUE;
    }
    return FALSE;
}

Interpretation:

  • HypervisorSharedUserVa != NULL: A hypervisor has mapped its shared page into user space. The page contains SYSTEM_HYPERVISOR_USER_SHARED_DATA with timing lock, multiplier, and bias for high-resolution QPC.
  • HypervisorSharedUserVa == NULL: Either bare metal, or a hypervisor that deliberately hides this page.

How a stealthy hypervisor can hide this:

The NtQuerySystemInformation system call with class 0xC5 reads a pointer stored in the kernel's ntoskrnl.exe data section. A stealth hypervisor can:

  1. Intercept the system call dispatch via an EPT hook: when rax = NtQuerySystemInformation and rcx = 0xC5, patch the output buffer to zero out the pointer.
  2. Modify the kernel data structure that stores the shared page pointer (e.g., ExpHypervisorSharedUserVa) to be NULL.
  3. Never populate the shared page - if the hypervisor never calls HvlMapSharedUserVa, the pointer stays NULL.
// Hypervisor-side: intercept NtQuerySystemInformation class 0xC5
void hv_intercept_shared_page_query(guest_regs* regs) {
    SYSTEM_INFORMATION_CLASS class = (SYSTEM_INFORMATION_CLASS)regs->rdx;
    if (class == (SYSTEM_INFORMATION_CLASS)0xC5) {
        // Let the query execute normally
        // Then zero out the return pointer
        regs->r8 = 0;  // NULL in the output buffer
    }
}

Tool Analysis

Bloodhound

Source files: Bloodhound.cpp, VPGATHER.cpp

Method: Bloodhound uses the VPGATHERQQ instruction (AVX2) to test address accessibility without actually causing a page fault. It:

  1. Locates a RET (0xC3) instruction in the target page.
  2. Executes the RET to force an EPT violation (if the page is hooked).
  3. Uses VPGATHERQQ to check if the address is still accessible after execution.
  4. Repeats 100 times; if confidence >= 90%, declares an EPT hook present.

Key insight from VPGATHER.cpp:

// VPGATHERQQ: if the test address faults, the #DB handler
// detects the second single-step on the same instruction
// and sets TlsAddressNotValid = TRUE
__m128i Addresses = _mm_set_epi64x((UINT64)Address, (UINT64)ValidRegion);
TlsAddressNotValid = FALSE;
__debugbreak();  // Start single-stepping
_mm_i64gather_epi64(NULL, Addresses, 1);  // VPGATHERQQ

Strengths: Novel use of VPGATHERQQ to probe address accessibility without page faults. Executing a RET is low-side-effect - it doesn't corrupt guest state.

Weaknesses: Requires AVX2 support. The 100-iteration loop is noisy. Only detects EPT hooks based on execute/read asymmetry - may miss hooks that use NPT or shadow pages with full R/W/X.

VPGATHERQQ mechanism in depth:

The VPGATHERQQ instruction gathers 64-bit values from memory using vector indices. Its critical property: if an address is inaccessible, the corresponding XMM element is left unchanged rather than faulting. This makes it useful for probing. Bloodhound exploits this:

  1. Set XMM0 to contain two addresses: a known-valid region and the test address.
  2. Execute VPGATHERQQ with a scale of 1.
  3. After execution, check if XMM0 changed. If the test address was accessible, both elements update. If inaccessible, the test element retains its original value.

The single-stepping via __debugbreak() (which raises #DB) is necessary because VPGATHERQQ itself does not signal faults conventionally. The VEH handler watches for the second #DB on the same instruction - if VPGATHERQQ faults internally, it triggers a second single-step before completing, which the handler detects. This is an intricate abuse of the debug exception delivery mechanism.

Detection flow:

┌─────────────┐     ┌─────────────┐     ┌──────────────┐
│ Find RET in │────→│ Execute RET │────→│ VPGATHERQQ   │
│   page      │     │ (force exit)│     │ check access │
└─────────────┘     └─────────────┘     └──────┬───────┘
                                              │
                                     ┌────────┴────────┐
                                     │ Confidence ≥ 90% │
                                     │ after 100 iters  │
                                     └────────┬────────┘
                                              │
                                   ┌──────────┴──────────┐
                                   │ EPT Hook Detected   │
                                   └─────────────────────┘

checkhv_um

Source file: hv_um.cpp

Method: User-mode hypervisor detection using multiple heuristics:

  • CheckSyntheticCPUID: Queries CPUID leaf 0x40000000 for Hyper-V/VMware/KVM/Xen signatures.
  • CheckCrystalClock: Compares CPUID-derived crystal clock frequency with measured RDTSC frequency over QPC wall time; mismatch >5% is suspicious.
  • CheckKUserSharedData: Reads KUSER_SHARED_DATA.InterruptTime and checks delta against RDTSC; deviation outside ±10% suggests timer virtualization.
  • CheckUMIP_SGDT: Measures SGDT latency - bare metal takes 5-15K cycles (UMIP emulation), very fast SGDT suggests hypervisor fast path.
  • CheckSchedulerSignature: Sleep(0) wakeup latency via QPC; >1ms mean suggests vCPU scheduling overhead.
  • CheckTLBEPTPressure: TLB hit vs page-walk latency ratio; >10x indicates nested paging.
  • CheckTSCNoiseFloor: Distribution of consecutive RDTSC deltas; >10% peaks above 1.5x mean is suspicious.
  • CheckERMSBEPT: Debug register (Dr0) set during REP MOVSB - a hypervisor may suppress the debug trap before it fires.

Key excerpt:

void CheckTLBEPTPressure() {
    // TLB-friendly: sequential 64B strides
    for (size_t i = 0; i < 10000; i += 64) {
        *(volatile char*)(region + i) = 0x42;
        hit_latencies.push_back(__rdtsc() - t1);
    }
    // TLB-unfriendly: sparse 64KB strides
    for (size_t i = 0; i < alloc_size; i += 4096 * 16) {
        *(volatile char*)(region + i) = 0x42;
        walk_latencies.push_back(__rdtsc() - t1);
    }
    double ratio = mean_walk / mean_hit;
    bool anomaly = (ratio > 10.0);
}

Strengths: Multi-signal approach - no single heuristic is definitive, but combined they provide strong evidence. The ERMSB debug register check is particularly clever.

Weaknesses: User-mode only - can be sandboxed or intercepted. Thresholds (5%, 10%, 10x) are heuristic and may produce false positives on some hardware. TSC noise check is sensitive to system load.

ept-hook-detection (momo5502)

Source files: main.cpp, ept_hook_checks.h, timing_check.cpp, write_check.cpp, thread_check.cpp

Method: Three detection primitives for EPT hooks:

Timing check (timing_check.cpp):

bool ept_hook_timing_check(void* pointer_in_page) {
    // Find a RET in the page
    auto* ret = find_ret_in_page(pointer_in_page);
    // Measure 20 reads, then 20 alternating read+execute
    auto reads = measure_reads(ret, 20);
    auto execs = measure_executions_with_alternating_reads(ret, 20);
    // If execs >> reads, likely an EPT hook
    return (reads * 4) < execs;
}

Write check (write_check.cpp): Overwrites a 0xCC (INT3) with 0xC3 (RET). If the write doesn't affect execution (still throws despite being a valid RET), an EPT hook re-mapped the page.

// Overwrite CC with C3
write_byte(pointer, 0xC3);
// If it STILL throws after the write, EPT is hiding the original CC byte
bool hooked = execute_throws(pointer);

Thread check (thread_check.cpp): Uses a secondary thread as a software clock (counter increment) to measure execution latency without relying on RDTSC - the counter thread runs on a different core, and the measurement is based on how many increments occur during the probe.

Strengths: Three complementary methods - timing catches latency anomalies, write catches EPT view-switching failures, thread check avoids TSC-based timing entirely. Multi-core detection catches per-VCPU view-switching bugs.

Weaknesses: The write check modifies memory (temporarily), which could cause issues in production. The timing check requires a RET instruction in the target page (not always present). False positives on heavily loaded systems.

ermsb-meme (ERMSB EPT Detection)

Source file: main.c

Method: Uses REP MOVSB (ERMSB - Enhanced REP MOVSB/STOSB) with a hardware debug breakpoint on the destination page. On bare metal, the debug trap fires reliably during the string copy. If the copy is under EPT control, the hypervisor may intercept before the debug logic triggers.

int probe_page(uint64_t page) {
    CONTEXT ctx = {0};
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
    ctx.Dr0 = page + 8;     // Break on address
    ctx.Dr7 = 0x30001;       // Enable, execution breakpoint
    SetThreadContext(GetCurrentThread(), &ctx);

    __try {
        // REP MOVSB copies 0x2000 bytes
        // On bare metal, DR0 fires mid-copy
        __movsb((PBYTE)destination, (PBYTE)(page - 0x1000), 0x2000);
    } __except(handler(...)) {
        detecc = 1;  // Exception handler fired - trap was NOT suppressed
    }

    if (detecc == 0) {
        // Trap was suppressed - hypervisor is hiding the page
        return DETECTED;  // EPT HOOK
    }
    return NOT_DETECTED;
}

Key insight: The ERMSB microcode reads source bytes in bulk. If the hypervisor is using EPT execute-only to hide a page, the hardware debug mechanism may trigger before the EPT violation check, or the hypervisor may fail to forward the debug exception to the guest.

Strengths: Extremely low false-positive rate. Exploits a real difference in the order of exception delivery between bare metal and virtualized environments.

Weaknesses: Requires one valid/present page preceeding the target (as a buffer). Only works on Intel CPUs with ERMSB support. The technique is subtle and may behave differently across microcode revisions.

Why ERMSB matters for detection:

ERMSB (Enhanced REP MOVSB/STOSB) was introduced in Ivy Bridge. The microcode implementation performs bulk reads from the source - it fetches cache-line-sized chunks rather than byte-by-byte. When a debug register breakpoint (Dr0-Dr3) matches an address in the destination, the expected behavior on bare metal is:

  1. ERMSB copies bytes until it reaches the breakpoint address.
  2. The CPU raises #DB (debug exception) mid-copy.
  3. The OS exception handler fires.

Under EPT control, the order may differ. If the source page is mapped through an EPT entry that causes a violation during the bulk read, the EPT fault may fire first. Or the hypervisor may consume the #DB internally and never deliver it to the guest. The detection exploit: if #DB does not fire when expected, the hypervisor intercepted it. This is not something the hypervisor can easily fix, because the exception delivery order is microarchitectural and varies by implementation.

hvdetecc

Source files: main.cpp, hypervisor_detection.cpp, environment.cpp, benchmark.hpp, interrupt_guard.hpp, os.cpp, upause.hpp

Method: A kernel-mode driver with the broadest test set in this group. It runs at IRQL = 2 (DPC) and performs:

Processor behavior tests (hypervisor_detection.cpp):

TestWhat It Checks
vm.hvFlagSetCPUID.1.ECX hypervisor bit
vm.vmwareIoVMware backdoor I/O port (0x5658)
vm.smiSuppressedSMI counter at I/O port 0xB2 - if count doesn't increment as expected
vm.strEmulFailSTR instruction emulation inconsistencies
vm.sldtEmulFailSLDT instruction emulation inconsistencies
vm.dbSuppressedSuppressed #DB - writes DR0, sets Dr7, triggers INT2, counts deliveries
vm.turboSuppressedWrites to IA32_MISC_ENABLE bit 38 (turbo boost) don't stick
vm.pmcMsrMismatchWritten PMC value not sustained - counter virtualization detected
vm.rdpmcMismatchRDPMC returns wrong value
vm.pmcDeadPMC doesn't increment when enabled
vm.pebsSuppressedPEBS enable bit not retained
vm.xgetbvEmulFailXGETBV with high trashed doesn't mask upper bits
vm.xsetbvLeafEmulFailXSETBV with invalid ECX doesn't fault
vm.smswEmulFailSMSW doesn't match CR0
vm.cpuidEcxSuppressedCPUID leaves 0xD.0 and 0xD.1 identical (ECX ignored)
vm.lbrSuppressedLBR enable bit not retained
vm.btsOsFaultBTS OS bit causes fault
vm.btfSuppressedBTF + Trap Flag interaction fails
vm.ptSuppressedIntel PT trace enable bit discarded
vm.msrDefaultInvalidRDMSR on invalid MSR doesn't #GP
vm.hvMsrsReads to 0x4B564D01 and 0x40000000 don't #GP (VMware/HV MSRs present)
vm.tscMsrEmulFailTSC MSR (0x10) delta >1500 cycles
vm.hiddenClocksMPERF or APERF missing
vm.tscWarpedTSC_ADJUST write causes TSC to regress
vm.eferNxDiscardDisabling NX in EFER, executing NX page - should fault

Environment validation (environment.cpp):

  • Verifies PatchGuard context ran (pg.noPgBoot)
  • Compares loaded module checksums with on-disk images (img.patch.*)
  • Checks driver dispatch tables for hijacked entries (img.dispatchHijacked.*)
  • Verifies HAL dispatch table integrity (hal.hook.*)

Benchmarking (benchmark.hpp):

Measures instruction execution time across multiple clock sources simultaneously: TSC, PMC fixed counters (core, TSC), MPERF, APERF, PPREF, HPET, multi-processor clock, TLB persistence, DRAM/package energy.

Key excerpt:

// SMI count test - hypervisors often suppress SMIs
void test_smi(...) {
    // Configure PMU counter for SMI received
    ia32::pmu::dynamic_set_state(0, ia32::pmu::event_id::smi_received, ...);

    // Cause random number of SMIs via I/O port 0xB2
    size_t count = xstd::make_random(1, 8);
    size_t ecount = read_counter();
    while (count) {
        ia32::write_io(0xB2, 0);
        --count; ++ecount;
    }
    size_t rcount = read_counter();
    detections["vm.smiSuppressed"] = rcount < ecount;
}

Strengths: Broadest test set I looked at: 30+ checks across processor behavior, MSR integrity, debug features, performance monitoring, and memory management. Runs at DPC level for steadier timing. Uses multiple clock sources so TSC virtualization is not the only signal.

Weaknesses: Kernel-mode only. Can trigger PatchGuard (Kernel Patch Protection) on 64-bit Windows. Some tests (NX disable, MOV SS+INT2) are inherently risky and could crash the system if the hypervisor handles them unexpectedly. Heavy - not suited for lightweight scanning.

Dispatch table integrity (environment.cpp - detailed):

hvdetecc's environment validation goes beyond simple presence checks. The dispatch table hijack detection is particularly thorough:

  1. PatchGuard context validation: Locates the PatchGuard context pointer in ntoskrnl.exe's .data section by searching for a known three-pointer suffix (KeBugCheckEx, KeBugCheck2, KiBugCheckDebugBreak). If the context pointer at offset -8 is NULL, PatchGuard never initialized - the kernel's integrity monitor was suppressed.
  2. Module checksum comparison: For every loaded kernel module, reads the on-disk file (via ntpp::read_file), compares the PE optional header checksum. If checksums match, compares the full image hash via ntpp::ci::compare. A mismatch with matching checksums is a strong indicator of in-memory patching.
  3. Dispatch table verification: Enumerates \Driver directory, obtains each driver object, checks every MajorFunction entry. An entry whose PXE (page table index) is neither kernel-space nor the expected shared-user-page index has been redirected - the dispatch pointer lands in an unexpected code region.
  4. HAL dispatch integrity: Validates both HalDispatchTable (0xA8 bytes → 20 entries) and HalPrivateDispatchTable (0x300 bytes → 95 entries). Any entry with a non-canonical or non-kernel-space target is flagged.

The PXE comparison method (checking ia32::mem::px_index(entry) against known kernel and user ranges) is faster than validating individual addresses against module bounds, and it has fewer moving parts. It catches any redirection that leaves kernel space, which covers all EPT-based dispatch table hooks.

Benchmark multi-clock analysis:

hvdetecc's benchmark.hpp doesn't just measure one clock - it measures across seven independent time sources simultaneously:

  • TSC: The primary timestamp counter, most likely to be spoofed
  • MPERF: Maximum core frequency counter, reads actual CPU P-states
  • APERF: Actual core frequency counter, reflects real cycles executed
  • PPERF: Product performance counter, accumulated across all P-states
  • Fixed PMC0 (instructions): Instructions retired, independent of frequency
  • Fixed PMC1 (core cycles): Actual unhalted core cycles
  • Fixed PMC2 (TSC reference): Reference cycles at constant rate
  • HPET: High Precision Event Timer, MMIO-based, hard to virtualize

Any hypervisor that virtualizes TSC but leaves one of these other counters unvirtualized creates a timing discrepancy. The ratio of MPERF:APERF reveals actual P-state residency; if it disagrees with TSC-derived timing, the hypervisor is manipulating TSC. Similarly, HPET vs TSC divergence over long measurement intervals cannot be explained by interrupt jitter.

VMAware

Source files: vmaware.hpp (~14,000 lines header-only), cli.cpp

Method: Cross-platform (Windows, Linux, macOS, ARM) detection library with ~90 techniques organized by platform. Uses a scoring system: each technique contributes 0-100 points to a brand-specific scoreboard. Total score >150 = VM detected. Brands with highest score are returned.

Key techniques:

TechniqueScoreMethod
VM::VMID100CPUID 0x40000000 vendor string match
VM::CPU_BRAND95CPU brand string matching (QEMU, KVM, VirtualBox, etc.)
VM::HYPERVISOR_BIT100CPUID.1.ECX bit 31
VM::HYPERVISOR_STR100CPUID hypervisor string leaf
VM::CPUID_SIGNATURE95APIC ID consistency cross-check (0xB vs 0x1F leaves)
VM::BOCHS_CPU100Bochs CPU ID quirks
VM::KGT_SIGNATURE80Intel KGT (Trusty) signature
VM::TIMER95Software-clock-based timing with cache-line isolation
VM::THREAD_MISMATCH50Cross-core timing discrepancy
VM::SYSTEM_REGISTERS50STR/SLDT/LAR emulation
VM::FIRMWARE100ACPI/FW tables contain VM signatures
VM::DEVICES95PCI device vendor/model matching
VM::MSR100MSR probing (see hvdetecc section for details)
VM::TRAP100Exception handling quirks
VM::DRIVERS100Loaded driver name scanning

The VMAware timer technique:

// VMAware's TIMER technique: uses a software clock instead of RDTSC
// A counter thread increments a volatile counter while a trigger thread
// executes CPUID. The counter rate difference reveals VM exit latency.

auto counter_thread = [&]() {
    // Bind to a non-sibling core
    SetThreadAffinityMask(current_thread, target_affinity);
    while (!state.start_test) {}
    while (!state.test_done) {
        state.counter++;  // software clock
    }
};

auto trigger_thread = [&]() {
    // Measure counter rate during CPUID (causes VM-exit)
    u64 before = counter;
    trigger_vmexit();  // CPUID instruction
    u64 after = counter;
    // Ratio of (after - before) for CPUID vs no-op reveals overhead
    double ratio = (double)(after - before) / baseline;
    return (ratio > 3.5);
};
VMAware's CPUID signature check uses an ABA pattern - it reads the APIC ID three consecutive times with CPUID leaves that should return the same value (0xB, 0x1F, 0xB). If the first and third reads match but the second is different, or if all three are different, the thread migrated between cores during the check - a tell of vCPU scheduling in a VM.

Strengths: Cross-platform, header-only, zero external dependencies. ~90 techniques with fine-grained control. The software-clock approach gets around simple TSC offsetting. The CPUID signature check with an ABA pattern is careful, and the brand identification is unusually detailed.

Weaknesses: Some techniques are platform-restricted (only Windows, only Linux). Scoring threshold (150) is heuristic. The full detection pass can take several seconds. Some file-based techniques (PCI device scan, DMI decode) depend on specific OS paths.

VMAware's brand scoreboard system:

The scoring system is the core of VMAware's design. Rather than a binary "VM or not?" answer, it maintains a per-brand accumulator:

struct brand_scoreboard {
    int VMware      = 0;
    int VirtualBox  = 0;
    int QEMU        = 0;
    int KVM         = 0;
    int HyperV      = 0;
    int Parallels   = 0;
    int Xen         = 0;
    int Bhyve       = 0;
    int QNX         = 0;
    int ACRN        = 0;
    int Bochs       = 0;
    int NVHypervisor = 0;
    int AppleHypervisor = 0;
};

// Each technique adds its score to matching brands
void add_score(const char* technique, int points, std::vector matches) {
    for (auto& b : matches) {
        scoreboard[b] += points;
    }
}

The brand identification is valuable beyond detection: knowing which hypervisor is present informs the appropriate response. Detection of a VMware workstation is different from detection of Hyper-V with VBS - the former may indicate an analysis environment, the latter is the default Windows security configuration.

Cross-platform architecture:

VMAware achieves cross-platform support through compile-time platform detection and runtime feature discovery:

  • Windows: Uses NtQuerySystemInformation, EnumDeviceDrivers, SetupDiGetClassDevs, WMI queries, registry keys (HKLM\HARDWARE\DESCRIPTION\System\BIOS)
  • Linux: Reads /sys/class/dmi/id/, /proc/cpuinfo, /proc/scsi/scsi, cpuid instruction directly
  • macOS: Uses IOKit (IOServiceGetMatchingServices), sysctl, IOPlatformExpertDevice
  • ARM: Detects hypervisor mode via MRS (reading HSTR / HCR), checks for EL2 exception level

The ARM support is noteworthy because most detection tools are x86-only. VMAware's ARM hypervisor checks handle both ARMv7 (Virtualization Extensions) and ARMv8 (EL2 exception level), covering Android emulators and Apple Silicon virtualized environments.

Practical Detection Engineering Guide

Choosing Detection Methods by Threat Model

Not every detection method is appropriate for every scenario. The choice depends on what kind of hypervisor you expect and what privilege level you have:

ScenarioRecommended MethodsAvoid
User-mode integrity scannerDriver scan, SharedPage(0xC5), thread-check timing, CPUID signatureKernel-mode tests, MSR probing (triggers EDR)
Malware analysis VM detectionSynthetic CPUID, TLB pressure, crystal clock, VM driver enumerationSMM probes (requires ring 0)
Kernel-mode EDR / rootkit scannerEPT hook scan, dispatch table integrity, MSR probe, PMU validationNothing - use everything available
Forensic acquisition (physical access)DMA read, SMM verification, firmware attestationGuest-side-only checks
Cloud VM (guest only)Timing analysis, cache contention, RSB probingMSR writes (hypervisor-controlled), SMM (absent in most clouds)

Building a Correlation Engine

Single-signal detection is fragile. If I were building a correlation engine for this, I would keep it boring and use four rules:

  1. Independent probes: Each probe should rely on a different hardware mechanism. Two timing probes using RDTSC are not independent - the hypervisor spoofs one, it spoofs both.
  2. Orthogonal measurement paths: Pair RDTSC-based timing with thread-counter-based timing, TLB profiling with cache contention, CPUID checks with MSR integrity checks.
  3. Baseline calibration: Run probes during low-contention periods and compare against a known-bare-metal baseline for the same CPU model.
  4. Bayesian updating: Each probe result shifts the probability of virtualization. A "high" result from a timing probe might shift from 50% to 70%; a matching "high" from TLB profiling shifts from 70% to 95%. A conflicting "low" from CPUID signature drops it back to 60%.
// Simplified correlation engine
typedef struct { double prob_vm; double weight; } probe_result_t;

probe_result_t probes[] = {
    { timing_probe(),     0.35 },
    { tlb_profile(),      0.25 },
    { cache_contention(), 0.20 },
    { dispatch_integrity(),0.15 },
    { shared_page(),      0.05 }
};

double aggregate = 0.0;
double total_weight = 0.0;
for (int i = 0; i < NUM_PROBES; i++) {
    aggregate += probes[i].prob_vm * probes[i].weight;
    total_weight += probes[i].weight;
}
double final_score = aggregate / total_weight;
bool vm_detected = (final_score > 0.75);  // threshold
The weight assignments above are illustrative. In practice, weights should be determined empirically from labeled datasets (known-VM vs known-bare-metal systems across different hardware configurations). A weight that works well on Intel desktop CPUs may perform poorly on AMD server CPUs. Per-platform calibration is mandatory.

Detection Difficulties & Limitations

False Positives

SignalLegitimate Cause of False Positive
High CPUID latencySMI handling, C-state transitions
TLB pressure >10xLarge working set, TLB thrashing
Suspected EPT hookHyper-V with VBS enabled, security hypervisors
Suspected driver scanSecurity software or EDR that loads its own drivers
Timer anomaliesPower management, frequency scaling

The Security Hypervisor Problem

Windows Virtualization-Based Security (VBS), Hyper-V, and KVM with SEV-SNP are legitimate hypervisors that use the same SLAT mechanisms as rootkits. Detection tools must distinguish:

  • "Is there a hypervisor?" → Easy, but not very useful.
  • "Is there an unexpected hypervisor?" → Much harder.
  • "Is there an EPT hook on this specific page?" → The real detection question.

This is why tools like Bloodhound and ept-hook-detection focus on page-local checks rather than global hypervisor presence detection.

Performance Impact

Detection Method Overhead Comparison
Figure 5: Relative overhead of each detection method on a logarithmic scale. Repeated driver scanning dominates at ~500ms, while simple CPUID checks complete in microseconds.

Kernel Patch Guard (KPP/PatchGuard)

On 64-bit Windows, PatchGuard monitors kernel code, data structures, and MSRs. Detection tools that modify page tables, debug registers, or MSRs risk triggering a bug check (0x109). hvdetecc addresses this by running at DPC level and being careful about state restoration, but the risk is inherent.

AMD NPT-Specific Limitations

AMD's NPT presents unique challenges for both attackers and defenders:

  • No execute-only pages: On most AMD silicon, setting X=1 on an NPT entry implicitly grants read access. This eliminates the clean split-view hook as a stealth option. Attackers must use more detectable methods: transient entry swapping (which produces more faults), or full R/W/X shadow pages (where integrity scanners can read the hook code).
  • #VMEXIT(NPF) granularity: The nested page fault delivers EXITINFO1 which distinguishes between faults during a guest page table walk and faults at the final GPA. A detector that deliberately causes page-table-walk faults and final-GPA faults can measure the hypervisor's handling latency for each case - they should be nearly identical on bare metal but diverge under NPT.
  • ASID management: AMD's ASIDs max out at 65535 on newer silicon, but many hypervisors only allocate a handful. Forcing rapid ASID exhaustion (by rapidly creating and destroying processes with different CR3 values) may reveal the hypervisor through ASID allocation failures that don't occur on bare metal.
  • AVIC (Advanced Virtual Interrupt Controller): AMD's hardware APIC virtualization reduces exits for interrupt handling. A detector can probe for AVIC by configuring x2APIC and measuring interrupt delivery latency - AVIC delivers in <100 cycles vs 500+ cycles through software emulation.

Calibrating Detection Thresholds

Static thresholds (e.g., "ratio > 10x") are fragile across hardware generations. Proper calibration requires:

  1. Per-CPU model baselines: Record probe results on known-bare-metal instances of the target CPU model. Store these as reference distributions.
  2. Runtime calibration: Before running detection probes, measure idle-system noise. Subtract this baseline from subsequent measurements.
  3. Adaptive thresholds: Set thresholds at mean + 5 * stddev of the bare-metal distribution, not a hardcoded number.
  4. Cross-boot consistency: If thresholds were valid during a previous boot on the same hardware, they should still be valid. A system that crosses the threshold since last boot without hardware changes is suspicious.

Intel PT and the Hypervisor Boundary

Intel Processor Trace is both a detection tool and a target of hypervisor evasion. The interaction is worth examining in detail because PT operates at a privilege level that straddles the hypervisor boundary:

How PT Crosses VM Boundaries

When Intel PT is active on the host, the hardware traces everything that executes on that logical core - host code, guest code, and the transitions between them. The VMCS provides controls to manage this:

  • VM_ENTRY_CTLS: VM_ENTRY_LOAD_RTIT_CTL: On VM entry, load IA32_RTIT_CTL from the VMCS. This allows the hypervisor to reconfigure PT for guest execution - disabling it, changing the ToPA, or switching to a guest-specific buffer.
  • VM_EXIT_CTLS: VM_EXIT_LOAD_RTIT_CTL: On VM exit, restore the host's IA32_RTIT_CTL. If this bit is set, the host's tracing configuration is restored before the exit handler runs, so the handler is never traced by the guest configuration.
  • VM_EXIT_CTLS: VM_EXIT_CLEAR_RTIT_CTL: On VM exit, zero out IA32_RTIT_CTL entirely. This guarantees no trace packets are generated after the exit, regardless of configuration.

A hypervisor that leaves PT enabled during guest execution creates a detection surface: the guest can read its own trace data and observe VM-exit boundaries as gaps in the trace. A hypervisor that correctly swaps PT configuration on boundaries removes this signal - but the PT MSRs themselves become a detection target (see hvdetecc's vm.ptSuppressed test).

Using PT as a Guest-Side Detector

If PT is available inside the guest (unlikely in most VM configurations, but possible in nested virtualization or misconfigured VMs), a detector can:

  1. Enable PT with TraceEn=1, User=1, OS=1 on a target logical CPU.
  2. Execute a known sequence of instructions with predictable timing (e.g., 1000 NOPs followed by a CPUID).
  3. Disable PT and decode the trace.
  4. Check for gaps in the instruction stream - a gap after CPUID indicates a VM-exit boundary where PT was suppressed.
  5. Check CYC packet frequency - if cycle counts are missing or compressed around VM-exit points, the hypervisor is suppressing timing packets.

This is a niche technique because PT is rarely available inside guests. In environments where it is exposed, such as KVM with PT passthrough or Xen with PT sharing, it gives a useful instruction-level trace.

Comparison Matrix

Detection MethodCPUIDTimingMSRTLBSMMEPT HookDriver ScanSharedPage
CPUID check✓
Timing analysis✓
MSR probe✓
TLB profile✓
SMM monitor✓✓✓✓✓✓
EPT hook scan✓
Driver scan✓
SharedPage (0xC5)✓
Bloodhound✓
checkhv_um✓✓✓✓
ept-hook-detection✓✓
ermsb-meme✓
hvdetecc✓✓✓✓✓✓
VMAware✓✓✓✓
Detection Technique Matrix
Figure 6: The detection technique matrix - six techniques mapped by stealth difficulty from Low to Impossible.

Windows Detection Stack Architecture

Windows Detection Stack
Figure 7: Layered architecture showing user-mode tools, kernel drivers, the hypervisor layer, and hardware. The SharedPage check (NtQuerySystemInformation 0xC5) bridges user mode and the hypervisor.

Where The Signals Hold Up

The easy signals are architectural: CPUID leaves, synthetic MSRs, shared pages, and obvious vendor strings. Treat those as quick triage, not proof.

Timing and microarchitectural signals are harder to clean up. A hypervisor can hide one counter or one instruction path, but it has a harder time keeping TSC, HPET, PMU counters, TLB pressure, cache contention, and exception ordering all believable at once.

TDX, SEV-SNP, and DRTM change the trust model rather than solving every runtime question. They are still worth tracking because they shrink the room a stealth hypervisor has to lie about memory.

The checks I would test next are plain ones: how stable timing baselines are across laptops, how much VBS noise looks like a malicious hypervisor, and which page-local probes survive a week of normal workstation use without false positives.

The practical question is not whether a single probe can "find the hypervisor." It is whether independent signals disagree with the machine's normal baseline in the same direction.

References & Further Reading

Papers:

  • SubVirt: Implementing malware with virtual machines (King et al., 2006)
  • Blue Pill (Rutkowska, 2006)
  • HyperLeech: DMA-based hypervisor injection (RAID 2020)
  • Detecting Introspection from Guests (DFRWS 2018)

Tools analyzed:

Intel/AMD Manuals:

  • Intel SDM Vol. 3, Chapters 23-29 (VMX, EPT, VPID)
  • AMD APM Vol. 2, Chapters 10-15 (SVM, NPT)

Other:

  • Ophion stealth hypervisor (VT-x, firmware-locked model)
  • Intel KGT (Trusty branch) - github.com/intel/ikgt-core
  • Windows Hyper-V TLFS (Top-Level Functional Specification)
Appendix: Research File Citations

Research documents

  • deep-research-report-GG.txt - Core analysis of EPT/NPT mechanics, split-view hooks, statistical IET analysis, RSB/cache probing, HPC monitoring
  • deep-research-report-GPT.md - Stealth SLAT hook detection, altp2m, VMFUNC, VPID/ASID, TSC compensation
  • deep-research-report-merged.txt - Combined analysis with detection priorities, case studies (SubVirt, Blue Pill, HyperLeech, Ophion), and cross-view comparison

Detection tools

  • Detections/Bloodhound-main/Bloodhound.cpp - VPGATHER-based EPT hook detection with execute/read asymmetry
  • Detections/Bloodhound-main/VPGATHER.cpp - AVX2 VPGATHERQQ address accessibility probe via #DB single-stepping
  • Detections/checkhv_um-master/hv_um.cpp - User-mode multi-heuristic detection (CPUID, crystal clock, TLB pressure, TSC noise, ERMSB)
  • Detections/ept-hook-detection-main/* - Three-pronged EPT hook detection (timing, write, thread-based)
  • Detections/ermsb-meme-main/main.c - REP MOVSB debug register EPT detection
  • Detections/hvdetecc-master/* - 30+ kernel-mode detection tests (SMI counting, MSR integrity, PMU, PEBS, XSAVE, debug features)
  • Detections/VMAware-main/src/vmaware.hpp - 90-technique cross-platform detection library with scoring system
  • Detections/VMAware-main/src/cli.cpp - CLI frontend for VMAware library

Manuals

  • Manuals/Intel Manuals/vol-3.pdf - Intel SDM Vol 3: VMX, EPT, VPID, APICv
  • Manuals/Intel Manuals/vol-4.pdf - Intel SDM Vol 4: MSR descriptions
  • Manuals/Amd Manuals/*.pdf - AMD APM: SVM, NPT, ASID

Supplemental Driver Scan/SharedPage data

  • Repeated driver scanning: 10+ passes, ~4,674 string compares, 11 blacklisted driver names
  • SystemHypervisorSharedPageInformation (NtQuerySystemInformation class 0xC5): returns user-space mapping of hypervisor shared page, or NULL if absent