astraguard 1.1.7

Official AstraGuard SDK - license validation, HWID binding, anti-debug, and offline cache for Rust applications
Documentation
//! Anti-VM detection for Windows (x86_64).
//!
//! Requires at least 2 independent signals to return `true` in order to
//! reduce false positives from hypervisors used for development (WSL, Hyper-V
//! developer environments, etc.).

use super::strings;

/// Returns `true` if the process is running inside a virtual machine.
///
/// Two or more independent signals must trigger to reduce false positives.
///
/// NOTE: `check_cpuid_hypervisor_bit()` is intentionally NOT counted as its
/// own signal here. It and `check_cpuid_vendor()` are not statistically
/// independent for Hyper-V: the hypervisor-present bit is set by design
/// whenever a Hyper-V vendor string is readable at CPUID leaf 0x40000000, so
/// counting both let a single underlying condition contribute 2 points and
/// cross the threshold alone. That combination is extremely common on real,
/// physical Windows 11 machines - WSL2, Docker Desktop, Windows Sandbox, and
/// Credential Guard / Device Guard (Virtualization-Based Security) all put
/// the *host* itself in a Hyper-V root partition, which reports the same
/// "Microsoft Hv" vendor a genuine Hyper-V *guest* VM would. Reproduced as a
/// false positive on a real, non-VM machine while building the reference SDK
/// tools - `check_cpuid_vendor()` alone (i.e. actually reading a guest-only
/// vendor string) is a fine signal on its own; the hypervisor-bit pre-check
/// added no real corroboration for the ambiguous Hyper-V case.
pub fn is_virtual_machine() -> bool {
    let mut signals: u32 = 0;
    if check_cpuid_vendor() {
        signals += 1;
    }
    if check_vm_processes() {
        signals += 1;
    }
    if check_vm_files() {
        signals += 1;
    }
    if check_mac_address() {
        signals += 1;
    }
    signals >= 2
}

/// Check CPUID leaf 0x40000000 for known hypervisor vendor strings.
fn check_cpuid_vendor() -> bool {
    #[cfg(target_arch = "x86_64")]
    {
        use std::arch::x86_64::__cpuid;
        #[allow(unused_unsafe)]
        let result = unsafe { __cpuid(0x4000_0000) };

        let mut vendor = [0u8; 12];
        vendor[0..4].copy_from_slice(&result.ebx.to_le_bytes());
        vendor[4..8].copy_from_slice(&result.ecx.to_le_bytes());
        vendor[8..12].copy_from_slice(&result.edx.to_le_bytes());

        let vmware = strings::decode(&strings::CPUID_VMWARE);
        let vbox = strings::decode(&strings::CPUID_VBOX);
        let kvm = strings::decode(&strings::CPUID_KVM);
        let hyperv = strings::decode(&strings::CPUID_HYPERV);

        vendor == vmware.as_bytes()
            || vendor == vbox.as_bytes()
            || vendor[..9] == kvm.as_bytes()[..9]
            || vendor == hyperv.as_bytes()
    }
    #[cfg(not(target_arch = "x86_64"))]
    {
        false
    }
}

/// Check the running process list for known VM guest agent processes.
fn check_vm_processes() -> bool {
    #[cfg(target_os = "windows")]
    {
        use winapi::um::handleapi::CloseHandle;
        use winapi::um::tlhelp32::{
            CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
            TH32CS_SNAPPROCESS,
        };

        let vm_procs = [
            strings::decode(&strings::VMTOOLSD),
            strings::decode(&strings::VMWARETRAY),
            strings::decode(&strings::VMWAREUSER),
            strings::decode(&strings::VBOXSERVICE),
            strings::decode(&strings::VBOXTRAY),
            strings::decode(&strings::VMUSRVC),
            strings::decode(&strings::VMSRVC),
        ];

        // SAFETY:
        // - `CreateToolhelp32Snapshot` with `TH32CS_SNAPPROCESS` and pid 0
        //   snapshots all processes; it is always safe to call.
        // - `PROCESSENTRY32W` is zero-initialised and `dwSize` is set to the
        //   correct struct size before the first `Process32FirstW` call.
        // - We close the snapshot handle before returning.
        unsafe {
            let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
            if snap == winapi::um::handleapi::INVALID_HANDLE_VALUE {
                return false;
            }

            let mut pe = PROCESSENTRY32W {
                dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
                ..std::mem::zeroed()
            };

            let mut found = false;
            if Process32FirstW(snap, &mut pe) != 0 {
                loop {
                    let name_raw: Vec<u16> = pe
                        .szExeFile
                        .iter()
                        .take_while(|&&c| c != 0)
                        .copied()
                        .collect();
                    let name = String::from_utf16_lossy(&name_raw).to_lowercase();
                    if vm_procs.iter().any(|p| name.starts_with(p.as_str())) {
                        found = true;
                        break;
                    }
                    if Process32NextW(snap, &mut pe) == 0 {
                        break;
                    }
                }
            }

            CloseHandle(snap);
            found
        }
    }
    #[cfg(not(target_os = "windows"))]
    {
        false
    }
}

/// Check for known VM guest driver files on disk.
fn check_vm_files() -> bool {
    #[cfg(target_os = "windows")]
    {
        let paths = [
            strings::decode(&strings::VMMOUSE_SYS),
            strings::decode(&strings::VMHGFS_SYS),
            strings::decode(&strings::VBOXGUEST_SYS),
            strings::decode(&strings::VBOXMOUSE_SYS),
        ];
        paths
            .iter()
            .any(|p| std::path::Path::new(p.trim_end_matches('\0')).exists())
    }
    #[cfg(not(target_os = "windows"))]
    {
        false
    }
}

/// Check MAC address OUI prefixes against known VM vendor assignments.
fn check_mac_address() -> bool {
    #[cfg(target_os = "windows")]
    {
        use winapi::shared::winerror::ERROR_SUCCESS;
        use winapi::um::iphlpapi::GetAdaptersInfo;
        use winapi::um::iptypes::IP_ADAPTER_INFO;

        let oui_vbox = strings::decode(&strings::MAC_VBOX);
        let oui_vmware1 = strings::decode(&strings::MAC_VMWARE1);
        let oui_vmware2 = strings::decode(&strings::MAC_VMWARE2);
        let oui_qemu = strings::decode(&strings::MAC_QEMU);

        // SAFETY:
        // - First call with a null buffer and `size = 0` is the documented way
        //   to query the required buffer size; `GetAdaptersInfo` writes the
        //   needed byte count into `size`.
        // - `adapters` is heap-allocated with enough capacity and
        //   zero-initialised via `resize_with(zeroed)` before the second call.
        // - We dereference `adapter_ptr` only while it is non-null and points
        //   into our allocated `adapters` Vec, which stays alive for the block.
        unsafe {
            let mut size: u32 = 0;
            GetAdaptersInfo(std::ptr::null_mut(), &mut size);
            if size == 0 {
                return false;
            }

            let count = (size as usize) / std::mem::size_of::<IP_ADAPTER_INFO>() + 1;
            let mut adapters: Vec<IP_ADAPTER_INFO> = Vec::with_capacity(count);
            adapters.resize_with(count, || std::mem::zeroed());

            if GetAdaptersInfo(adapters.as_mut_ptr(), &mut size) != ERROR_SUCCESS {
                return false;
            }

            let mut ptr: *const IP_ADAPTER_INFO = adapters.as_ptr();
            while !ptr.is_null() {
                let a = &*ptr;
                if a.AddressLength as usize >= 6 {
                    let m = &a.Address[..6];
                    let oui = format!("{:02X}{:02X}{:02X}", m[0], m[1], m[2]);
                    if oui == oui_vbox
                        || oui == oui_vmware1
                        || oui == oui_vmware2
                        || oui == oui_qemu
                    {
                        return true;
                    }
                }
                ptr = a.Next;
            }
        }
        false
    }
    #[cfg(not(target_os = "windows"))]
    {
        false
    }
}