Skip to content

Commit 30990e0

Browse files
Add VM guest services and display adapter detection
Lift detection methodologies from vmaware — check for VM-specific artifacts that only exist inside guest VMs, never on bare metal: Guest services (registry key existence under Services\): - Hyper-V: vmicheartbeat, vmicshutdown, vmickvpexchange, etc. - VMware: vmci, vmhgfs, vmxnet3, VMTools - VirtualBox: VBoxGuest, VBoxSF, VBoxMouse, VBoxVideo - KVM/virtio: vioscsi, viostor, netkvm, vioinput, balloon - QEMU: QEMU-GA guest agent - Parallels: prl_strg, prl_tg, prl_eth - Xen: xenevtchn, xenvbd, xennet, xenvif Display adapter (GPU class registry DriverDesc): - Microsoft Hyper-V Video, VMware SVGA, VirtualBox Graphics, Red Hat QXL, virtio GPU, Citrix Indirect Display, etc. These checks run after BIOS/manufacturer strings but before CPUID, giving them priority as concrete guest artifacts that cannot be present on physical hardware. Also exposed in VmDiagnostics struct for consumer-side diagnostic logging.
1 parent 1842663 commit 30990e0

1 file changed

Lines changed: 179 additions & 71 deletions

File tree

crates/enclaveapp-windows/src/dpapi_fallback.rs

Lines changed: 179 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ pub struct VmDiagnostics {
3636
pub cpuid_hypervisor_vendor: Option<String>,
3737
/// Whether the Hyper-V guest integration services registry key exists.
3838
pub hyperv_guest_integration: bool,
39+
/// VM guest services found in the Windows service registry.
40+
pub guest_services_found: Vec<String>,
41+
/// Display adapter description from the GPU class registry.
42+
pub display_adapter: Option<String>,
3943
/// Architecture of the running process.
4044
pub arch: &'static str,
4145
}
@@ -79,6 +83,8 @@ pub fn collect_vm_diagnostics() -> VmDiagnostics {
7983
registry_values: vec![],
8084
cpuid_hypervisor_vendor: None,
8185
hyperv_guest_integration: false,
86+
guest_services_found: vec![],
87+
display_adapter: None,
8288
arch: std::env::consts::ARCH,
8389
}
8490
}
@@ -174,8 +180,9 @@ fn collect_vm_diagnostics_windows() -> VmDiagnostics {
174180
.collect();
175181

176182
let hyperv_guest = hyperv_guest_parameters_exist();
177-
178183
let cpuid_vendor = cpuid_hypervisor_vendor();
184+
let guest_services = detect_guest_services();
185+
let display_adapter = detect_display_adapter();
179186

180187
// Build the joined string from identity-relevant registry values for vm_string_signal.
181188
// Use only the first 9 values (manufacturer/product/bios strings, not disk/processor).
@@ -191,118 +198,202 @@ fn collect_vm_diagnostics_windows() -> VmDiagnostics {
191198
.find(|(label, _)| label == "Disk\\Enum\\0")
192199
.and_then(|(_, v)| v.as_deref());
193200

201+
let make_result = |vm_detected: bool, detection_reason: String| VmDiagnostics {
202+
vm_detected,
203+
detection_reason,
204+
registry_values: registry_values.clone(),
205+
cpuid_hypervisor_vendor: cpuid_vendor.clone(),
206+
hyperv_guest_integration: hyperv_guest,
207+
guest_services_found: guest_services.clone(),
208+
display_adapter: display_adapter.clone(),
209+
arch: std::env::consts::ARCH,
210+
};
211+
194212
// --- Detection logic ---
195213

196214
// 1. Registry identity strings (manufacturer, product, BIOS, baseboard)
197215
if vm_string_signal(&identity_joined) {
198-
return VmDiagnostics {
199-
vm_detected: true,
200-
detection_reason: format!("registry VM marker: {identity_joined}"),
201-
registry_values,
202-
cpuid_hypervisor_vendor: cpuid_vendor,
203-
hyperv_guest_integration: hyperv_guest,
204-
arch: std::env::consts::ARCH,
205-
};
216+
return make_result(true, format!("registry VM marker: {identity_joined}"));
206217
}
207218

208219
// 2. Disk device name (virtual disk controllers)
209220
if let Some(disk) = disk_device {
210221
if vm_string_signal(disk) {
211-
return VmDiagnostics {
212-
vm_detected: true,
213-
detection_reason: format!("virtual disk device: {disk}"),
214-
registry_values,
215-
cpuid_hypervisor_vendor: cpuid_vendor,
216-
hyperv_guest_integration: hyperv_guest,
217-
arch: std::env::consts::ARCH,
218-
};
222+
return make_result(true, format!("virtual disk device: {disk}"));
223+
}
224+
}
225+
226+
// 3. VM guest services present (vmicheartbeat, VBoxGuest, vmci, etc.)
227+
if !guest_services.is_empty() {
228+
let svc_list = guest_services.join(", ");
229+
return make_result(true, format!("VM guest services installed: {svc_list}"));
230+
}
231+
232+
// 4. Virtual display adapter
233+
if let Some(ref adapter) = display_adapter {
234+
if vm_display_signal(adapter) {
235+
return make_result(true, format!("virtual display adapter: {adapter}"));
219236
}
220237
}
221238

222-
// 3. CPUID hypervisor vendor
239+
// 5. CPUID hypervisor vendor
223240
if let Some(ref vendor) = cpuid_vendor {
224241
if vm_string_signal(vendor) && !vendor.eq_ignore_ascii_case("Microsoft Hv") {
225-
return VmDiagnostics {
226-
vm_detected: true,
227-
detection_reason: format!("CPUID hypervisor vendor: {vendor}"),
228-
registry_values,
229-
cpuid_hypervisor_vendor: cpuid_vendor,
230-
hyperv_guest_integration: hyperv_guest,
231-
arch: std::env::consts::ARCH,
232-
};
242+
return make_result(true, format!("CPUID hypervisor vendor: {vendor}"));
233243
}
234244
// "Microsoft Hv" — reported by both VBS on physical hardware and Hyper-V guests.
235245
if vendor.eq_ignore_ascii_case("Microsoft Hv") {
236-
// 4. Microsoft Hv + "Microsoft Corporation" manufacturer = Hyper-V guest
246+
// 6. Microsoft Hv + "Microsoft Corporation" manufacturer = Hyper-V guest
237247
if identity_joined
238248
.to_ascii_lowercase()
239249
.contains("microsoft corporation")
240250
{
241-
return VmDiagnostics {
242-
vm_detected: true,
243-
detection_reason: format!(
251+
return make_result(
252+
true,
253+
format!(
244254
"Hyper-V guest: Microsoft Hv CPUID + Microsoft Corporation manufacturer ({identity_joined})"
245255
),
246-
registry_values,
247-
cpuid_hypervisor_vendor: cpuid_vendor,
248-
hyperv_guest_integration: hyperv_guest,
249-
arch: std::env::consts::ARCH,
250-
};
256+
);
251257
}
252-
// 5. Microsoft Hv + Hyper-V guest integration services = VDI on Hyper-V
253-
// (catches CyberArk, Citrix, etc. with non-standard manufacturer)
258+
// 7. Microsoft Hv + Hyper-V guest integration services = VDI on Hyper-V
254259
if hyperv_guest {
255-
return VmDiagnostics {
256-
vm_detected: true,
257-
detection_reason: format!(
260+
return make_result(
261+
true,
262+
format!(
258263
"Hyper-V VDI: Microsoft Hv CPUID + guest integration services present ({identity_joined})"
259264
),
260-
registry_values,
261-
cpuid_hypervisor_vendor: cpuid_vendor,
262-
hyperv_guest_integration: hyperv_guest,
263-
arch: std::env::consts::ARCH,
264-
};
265+
);
265266
}
266-
return VmDiagnostics {
267-
vm_detected: false,
268-
detection_reason: format!(
267+
return make_result(
268+
false,
269+
format!(
269270
"hypervisor bit set without VM indicators: {vendor} (manufacturer: {identity_joined})"
270271
),
271-
registry_values,
272-
cpuid_hypervisor_vendor: cpuid_vendor,
273-
hyperv_guest_integration: hyperv_guest,
274-
arch: std::env::consts::ARCH,
275-
};
272+
);
276273
}
277274
}
278275

279-
// 6. No CPUID hypervisor, but check Hyper-V guest integration (ARM64 path)
276+
// 8. No CPUID hypervisor, but check Hyper-V guest integration (ARM64 path)
280277
if hyperv_guest {
281-
return VmDiagnostics {
282-
vm_detected: true,
283-
detection_reason: format!(
278+
return make_result(
279+
true,
280+
format!(
284281
"Hyper-V guest integration services present without CPUID hypervisor ({identity_joined})"
285282
),
286-
registry_values,
287-
cpuid_hypervisor_vendor: cpuid_vendor,
288-
hyperv_guest_integration: hyperv_guest,
289-
arch: std::env::consts::ARCH,
290-
};
283+
);
291284
}
292285

293-
VmDiagnostics {
294-
vm_detected: false,
295-
detection_reason: if identity_joined.is_empty() {
296-
"no VM registry markers, no CPUID hypervisor vendor, no Hyper-V guest integration"
297-
.into()
286+
make_result(
287+
false,
288+
if identity_joined.is_empty() {
289+
"no VM indicators detected (no registry markers, no CPUID hypervisor, no guest services, no virtual display)".into()
298290
} else {
299291
format!("no VM indicators detected: {identity_joined}")
300292
},
301-
registry_values,
302-
cpuid_hypervisor_vendor: cpuid_vendor,
303-
hyperv_guest_integration: hyperv_guest,
304-
arch: std::env::consts::ARCH,
293+
)
294+
}
295+
296+
/// Check for known VM guest service drivers in the Windows service registry.
297+
/// These services are only installed by hypervisor guest tools — never on bare metal.
298+
#[cfg(target_os = "windows")]
299+
fn detect_guest_services() -> Vec<String> {
300+
use windows::Win32::System::Registry::{RegCloseKey, RegOpenKeyExW, HKEY, KEY_READ};
301+
302+
const VM_SERVICES: &[(&str, &str)] = &[
303+
// Hyper-V guest integration
304+
("vmicheartbeat", "Hyper-V Heartbeat"),
305+
("vmicshutdown", "Hyper-V Shutdown"),
306+
("vmickvpexchange", "Hyper-V KVP Exchange"),
307+
("vmicguestinterface", "Hyper-V Guest Service Interface"),
308+
("vmicvss", "Hyper-V VSS"),
309+
("vmictimesync", "Hyper-V Time Sync"),
310+
// VMware Tools
311+
("vmci", "VMware VMCI"),
312+
("vmhgfs", "VMware Host-Guest Filesystem"),
313+
("vmxnet", "VMware vmxnet"),
314+
("vmxnet3", "VMware vmxnet3"),
315+
("vmvss", "VMware VSS"),
316+
("VMTools", "VMware Tools"),
317+
// VirtualBox Guest Additions
318+
("VBoxGuest", "VirtualBox Guest"),
319+
("VBoxSF", "VirtualBox Shared Folders"),
320+
("VBoxMouse", "VirtualBox Mouse"),
321+
("VBoxVideo", "VirtualBox Video"),
322+
// KVM/virtio (Red Hat)
323+
("vioscsi", "virtio SCSI"),
324+
("viostor", "virtio Storage"),
325+
("netkvm", "virtio Network"),
326+
("vioinput", "virtio Input"),
327+
("vioser", "virtio Serial"),
328+
("balloon", "virtio Balloon"),
329+
// QEMU Guest Agent
330+
("QEMU-GA", "QEMU Guest Agent"),
331+
("qemu-ga", "QEMU Guest Agent"),
332+
// Parallels
333+
("prl_strg", "Parallels Storage"),
334+
("prl_tg", "Parallels Tools Gate"),
335+
("prl_eth", "Parallels Network"),
336+
// Xen
337+
("xenevtchn", "Xen Event Channel"),
338+
("xenvbd", "Xen Block Device"),
339+
("xennet", "Xen Network"),
340+
("xenvif", "Xen Virtual Interface"),
341+
];
342+
343+
let mut found = Vec::new();
344+
for (svc_name, label) in VM_SERVICES {
345+
let subkey = format!("SYSTEM\\CurrentControlSet\\Services\\{svc_name}");
346+
let subkey_wide = wide_null(&subkey);
347+
let mut hkey = HKEY::default();
348+
// SAFETY: Standard registry probe — open for read and immediately close.
349+
let status = unsafe {
350+
RegOpenKeyExW(
351+
HKEY_LOCAL_MACHINE,
352+
windows::core::PCWSTR(subkey_wide.as_ptr()),
353+
Some(0),
354+
KEY_READ,
355+
&mut hkey,
356+
)
357+
};
358+
if status.is_ok() {
359+
unsafe {
360+
let _ = RegCloseKey(hkey);
361+
}
362+
found.push(format!("{svc_name} ({label})"));
363+
}
305364
}
365+
found
366+
}
367+
368+
/// Read the primary display adapter description from the GPU class registry.
369+
#[cfg(target_os = "windows")]
370+
fn detect_display_adapter() -> Option<String> {
371+
// The display adapter class GUID is {4d36e968-e325-11ce-bfc1-08002be10318}.
372+
// Subkey \0000 is the primary adapter.
373+
registry_string(
374+
"SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\0000",
375+
"DriverDesc",
376+
)
377+
}
378+
379+
/// Check if a display adapter description indicates a virtual GPU.
380+
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
381+
fn vm_display_signal(adapter: &str) -> bool {
382+
let lower = adapter.to_ascii_lowercase();
383+
[
384+
"microsoft hyper-v video",
385+
"vmware svga",
386+
"vmware soda",
387+
"virtualbox graphics",
388+
"red hat qxl",
389+
"virtio gpu",
390+
"citrix indirect display",
391+
"parallels display",
392+
"qxl",
393+
"xen display",
394+
]
395+
.iter()
396+
.any(|needle| lower.contains(needle))
306397
}
307398

308399
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
@@ -526,6 +617,23 @@ mod tests {
526617
assert!(!vm_string_signal("Seagate Barracuda"));
527618
}
528619

620+
#[test]
621+
fn vm_display_signal_detects_virtual_adapters() {
622+
assert!(vm_display_signal("Microsoft Hyper-V Video"));
623+
assert!(vm_display_signal("VMware SVGA 3D"));
624+
assert!(vm_display_signal("VirtualBox Graphics Adapter"));
625+
assert!(vm_display_signal("Red Hat QXL controller"));
626+
assert!(vm_display_signal("Citrix Indirect Display Adapter"));
627+
}
628+
629+
#[test]
630+
fn vm_display_signal_rejects_real_gpus() {
631+
assert!(!vm_display_signal("NVIDIA GeForce RTX 4090"));
632+
assert!(!vm_display_signal("AMD Radeon RX 7900 XTX"));
633+
assert!(!vm_display_signal("Intel UHD Graphics 770"));
634+
assert!(!vm_display_signal("Intel Iris Xe Graphics"));
635+
}
636+
529637
#[test]
530638
fn collect_vm_diagnostics_returns_valid_struct() {
531639
let diag = collect_vm_diagnostics();

0 commit comments

Comments
 (0)