Skip to main content

vmaware/techniques/
linux.rs

1/// Linux-specific VM detection techniques.
2
3use crate::engine::TechniqueResult;
4
5#[cfg(target_os = "linux")]
6use crate::brands;
7#[cfg(target_os = "linux")]
8use crate::util;
9
10/// Check if the chassis vendor is a VM vendor.
11pub fn chassis_vendor() -> TechniqueResult {
12    #[cfg(target_os = "linux")]
13    {
14        if let Some(vendor) = util::linux::read_dmi_field("chassis_vendor") {
15            let v = vendor.to_lowercase();
16            if v.contains("qemu") { return TechniqueResult::detected_with_brand(brands::QEMU); }
17            if v.contains("vmware") { return TechniqueResult::detected_with_brand(brands::VMWARE); }
18            if v.contains("virtualbox") || v.contains("oracle") || v.contains("innotek") {
19                return TechniqueResult::detected_with_brand(brands::VBOX);
20            }
21            if v.contains("microsoft") { return TechniqueResult::detected_with_brand(brands::HYPERV); }
22            if v.contains("xen") { return TechniqueResult::detected_with_brand(brands::XEN); }
23            if v.contains("parallels") { return TechniqueResult::detected_with_brand(brands::PARALLELS); }
24            if v.contains("bochs") { return TechniqueResult::detected_with_brand(brands::BOCHS); }
25        }
26    }
27    TechniqueResult::not_detected()
28}
29
30/// Check if the chassis type is valid (often invalid in VMs).
31pub fn chassis_type() -> TechniqueResult {
32    #[cfg(target_os = "linux")]
33    {
34        if let Some(ctype) = util::linux::read_dmi_field("chassis_type") {
35            if let Ok(t) = ctype.parse::<u32>() {
36                // Type 1 = "Other" — commonly used by VMs
37                if t == 1 {
38                    return TechniqueResult::detected();
39                }
40            }
41        }
42    }
43    TechniqueResult::not_detected()
44}
45
46/// Check if /.dockerenv or /.dockerinit file is present.
47pub fn dockerenv() -> TechniqueResult {
48    #[cfg(target_os = "linux")]
49    {
50        if util::file_exists("/.dockerenv") || util::file_exists("/.dockerinit") {
51            return TechniqueResult::detected_with_brand(brands::DOCKER);
52        }
53    }
54    TechniqueResult::not_detected()
55}
56
57/// Check if dmidecode output matches a VM brand.
58pub fn dmidecode() -> TechniqueResult {
59    #[cfg(target_os = "linux")]
60    {
61        if let Some(output) = util::run_command("dmidecode", &["-s", "system-manufacturer"]) {
62            let lower = output.to_lowercase();
63            if lower.contains("vmware") { return TechniqueResult::detected_with_brand(brands::VMWARE); }
64            if lower.contains("qemu") { return TechniqueResult::detected_with_brand(brands::QEMU); }
65            if lower.contains("virtualbox") || lower.contains("innotek") {
66                return TechniqueResult::detected_with_brand(brands::VBOX);
67            }
68            if lower.contains("microsoft") { return TechniqueResult::detected_with_brand(brands::HYPERV); }
69            if lower.contains("xen") { return TechniqueResult::detected_with_brand(brands::XEN); }
70            if lower.contains("parallels") { return TechniqueResult::detected_with_brand(brands::PARALLELS); }
71            if lower.contains("bochs") { return TechniqueResult::detected_with_brand(brands::BOCHS); }
72        }
73    }
74    TechniqueResult::not_detected()
75}
76
77/// Check if dmesg output matches a VM brand.
78pub fn dmesg() -> TechniqueResult {
79    #[cfg(target_os = "linux")]
80    {
81        if let Some(output) = util::run_command("dmesg", &[]) {
82            let lower = output.to_lowercase();
83            if lower.contains("vmware") { return TechniqueResult::detected_with_brand(brands::VMWARE); }
84            if lower.contains("virtualbox") || lower.contains("vbox") {
85                return TechniqueResult::detected_with_brand(brands::VBOX);
86            }
87            if lower.contains("qemu") { return TechniqueResult::detected_with_brand(brands::QEMU); }
88            if lower.contains("hyper-v") { return TechniqueResult::detected_with_brand(brands::HYPERV); }
89            if lower.contains("xen") { return TechniqueResult::detected_with_brand(brands::XEN); }
90            if lower.contains("kvm") { return TechniqueResult::detected_with_brand(brands::KVM); }
91        }
92    }
93    TechniqueResult::not_detected()
94}
95
96/// Check if /sys/class/hwmon/ directory is present.
97pub fn hwmon() -> TechniqueResult {
98    #[cfg(target_os = "linux")]
99    {
100        if !util::dir_exists("/sys/class/hwmon") {
101            return TechniqueResult::detected();
102        }
103        // Even if the directory exists, check if it has entries
104        if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") {
105            if entries.count() == 0 {
106                return TechniqueResult::detected();
107            }
108        }
109    }
110    TechniqueResult::not_detected()
111}
112
113/// Check result from systemd-detect-virt tool.
114pub fn systemd_virt() -> TechniqueResult {
115    #[cfg(target_os = "linux")]
116    {
117        if let Some(output) = util::run_command("systemd-detect-virt", &[]) {
118            let trimmed = output.trim().to_lowercase();
119            if trimmed != "none" && !trimmed.is_empty() {
120                // Map systemd output to brands
121                if trimmed.contains("vmware") { return TechniqueResult::detected_with_brand(brands::VMWARE); }
122                if trimmed.contains("oracle") || trimmed.contains("vbox") {
123                    return TechniqueResult::detected_with_brand(brands::VBOX);
124                }
125                if trimmed.contains("kvm") { return TechniqueResult::detected_with_brand(brands::KVM); }
126                if trimmed.contains("qemu") { return TechniqueResult::detected_with_brand(brands::QEMU); }
127                if trimmed.contains("microsoft") || trimmed.contains("hyper") {
128                    return TechniqueResult::detected_with_brand(brands::HYPERV);
129                }
130                if trimmed.contains("xen") { return TechniqueResult::detected_with_brand(brands::XEN); }
131                if trimmed.contains("docker") { return TechniqueResult::detected_with_brand(brands::DOCKER); }
132                if trimmed.contains("podman") { return TechniqueResult::detected_with_brand(brands::PODMAN); }
133                if trimmed.contains("wsl") { return TechniqueResult::detected_with_brand(brands::WSL); }
134                if trimmed.contains("openvz") { return TechniqueResult::detected_with_brand(brands::OPENVZ); }
135                return TechniqueResult::detected();
136            }
137        }
138    }
139    TechniqueResult::not_detected()
140}
141
142/// Check for default VM username and hostname for linux.
143pub fn linux_user_host() -> TechniqueResult {
144    #[cfg(target_os = "linux")]
145    {
146        let vm_usernames = ["user", "admin", "test", "vm", "sandbox", "virus", "malware"];
147        let vm_hostnames = ["ubuntu", "debian", "centos", "fedora", "sandbox", "vm", "virtual"];
148
149        if let Some(user) = util::get_username() {
150            let lower = user.to_lowercase();
151            for &vu in &vm_usernames {
152                if lower == vu {
153                    return TechniqueResult::detected();
154                }
155            }
156        }
157        if let Some(host) = util::get_hostname() {
158            let lower = host.to_lowercase();
159            for &vh in &vm_hostnames {
160                if lower == vh {
161                    return TechniqueResult::detected();
162                }
163            }
164        }
165    }
166    TechniqueResult::not_detected()
167}
168
169/// Check for VMware string in /proc/iomem.
170pub fn vmware_iomem() -> TechniqueResult {
171    #[cfg(target_os = "linux")]
172    {
173        if let Some(content) = util::read_file("/proc/iomem") {
174            if util::find_ci(&content, "vmware") {
175                return TechniqueResult::detected_with_brand(brands::VMWARE);
176            }
177        }
178    }
179    TechniqueResult::not_detected()
180}
181
182/// Check for VMware string in /proc/ioports.
183pub fn vmware_ioports() -> TechniqueResult {
184    #[cfg(target_os = "linux")]
185    {
186        if let Some(content) = util::read_file("/proc/ioports") {
187            if util::find_ci(&content, "vmware") {
188                return TechniqueResult::detected_with_brand(brands::VMWARE);
189            }
190        }
191    }
192    TechniqueResult::not_detected()
193}
194
195/// Check for VMware string in /proc/scsi/scsi.
196pub fn vmware_scsi() -> TechniqueResult {
197    #[cfg(target_os = "linux")]
198    {
199        if let Some(content) = util::read_file("/proc/scsi/scsi") {
200            if util::find_ci(&content, "vmware") {
201                return TechniqueResult::detected_with_brand(brands::VMWARE);
202            }
203        }
204    }
205    TechniqueResult::not_detected()
206}
207
208/// Check for VMware-specific device name in dmesg output.
209pub fn vmware_dmesg() -> TechniqueResult {
210    #[cfg(target_os = "linux")]
211    {
212        if let Some(output) = util::run_command("dmesg", &[]) {
213            if output.contains("VMware") || output.contains("vmw_") {
214                return TechniqueResult::detected_with_brand(brands::VMWARE);
215            }
216        }
217    }
218    TechniqueResult::not_detected()
219}
220
221/// Check for QEMU in /sys/devices/virtual/dmi/id.
222pub fn qemu_virtual_dmi() -> TechniqueResult {
223    #[cfg(target_os = "linux")]
224    {
225        let dmi_fields = ["sys_vendor", "product_name", "board_vendor", "board_name", "bios_vendor"];
226        for field in &dmi_fields {
227            if let Some(val) = util::linux::read_dmi_field(field) {
228                if util::find_ci(&val, "qemu") {
229                    return TechniqueResult::detected_with_brand(brands::QEMU);
230                }
231            }
232        }
233    }
234    TechniqueResult::not_detected()
235}
236
237/// Check for QEMU in /sys/kernel/debug/usb/devices.
238pub fn qemu_usb() -> TechniqueResult {
239    #[cfg(target_os = "linux")]
240    {
241        if let Some(content) = util::read_file("/sys/kernel/debug/usb/devices") {
242            if util::find_ci(&content, "qemu") {
243                return TechniqueResult::detected_with_brand(brands::QEMU);
244            }
245        }
246    }
247    TechniqueResult::not_detected()
248}
249
250/// Check for files in /sys/hypervisor directory.
251pub fn hypervisor_dir() -> TechniqueResult {
252    #[cfg(target_os = "linux")]
253    {
254        if util::dir_exists("/sys/hypervisor") {
255            if let Ok(entries) = std::fs::read_dir("/sys/hypervisor") {
256                if entries.count() > 0 {
257                    // Check for Xen specifically
258                    if util::file_exists("/sys/hypervisor/type") {
259                        if let Some(hv_type) = util::read_file("/sys/hypervisor/type") {
260                            if hv_type.trim() == "xen" {
261                                return TechniqueResult::detected_with_brand(brands::XEN);
262                            }
263                        }
264                    }
265                    return TechniqueResult::detected();
266                }
267            }
268        }
269    }
270    TechniqueResult::not_detected()
271}
272
273/// Check for "UML" string in CPU brand.
274pub fn uml_cpu() -> TechniqueResult {
275    #[cfg(target_os = "linux")]
276    {
277        if let Some(cpuinfo) = util::read_file("/proc/cpuinfo") {
278            for line in cpuinfo.lines() {
279                if line.starts_with("model name") && line.contains("UML") {
280                    return TechniqueResult::detected_with_brand(brands::UML);
281                }
282            }
283        }
284    }
285    TechniqueResult::not_detected()
286}
287
288/// Check for hypervisor indications in kernel message logs.
289pub fn kmsg() -> TechniqueResult {
290    #[cfg(target_os = "linux")]
291    {
292        if let Some(content) = util::read_file("/dev/kmsg") {
293            let lower = content.to_lowercase();
294            if lower.contains("hypervisor") || lower.contains("vmware")
295                || lower.contains("virtualbox") || lower.contains("kvm") {
296                return TechniqueResult::detected();
297            }
298        }
299    }
300    TechniqueResult::not_detected()
301}
302
303/// Check for a VBox kernel module.
304pub fn vbox_module() -> TechniqueResult {
305    #[cfg(target_os = "linux")]
306    {
307        let modules = ["vboxguest", "vboxsf", "vboxvideo"];
308        for module in &modules {
309            if util::linux::is_module_loaded(module) {
310                return TechniqueResult::detected_with_brand(brands::VBOX);
311            }
312        }
313    }
314    TechniqueResult::not_detected()
315}
316
317/// Check for VM info in /proc/sysinfo.
318pub fn sysinfo_proc() -> TechniqueResult {
319    #[cfg(target_os = "linux")]
320    {
321        if let Some(content) = util::read_file("/proc/sysinfo") {
322            let lower = content.to_lowercase();
323            if lower.contains("vm") || lower.contains("lpar") {
324                return TechniqueResult::detected_with_brand(brands::POWERVM);
325            }
326        }
327    }
328    TechniqueResult::not_detected()
329}
330
331/// Check for string matches of VM brands in Linux DMI.
332pub fn dmi_scan() -> TechniqueResult {
333    #[cfg(target_os = "linux")]
334    {
335        let dmi_files = [
336            "sys_vendor", "product_name", "product_version",
337            "board_vendor", "board_name", "bios_vendor", "bios_version",
338        ];
339
340        let vm_strings: &[(&str, &str)] = &[
341            ("vmware", brands::VMWARE),
342            ("virtualbox", brands::VBOX),
343            ("innotek", brands::VBOX),
344            ("oracle", brands::VBOX),
345            ("qemu", brands::QEMU),
346            ("bochs", brands::BOCHS),
347            ("microsoft", brands::HYPERV),
348            ("hyper-v", brands::HYPERV),
349            ("xen", brands::XEN),
350            ("parallels", brands::PARALLELS),
351            ("kvm", brands::KVM),
352            ("bhyve", brands::BHYVE),
353            ("google", brands::GCE),
354            ("amazon", brands::AWS_NITRO),
355            ("openstack", brands::OPENSTACK),
356        ];
357
358        for field in &dmi_files {
359            if let Some(val) = util::linux::read_dmi_field(field) {
360                let lower = val.to_lowercase();
361                for &(pattern, brand) in vm_strings {
362                    if lower.contains(pattern) {
363                        return TechniqueResult::detected_with_brand(brand);
364                    }
365                }
366            }
367        }
368    }
369    TechniqueResult::not_detected()
370}
371
372/// Check for the VM bit in SMBIOS data.
373pub fn smbios_vm_bit() -> TechniqueResult {
374    #[cfg(target_os = "linux")]
375    {
376        // Check /sys/firmware/dmi/entries for VM indicators
377        if let Some(content) = util::read_file("/sys/firmware/dmi/tables/smbios_entry_point") {
378            if content.len() > 0 {
379                // A simplified check — the full version parses SMBIOS structures
380                return TechniqueResult::not_detected();
381            }
382        }
383    }
384    TechniqueResult::not_detected()
385}
386
387/// Check for podman file in /run/.
388pub fn podman_file() -> TechniqueResult {
389    #[cfg(target_os = "linux")]
390    {
391        if util::file_exists("/run/.containerenv") {
392            return TechniqueResult::detected_with_brand(brands::PODMAN);
393        }
394    }
395    TechniqueResult::not_detected()
396}
397
398/// Check for WSL or microsoft indications in /proc/ subdirectories.
399pub fn wsl_proc() -> TechniqueResult {
400    #[cfg(target_os = "linux")]
401    {
402        if let Some(version) = util::read_file("/proc/version") {
403            let lower = version.to_lowercase();
404            if lower.contains("microsoft") || lower.contains("wsl") {
405                return TechniqueResult::detected_with_brand(brands::WSL);
406            }
407        }
408        if let Some(osrelease) = util::read_file("/proc/sys/kernel/osrelease") {
409            let lower = osrelease.to_lowercase();
410            if lower.contains("microsoft") || lower.contains("wsl") {
411                return TechniqueResult::detected_with_brand(brands::WSL);
412            }
413        }
414    }
415    TechniqueResult::not_detected()
416}
417
418/// Detect QEMU fw_cfg interface.
419pub fn qemu_fw_cfg() -> TechniqueResult {
420    #[cfg(target_os = "linux")]
421    {
422        let paths = [
423            "/sys/firmware/qemu_fw_cfg",
424            "/sys/module/qemu_fw_cfg",
425        ];
426        for path in &paths {
427            if util::dir_exists(path) {
428                return TechniqueResult::detected_with_brand(brands::QEMU);
429            }
430        }
431        // Check device tree
432        if let Some(compatible) = util::read_file("/sys/firmware/devicetree/base/hypervisor/compatible") {
433            if util::find_ci(&compatible, "qemu") {
434                return TechniqueResult::detected_with_brand(brands::QEMU);
435            }
436        }
437    }
438    TechniqueResult::not_detected()
439}
440
441/// Check if accessed file count is too low for a human-managed environment.
442pub fn file_access_history() -> TechniqueResult {
443    #[cfg(target_os = "linux")]
444    {
445        let home = std::env::var("HOME").unwrap_or_default();
446        if home.is_empty() {
447            return TechniqueResult::not_detected();
448        }
449
450        let dirs_to_check = [
451            format!("{}/Desktop", home),
452            format!("{}/Documents", home),
453            format!("{}/Downloads", home),
454        ];
455
456        let mut total_files = 0usize;
457        for dir in &dirs_to_check {
458            if let Ok(entries) = std::fs::read_dir(dir) {
459                total_files += entries.count();
460            }
461        }
462
463        // If all common directories have very few files, it might be a fresh VM
464        if total_files <= 3 {
465            return TechniqueResult::detected();
466        }
467    }
468    TechniqueResult::not_detected()
469}
470
471/// Check if MAC address starts with VM-designated values.
472pub fn mac_address_check() -> TechniqueResult {
473    #[cfg(target_os = "linux")]
474    {
475        if let Some(mac) = util::linux::get_mac_address() {
476            let upper = mac.to_uppercase();
477            let vm_mac_prefixes: &[(&str, &str)] = &[
478                ("00:05:69", brands::VMWARE),
479                ("00:0C:29", brands::VMWARE),
480                ("00:1C:14", brands::VMWARE),
481                ("00:50:56", brands::VMWARE),
482                ("08:00:27", brands::VBOX),
483                ("0A:00:27", brands::VBOX),
484                ("00:03:FF", brands::HYPERV),
485                ("00:15:5D", brands::HYPERV),
486                ("00:1A:4A", brands::QEMU),
487                ("52:54:00", brands::QEMU),
488                ("00:16:3E", brands::XEN),
489                ("00:1C:42", brands::PARALLELS),
490            ];
491
492            for &(prefix, brand) in vm_mac_prefixes {
493                if upper.starts_with(prefix) {
494                    return TechniqueResult::detected_with_brand(brand);
495                }
496            }
497        }
498    }
499    TechniqueResult::not_detected()
500}
501
502/// Check if process status matches nsjail patterns.
503pub fn nsjail_pid() -> TechniqueResult {
504    #[cfg(target_os = "linux")]
505    {
506        // nsjail typically runs processes with PID 1 that aren't init/systemd
507        if let Some(comm) = util::read_file("/proc/1/comm") {
508            let name = comm.trim();
509            if name != "init" && name != "systemd" && name != "launchd" {
510                // Check if it's nsjail
511                if let Some(status) = util::read_file("/proc/1/status") {
512                    if status.contains("nsjail") {
513                        return TechniqueResult::detected_with_brand(brands::NSJAIL);
514                    }
515                }
516            }
517        }
518    }
519    TechniqueResult::not_detected()
520}
521
522/// Check for BlueStacks-specific folders.
523pub fn bluestacks_folders() -> TechniqueResult {
524    #[cfg(target_os = "linux")]
525    {
526        let paths = [
527            "/sdcard/windows/BstSharedFolder",
528            "/mnt/windows/BstSharedFolder",
529        ];
530        for path in &paths {
531            if util::dir_exists(path) {
532                return TechniqueResult::detected_with_brand(brands::BLUESTACKS);
533            }
534        }
535    }
536    TechniqueResult::not_detected()
537}
538
539/// Check for AMD-SEV MSR.
540pub fn amd_sev() -> TechniqueResult {
541    #[cfg(target_os = "linux")]
542    {
543        // Check /sys/module/kvm_amd/parameters/sev
544        if let Some(val) = util::read_file("/sys/module/kvm_amd/parameters/sev") {
545            if val.trim() == "Y" || val.trim() == "1" {
546                return TechniqueResult::detected_with_brand(brands::AMD_SEV);
547            }
548        }
549        // Check dmesg for AMD SEV
550        if let Some(dmesg_out) = util::run_command("dmesg", &[]) {
551            if dmesg_out.contains("AMD SEV") {
552                return TechniqueResult::detected_with_brand(brands::AMD_SEV);
553            }
554        }
555    }
556    TechniqueResult::not_detected()
557}
558
559/// Check for device's temperature sensors.
560pub fn temperature() -> TechniqueResult {
561    #[cfg(target_os = "linux")]
562    {
563        // VMs typically don't have temperature sensors
564        let temp_paths = [
565            "/sys/class/thermal/thermal_zone0/temp",
566            "/sys/class/hwmon/hwmon0/temp1_input",
567        ];
568
569        let mut has_sensor = false;
570        for path in &temp_paths {
571            if let Some(val) = util::read_file(path) {
572                if let Ok(temp) = val.trim().parse::<i64>() {
573                    if temp > 0 {
574                        has_sensor = true;
575                        break;
576                    }
577                }
578            }
579        }
580
581        if !has_sensor {
582            return TechniqueResult::detected();
583        }
584    }
585    TechniqueResult::not_detected()
586}
587
588/// Check for VM processes that are active.
589pub fn processes() -> TechniqueResult {
590    #[cfg(target_os = "linux")]
591    {
592        let vm_processes: &[(&str, &str)] = &[
593            ("VBoxService", brands::VBOX),
594            ("VBoxClient", brands::VBOX),
595            ("vmtoolsd", brands::VMWARE),
596            ("vmwaretray", brands::VMWARE),
597            ("vmwareuser", brands::VMWARE),
598            ("vmware-vmblock-fuse", brands::VMWARE),
599            ("qemu-ga", brands::QEMU),
600            ("spice-vdagent", brands::QEMU),
601            ("xe-daemon", brands::XEN),
602            ("prl_cc", brands::PARALLELS),
603            ("prl_tools", brands::PARALLELS),
604        ];
605
606        let running = util::linux::list_processes();
607        for &(proc_name, brand) in vm_processes {
608            if running.iter().any(|p| p == proc_name) {
609                return TechniqueResult::detected_with_brand(brand);
610            }
611        }
612    }
613    TechniqueResult::not_detected()
614}