Skip to main content

vmaware/techniques/
cross_platform.rs

1//! Cross-platform VM detection techniques.
2//! These work on Windows, Linux, and macOS (where applicable).
3
4use crate::engine::TechniqueResult;
5use crate::cpu;
6use crate::brands;
7use crate::util;
8
9/// Check CPUID output of manufacturer ID for known VMs/hypervisors
10/// at leaf 0 and 0x40000000-0x40000100.
11pub fn vmid() -> TechniqueResult {
12    #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
13    {
14        return TechniqueResult::not_detected();
15    }
16
17    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
18    {
19        // Check standard hypervisor leaves
20        let leaves_to_check: Vec<u32> = {
21            let mut v = vec![0u32];
22            for leaf in (0x4000_0000u32..=0x4000_0100).step_by(0x100) {
23                v.push(leaf);
24            }
25            v
26        };
27
28        for &leaf_id in &leaves_to_check {
29            if leaf_id != 0 && !cpu::is_leaf_supported(leaf_id) {
30                continue;
31            }
32
33            let brand_str = cpu::cpu_manufacturer(leaf_id);
34            if brand_str.is_empty() {
35                continue;
36            }
37
38            // Check known VM brand strings
39            if brand_str == "Microsoft Hv" {
40                return TechniqueResult::detected_with_brands(brands::HYPERV, brands::VPC);
41            }
42            if brand_str.contains("KVM") {
43                return TechniqueResult::detected_with_brand(brands::KVM);
44            }
45
46            let brand_map: &[(&str, &str)] = &[
47                ("VMwareVMware", brands::VMWARE),
48                ("VBoxVBoxVBox", brands::VBOX),
49                ("TCGTCGTCGTCG", brands::QEMU),
50                ("XenVMMXenVMM", brands::XEN),
51                ("Linux KVM Hv", brands::KVM_HYPERV),
52                (" prl hyperv ", brands::PARALLELS),
53                (" lrpepyh  vr", brands::PARALLELS),
54                ("bhyve bhyve ", brands::BHYVE),
55                ("BHyVE BHyVE ", brands::BHYVE),
56                ("ACRNACRNACRN", brands::ACRN),
57                (" QNXQVMBSQG ", brands::QNX),
58                ("___ NVMM ___", brands::NVMM),
59                ("OpenBSDVMM58", brands::BSD_VMM),
60                ("HAXMHAXMHAXM", brands::INTEL_HAXM),
61                ("UnisysSpar64", brands::UNISYS),
62                ("SRESRESRESRE", brands::LMHS),
63                ("EVMMEVMMEVMM", brands::INTEL_KGT),
64                ("LKVMLKVMLKVM", brands::LKVM),
65                ("Neko Project", brands::NEKO_PROJECT),
66                ("NoirVisor ZT", brands::NOIRVISOR),
67            ];
68
69            for &(pattern, brand) in brand_map {
70                if brand_str == pattern {
71                    return TechniqueResult::detected_with_brand(brand);
72                }
73            }
74
75            if brand_str.contains("QXNQSBMV") {
76                return TechniqueResult::detected_with_brand(brands::QNX);
77            }
78            if brand_str.contains("Apple VZ") {
79                return TechniqueResult::detected_with_brand(brands::APPLE_VZ);
80            }
81        }
82
83        TechniqueResult::not_detected()
84    }
85}
86
87/// Check if CPU brand model contains any VM-specific string snippets.
88pub fn cpu_brand() -> TechniqueResult {
89    let brand = cpu::get_brand();
90    if brand == "Unknown" || brand.is_empty() {
91        return TechniqueResult::not_detected();
92    }
93
94    let lower = brand.to_lowercase();
95
96    let vm_strings: &[(&str, &str)] = &[
97        ("qemu", brands::QEMU),
98        ("virtual", brands::NULL_BRAND),
99        ("vmware", brands::VMWARE),
100        ("virtualbox", brands::VBOX),
101        ("vbox", brands::VBOX),
102        ("kvm", brands::KVM),
103        ("xen", brands::XEN),
104        ("bochs", brands::BOCHS),
105        ("parallels", brands::PARALLELS),
106        ("bhyve", brands::BHYVE),
107        ("hyperv", brands::HYPERV),
108    ];
109
110    for &(pattern, brand) in vm_strings {
111        if lower.contains(pattern) {
112            if brand == brands::NULL_BRAND {
113                return TechniqueResult::detected();
114            }
115            return TechniqueResult::detected_with_brand(brand);
116        }
117    }
118
119    TechniqueResult::not_detected()
120}
121
122/// Check if hypervisor feature bit in CPUID ECX bit 31 is enabled.
123/// Always false for physical CPUs.
124pub fn hypervisor_bit() -> TechniqueResult {
125    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
126    {
127        let r = cpu::cpuid(1, 0);
128        let hv_bit = (r.ecx >> 31) & 1;
129        if hv_bit == 1 {
130            return TechniqueResult::detected();
131        }
132    }
133    TechniqueResult::not_detected()
134}
135
136/// Check for hypervisor brand string length.
137/// On physical machines the string is typically very short or empty.
138pub fn hypervisor_str() -> TechniqueResult {
139    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
140    {
141        if !cpu::is_leaf_supported(cpu::leaf::HYPERVISOR) {
142            return TechniqueResult::not_detected();
143        }
144
145        let brand = cpu::cpu_manufacturer(cpu::leaf::HYPERVISOR);
146        if brand.len() > 2 && !brand.trim().is_empty() {
147            return TechniqueResult::detected();
148        }
149    }
150    TechniqueResult::not_detected()
151}
152
153/// Check for timing anomalies (RDTSC-based).
154pub fn timer() -> TechniqueResult {
155    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
156    {
157        // Measure time for CPUID calls — VMs typically take much longer
158        let iterations = 10u32;
159        let mut total: u64 = 0;
160
161        for _ in 0..iterations {
162            let start: u64;
163            let end: u64;
164
165            #[cfg(target_arch = "x86_64")]
166            unsafe {
167                start = std::arch::x86_64::_rdtsc();
168                // Execute a CPUID (serializing instruction)
169                let _ = std::arch::x86_64::__cpuid(0);
170                end = std::arch::x86_64::_rdtsc();
171            }
172
173            #[cfg(target_arch = "x86")]
174            unsafe {
175                start = std::arch::x86::_rdtsc();
176                let _ = std::arch::x86::__cpuid(0);
177                end = std::arch::x86::_rdtsc();
178            }
179
180            total += end.wrapping_sub(start);
181        }
182
183        let avg = total / iterations as u64;
184        // Threshold: bare metal typically < 500 cycles, VMs often > 1000
185        if avg > 1000 {
186            return TechniqueResult::detected();
187        }
188    }
189    TechniqueResult::not_detected()
190}
191
192/// Check if thread count is suspiciously low (1-2 threads).
193pub fn thread_count() -> TechniqueResult {
194    let count = util::thread_count();
195    if count <= 2 {
196        return TechniqueResult::detected();
197    }
198    TechniqueResult::not_detected()
199}
200
201/// Check if thread count mismatches the expected count for the CPU model.
202pub fn thread_mismatch() -> TechniqueResult {
203    let brand = cpu::get_brand();
204    let count = util::thread_count();
205
206    if brand == "Unknown" || brand.is_empty() {
207        return TechniqueResult::not_detected();
208    }
209
210    // Extract expected thread count from known CPU model patterns
211    // Ryzen 9 -> typically 16-32 threads, if only 2-4 that's suspicious
212    let lower = brand.to_lowercase();
213
214    let expected_min = if lower.contains("ryzen 9") || lower.contains("i9-") || lower.contains("xeon") {
215        8
216    } else if lower.contains("ryzen 7") || lower.contains("i7-") {
217        6
218    } else if lower.contains("ryzen 5") || lower.contains("i5-") {
219        4
220    } else {
221        return TechniqueResult::not_detected();
222    };
223
224    if count < expected_min {
225        return TechniqueResult::detected();
226    }
227
228    TechniqueResult::not_detected()
229}
230
231/// Check for signatures in CPUID leaf 0x40000001.
232pub fn cpuid_signature() -> TechniqueResult {
233    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
234    {
235        if !cpu::is_leaf_supported(0x4000_0001) {
236            return TechniqueResult::not_detected();
237        }
238
239        let r = cpu::cpuid(0x4000_0001, 0);
240        // Non-zero EAX on this leaf typically indicates a hypervisor
241        if r.eax != 0 {
242            return TechniqueResult::detected();
243        }
244    }
245    TechniqueResult::not_detected()
246}
247
248/// Check for various Bochs-related emulation oversights through CPU checks.
249pub fn bochs_cpu() -> TechniqueResult {
250    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
251    {
252        if !cpu::is_leaf_supported(cpu::leaf::PROC_EXT) {
253            return TechniqueResult::not_detected();
254        }
255
256        let r = cpu::cpuid(cpu::leaf::PROC_EXT, 0);
257
258        // Bochs has known quirks: AMD Easter Egg leaf returns specific values
259        if cpu::is_leaf_supported(cpu::leaf::AMD_EASTER_EGG) {
260            let easter = cpu::cpuid(cpu::leaf::AMD_EASTER_EGG, 0);
261            // Check for "IT'S HAMMER TIME" string (Bochs signature)
262            if easter.ecx == 0x4d414821 {
263                return TechniqueResult::detected_with_brand(brands::BOCHS);
264            }
265        }
266
267        // Another Bochs quirk: extended CPUID features inconsistency
268        // If AMD features are reported on an Intel CPU, it's likely Bochs
269        if cpu::is_intel() {
270            let amd_features = r.ecx & (1 << 6); // SSE4a (AMD-only)
271            if amd_features != 0 {
272                return TechniqueResult::detected_with_brand(brands::BOCHS);
273            }
274        }
275    }
276    TechniqueResult::not_detected()
277}
278
279/// Check for Intel KGT (Trusty branch) hypervisor signature in CPUID.
280pub fn kgt_signature() -> TechniqueResult {
281    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
282    {
283        if !cpu::is_leaf_supported(cpu::leaf::HYPERVISOR) {
284            return TechniqueResult::not_detected();
285        }
286
287        let brand = cpu::cpu_manufacturer(cpu::leaf::HYPERVISOR);
288        if brand == "EVMMEVMMEVMM" {
289            return TechniqueResult::detected_with_brand(brands::INTEL_KGT);
290        }
291    }
292    TechniqueResult::not_detected()
293}
294
295/// Check for VM signatures on firmware tables (Linux + Windows).
296#[cfg(any(target_os = "linux", target_os = "windows"))]
297pub fn firmware() -> TechniqueResult {
298    #[cfg(target_os = "linux")]
299    {
300        // Check ACPI tables
301        let acpi_paths = [
302            "/sys/firmware/acpi/tables/DSDT",
303            "/sys/firmware/acpi/tables/SLIC",
304            "/sys/firmware/acpi/tables/MSDM",
305        ];
306
307        for path in &acpi_paths {
308            if let Ok(data) = std::fs::read(path) {
309                let s = String::from_utf8_lossy(&data).to_lowercase();
310                if s.contains("vmware") {
311                    return TechniqueResult::detected_with_brand(brands::VMWARE);
312                }
313                if s.contains("vbox") || s.contains("virtualbox") {
314                    return TechniqueResult::detected_with_brand(brands::VBOX);
315                }
316                if s.contains("qemu") {
317                    return TechniqueResult::detected_with_brand(brands::QEMU);
318                }
319                if s.contains("hyper-v") || s.contains("microsoft") {
320                    return TechniqueResult::detected_with_brand(brands::HYPERV);
321                }
322            }
323        }
324
325        // Check DMI/SMBIOS
326        if let Some(vendor) = util::linux::read_dmi_field("sys_vendor") {
327            let v = vendor.to_lowercase();
328            if v.contains("vmware") { return TechniqueResult::detected_with_brand(brands::VMWARE); }
329            if v.contains("qemu") { return TechniqueResult::detected_with_brand(brands::QEMU); }
330            if v.contains("virtualbox") || v.contains("innotek") { return TechniqueResult::detected_with_brand(brands::VBOX); }
331            if v.contains("microsoft") { return TechniqueResult::detected_with_brand(brands::HYPERV); }
332            if v.contains("xen") { return TechniqueResult::detected_with_brand(brands::XEN); }
333            if v.contains("parallels") { return TechniqueResult::detected_with_brand(brands::PARALLELS); }
334        }
335    }
336
337    #[cfg(target_os = "windows")]
338    {
339        // Check firmware tables via registry
340        if let Some(bios_vendor) = util::win::read_registry_string(
341            "HKLM",
342            r"HARDWARE\DESCRIPTION\System\BIOS",
343            "SystemManufacturer",
344        ) {
345            let v = bios_vendor.to_lowercase();
346            if v.contains("vmware") { return TechniqueResult::detected_with_brand(brands::VMWARE); }
347            if v.contains("qemu") { return TechniqueResult::detected_with_brand(brands::QEMU); }
348            if v.contains("virtualbox") || v.contains("innotek") { return TechniqueResult::detected_with_brand(brands::VBOX); }
349            if v.contains("microsoft") { return TechniqueResult::detected_with_brand(brands::HYPERV); }
350            if v.contains("xen") { return TechniqueResult::detected_with_brand(brands::XEN); }
351            if v.contains("parallels") { return TechniqueResult::detected_with_brand(brands::PARALLELS); }
352        }
353    }
354
355    TechniqueResult::not_detected()
356}
357
358/// Check for PCI vendor and device IDs that are VM-specific.
359#[cfg(any(target_os = "linux", target_os = "windows"))]
360pub fn pci_devices() -> TechniqueResult {
361    #[cfg(target_os = "linux")]
362    {
363        // Known VM PCI vendor IDs
364        let vm_pci_vendors: &[(&str, &str)] = &[
365            ("15ad", brands::VMWARE),   // VMware
366            ("80ee", brands::VBOX),     // VirtualBox
367            ("1af4", brands::QEMU),     // Red Hat / virtio (QEMU/KVM)
368            ("1414", brands::HYPERV),   // Microsoft Hyper-V
369            ("5853", brands::XEN),      // Xen / Citrix
370            ("1ab8", brands::PARALLELS),// Parallels
371        ];
372
373        if let Ok(entries) = std::fs::read_dir("/sys/bus/pci/devices") {
374            for entry in entries.flatten() {
375                let vendor_path = format!("{}/vendor", entry.path().display());
376                if let Ok(vendor) = std::fs::read_to_string(&vendor_path) {
377                    let vendor = vendor.trim().trim_start_matches("0x").to_lowercase();
378                    for &(vid, brand) in vm_pci_vendors {
379                        if vendor == vid {
380                            return TechniqueResult::detected_with_brand(brand);
381                        }
382                    }
383                }
384            }
385        }
386    }
387
388    #[cfg(target_os = "windows")]
389    {
390        // Check for VM-specific PCI devices via registry
391        let subkeys = util::win::enum_registry_subkeys(
392            "HKLM",
393            r"SYSTEM\CurrentControlSet\Enum\PCI",
394        );
395        for key in &subkeys {
396            let lower = key.to_lowercase();
397            if lower.contains("ven_15ad") { return TechniqueResult::detected_with_brand(brands::VMWARE); }
398            if lower.contains("ven_80ee") { return TechniqueResult::detected_with_brand(brands::VBOX); }
399            if lower.contains("ven_1af4") { return TechniqueResult::detected_with_brand(brands::QEMU); }
400            if lower.contains("ven_1414") { return TechniqueResult::detected_with_brand(brands::HYPERV); }
401            if lower.contains("ven_5853") { return TechniqueResult::detected_with_brand(brands::XEN); }
402        }
403    }
404
405    TechniqueResult::not_detected()
406}
407
408/// Check system registers / task segment for anomalies.
409#[cfg(any(target_os = "linux", target_os = "windows"))]
410pub fn system_registers() -> TechniqueResult {
411    // This is a simplified version — the full C++ version uses inline assembly
412    // which is not portable in safe Rust. We use CPUID-based heuristics instead.
413    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
414    {
415        // Check if CPUID indicates a hypervisor is present
416        let r = cpu::cpuid(1, 0);
417        let hv_present = (r.ecx >> 31) & 1 == 1;
418
419        if hv_present {
420            // Double-check with timing — STR instruction behaves differently in VMs
421            // We approximate this with CPUID timing
422            let start: u64;
423            let end: u64;
424
425            #[cfg(target_arch = "x86_64")]
426            unsafe {
427                start = std::arch::x86_64::_rdtsc();
428                let _ = std::arch::x86_64::__cpuid(0x4000_0000);
429                end = std::arch::x86_64::_rdtsc();
430            }
431            #[cfg(target_arch = "x86")]
432            unsafe {
433                start = std::arch::x86::_rdtsc();
434                let _ = std::arch::x86::__cpuid(0x4000_0000);
435                end = std::arch::x86::_rdtsc();
436            }
437
438            let elapsed = end.wrapping_sub(start);
439            if elapsed > 500 {
440                return TechniqueResult::detected();
441            }
442        }
443    }
444    TechniqueResult::not_detected()
445}
446
447/// Check for Azure hostname patterns.
448#[cfg(any(target_os = "linux", target_os = "windows"))]
449pub fn azure() -> TechniqueResult {
450    if let Some(hostname) = util::get_hostname() {
451        // Azure VM hostnames often follow patterns like "AZ-xxxxx" or contain "azure"
452        let lower = hostname.to_lowercase();
453        if lower.contains("azure") || lower.starts_with("az-") {
454            return TechniqueResult::detected_with_brand(brands::AZURE_HYPERV);
455        }
456    }
457    TechniqueResult::not_detected()
458}