Skip to main content

vmaware/techniques/
linux.rs

1//! Linux-specific VM detection techniques.
2
3#![cfg(target_os = "linux")]
4
5use crate::core::add_brand_score;
6use crate::types::VMBrand;
7use crate::util;
8
9// ── smbios_vm_bit ─────────────────────────────────────────────────────────────
10
11/// Check the SMBIOS Chassis Type for VM values.
12pub fn smbios_vm_bit() -> bool {
13    // Chassis type 1 = Other, many VMs report unusual chassis types
14    if let Some(ct) = util::read_file("/sys/class/dmi/id/chassis_type") {
15        let ct = ct.trim().parse::<u32>().unwrap_or(0);
16        // VM chassis types: 1 (Other), 2 (Unknown), sometimes unusual high values
17        if ct == 1 || ct == 2 {
18            return true;
19        }
20    }
21    false
22}
23
24// ── kmsg ─────────────────────────────────────────────────────────────────────
25
26/// Scan /dev/kmsg or /var/log/kern.log for VM-related kernel messages.
27pub fn kmsg() -> bool {
28    static VM_STRINGS: &[(&str, VMBrand)] = &[
29        ("VMware VMCI",      VMBrand::VMware),
30        ("VirtualBox",       VMBrand::VBox),
31        ("QEMU",             VMBrand::QEMU),
32        ("Booting paravirtualized kernel", VMBrand::KVM),
33        ("Hypervisor detected", VMBrand::Invalid),
34        ("kvm-clock",        VMBrand::KVM),
35        ("hv_vmbus",         VMBrand::HyperV),
36    ];
37
38    let paths = ["/proc/kmsg", "/var/log/kern.log", "/var/log/syslog"];
39    for path in &paths {
40        if let Some(content) = util::read_file(path) {
41            for &(kw, brand) in VM_STRINGS {
42                if content.contains(kw) {
43                    if brand != VMBrand::Invalid {
44                        add_brand_score(brand, 0);
45                    }
46                    return true;
47                }
48            }
49        }
50    }
51    false
52}
53
54// ── cvendor ──────────────────────────────────────────────────────────────────
55
56/// Check /sys/class/dmi/id/* vendor fields.
57pub fn cvendor() -> bool {
58    static PATHS: &[(&str, &[(&str, VMBrand)])] = &[
59        ("/sys/class/dmi/id/sys_vendor", &[
60            ("QEMU",         VMBrand::QEMU),
61            ("VMware",       VMBrand::VMware),
62            ("VirtualBox",   VMBrand::VBox),
63            ("innotek GmbH", VMBrand::VBox),
64            ("Xen",          VMBrand::Xen),
65            ("Microsoft",    VMBrand::HyperV),
66            ("KVM",          VMBrand::KVM),
67            ("Parallels",    VMBrand::Parallels),
68            ("OpenStack",    VMBrand::OpenStack),
69            ("Google",       VMBrand::GCE),
70            ("Amazon",       VMBrand::AWSNitro),
71            ("bhyve",        VMBrand::Bhyve),
72        ]),
73    ];
74
75    for &(path, table) in PATHS {
76        if let Some(content) = util::read_file(path) {
77            let lower = content.to_lowercase();
78            for &(kw, brand) in table {
79                if lower.contains(&kw.to_lowercase()) {
80                    add_brand_score(brand, 0);
81                    return true;
82                }
83            }
84        }
85    }
86    false
87}
88
89// ── qemu_fw_cfg ──────────────────────────────────────────────────────────────
90
91/// Check for the QEMU fw_cfg device.
92pub fn qemu_fw_cfg() -> bool {
93    util::exists("/sys/firmware/qemu_fw_cfg") || util::exists("/dev/mem")
94        && util::file_contains("/sys/firmware/qemu_fw_cfg/by_name/opt/qemu_fw_cfg_version/raw", "")
95}
96
97// ── systemd ───────────────────────────────────────────────────────────────────
98
99/// Check systemd virtualization detection result.
100pub fn systemd() -> bool {
101    if let Some(v) = util::read_file("/run/systemd/detect-virt") {
102        let v = v.trim();
103        return v != "none" && !v.is_empty();
104    }
105    // Try running systemd-detect-virt
106    if let Ok(out) = std::process::Command::new("systemd-detect-virt").output() {
107        let s = String::from_utf8_lossy(&out.stdout).to_lowercase();
108        return s.trim() != "none" && !s.trim().is_empty() && out.status.success();
109    }
110    false
111}
112
113// ── ctype ─────────────────────────────────────────────────────────────────────
114
115/// Check /proc/cpuinfo for VM hypervisor flag.
116pub fn ctype() -> bool {
117    if let Some(cpuinfo) = util::read_file("/proc/cpuinfo") {
118        return cpuinfo.contains("hypervisor");
119    }
120    false
121}
122
123// ── dockerenv ─────────────────────────────────────────────────────────────────
124
125/// Check for Docker environment files.
126pub fn dockerenv() -> bool {
127    if util::exists("/.dockerenv") {
128        add_brand_score(VMBrand::Docker, 0);
129        return true;
130    }
131    if util::exists("/.dockerinit") {
132        add_brand_score(VMBrand::Docker, 0);
133        return true;
134    }
135    false
136}
137
138// ── dmidecode ─────────────────────────────────────────────────────────────────
139
140/// Run dmidecode and check output for VM strings.
141pub fn dmidecode() -> bool {
142    if let Ok(out) = std::process::Command::new("dmidecode").arg("-t").arg("system").output() {
143        let s = String::from_utf8_lossy(&out.stdout).to_lowercase();
144        static VM_KW: &[(&str, VMBrand)] = &[
145            ("virtualbox",   VMBrand::VBox),
146            ("vmware",       VMBrand::VMware),
147            ("qemu",         VMBrand::QEMU),
148            ("xen",          VMBrand::Xen),
149            ("kvm",          VMBrand::KVM),
150            ("bochs",        VMBrand::Bochs),
151            ("microsoft",    VMBrand::HyperV),
152            ("parallels",    VMBrand::Parallels),
153        ];
154        for &(kw, brand) in VM_KW {
155            if s.contains(kw) {
156                add_brand_score(brand, 0);
157                return true;
158            }
159        }
160    }
161    false
162}
163
164// ── dmesg ─────────────────────────────────────────────────────────────────────
165
166/// Check dmesg output for hypervisor messages.
167pub fn dmesg() -> bool {
168    if let Ok(out) = std::process::Command::new("dmesg").output() {
169        let s = String::from_utf8_lossy(&out.stdout).to_lowercase();
170        static KW: &[(&str, VMBrand)] = &[
171            ("hypervisor",      VMBrand::Invalid),
172            ("vmware",          VMBrand::VMware),
173            ("virtualbox",      VMBrand::VBox),
174            ("kvm",             VMBrand::KVM),
175            ("xen",             VMBrand::Xen),
176            ("virt",            VMBrand::Invalid),
177            ("paravirt",        VMBrand::KVM),
178            ("hv_vmbus",        VMBrand::HyperV),
179        ];
180        for &(kw, brand) in KW {
181            if s.contains(kw) {
182                if brand != VMBrand::Invalid {
183                    add_brand_score(brand, 0);
184                }
185                return true;
186            }
187        }
188    }
189    false
190}
191
192// ── hwmon ─────────────────────────────────────────────────────────────────────
193
194/// VMs typically have no hardware monitoring sensors.
195pub fn hwmon() -> bool {
196    if let Ok(rd) = std::fs::read_dir("/sys/class/hwmon") {
197        return rd.count() == 0;
198    }
199    true // If hwmon doesn't exist at all → VM likely
200}
201
202// ── linux_user_host ───────────────────────────────────────────────────────────
203
204/// Check /etc/hostname and username for sandbox-typical values.
205pub fn linux_user_host() -> bool {
206    static VM_HOSTNAMES: &[&str] = &[
207        "sandbox", "cuckoo", "malware", "analysis", "lab",
208        "honeypot", "box", "win7vm", "vm-",
209    ];
210
211    let hostname = util::read_file("/etc/hostname").unwrap_or_default();
212    let lower = hostname.to_lowercase();
213    for kw in VM_HOSTNAMES {
214        if lower.contains(kw) {
215            return true;
216        }
217    }
218    false
219}
220
221// ── vmware_iomem ──────────────────────────────────────────────────────────────
222
223pub fn vmware_iomem() -> bool {
224    util::file_contains("/proc/iomem", "VMware")
225}
226
227// ── vmware_ioports ────────────────────────────────────────────────────────────
228
229pub fn vmware_ioports() -> bool {
230    util::file_contains("/proc/ioports", "VMware")
231}
232
233// ── vmware_scsi ───────────────────────────────────────────────────────────────
234
235pub fn vmware_scsi() -> bool {
236    util::file_contains("/proc/scsi/scsi", "VMware")
237}
238
239// ── vmware_dmesg ──────────────────────────────────────────────────────────────
240
241pub fn vmware_dmesg() -> bool {
242    if let Ok(out) = std::process::Command::new("dmesg").output() {
243        let s = String::from_utf8_lossy(&out.stdout);
244        return s.contains("VMware") || s.contains("VMWARE");
245    }
246    false
247}
248
249// ── qemu_virtual_dmi ─────────────────────────────────────────────────────────
250
251pub fn qemu_virtual_dmi() -> bool {
252    util::file_contains("/sys/firmware/dmi/tables/DMI", "QEMU")
253        || util::file_contains("/sys/class/dmi/id/bios_vendor", "QEMU")
254}
255
256// ── qemu_usb ─────────────────────────────────────────────────────────────────
257
258pub fn qemu_usb() -> bool {
259    if let Ok(rd) = std::fs::read_dir("/sys/bus/usb/devices") {
260        for entry in rd.flatten() {
261            let idv = entry.path().join("idVendor");
262            if let Ok(v) = std::fs::read_to_string(&idv) {
263                if v.trim() == "1234" {
264                    // QEMU USB vendor
265                    add_brand_score(VMBrand::QEMU, 0);
266                    return true;
267                }
268            }
269        }
270    }
271    false
272}
273
274// ── hypervisor_dir ────────────────────────────────────────────────────────────
275
276pub fn hypervisor_dir() -> bool {
277    static DIRS: &[(&str, VMBrand)] = &[
278        ("/proc/vz",             VMBrand::OpenVZ),
279        ("/proc/bc",             VMBrand::OpenVZ),
280        ("/proc/xen",            VMBrand::Xen),
281        ("/proc/xenolinux-banner", VMBrand::Xen),
282    ];
283    for &(path, brand) in DIRS {
284        if util::exists(path) {
285            add_brand_score(brand, 0);
286            return true;
287        }
288    }
289    false
290}
291
292// ── uml_cpu ───────────────────────────────────────────────────────────────────
293
294pub fn uml_cpu() -> bool {
295    if let Some(cpuinfo) = util::read_file("/proc/cpuinfo") {
296        if cpuinfo.contains("User Mode Linux") || cpuinfo.contains("UML") {
297            add_brand_score(VMBrand::UML, 0);
298            return true;
299        }
300    }
301    false
302}
303
304// ── vbox_module ───────────────────────────────────────────────────────────────
305
306pub fn vbox_module() -> bool {
307    if let Some(mods) = util::read_file("/proc/modules") {
308        let vm_mods = ["vboxdrv", "vboxguest", "vboxpci", "vboxnetflt", "vboxnetadp"];
309        for m in &vm_mods {
310            if mods.contains(m) {
311                add_brand_score(VMBrand::VBox, 0);
312                return true;
313            }
314        }
315    }
316    false
317}
318
319// ── sysinfo_proc ─────────────────────────────────────────────────────────────
320
321pub fn sysinfo_proc() -> bool {
322    util::file_contains("/proc/sysinfo", "VM00") || util::file_contains("/proc/sysinfo", "z/VM")
323}
324
325// ── dmi_scan ─────────────────────────────────────────────────────────────────
326
327pub fn dmi_scan() -> bool {
328    cvendor() // reuses the same DMI scan logic
329}
330
331// ── podman_file ───────────────────────────────────────────────────────────────
332
333pub fn podman_file() -> bool {
334    if util::exists("/run/.containerenv") {
335        add_brand_score(VMBrand::Podman, 0);
336        return true;
337    }
338    false
339}
340
341// ── wsl_proc ─────────────────────────────────────────────────────────────────
342
343pub fn wsl_proc() -> bool {
344    if util::file_contains("/proc/version", "Microsoft")
345        || util::file_contains("/proc/version", "WSL")
346    {
347        add_brand_score(VMBrand::WSL, 0);
348        return true;
349    }
350    if util::exists("/proc/sys/fs/binfmt_misc/WSLInterop") {
351        add_brand_score(VMBrand::WSL, 0);
352        return true;
353    }
354    false
355}
356
357// ── file_access_history ───────────────────────────────────────────────────────
358
359pub fn file_access_history() -> bool {
360    // Check recent file access history for sandbox indicators
361    util::exists("/root/.bash_history") && {
362        util::read_file("/root/.bash_history")
363            .map(|h| h.lines().count() == 0)
364            .unwrap_or(false)
365    }
366}
367
368// ── mac ───────────────────────────────────────────────────────────────────────
369
370pub fn mac() -> bool {
371    // Check MAC address OUIs for known VM vendors
372    if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
373        for entry in rd.flatten() {
374            let mac_path = entry.path().join("address");
375            if let Ok(addr) = std::fs::read_to_string(&mac_path) {
376                let addr = addr.trim().to_lowercase();
377                // VMware: 00:0c:29, 00:50:56, 00:05:69
378                // VirtualBox: 08:00:27
379                // QEMU: 52:54:00
380                // Parallels: 00:1c:42
381                // Hyper-V: 00:15:5d
382                static VM_MACS: &[(&str, VMBrand)] = &[
383                    ("00:0c:29", VMBrand::VMware),
384                    ("00:50:56", VMBrand::VMware),
385                    ("00:05:69", VMBrand::VMware),
386                    ("08:00:27", VMBrand::VBox),
387                    ("52:54:00", VMBrand::QEMUKVM),
388                    ("00:1c:42", VMBrand::Parallels),
389                    ("00:15:5d", VMBrand::HyperV),
390                ];
391                for &(prefix, brand) in VM_MACS {
392                    if addr.starts_with(prefix) {
393                        add_brand_score(brand, 0);
394                        return true;
395                    }
396                }
397            }
398        }
399    }
400    false
401}
402
403// ── nsjail_pid ────────────────────────────────────────────────────────────────
404
405pub fn nsjail_pid() -> bool {
406    // nsjail sets PID namespace so PID 1 is not init/systemd
407    if let Some(comm) = util::read_file("/proc/1/comm") {
408        let c = comm.trim();
409        if c != "systemd" && c != "init" && c != "upstart" && c != "svscan" {
410            // Check if we're in a nested PID namespace
411            if util::file_contains("/proc/1/environ", "NSJAIL") {
412                add_brand_score(VMBrand::NSJail, 0);
413                return true;
414            }
415        }
416    }
417    false
418}
419
420// ── bluestacks_folders ────────────────────────────────────────────────────────
421
422pub fn bluestacks_folders() -> bool {
423    static PATHS: &[&str] = &[
424        "/sdcard",
425        "/data/app",
426        "/system/priv-app",
427    ];
428    // BlueStacks specific: all three exist together
429    let count = PATHS.iter().filter(|p| util::exists(p)).count();
430    if count >= 2 {
431        add_brand_score(VMBrand::BlueStacks, 0);
432        return true;
433    }
434    false
435}
436
437// ── amd_sev_msr ──────────────────────────────────────────────────────────────
438
439pub fn amd_sev_msr() -> bool {
440    use crate::cpu::{cpuid, is_amd, is_leaf_supported};
441    if !is_amd() {
442        return false;
443    }
444
445    // CRITICAL correctness check: CPUID 0x8000001F EAX bits are *capability*
446    // bits that say the hardware SUPPORTS SEV/SEV-ES/SEV-SNP, **not** that the
447    // current execution is running inside an SEV-protected guest VM.
448    //
449    // On a bare-metal AMD EPYC / Ryzen Pro machine with SEV firmware support,
450    // these bits will be set even though no VM is present – which would produce
451    // a false positive.
452    //
453    // Mitigation: also require the hypervisor-present bit (CPUID leaf 1 ECX
454    // bit 31) to be set.  On bare metal this bit is always 0; it is only set
455    // when the processor is running as a guest under a hypervisor.  Combined
456    // with the SEV capability bits this gives a reliable "inside an SEV guest"
457    // signal without ring-0 MSR access.
458    let leaf1 = cpuid(1, 0);
459    let hypervisor_present = (leaf1.ecx >> 31) & 1 != 0;
460    if !hypervisor_present {
461        return false;
462    }
463
464    if is_leaf_supported(0x8000_001F) {
465        let r = cpuid(0x8000_001F, 0);
466        let sev_snp = (r.eax >> 4) & 1 != 0;
467        let sev_es = (r.eax >> 3) & 1 != 0;
468        let sev = r.eax & 1 != 0;
469        if sev_snp {
470            add_brand_score(VMBrand::AMDSEVsnp, 0);
471            return true;
472        } else if sev_es {
473            add_brand_score(VMBrand::AMDSEVes, 0);
474            return true;
475        } else if sev {
476            add_brand_score(VMBrand::AMDSEV, 0);
477            return true;
478        }
479    }
480    false
481}
482
483// ── temperature ───────────────────────────────────────────────────────────────
484
485/// VMs usually have no thermal sensors.
486pub fn temperature() -> bool {
487    if let Ok(rd) = std::fs::read_dir("/sys/class/thermal") {
488        return rd.count() == 0;
489    }
490    true
491}
492
493// ── processes ─────────────────────────────────────────────────────────────────
494
495/// Check running processes for known VM guest agent names.
496pub fn processes() -> bool {
497    static VM_PROCS: &[(&str, VMBrand)] = &[
498        ("vmtoolsd",     VMBrand::VMware),
499        ("vmwaretray",   VMBrand::VMware),
500        ("vmwareuser",   VMBrand::VMware),
501        ("VBoxService",  VMBrand::VBox),
502        ("VBoxClient",   VMBrand::VBox),
503        ("prl_tools",    VMBrand::Parallels),
504        ("qemu-ga",      VMBrand::QEMU),
505        ("cuckoo",       VMBrand::Cuckoo),
506    ];
507
508    if let Ok(rd) = std::fs::read_dir("/proc") {
509        for entry in rd.flatten() {
510            let comm = entry.path().join("comm");
511            if let Ok(name) = std::fs::read_to_string(&comm) {
512                let name = name.trim().to_lowercase();
513                for &(proc_name, brand) in VM_PROCS {
514                    if name.contains(&proc_name.to_lowercase()) {
515                        add_brand_score(brand, 0);
516                        return true;
517                    }
518                }
519            }
520        }
521    }
522    false
523}
524
525// ── thread_count ─────────────────────────────────────────────────────────────
526
527pub fn thread_count() -> bool {
528    crate::techniques::cross::thread_count()
529}