Skip to main content

vmaware/techniques/
cross.rs

1//! Cross-platform CPUID-based VM detection techniques.
2//!
3//! Mirrors the cross-platform section of vmaware_core.c:
4//!   vmid, cpu_brand, hypervisor_bit, hypervisor_str, bochs_cpu,
5//!   timer, thread_mismatch, cpuid_signature, kgt_signature.
6
7use crate::core::add_brand_score;
8use crate::cpu;
9use crate::memo;
10use crate::types::VMBrand;
11use crate::util;
12
13// ── vmid ──────────────────────────────────────────────────────────────────────
14
15/// Check CPUID hypervisor-vendor leaf (0x40000000..0x40000010) against known
16/// VM signatures.
17pub fn vmid() -> bool {
18    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
19    return false;
20
21    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
22    {
23        // Probe leaves 0x40000000 through 0x40000010
24        for leaf in (0x4000_0000u32..=0x4000_0010).step_by(0x10) {
25            let (found, brand) = cpu::vmid_template(leaf);
26            if found {
27                add_brand_score(brand, 0);
28                return true;
29            }
30        }
31        false
32    }
33}
34
35// ── cpu_brand ─────────────────────────────────────────────────────────────────
36
37/// Check for VM-related keywords in the CPU brand string.
38pub fn cpu_brand() -> bool {
39    // Retrieve or compute the CPU brand string
40    let brand = match memo::get_cpu_brand() {
41        Some(b) => b,
42        None => {
43            let b = cpu::cpu_brand_string();
44            memo::set_cpu_brand(b.clone());
45            b
46        }
47    };
48
49    if brand.is_empty() {
50        return false;
51    }
52
53    static KEYWORDS: &[(&str, VMBrand)] = &[
54        ("QEMU",         VMBrand::QEMU),
55        ("KVM",          VMBrand::KVM),
56        ("Virtual CPU",  VMBrand::QEMUKVM),
57        ("VMware",       VMBrand::VMware),
58        ("VirtualBox",   VMBrand::VBox),
59        ("Hyper-V",      VMBrand::HyperV),
60        ("BOCHS",        VMBrand::Bochs),
61        ("Xen",          VMBrand::Xen),
62        ("bhyve",        VMBrand::Bhyve),
63        ("ACRN",         VMBrand::ACRN),
64        ("GenuineIntel", VMBrand::Invalid),  // Intel host – not a VM indicator
65        ("AuthenticAMD", VMBrand::Invalid),  // AMD host  – not a VM indicator
66    ];
67
68    let brand_lc = brand.to_lowercase();
69    for &(kw, vm_brand) in KEYWORDS {
70        if brand_lc.contains(&kw.to_lowercase()) {
71            if vm_brand != VMBrand::Invalid {
72                add_brand_score(vm_brand, 0);
73                return true;
74            }
75        }
76    }
77    false
78}
79
80// ── hypervisor_bit ────────────────────────────────────────────────────────────
81
82/// Check the hypervisor present bit in CPUID leaf 1 ECX (bit 31).
83///
84/// **Root-partition guard**: on a Windows host that runs Hyper-V, this bit is
85/// ALSO set (the root partition itself runs virtualised).  We check
86/// `hyper_x() == Enlightenment` and return false so we don't award 100 VM
87/// points to a bare-metal machine that simply has Hyper-V enabled.
88pub fn hypervisor_bit() -> bool {
89    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
90    return false;
91
92    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
93    {
94        use crate::types::HyperXState;
95        // Root partition: Hyper-V sets the hypervisor bit even on the host OS.
96        if crate::util::hyper_x() == HyperXState::Enlightenment {
97            return false;
98        }
99        let ecx = cpu::cpuid(1, 0).ecx;
100        (ecx >> 31) & 1 != 0
101    }
102}
103
104// ── hypervisor_str ────────────────────────────────────────────────────────────
105
106/// Check the hypervisor vendor string at leaf 0x40000000 for known brands.
107///
108/// **Root-partition guard**: the Hyper-V root partition also exposes
109/// `"Microsoft Hv"` at this leaf, so we skip on `Enlightenment`.
110pub fn hypervisor_str() -> bool {
111    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
112    return false;
113
114    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
115    {
116        use crate::types::HyperXState;
117        if crate::util::hyper_x() == HyperXState::Enlightenment {
118            return false;
119        }
120
121        if !cpu::is_leaf_supported(0x4000_0000) {
122            return false;
123        }
124        let r = cpu::cpuid(0x4000_0000, 0);
125        let vendor = cpu::vendor_string(r.ebx, r.ecx, r.edx);
126
127        // A non-empty, non-CPU-manufacturer string indicates a hypervisor
128        let is_cpu_vendor = vendor.starts_with("GenuineIntel")
129            || vendor.starts_with("AuthenticAMD")
130            || vendor.starts_with("HygonGenuine");
131
132        if !vendor.trim_matches('\0').is_empty() && !is_cpu_vendor {
133            let (found, brand) = cpu::vmid_template(0x4000_0000);
134            if found {
135                add_brand_score(brand, 0);
136            }
137            return true;
138        }
139        false
140    }
141}
142
143// ── bochs_cpu ─────────────────────────────────────────────────────────────────
144
145/// Detect Bochs by checking reserved CPUID fields that Bochs sets to 0.
146pub fn bochs_cpu() -> bool {
147    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
148    return false;
149
150    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
151    {
152        if !cpu::is_intel() {
153            return false;
154        }
155
156        // CPUID leaf 1: on real Intel CPUs bits 31..22 of ECX are reserved 0.
157        // Bochs leaves them as non-zero (typically mirrors EAX).
158        // Also check brand string quirk.
159        let brand = match memo::get_cpu_brand() {
160            Some(b) => b,
161            None => {
162                let b = cpu::cpu_brand_string();
163                memo::set_cpu_brand(b.clone());
164                b
165            }
166        };
167
168        if brand.contains("BOCHSCPU") || brand.to_lowercase().contains("bochs") {
169            add_brand_score(VMBrand::Bochs, 0);
170            return true;
171        }
172
173        // Check stepping: Bochs uses family=6 model=2 stepping=3 (06_02_03)
174        let steps = cpu::fetch_steppings();
175        if steps.family == 6 && steps.model == 2 && steps.extmodel == 0 {
176            // Ambiguous – only count if brand string also looks artificial
177            // (real Pentium Pro was 06_01, not 06_02 with step 3)
178            if brand.is_empty() {
179                add_brand_score(VMBrand::Bochs, 0);
180                return true;
181            }
182        }
183
184        false
185    }
186}
187
188// ── timer ─────────────────────────────────────────────────────────────────────
189
190/// Measure CPU cycle overhead of a CPUID call using RDTSC; unusually high
191/// values indicate a hypervisor translating RDTSC.
192pub fn timer() -> bool {
193    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
194    return false;
195
196    #[cfg(target_arch = "x86_64")]
197    {
198        use std::arch::x86_64::{_mm_lfence, _rdtsc};
199
200        const ITERATIONS: u64 = 10;
201        const THRESHOLD_CYCLES: u64 = 750;
202
203        let mut total: u64 = 0;
204        unsafe {
205            for _ in 0..ITERATIONS {
206                _mm_lfence();
207                let t1 = _rdtsc();
208                // Force a CPUID call (serialising instruction)
209                std::arch::x86_64::__cpuid(0);
210                _mm_lfence();
211                let t2 = _rdtsc();
212                total = total.saturating_add(t2.wrapping_sub(t1));
213            }
214        }
215        let avg = total / ITERATIONS;
216        avg > THRESHOLD_CYCLES
217    }
218
219    #[cfg(target_arch = "x86")]
220    {
221        // 32-bit: use asm to read RDTSC
222        false // stub – platform-specific asm omitted for brevity
223    }
224}
225
226// ── thread_mismatch ───────────────────────────────────────────────────────────
227
228/// Compare the logical-thread count reported by CPUID with the actual OS
229/// thread count. A mismatch can indicate that the hypervisor exposes fewer
230/// CPUs than the underlying host.
231pub fn thread_mismatch() -> bool {
232    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
233    return false;
234
235    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
236    {
237        let brand = match memo::get_cpu_brand() {
238            Some(b) => b,
239            None => {
240                let b = cpu::cpu_brand_string();
241                memo::set_cpu_brand(b.clone());
242                b
243            }
244        };
245
246        // Look up expected thread count from the database
247        let expected = match cpu::lookup_expected_threads(&brand) {
248            Some(n) => n,
249            None => return false, // Unknown model – skip
250        };
251
252        let actual = util::get_logical_cpu_count();
253
254        // Mismatch: hypervisor is presenting fewer hardware threads
255        expected != actual && actual < expected
256    }
257}
258
259// ── cpuid_signature ───────────────────────────────────────────────────────────
260
261/// Check for known VM CPUID signatures at leaves 0x1 and 0x40000000.
262pub fn cpuid_signature() -> bool {
263    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
264    return false;
265
266    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
267    {
268        // Leaf 0x1 EAX upper nibble: some VMs report a distinctive family/model
269        let eax1 = cpu::cpuid(1, 0).eax;
270        let family = (eax1 >> 8) & 0xF;
271        let model  = (eax1 >> 4) & 0xF;
272
273        // Check for VMWARE SVGA model=0x01 family=0x06 (unusual EAX signature)
274        // or VirtualBox's EAX==0x000306A9 pattern etc.
275
276        // Also probe some known leaf signatures
277        static SIG_TABLE: &[(u32, u32, VMBrand)] = &[
278            // (leaf, expected_eax_mask, brand) – simplified check
279            (0x4000_0000, 0x4000_0001, VMBrand::VMware),
280        ];
281
282        let _ = SIG_TABLE;
283
284        // More reliably: check leaf 0x40000010 (VMware TSC frequency leaf)
285        if cpu::is_leaf_supported(0x4000_0010) {
286            let r = cpu::cpuid(0x4000_0010, 0);
287            if r.eax != 0 {
288                // VMware-specific: EAX = virtual TSC frequency in kHz
289                add_brand_score(VMBrand::VMware, 0);
290                return true;
291            }
292        }
293
294        // Check for KVM signature leaf
295        if cpu::is_leaf_supported(0x4000_0001) {
296            let r = cpu::cpuid(0x4000_0001, 0);
297            // KVM: EAX has feature bits; a non-zero EAX with KVMKVMKVM signature
298            // at 0x40000000 is already caught by vmid(), but cross-check here
299            let base_vendor = {
300                let r0 = cpu::cpuid(0x4000_0000, 0);
301                cpu::vendor_string(r0.ebx, r0.ecx, r0.edx)
302            };
303            if base_vendor.contains("KVMKVMKVM") {
304                let kvm_features = r.eax;
305                if kvm_features != 0 {
306                    add_brand_score(VMBrand::KVM, 0);
307                    return true;
308                }
309            }
310        }
311
312        // Check CPUID leaf 0x1 ECX bits that are typically unset on bare metal
313        // but may be set in VMs: bit 5 (VMX), bit 31 (Hypervisor)
314        let _ecx1 = cpu::cpuid(1, 0).ecx;
315        // bit 31 is hypervisor present – already checked by hypervisor_bit
316        // bit 5 is VMX – not a reliable VM indicator on its own
317
318        // Family 15 + model 0: may indicate Bochs or old VM
319        if family == 15 && model == 0 {
320            add_brand_score(VMBrand::Bochs, 0);
321            return true;
322        }
323
324        false
325    }
326}
327
328// ── kgt_signature ─────────────────────────────────────────────────────────────
329
330/// Detect Intel KGT (Trusty) by its CPUID leaf 0x40000001 EAX signature.
331pub fn kgt_signature() -> bool {
332    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
333    return false;
334
335    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
336    {
337        if !cpu::is_leaf_supported(0x4000_0001) {
338            return false;
339        }
340        let eax = cpu::cpuid(0x4000_0001, 0).eax;
341        // KGT signature: "TKIM" = 0x4D494B54
342        if eax == 0x4D49_4B54 {
343            add_brand_score(VMBrand::IntelKGT, 0);
344            return true;
345        }
346        false
347    }
348}
349
350// ── thread_count (Linux + macOS) ──────────────────────────────────────────────
351
352/// Compare OS-reported thread count with CPUID topology.
353#[cfg(any(target_os = "linux", target_os = "macos"))]
354pub fn thread_count() -> bool {
355    let os_threads = util::get_logical_cpu_count();
356
357    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
358    {
359        if cpu::is_leaf_supported(0x0B) {
360            let r = cpu::cpuid(0x0B, 1); // level 1 = core level
361            let cpuid_threads = r.ebx & 0xFFFF;
362            if cpuid_threads > 0 && cpuid_threads != os_threads {
363                return true;
364            }
365        }
366    }
367
368    // Fallback: a single-thread environment is suspicious
369    os_threads <= 1
370}
371
372// ── Linux + Windows shared ────────────────────────────────────────────────────
373
374/// Check for Azure Hyper-V specific registry / SMBIOS markers.
375#[cfg(any(windows, target_os = "linux"))]
376pub fn azure() -> bool {
377    #[cfg(windows)]
378    {
379        crate::techniques::win::azure()
380    }
381    #[cfg(not(windows))]
382    {
383        // Linux: check SMBIOS chassis manufacturer for "Microsoft Corporation"
384        let mfr = util::read_file("/sys/class/dmi/id/chassis_vendor")
385            .unwrap_or_default();
386        let host = util::read_file("/etc/hostname")
387            .unwrap_or_default();
388        if mfr.contains("Microsoft") && host.to_lowercase().contains("azure") {
389            add_brand_score(VMBrand::AzureHyperV, 0);
390            return true;
391        }
392        false
393    }
394}
395
396/// Check system_registers for hypervisor clues (Linux: MSR via file).
397#[cfg(any(windows, target_os = "linux"))]
398pub fn system_registers() -> bool {
399    #[cfg(windows)]
400    {
401        crate::techniques::win::system_registers()
402    }
403    #[cfg(not(windows))]
404    {
405        false
406    }
407}
408
409/// Firmware string scan.
410#[cfg(any(windows, target_os = "linux"))]
411pub fn firmware() -> bool {
412    #[cfg(windows)]
413    {
414        crate::techniques::win::firmware()
415    }
416    #[cfg(not(windows))]
417    {
418        // Linux: scan /sys/class/dmi/id/* for VM strings
419        static PATHS: &[(&str, &[(&str, VMBrand)])] = &[
420            ("/sys/class/dmi/id/bios_vendor", &[
421                ("SeaBIOS",   VMBrand::QEMU),
422                ("VBOX",      VMBrand::VBox),
423                ("bochs",     VMBrand::Bochs),
424                ("Parallels", VMBrand::Parallels),
425            ]),
426            ("/sys/class/dmi/id/sys_vendor", &[
427                ("QEMU",      VMBrand::QEMU),
428                ("VMware",    VMBrand::VMware),
429                ("VirtualBox", VMBrand::VBox),
430                ("Xen",       VMBrand::Xen),
431                ("KVM",       VMBrand::KVM),
432                ("Microsoft", VMBrand::HyperV),
433                ("innotek",   VMBrand::VBox),
434                ("Parallels", VMBrand::Parallels),
435            ]),
436            ("/sys/class/dmi/id/product_name", &[
437                ("Virtual Machine", VMBrand::HyperV),
438                ("VMware",    VMBrand::VMware),
439                ("VirtualBox", VMBrand::VBox),
440                ("KVM",       VMBrand::KVM),
441                ("BHYVE",     VMBrand::Bhyve),
442                ("QEMU",      VMBrand::QEMU),
443                ("Bochs",     VMBrand::Bochs),
444                ("Standard PC", VMBrand::QEMU),
445            ]),
446        ];
447
448        for &(path, table) in PATHS {
449            if let Some(content) = util::read_file(path) {
450                let lower = content.to_lowercase();
451                for &(kw, brand) in table {
452                    if lower.contains(&kw.to_lowercase()) {
453                        add_brand_score(brand, 0);
454                        return true;
455                    }
456                }
457            }
458        }
459        false
460    }
461}
462
463/// PCI device IDs scan.
464#[cfg(any(windows, target_os = "linux"))]
465pub fn devices() -> bool {
466    #[cfg(windows)]
467    {
468        crate::techniques::win::devices()
469    }
470    #[cfg(not(windows))]
471    {
472        // Linux: scan /sys/bus/pci/devices/*/uevent for VM vendor IDs
473        static VM_VENDOR_IDS: &[(&str, VMBrand)] = &[
474            ("0x15ad", VMBrand::VMware),   // VMware
475            ("0x80ee", VMBrand::VBox),     // VirtualBox
476            ("0x1af4", VMBrand::QEMU),     // Virtio (QEMU/KVM)
477            ("0x1414", VMBrand::HyperV),   // Hyper-V
478            ("0x5853", VMBrand::Xen),      // Xen
479            ("0x1ab8", VMBrand::Parallels),// Parallels
480            ("0x1b36", VMBrand::QEMU),     // QEMU additional
481        ];
482
483        let pci_dir = std::path::Path::new("/sys/bus/pci/devices");
484        if !pci_dir.exists() {
485            return false;
486        }
487        if let Ok(entries) = std::fs::read_dir(pci_dir) {
488            for entry in entries.flatten() {
489                let uevent = entry.path().join("vendor");
490                if let Ok(data) = std::fs::read_to_string(&uevent) {
491                    let lower = data.trim().to_lowercase();
492                    for &(vid, brand) in VM_VENDOR_IDS {
493                        if lower == vid {
494                            add_brand_score(brand, 0);
495                            return true;
496                        }
497                    }
498                }
499            }
500        }
501        false
502    }
503}