WMI vs NtQuerySystemInformation: Which Check Surface Actually Matters
Windows has at least five different ways to report whether VBS and HVCI are running. They don't all agree. They don't all query the same source. And they don't all have the same trust level. Understanding which surface checks what -- and which ones I can fake -- is the difference between a convincing spoof and a half-broken illusion.
I spent two days mapping every check surface, tracing the calls through the kernel, and figuring out exactly where each one gets its answer. This post is the result.
Surface 1: NtQuerySystemInformation
The most common programmatic check. Anti-cheats call NtQuerySystemInformation with SystemCodeIntegrityInformation (class 0x67). The kernel returns a SYSTEM_CODEINTEGRITY_INFORMATION structure with a CodeIntegrityOptions field. Bit 0x400 = HVCI enforced.
// how anti-cheats check HVCI
SYSTEM_CODEINTEGRITY_INFORMATION ci = { 0 };
ci.Length = sizeof(ci);
NtQuerySystemInformation(
SystemCodeIntegrityInformation,
&ci, sizeof(ci), NULL
);
if (ci.CodeIntegrityOptions & 0x400) {
// HVCI is "enabled"
}
// under the hood, ntoskrnl does:
// return g_CiOptions; (from CI.dll)
// I control g_CiOptions via NPT shadow
// so this check returns whatever I want
This is the easiest surface to fake. NtQuerySystemInformation reads g_CiOptions, which lives in CI.dll's data section. My NPT shadow intercepts that read and returns a version with the HVCI bits set. Done.
g_CiOptions. Any code path that reads this variable sees my shadow value. This includes NtQuerySystemInformation, CI.dll's own internal checks, and any kernel driver that imports g_CiOptions directly.Surface 2: Registry keys
Several tools check the registry. HVCIEnabled under CI, EnableVirtualizationBasedSecurity under DeviceGuard, and the State subkeys. PowerShell's Get-CimInstance Win32_DeviceGuard reads some of these.
// registry paths that report VBS/HVCI status
// 1. boot-time config (DO NOT MODIFY -- see post 22)
HKLM\...\DeviceGuard\EnableVirtualizationBasedSecurity
// 2. HVCI scenario config
HKLM\...\DeviceGuard\Scenarios\HVCI\Enabled
// 3. runtime state (volatile -- I set these)
HKLM\...\CI\State\HVCIEnabled = 1
// 4. DeviceGuard runtime state (volatile)
HKLM\...\DeviceGuard\State\SecurityServicesRunning = 0x02
// 5. CI options persistent value
HKLM\...\CI\HVCIEnabled
I set the volatile runtime keys (3 and 4) during driver init. The persistent boot keys (1 and 2) I leave alone -- modifying those would launch real Hyper-V on reboot. The volatile keys satisfy the queries that the Windows Security app and most tools make.
Surface 3: CPUID
CPUID leaf 0x40000000 returns the hypervisor vendor string. I return "Microsoft Hv". CPUID leaf 0x40000003 returns feature flags including VBS capabilities. Some software checks these to determine if Hyper-V is present and what features it supports.
// CPUID hypervisor identification
// leaf 0x40000000: hypervisor vendor
EBX:ECX:EDX = "Microsoft Hv"
// leaf 0x40000001: hypervisor interface
EAX = "Hv#1"
// leaf 0x40000003: feature identification
EAX = 0x2E7F // feature flags matching real Hyper-V
EBX = 0x3C09 // implementation recommendations
CPUID interception is the oldest part of my stealth layer. Every CPUID instruction exits to my handler. I control every bit of every return value. This surface is completely fakeable.
Surface 4: WMI Win32_DeviceGuard
This is where it gets interesting. The Windows Security app -- the one with the "Core Isolation" toggle -- queries the Win32_DeviceGuard WMI class. This class is provided by the SecurityHealthService and reads from a combination of sources: registry, NtQuerySystemInformation, and -- critically -- direct Secure Kernel queries.
// Win32_DeviceGuard properties
VirtualizationBasedSecurityStatus = 2 // running
SecurityServicesRunning = [2] // HVCI
SecurityServicesConfigured = [2] // HVCI configured
// some of these fields come from:
// 1. registry keys (I control via volatile keys)
// 2. NtQuerySystemInformation (I control via g_CiOptions)
// 3. Secure Kernel IPC (I CANNOT fake this)
Most of Win32_DeviceGuard's properties I can influence through the registry and g_CiOptions. But some fields involve a call into the Secure Kernel through a secure hypercall. On a real VBS system, the Secure Kernel runs in VTL1 (Virtual Trust Level 1) alongside the normal kernel in VTL0. The WMI provider asks the Secure Kernel to confirm its state.
I don't have a real Secure Kernel. There is no VTL1 on my machine. When the WMI provider tries to make that secure call, it fails silently and falls back to the registry/NtQuery values. This fallback is what makes my spoof work for WMI -- but it's a fragile dependency. If Microsoft ever made the Secure Kernel query mandatory, the spoof would break.
Surface 5: Direct Secure Kernel query
This is the one I can't fake. If software makes a VslpEnterIumSecureMode call -- which transitions to VTL1 and queries the Secure Kernel directly -- there's nothing I can do. The Secure Kernel runs in a hardware-isolated address space. It has its own page tables, its own privilege level, its own view of memory. My NPT shadows don't affect it because VTL1 has a separate set of nested page tables that the hypervisor manages.
// trust levels of each surface
// FULLY SPOOFABLE (I control the data source):
NtQuerySystemInformation // reads g_CiOptions (my shadow)
Registry keys // I set volatile keys
CPUID leaves // I control all return values
// PARTIALLY SPOOFABLE (fallback behavior helps):
WMI Win32_DeviceGuard // uses fallback path
// NOT SPOOFABLE (hardware isolation):
Secure Kernel IPC // no VTL1 exists on my machine
TPM attestation // hardware-bound key
What actually gets checked in practice
I've tested against three anti-cheats and the Windows Security app. Every single one uses either NtQuerySystemInformation or the registry. None of them make direct Secure Kernel queries. None of them do TPM attestation of VBS state.
This makes sense from an engineering perspective. NtQuerySystemInformation is a simple syscall. It works on every version of Windows. It doesn't require special privileges. The Secure Kernel query requires VTL1 to be running, which means VBS must already be enabled -- so if you're querying the Secure Kernel to check if VBS is on, you already know the answer.
The check surfaces form a trust hierarchy:
// trust hierarchy (least to most trustworthy)
// tier 1: user-mode data (spoofable from kernel)
// registry, environment variables, WMI cache
// tier 2: kernel-mode data (spoofable from hypervisor)
g_CiOptions
NtQuerySystemInformation
// tier 3: hypervisor-verified (spoofable from hardware)
Secure Kernel state
// tier 4: hardware-bound (not spoofable without physical access)
TPM attestation
// I operate between tier 2 and tier 3
// I can fake everything in tier 1 and 2
// tier 3 requires a real Secure Kernel (I don't have one)
// tier 4 requires TPM key access (hardware-bound)
The takeaway
Different check surfaces have different trust levels, and the trust level determines whether I can fake them. Right now, everything in the wild checks surfaces I control. If anti-cheats moved to Secure Kernel queries or TPM attestation, the game changes. But that requires VBS to be genuinely enabled on the target machine, which brings its own set of constraints -- mainly that a blue-pill can't coexist with a running Hyper-V without significant nested virtualization work.
For now, the situation is: if VBS isn't really running, I can make it look like it is. If VBS is really running, I have bigger problems than faking registry keys.