CPUID Masking: Becoming Microsoft Hyper-V
When Windows boots and detects a hypervisor, the first thing it does is check CPUID leaf 0x40000000 for the vendor string. If it sees "Microsoft Hv", it enables a whole stack of Hyper-V enlightenments -- synthetic timers, hypercall page, MSR-based APIC access. If it sees anything else, or nothing at all, it either panics or falls back to a slower code path that might expose timing inconsistencies.
I chose to impersonate Hyper-V because Windows expects it. It's not suspicious -- it's normal. Every Windows 10/11 machine with VBS enabled runs under Hyper-V. By presenting the right CPUID leaves, I make my hypervisor look like the one Microsoft ships. Anti-cheat software that checks "is a hypervisor running?" sees Hyper-V and moves on.
The hypervisor present bit
Before any vendor string check, the very first signal is CPUID leaf 1, ECX bit 31. This is the "hypervisor present" bit. On bare metal, it's 0. Under any hypervisor, it should be 1. I have two modes:
In stealth mode, I clear bit 31 -- pretending no hypervisor exists at all. This works against simple detectors that just check this one bit. But it breaks Windows' Hyper-V enlightenments, which means synthetic timers don't work, and the kernel falls back to legacy APIC timer code.
In Hyper-V impersonation mode (what I actually use), I set bit 31 and provide the full Hyper-V CPUID interface. Windows enables all its optimizations, and everything looks exactly like a normal VBS-enabled system.
Leaf 0x40000000 -- vendor identity
This is the most important leaf. The guest reads it to identify which hypervisor is running. I return "Microsoft Hv" encoded as three 32-bit register values, plus the maximum supported hypervisor leaf in EAX.
// CPUID leaf 0x40000000 -- hypervisor vendor identity
fn handle_cpuid_40000000(regs: &mut GuestRegs) {
regs.rax = 0x40000006;
regs.rbx = 0x7263694D; // 'Micr'
regs.rcx = 0x666F736F; // 'osof'
regs.rdx = 0x76482074; // 't Hv'
}
Windows concatenates EBX+ECX+EDX to get the 12-byte vendor string "Microsoft Hv". If this doesn't match exactly, Windows won't enable Hyper-V enlightenments and falls back to generic hypervisor handling.
Leaves 0x40000001 through 0x40000006
Once Windows identifies the hypervisor as Hyper-V, it queries additional leaves for version info, feature flags, and hardware capabilities. I have to be careful here -- returning too many features triggers code paths that expect working implementations behind them. Returning too few looks suspicious compared to real Hyper-V.
// The full Hyper-V CPUID interface
fn handle_hyperv_cpuid(leaf: u32, regs: &mut GuestRegs) {
match leaf {
// Leaf 1: Hyper-V interface identification
0x40000001 => {
regs.rax = 0x31237648; // 'Hv#1' signature
}
// Leaf 2: Hyper-V version
0x40000002 => {
regs.rax = 0x00003839; // build number
regs.rbx = 0x000A0000; // version 10.0
regs.rcx = 0;
regs.rdx = 0;
}
// Leaf 3: Feature flags -- THE critical one
0x40000003 => {
regs.rax = 0x00002E7F; // partition privileges
regs.rbx = 0x00002C09; // recommended features
regs.rcx = 0; // power management: none
regs.rdx = 0x000000BF; // misc features
}
// Leaf 4: Implementation limits
0x40000004 => {
regs.rax = 0x00000040; // max virtual processors
regs.rbx = 0x00000040; // max logical processors
}
// Leaf 5: Implementation recommendations (zeroed)
0x40000005 => {}
// Leaf 6: Hardware features exposed
0x40000006 => {
regs.rax = 0x00000002; // AMD-V support
}
_ => {} // leaves beyond 0x40000006: return zeros
}
}
Leaf 3 -- the feature flags minefield
Leaf 3 is where I spent the most time. The EAX register contains "partition privilege" flags -- each bit enables a specific Hyper-V feature. If I set a bit, Windows will try to use that feature. If my hypervisor doesn't actually implement it, things crash.
I use a conservative set: AccessVpRunTimeReg, AccessPartitionReferenceCounter, AccessSynicRegs, AccessSyntheticTimerRegs, AccessApicRegs, AccessHypercallMsrs, and AccessVpIndex. These are the flags that a real Hyper-V root partition advertises. I skip things like AccessGuestDebugging, AccessReenlightenmentControls, and AccessFrequencyRegs because they require functionality I haven't implemented.
Why Hyper-V specifically
I could impersonate any hypervisor -- VMware ("VMwareVMware"), KVM ("KVMKVMKVM\0\0\0"), Xen ("XenVMMXenVMM"). But Hyper-V has unique advantages:
- Windows has built-in Hyper-V support -- hundreds of kernel code paths check for and optimize against it
- VBS (Virtualization-Based Security) runs on Hyper-V -- if an anti-cheat checks "is VBS enabled?", seeing Hyper-V makes the answer yes
- On AMD systems, Hyper-V actually uses AMD-V (SVM) under the hood -- so my CPUID responses about the hardware are truthful
- Hyper-V is the default hypervisor on Windows 11 -- its presence isn't suspicious, it's expected
If I impersonated VMware, that would be suspicious on a desktop machine. If I impersonated KVM, Windows wouldn't know how to use it. Hyper-V is the Goldilocks choice -- normal enough to not raise flags, well-supported enough that Windows runs optimally under it.
The other CPUID leaves
Beyond the hypervisor range, I also patch a few standard CPUID leaves. Leaf 0x1 ECX bit 31 (hypervisor present) gets set. Leaf 0x8000001F (AMD SEV capabilities) gets zeroed -- my system doesn't have SEV, but some detectors check for inconsistencies between SEV flags and hypervisor presence. Leaf 0x80000008 EAX bits [23:16] (GuestPhysAddrSize) get zeroed -- when NPT is active, this field becomes non-zero, which is a trivial detection vector.
Every other CPUID leaf passes through to the real hardware. I only intercept what I need to modify. The fewer exits I cause, the less timing overhead I introduce, and the harder I am to detect through statistical timing analysis.