//this requires being able to run at kernel mode and assumes you're using MSVC //this also uses an unnamed structure for cr0_t, which is a nonstandard extension of the C language //data structure for cr0 typedef union _cr0_t { struct { uint64_t protection_enable : 1; uint64_t monitor_coprocessor : 1; uint64_t emulation : 1; uint64_t task_switched : 1; uint64_t extension_type : 1; uint64_t numeric_error : 1; uint64_t reserved : 10; uint64_t write_protect : 1; uint64_t reserved_1 : 1; uint64_t alignment_mask : 1; uint64_t reserved_2 : 10; uint64_t not_write_through : 1; uint64_t cache_disable : 1; uint64_t paging : 1; uint64_t reserved_3 : 32; }; uint64_t full; } cr0_t; bool is_lazy_hypervisor_running(void) { //hypervisors like VMWare don't check if the change to cr0.pe is a valid one and just write it to the control register //since cr0.pg will be set, this is an invalid cr0 state and causes a VM entry failure __try { cr0_t cr0; cr0.full = __readcr0(); cr0.numeric_error = !cr0.numeric_error; //force a VM exit by toggling the VMX required numeric error bit; this isn't required for VMWare since it VM exits on cr0.pe cr0.protection_enable = 0; //disable the PE bit __writecr0(cr0.full); //write was ignored, return true since this should have caused a #GP(0) return true; } __except (EXCEPTION_EXECUTE_HANDLER) { //this causes VMWare to close; good hypervisors will inject a #GP(0) } //alternative check: a hypervisor may dislike VMX bits being toggled and inject a #GP(0) when it shouldn't //cr4.vmxe isn't checked as the hypervisor may have an excuse for that by setting CPUID.1:ECX.VMX[bit 5] to 0 and injecting a #UD //they also may ignore the write which must be checked { cr0_t old_cr0; old_cr0.full = __readcr0(); __try { //disable interrupts as we're modifying the state of a control register, which value won't be upheld on context switches _disable(); { cr0_t cr0 = old_cr0; cr0.numeric_error = !cr0.numeric_error; __writecr0(cr0.full); //additionally, check if the hypervisor actually toggles the bit cr0.full = __readcr0(); if (cr0.numeric_error == old_cr0.numeric_error) { //the write did not update cr0.ne _enable(); return true; } } __writecr0(old_cr0.full); } __except (EXCEPTION_EXECUTE_HANDLER) { //only reachable in a virtualized environment _enable(); return true; } _enable(); } //many hypervisors (including ones made by people I know, many projects online, as well as mine up until recently) don't properly handle reserved bits of cr0 and cr4 //this could cause either a VM entry failure or a triple fault (repeated #GP(0)) since the processor can't reset cr4 //a previous version used cr0 to test this; that was incorrect as reserved CR0 bits will just get forced to 0 by the processor __try { __writecr4(__readcr4() | (1 << 23)); //non-existant bit //this point is only reachable if the hypervisor ignores the change return true; } __except (EXCEPTION_EXECUTE_HANDLER) { //if properly handled, a #GP(0) is generated and cr4's value is never changed } //along with realizing my previous mistake, this also opens up a detection vector for hypervisors that inject a #GP(0) upon changing cr0 reserved bits __try { __writecr0(__readcr0() | (1 << 23)); } __except (EXCEPTION_EXECUTE_HANDLER) { return true; //should never cause a fault } //check if cr0's value isn't changed after a #GP(0) { _disable(); cr0_t old_cr0; old_cr0.full = __readcr0(); __try { cr0_t cr0 = old_cr0; cr0.numeric_error = !cr0.numeric_error; //a bit which is guaranteed to cause a VM exit, and which we've verified won't cause a #GP(0) cr0.protection_enable = 0; //a bit which is guaranteed not to cause VM entry failure, and which will cause a #GP(0) cr0.write_protect = !cr0.write_protect; //a bit which won't cause a VM entry failure nor a #GP(0) __writecr0(cr0.full); } __except (EXCEPTION_EXECUTE_HANDLER) { cr0_t cr0; cr0.full = __readcr0(); //if everything was handled correctly, cr0.write_protect will be equal to its old value if (cr0.write_protect != old_cr0.write_protect) { _enable(); __writecr0(old_cr0.full); return true; } } _enable(); } //all checks passed return false; }