Skip to content

Commit 4b99961

Browse files
committed
feat: hardened detections for legitimate Hyper-V instances
1 parent e29fecf commit 4b99961

2 files changed

Lines changed: 97 additions & 16 deletions

File tree

src/cli/output.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ const char* get_vm_description(const std::string& vm_brand) {
191191
{ VM::brands::AZURE_HYPERV, "Azure Hyper-V is Microsoft's cloud-optimized hypervisor variant powering Azure VMs. Implements Azure-specific virtual devices like NVMe Accelerated Networking and vTPMs. Supports nested virtualization for running Hyper-V/containers within Azure VMs, enabling cloud-based CI/CD pipelines and dev/test environments." },
192192
{ VM::brands::SIMPLEVISOR, "SimpleVisor is a minimalist Intel VT-x hypervisor by Alex Ionescu for Windows/Linux research. Demonstrates EPT-based memory isolation and hypercall handling. Used to study VM escapes and hypervisor rootkits, with hooks for intercepting CR3 changes and MSR accesses." },
193193
{ VM::brands::HYPERV_ROOT, "VMAware detected Hyper-V operating as a type 1 hypervisor, not as a guest virtual machine. This prevents false positives, as Windows sometimes runs under Hyper-V." },
194+
{ VM::brands::HYPERV_SPOOF, "VMAware detected another hypervisor impersonating Microsoft Hyper-V." },
194195
{ VM::brands::UML, "User-Mode Linux (UML) allows running Linux kernels as user-space processes using ptrace-based virtualization. Primarily used for kernel debugging and network namespace testing. Offers lightweight isolation without hardware acceleration, but requires host/guest kernel version matching for stable operation." },
195196
{ VM::brands::POWERVM, "IBM PowerVM is a type 1 hypervisor for POWER9/10 systems, supporting Live Partition Mobility and Shared Processor Pools. Implements VIOS (Virtual I/O Server) for storage/networking virtualization, enabling concurrent AIX, IBM i, and Linux workloads with RAS features like predictive failure analysis." },
196197
{ VM::brands::GCE, "Google Compute Engine (GCE) utilizes KVM-based virtualization with custom Titanium security chips for hardware root of trust. Features live migration during host maintenance and shielded VMs with UEFI secure boot. Underpins Google Cloud's Confidential Computing offering using AMD SEV-SNP memory encryption." },

src/vmaware.hpp

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,7 @@ struct VM {
638638
AZURE_HYPERV,
639639
SIMPLEVISOR,
640640
HYPERV_ROOT,
641+
HYPERV_SPOOF,
641642
UML,
642643
POWERVM,
643644
GCE,
@@ -3893,7 +3894,7 @@ struct VM {
38933894
return memo::hyperx::fetch();
38943895
}
38953896

3896-
// Check if hypervisor feature bit in CPUID Leaf 1, ECX bit 31 is enabled
3897+
// check if hypervisor feature bit in CPUID Leaf 1, ECX bit 31 is enabled
38973898
auto is_hyperv_present = []() noexcept -> bool {
38983899
u32 unused, ecx = 0;
38993900
cpu::cpuid(unused, unused, ecx, unused, 1);
@@ -3911,11 +3912,11 @@ struct VM {
39113912
};
39123913

39133914
/**
3914-
* On Hyper-V virtual machines, the cpuid function reports an EAX value of 11
3915-
* This value is tied to the Hyper-V partition model, where each virtual machine runs as a child partition
3916-
* These child partitions have limited privileges and access to hypervisor resources,
3915+
* on Hyper-V virtual machines, the cpuid function reports an EAX value of 11
3916+
* this value is tied to the Hyper-V partition model, where each virtual machine runs as a child partition
3917+
* these child partitions have limited privileges and access to hypervisor resources,
39173918
* which is reflected in the maximum input value for hypervisor CPUID information as 11
3918-
* Essentially, it indicates that the hypervisor is managing the VM and that the VM is not running directly on hardware but rather in a virtualized environment
3919+
* essentially, it indicates that the hypervisor is managing the VM and that the VM is not running directly on hardware but rather in a virtualized environment
39193920
*/
39203921
auto eax = []() noexcept -> u32 {
39213922
u32 eax_reg, unused = 0;
@@ -3925,7 +3926,7 @@ struct VM {
39253926
return eax_reg & 0xFF;
39263927
};
39273928

3928-
// Check whether a hypervisor is nested within a Hyper-V partition
3929+
// check whether a hypervisor is nested within a Hyper-V partition
39293930
auto is_hyperv_nested = []() noexcept -> bool {
39303931
u32 eax = 0, ebx = 0, ecx = 0, edx = 0;
39313932
cpu::cpuid(eax, ebx, ecx, edx, 0x40000004);
@@ -3935,6 +3936,72 @@ struct VM {
39353936
return nested_partition_bit;
39363937
};
39373938

3939+
// check if the Windows Hypervisor Platform interface is responsive and confirms a running hypervisor
3940+
auto is_hyperv_interface_present = []() noexcept -> bool {
3941+
enum WHV_CAPABILITY_CODE {
3942+
WHvCapabilityCodeHypervisorPresent = 0x00000000,
3943+
};
3944+
3945+
typedef HRESULT(__stdcall* whv_get_capability_fn)(
3946+
WHV_CAPABILITY_CODE CapabilityCode,
3947+
VOID* CapabilityBuffer,
3948+
UINT32 CapabilityBufferSize,
3949+
UINT32* WrittenBufferSize
3950+
);
3951+
3952+
HMODULE h_whp = LoadLibraryW(L"WinHvPlatform.dll");
3953+
if (!h_whp) {
3954+
return false;
3955+
}
3956+
3957+
auto whv_get_capability = reinterpret_cast<whv_get_capability_fn>(
3958+
GetProcAddress(h_whp, "WHvGetCapability")
3959+
);
3960+
3961+
bool is_present = false;
3962+
if (whv_get_capability) {
3963+
BOOL present_val = FALSE;
3964+
UINT32 written = 0;
3965+
HRESULT hr = whv_get_capability(
3966+
WHvCapabilityCodeHypervisorPresent,
3967+
&present_val,
3968+
sizeof(present_val),
3969+
&written
3970+
);
3971+
if (SUCCEEDED(hr)) {
3972+
is_present = (present_val == TRUE);
3973+
}
3974+
}
3975+
3976+
FreeLibrary(h_whp);
3977+
return is_present;
3978+
};
3979+
3980+
// check if the host-only virtualization infrastructure driver is present
3981+
auto is_hyperv_service_running = []() noexcept -> bool {
3982+
const wchar_t* service_name = L"vid";
3983+
SC_HANDLE sc_manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
3984+
if (!sc_manager) {
3985+
return false;
3986+
}
3987+
3988+
SC_HANDLE service = OpenServiceW(sc_manager, service_name, SERVICE_QUERY_STATUS);
3989+
if (!service) {
3990+
CloseServiceHandle(sc_manager);
3991+
return false;
3992+
}
3993+
3994+
SERVICE_STATUS status = {};
3995+
bool is_running = false;
3996+
if (QueryServiceStatus(service, &status)) {
3997+
is_running = (status.dwCurrentState == SERVICE_RUNNING);
3998+
}
3999+
4000+
CloseServiceHandle(service);
4001+
CloseServiceHandle(sc_manager);
4002+
return is_running;
4003+
};
4004+
39384005
hyperx_state state = HYPERV_UNKNOWN;
39394006

39404007
if (is_hyperv_nested()) {
@@ -3943,7 +4010,7 @@ struct VM {
39434010
}
39444011
else {
39454012
if (!is_root_partition()) {
3946-
// A QEMU/KVM guest running with Hyper-V enlightenments presents "Microsoft Hv" at
4013+
// a QEMU/KVM guest running with Hyper-V enlightenments presents "Microsoft Hv" at
39474014
// leaf 0x40000000 (so the Windows guest uses the fast Hyper-V ABI) while KVM relocates
39484015
// its own "KVMKVMKVM" signature to leaf 0x40000100. That secondary signature is an
39494016
// unambiguous tell of QEMU/KVM. A guest is not a root partition, so this must be
@@ -3969,7 +4036,7 @@ struct VM {
39694036
}
39704037
}
39714038
else {
3972-
// Windows machine running under Hyper-V type 1
4039+
// windows machine running under Hyper-V type 1
39734040
std::string brand_str = cpu::cpu_manufacturer(cpu::leaf::hypervisor + 0x100);
39744041

39754042
if (util::find(brand_str, "KVM")) {
@@ -3978,7 +4045,7 @@ struct VM {
39784045
state = HYPERV_ENLIGHTENMENT;
39794046
}
39804047
else {
3981-
// If we reach here, we do some sanity checks to ensure a hypervisor is not trying to spoof itself as Hyper-V, attempting to bypass some detections
4048+
// if we reach here, we do some sanity checks to ensure a hypervisor is not trying to spoof itself as Hyper-V, attempting to bypass some detections
39824049
brand_str = cpu::cpu_manufacturer(cpu::leaf::hypervisor);
39834050

39844051
bool is_hyper_v_host = false;
@@ -4007,22 +4074,32 @@ struct VM {
40074074
ULONG_PTR idt_base = 0;
40084075
memcpy(&idt_base, &idtr_buffer[2], sizeof(idt_base));
40094076

4010-
// if running under Hyper-V in AMD64 (doesnt matter the VTL/partition level), this value is hardcoded and intercepted/emulated at kernel level
4077+
// if running under Hyper-V in AMD64 (doesnt matter the VTL/partition level), this value is hardcoded and emulated at kernel level to prevent kernel address leakage
40114078
// specifically at KiPreprocessFault -> KiOpDecode -> KiOpLocateDecodeEntry (KiOp_SLDTSTRSMSW)
4012-
// this is intercepted by the kernel before handling execution to the hypervisor, so it's a decent safeguard against basic cpuid spoofing
40134079
// additionally, brand has to be "Microsoft Hv"
4014-
is_hyper_v_host = idt_base == 0xfffff80000001000 && brand_str == "Microsoft Hv";
4080+
const bool base_hardware_checks = (idt_base == 0xfffff80000001000) && (brand_str == "Microsoft Hv");
40154081
#else
4016-
is_hyper_v_host = brand_str == "Microsoft Hv";
4082+
const bool base_hardware_checks = (brand_str == "Microsoft Hv");
40174083
#endif
40184084

4085+
if (base_hardware_checks) {
4086+
const bool host_drivers_active = is_hyperv_service_running();
4087+
const bool whp_api_active = is_hyperv_interface_present();
4088+
4089+
debug("HYPER-X: Hyper-V service running: ", host_drivers_active);
4090+
debug("HYPER-X: Hyper-V interface running: ", whp_api_active);
4091+
4092+
is_hyper_v_host = host_drivers_active && whp_api_active;
4093+
}
4094+
40194095
if (is_hyper_v_host) {
40204096
debug("HYPER-X: Detected Hyper-V host machine");
40214097
core::add(brand_enum::HYPERV_ROOT);
40224098
state = HYPERV_HOST;
40234099
}
40244100
else {
40254101
debug("HYPER-X: Detected hypervisor trying to spoof itself as Hyper-V");
4102+
core::add(brand_enum::HYPERV_SPOOF, 150);
40264103
state = HYPERV_SPOOFED;
40274104
}
40284105
}
@@ -4621,6 +4698,7 @@ struct VM {
46214698
static constexpr const char* AZURE_HYPERV = "Microsoft Azure Hyper-V";
46224699
static constexpr const char* SIMPLEVISOR = "SimpleVisor";
46234700
static constexpr const char* HYPERV_ROOT = "Hyper-V root partition (host system)";
4701+
static constexpr const char* HYPERV_SPOOF = "Impersonated Microsoft Hyper-V";
46244702
static constexpr const char* UML = "User-mode Linux";
46254703
static constexpr const char* POWERVM = "IBM PowerVM";
46264704
static constexpr const char* GCE = "Google Compute Engine (KVM)";
@@ -4641,7 +4719,7 @@ struct VM {
46414719
static constexpr const char* NEKO_PROJECT = "Neko Project II";
46424720
static constexpr const char* NOIRVISOR = "NoirVisor";
46434721
static constexpr const char* QIHOO = "Qihoo 360 Sandbox";
4644-
static constexpr const char* DBVM = "DBVM";
4722+
static constexpr const char* DBVM = "Dark Byte's VM";
46454723
static constexpr const char* UTM = "UTM";
46464724
static constexpr const char* COMPAQ = "Compaq FX!32";
46474725
static constexpr const char* INSIGNIA = "Insignia RealPC";
@@ -4704,7 +4782,7 @@ struct VM {
47044782

47054783
// if filtering emptied the vector, fall back to NULL_BRAND
47064784
if (active_brands.empty()) {
4707-
active_brands.emplace_back(brand_enum::NULL_BRAND, 1);
4785+
active_brands.emplace_back(brand_enum::NULL_BRAND, 0);
47084786
}
47094787

47104788
// capture initial hit presence
@@ -4861,6 +4939,7 @@ struct VM {
48614939
case brand_enum::AZURE_HYPERV: return VM::brands::AZURE_HYPERV;
48624940
case brand_enum::SIMPLEVISOR: return VM::brands::SIMPLEVISOR;
48634941
case brand_enum::HYPERV_ROOT: return VM::brands::HYPERV_ROOT;
4942+
case brand_enum::HYPERV_SPOOF: return VM::brands::HYPERV_SPOOF;
48644943
case brand_enum::UML: return VM::brands::UML;
48654944
case brand_enum::POWERVM: return VM::brands::POWERVM;
48664945
case brand_enum::GCE: return VM::brands::GCE;
@@ -13970,7 +14049,8 @@ struct VM {
1397014049
case brand_enum::BAREVISOR: return "Hypervisor (type 1)";
1397114050
case brand_enum::HYPERPLATFORM: return "Hypervisor (type 1)";
1397214051
case brand_enum::MINIVISOR: return "Hypervisor (type 1)";
13973-
case brand_enum::HYPERV_ROOT: return "Host machine"; // This refers to the type 1 hypervisor where Windows normally runs under, we put "Host machine" to clarify you're not running under a traditional VM if this is detected
14052+
case brand_enum::HYPERV_ROOT: return "Host machine"; // this refers to the type 1 hypervisor where Windows normally runs under, we put "Host machine" to clarify you're not running under a traditional VM if this is detected
14053+
case brand_enum::HYPERV_SPOOF: return "Unknown"; // this refers to any hypervisor trying to disguise itself as a legitimate Hyper-V instance
1397414054
case brand_enum::NULL_BRAND: return "Unknown";
1397514055
case brand_enum::INVALID: return "Invalid";
1397614056
}

0 commit comments

Comments
 (0)