[Hypervisor Part 1] What a Hypervisor Actually Does (And Why Your Ring-0 Code Should Care)
There’s a layer below your kernel. It’s been there since Windows 8 in any meaningful sense and since Windows 10 on most enterprise deployments. It doesn’t appear in WinDbg sessions, doesn’t show up in driver stacks, and the only time most kernel developers think about it is when HVCI stops something from working. But if you’re writing detection tooling, analysing rootkits, or trying to understand why certain categories of kernel exploitation stopped working on modern Windows — you need a working model of what’s actually happening below ntoskrnl.exe.
This is the first post in a three-part series on hypervisor internals and EPT-based hooking. It’s deliberately introductory. If you already know what a VM-exit is and what the VMCS looks like, skip to Part 2 where we hijack Hyper-V’s exit handler. If not, read on.
Hardware virtualisation: VMX root and VMX non-root
Intel added hardware virtualisation support (VT-x) in 2005. AMD followed shortly after with AMD-V. Both add the same fundamental thing: a new CPU operating mode specifically for running guest operating systems with hardware-enforced isolation.
VT-x defines two modes:
VMX root operation — where the hypervisor (Virtual Machine Monitor, VMM) runs. Has access to VMCS (Virtual Machine Control Structure) and special instructions (VMREAD, VMWRITE, VMLAUNCH, VMRESUME, VMPTRLD, etc.).
VMX non-root operation — where the guest runs. This is where Windows’s kernel lives. Ring-0 code in the guest still executes as ring-0 — it has full kernel privilege within the guest’s view — but a subset of instructions and conditions cause the CPU to automatically stop the guest and call the hypervisor’s exit handler.
The critical point: ring-0 in a guest is not the same as ring-0 on bare metal. The guest’s kernel executes most code at native speed, but it cannot prevent the hypervisor from intercepting certain operations. HVCI exploits this.
The VM-exit: when the guest loses control
A VM-exit is the hardware mechanism by which the CPU leaves VMX non-root mode and transfers control to the hypervisor. When a VM-exit fires:
- The CPU atomically saves the guest’s complete register state into the VMCS (instruction pointer, registers, stack pointer, control registers, segment descriptors — everything)
- Execution switches to VMX root mode
- The CPU jumps to the address stored in the VMCS’s
VM-exit handlerfield — the hypervisor’s exit handler - The hypervisor inspects the exit reason and guest state, does whatever it needs to, and either modifies guest state or falls through
- The hypervisor executes
VMRESUME(orVMLAUNCHfor first entry) to resume the guest from where it stopped
The guest doesn’t see any of this. From its perspective, certain instructions appear to execute normally (because the hypervisor emulates a result and resumes), or they complete slightly more slowly. The round-trip to VMX root mode and back typically takes a few hundred nanoseconds — measureable with RDTSC, but rarely significant.
What triggers a VM-exit
The VMM configures which operations exit using fields in the VMCS (specifically the VM-execution control fields). Common exit triggers:
| Exit reason | Default in Hyper-V | Notes |
|---|---|---|
CPUID | Always exits | Unconditional — cannot be suppressed |
| EPT violation | Always exits | Guest accessed unmapped/protected guest-physical page |
RDMSR / WRMSR | Configurable per MSR bitmap | Exits on specific MSRs |
MOV CR3 | Can be enabled | Fires on process switches |
VMCALL | Always exits | Explicit guest→VMM hypercall instruction |
RDTSC | Often disabled | Enabled to prevent timing-based VM detection |
HLT | Enabled | VP scheduling |
CPUID is the one that matters for this series. CPUID exits unconditionally — in VMX non-root mode, the hardware always stops the guest and calls the VMM, regardless of VMCS configuration. This makes it a universal, stable communication channel from guest to hypervisor. No kernel privilege required; user-mode code can fire a CPUID and reach the hypervisor.
In assembly:
; Guest fires a hypercall by executing CPUID with a packed sentinel value in EAX/RCX
; The CPU immediately exits to the hypervisor
; Inputs in RCX, RDX, R8, R9 are visible to the hypervisor via VMCS guest registers
mov eax, 0 ; CPUID leaf (ignored by our handler, leaf check skipped)
cpuid ; → VM-exit to hypervisor
; hypervisor has now run and we're back; RAX contains the return valueThe hypervisor identifies the CPUID exit by reading the exit reason from the VMCS (VMREAD VM_EXIT_REASON), checks our authentication keys in RCX (more on this in Part 2), and handles or falls through.
Windows runs under Hyper-V — always
On any Windows 11 machine (and most Windows 10 enterprise installs), Hyper-V is active. The Windows NT kernel runs in VMX non-root mode as Hyper-V’s “root partition”. WSL2, Windows Sandbox, Credential Guard, HVCI — all depend on this. Hyper-V is not optional; it’s a foundational layer of the modern Windows security model.
You can detect this at any privilege level:
; CPUID leaf 0x40000000 is the hypervisor identification leaf
; If a hypervisor is running, it intercepts this exit and writes its signature
mov eax, 40000000h
cpuid
; On Hyper-V: EBX:ECX:EDX contains the string "Microsoft Hv"
; On bare metal (no hypervisor): returns 0 or garbage from hardware// C version
int regs[4];
__cpuid(regs, 0x40000000);
// regs[1] = 'rciM', regs[2] = 'foso', regs[3] = 'vH t'
// together: "Microsoft Hv" in little-endian dword layout
The Hyper-V CPUID interface is documented: leaf 0x40000001 returns supported features, 0x40000003 returns implementation-specific details. These are the same leaves that utilities like coreinfo.exe and cpuid query to detect virtualisation.
HVCI: how the hypervisor enforces code integrity
HVCI (Hypervisor-Protected Code Integrity) uses the hypervisor’s control over EPT (Extended Page Tables) to enforce kernel code signing in a way that the kernel itself cannot bypass, even if fully compromised.
The mechanism:
- All kernel-mode page table modifications pass through the hypervisor (EPT grants write access only to approved page table entries)
- Before any kernel page is marked executable, the hypervisor verifies the content is signed code
- Even with arbitrary kernel write (e.g., via BYOVD), you cannot mark your shellcode executable — the hypervisor intercepts the page table write and refuses
This is why unsigned kernel shellcode injection stopped working on HVCI-enabled systems, why you can’t clear CR0.WP to patch kernel text, and why the interesting exploitation work moved below the hypervisor level. If the hypervisor enforces the constraint, you have to subvert the hypervisor — not the kernel.
The attack surface no one patches
Hyper-V is large, complex, ships in every Windows installation, and runs with more privilege than anything else on the machine. Its attack surface includes:
- The VM-exit handler that processes every guest exit from every virtual processor
- The VMCS management code
- The hypercall interface (Hyper-V exposes
VMCALL-based hypercalls of its own) - The scheduler, memory manager, and device emulation paths
EPTraitor targets the VM-exit handler directly. If we can install a detour in that function before the system is fully locked down, we intercept every VM-exit and operate below any guest-side security mechanism.
Part 2 covers how to find the VM-exit handler, install the detour from UEFI boot space, and validate the CPUID-based hypercall ABI. Part 3 covers EPT shadow page hooks — invisible kernel function interception using the same SLAT machinery that HVCI depends on.