hyperdetect.c

November 5, 2018 ยท View on GitHub

//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;

}