Merge pull request #1 from bllryy/bruh
almost done however kernel base scan messes up due to low cores
This commit is contained in:
commit
ebf987f0be
7 changed files with 461 additions and 54 deletions
120
src/eprocess.rs
Normal file
120
src/eprocess.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
use crate::kernel;
|
||||||
|
use crate::mem::GuestMem;
|
||||||
|
use crate::offsets;
|
||||||
|
use crate::paging;
|
||||||
|
|
||||||
|
pub struct Process {
|
||||||
|
pub eprocess_va: u64,
|
||||||
|
pub pid: u64,
|
||||||
|
pub dtb: u64,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the current EPROCESS via the KPCR on the vCPU whose GS.base is supplied.
|
||||||
|
///
|
||||||
|
/// SAFETY precondition: the vCPU must be in ring 0 (CS.selector & 3 == 0)
|
||||||
|
/// In ring 3 GS.base points at the TEB, not the KPCR, and the offsets are wrong
|
||||||
|
pub fn find_initial_eprocess(
|
||||||
|
mem: &GuestMem,
|
||||||
|
cr3: u64,
|
||||||
|
gs_base: u64,
|
||||||
|
) -> Result<u64, String> {
|
||||||
|
let kthread = paging::read_virt_u64(
|
||||||
|
mem, cr3,
|
||||||
|
gs_base + offsets::KPCR_PRCB + offsets::KPRCB_CURRENT_THREAD,
|
||||||
|
).map_err(|e| format!("read KPRCB.CurrentThread: {}", e))?;
|
||||||
|
|
||||||
|
if kthread < 0xffff_8000_0000_0000 {
|
||||||
|
return Err(format!("CurrentThread {:#x} is not a kernel VA", kthread));
|
||||||
|
}
|
||||||
|
|
||||||
|
let eprocess = paging::read_virt_u64(mem, cr3, kthread + offsets::KTHREAD_PROCESS)
|
||||||
|
.map_err(|e| format!("read KTHREAD.Process: {}", e))?;
|
||||||
|
|
||||||
|
if eprocess < 0xffff_8000_0000_0000 {
|
||||||
|
return Err(format!("EPROCESS {:#x} is not a kernel VA", eprocess));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(eprocess)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk the circular ActiveProcessLinks list starting from `initial_eprocess_va`.
|
||||||
|
///
|
||||||
|
/// Flink points into the *next* EPROCESS's links field, so subtracting
|
||||||
|
/// EPROC_LINKS recovers the EPROCESS base. The loop ends when Flink brings
|
||||||
|
/// us back to the starting links entry (full circle) or a bad VA is read.
|
||||||
|
pub fn walk_eprocess_list(
|
||||||
|
mem: &GuestMem,
|
||||||
|
cr3: u64,
|
||||||
|
initial_eprocess_va: u64,
|
||||||
|
) -> Vec<Process> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut current = initial_eprocess_va;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match read_process(mem, cr3, current) {
|
||||||
|
Some(p) => out.push(p),
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
|
||||||
|
let flink = match paging::read_virt_u64(mem, cr3, current + offsets::EPROC_LINKS) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flink is a VA pointing at the links field of the next node.
|
||||||
|
if flink < 0xffff_8000_0000_0000 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = flink - offsets::EPROC_LINKS;
|
||||||
|
|
||||||
|
// Full circle: Flink of last entry == links field of the first entry.
|
||||||
|
if next == initial_eprocess_va {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_process(mem: &GuestMem, cr3: u64, eprocess_va: u64) -> Option<Process> {
|
||||||
|
let pid = paging::read_virt_u64(mem, cr3, eprocess_va + offsets::EPROC_PID).ok()?;
|
||||||
|
let dtb = paging::read_virt_u64(mem, cr3, eprocess_va + offsets::EPROC_DTB).ok()?;
|
||||||
|
let name = read_name(mem, cr3, eprocess_va + offsets::EPROC_NAME);
|
||||||
|
|
||||||
|
Some(Process { eprocess_va, pid, dtb, name })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the System EPROCESS via `PsInitialSystemProcess` in the kernel export table.
|
||||||
|
/// Doesn't depend on KTHREAD.Process offset — works regardless of build.
|
||||||
|
///
|
||||||
|
/// PsInitialSystemProcess is a PEPROCESS (pointer to EPROCESS), so we read the
|
||||||
|
/// export VA to get the pointer, then dereference it to get the EPROCESS itself.
|
||||||
|
pub fn find_initial_eprocess_via_export(
|
||||||
|
mem: &GuestMem,
|
||||||
|
cr3: u64,
|
||||||
|
kernel_base: u64,
|
||||||
|
) -> Result<u64, String> {
|
||||||
|
let ps_va = kernel::find_export(mem, cr3, kernel_base, "PsInitialSystemProcess")
|
||||||
|
.ok_or("PsInitialSystemProcess not found in export table")?;
|
||||||
|
|
||||||
|
let eprocess = paging::read_virt_u64(mem, cr3, ps_va)
|
||||||
|
.map_err(|e| format!("deref PsInitialSystemProcess at {:#x}: {}", ps_va, e))?;
|
||||||
|
|
||||||
|
if eprocess < 0xffff_8000_0000_0000 {
|
||||||
|
return Err(format!("PsInitialSystemProcess value {:#x} is not a kernel VA", eprocess));
|
||||||
|
}
|
||||||
|
Ok(eprocess)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_name(mem: &GuestMem, cr3: u64, gva: u64) -> String {
|
||||||
|
let mut buf = [0u8; 15];
|
||||||
|
if paging::read_virt(mem, cr3, gva, &mut buf).is_err() {
|
||||||
|
return String::from("?");
|
||||||
|
}
|
||||||
|
let len = buf.iter().position(|&b| b == 0).unwrap_or(15);
|
||||||
|
String::from_utf8_lossy(&buf[..len]).into_owned()
|
||||||
|
}
|
||||||
109
src/kernel.rs
Normal file
109
src/kernel.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
use crate::mem::GuestMem;
|
||||||
|
use crate::paging;
|
||||||
|
|
||||||
|
// Windows KASLR aligns the kernel load address to 2 MiB boundaries.
|
||||||
|
const STEP_2M: u64 = 0x200_000;
|
||||||
|
const MAX_SCAN: u64 = 0x4000_0000; // 1 GiB
|
||||||
|
|
||||||
|
/// Find ntoskrnl base from a VA known to be inside it.
|
||||||
|
///
|
||||||
|
/// Two-pass strategy:
|
||||||
|
/// 1. Coarse: 2 MiB steps over 1 GiB (fast, handles standard KASLR alignment).
|
||||||
|
/// 2. Fine: 4 KiB steps over 32 MiB (fallback if the base is not 2 MiB-aligned).
|
||||||
|
/// Each candidate is validated against the `PsInitialSystemProcess` export so we
|
||||||
|
/// don't mistake a driver or HAL for ntoskrnl.
|
||||||
|
pub fn find_kernel_base(mem: &GuestMem, cr3: u64, hint_va: u64) -> Option<u64> {
|
||||||
|
if let Some(b) = scan(mem, cr3, hint_va, STEP_2M, MAX_SCAN) {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
scan(mem, cr3, hint_va, 0x1000, 0x200_0000) // 4 KiB steps, 32 MiB
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan(mem: &GuestMem, cr3: u64, hint_va: u64, step: u64, range: u64) -> Option<u64> {
|
||||||
|
let mut addr = hint_va & !(step - 1);
|
||||||
|
let floor = hint_va.saturating_sub(range) & !(step - 1);
|
||||||
|
|
||||||
|
while addr >= floor {
|
||||||
|
if is_pe_header(mem, cr3, addr) &&
|
||||||
|
find_export(mem, cr3, addr, "PsInitialSystemProcess").is_some()
|
||||||
|
{
|
||||||
|
return Some(addr);
|
||||||
|
}
|
||||||
|
if addr < step { break; }
|
||||||
|
addr -= step;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an x64 IDT gate entry and return the full 64-bit handler VA.
|
||||||
|
///
|
||||||
|
/// x64 IDT entry layout (16 bytes):
|
||||||
|
/// +0 u16 offset[15:0]
|
||||||
|
/// +2 u16 selector
|
||||||
|
/// +4 u8 ist
|
||||||
|
/// +5 u8 type/attr
|
||||||
|
/// +6 u16 offset[31:16]
|
||||||
|
/// +8 u32 offset[63:32]
|
||||||
|
/// +12 u32 reserved
|
||||||
|
pub fn read_idt_handler(mem: &GuestMem, cr3: u64, idt_va: u64, idx: u64) -> Option<u64> {
|
||||||
|
let mut e = [0u8; 16];
|
||||||
|
paging::read_virt(mem, cr3, idt_va + idx * 16, &mut e).ok()?;
|
||||||
|
let lo = u16::from_le_bytes([e[0], e[1]]) as u64;
|
||||||
|
let mid = u16::from_le_bytes([e[6], e[7]]) as u64;
|
||||||
|
let hi = u32::from_le_bytes([e[8], e[9], e[10], e[11]]) as u64;
|
||||||
|
Some((hi << 32) | (mid << 16) | lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_pe_header(mem: &GuestMem, cr3: u64, va: u64) -> bool {
|
||||||
|
let mut mz = [0u8; 2];
|
||||||
|
if paging::read_virt(mem, cr3, va, &mut mz).is_err() { return false; }
|
||||||
|
if mz != [0x4D, 0x5A] { return false; }
|
||||||
|
|
||||||
|
let pe_off = match paging::read_virt_u32(mem, cr3, va + 0x3C) {
|
||||||
|
Ok(v) if v < 0x1000 => v as u64,
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut sig = [0u8; 4];
|
||||||
|
if paging::read_virt(mem, cr3, va + pe_off, &mut sig).is_err() { return false; }
|
||||||
|
sig == [0x50, 0x45, 0x00, 0x00]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a named export from the PE image at `base_va`.
|
||||||
|
/// For data exports (like PsInitialSystemProcess) this is the variable's VA;
|
||||||
|
/// the caller must dereference it to get the pointed-to value.
|
||||||
|
///
|
||||||
|
/// IMAGE_OPTIONAL_HEADER64 starts at pe_off + 4 (PE sig) + 20 (IMAGE_FILE_HEADER).
|
||||||
|
/// DataDirectory[0] (export) is at optional_header + 0x70.
|
||||||
|
pub fn find_export(mem: &GuestMem, cr3: u64, base_va: u64, target: &str) -> Option<u64> {
|
||||||
|
let pe_off = paging::read_virt_u32(mem, cr3, base_va + 0x3C).ok()? as u64;
|
||||||
|
|
||||||
|
let export_rva = paging::read_virt_u32(mem, cr3, base_va + pe_off + 0x18 + 0x70).ok()? as u64;
|
||||||
|
if export_rva == 0 { return None; }
|
||||||
|
let export_va = base_va + export_rva;
|
||||||
|
|
||||||
|
let num_names = paging::read_virt_u32(mem, cr3, export_va + 0x18).ok()? as u64;
|
||||||
|
let names_rva = paging::read_virt_u32(mem, cr3, export_va + 0x20).ok()? as u64;
|
||||||
|
let ords_rva = paging::read_virt_u32(mem, cr3, export_va + 0x24).ok()? as u64;
|
||||||
|
let funcs_rva = paging::read_virt_u32(mem, cr3, export_va + 0x1C).ok()? as u64;
|
||||||
|
|
||||||
|
for i in 0..num_names {
|
||||||
|
let name_rva = paging::read_virt_u32(mem, cr3, base_va + names_rva + i * 4).ok()? as u64;
|
||||||
|
if !name_matches(mem, cr3, base_va + name_rva, target) { continue; }
|
||||||
|
|
||||||
|
let mut ord_bytes = [0u8; 2];
|
||||||
|
paging::read_virt(mem, cr3, base_va + ords_rva + i * 2, &mut ord_bytes).ok()?;
|
||||||
|
let ordinal = u16::from_le_bytes(ord_bytes) as u64;
|
||||||
|
|
||||||
|
let func_rva = paging::read_virt_u32(mem, cr3, base_va + funcs_rva + ordinal * 4).ok()? as u64;
|
||||||
|
return Some(base_va + func_rva);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name_matches(mem: &GuestMem, cr3: u64, va: u64, target: &str) -> bool {
|
||||||
|
let mut buf = [0u8; 64];
|
||||||
|
if paging::read_virt(mem, cr3, va, &mut buf).is_err() { return false; }
|
||||||
|
let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
|
||||||
|
&buf[..len] == target.as_bytes()
|
||||||
|
}
|
||||||
86
src/main.rs
86
src/main.rs
|
|
@ -1,6 +1,9 @@
|
||||||
mod asm;
|
mod asm;
|
||||||
|
mod eprocess;
|
||||||
|
mod kernel;
|
||||||
mod kvm;
|
mod kvm;
|
||||||
mod mem;
|
mod mem;
|
||||||
|
mod offsets;
|
||||||
mod paging;
|
mod paging;
|
||||||
mod ptrace_inject;
|
mod ptrace_inject;
|
||||||
mod qemu;
|
mod qemu;
|
||||||
|
|
@ -10,31 +13,82 @@ fn main() {
|
||||||
println!("[+] pid={} vm_fd={} vcpu_fds={:?} remote={:?}",
|
println!("[+] pid={} vm_fd={} vcpu_fds={:?} remote={:?}",
|
||||||
vm.pid, vm.vm_fd, vm.vcpu_fds, vm.vcpu_fds_remote);
|
vm.pid, vm.vm_fd, vm.vcpu_fds, vm.vcpu_fds_remote);
|
||||||
|
|
||||||
vm.freeze();
|
|
||||||
let sregs = kvm::get_sregs_injected(vm.pid, vm.vcpu_fds_remote[0])
|
|
||||||
.expect("KVM_GET_SREGS injection failed");
|
|
||||||
let regs = kvm::get_regs_injected(vm.pid, vm.vcpu_fds_remote[0])
|
|
||||||
.expect("KVM_GET_REGS injection failed");
|
|
||||||
vm.thaw();
|
|
||||||
println!("[+] vcpu0 CR3={:#018x}", sregs.cr3);
|
|
||||||
println!("[+] vcpu0 RIP={:#018x} RSP={:#018x}", regs.rip, regs.rsp);
|
|
||||||
|
|
||||||
let (eax, _, _, _) = asm::cpuid(0, 0);
|
let (eax, _, _, _) = asm::cpuid(0, 0);
|
||||||
println!("[+] CPUID max_leaf={:#x} svm={}", eax, asm::svm_supported());
|
println!("[+] CPUID max_leaf={:#x} svm={}", eax, asm::svm_supported());
|
||||||
|
|
||||||
|
// Try each vCPU looking for one in ring 0 (kernel mode).
|
||||||
|
// Windows KVAS gives user-mode vCPUs a shadow CR3 with no kernel mapping;
|
||||||
|
// the kernel scan only works from a ring-0 CR3.
|
||||||
|
// inject_ioctl does PTRACE_DETACH after each call which resumes the main
|
||||||
|
// thread — re-freeze before the next injection to put it back into group-stop.
|
||||||
|
let mut found = None;
|
||||||
|
for i in 0..vm.vcpu_fds_remote.len() {
|
||||||
|
vm.freeze();
|
||||||
|
let Ok(sregs) = kvm::get_sregs_injected(vm.pid, vm.vcpu_fds_remote[i]) else { continue };
|
||||||
|
let cpl = sregs.cs.selector & 3;
|
||||||
|
if cpl != 0 {
|
||||||
|
println!("[~] vCPU{} CPL={} (user mode), skipping", i, cpl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
vm.freeze();
|
||||||
|
let Ok(regs) = kvm::get_regs_injected(vm.pid, vm.vcpu_fds_remote[i]) else { continue };
|
||||||
|
found = Some((sregs, regs, i));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
vm.thaw();
|
||||||
|
|
||||||
|
let (sregs, regs, vcpu_idx) = match found {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
println!("[-] no vCPU found in ring 0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("[+] vCPU{} ring0: CR3={:#018x} RIP={:#018x} RSP={:#018x}",
|
||||||
|
vcpu_idx, sregs.cr3, regs.rip, regs.rsp);
|
||||||
|
|
||||||
let guest = mem::GuestMem::attach(vm.pid).expect("failed to map guest RAM");
|
let guest = mem::GuestMem::attach(vm.pid).expect("failed to map guest RAM");
|
||||||
for r in &guest.regions {
|
for r in &guest.regions {
|
||||||
println!("[+] mem region GPA {:#010x}..{:#010x} (HVA {:#010x})",
|
println!("[+] mem region GPA {:#010x}..{:#010x} (HVA {:#010x})",
|
||||||
r.gpa_base, r.gpa_base + r.size, r.hva_base);
|
r.gpa_base, r.gpa_base + r.size, r.hva_base);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smoke-test the page table walker: translate CR3 itself (it's a GPA, not
|
|
||||||
// a GVA, so we translate an arbitrary kernel VA to prove the walk works)
|
|
||||||
// Real EPROCESS walk comes next
|
|
||||||
let cr3 = sregs.cr3;
|
let cr3 = sregs.cr3;
|
||||||
println!("[+] CR3 (guest PML4 GPA) = {:#018x}", cr3);
|
// Primary hint: RIP (usually inside ntoskrnl).
|
||||||
match paging::translate(&guest, cr3, sregs.cr3) {
|
// Fallback: IDT entry 0 = KiDivideErrorFault, which is always in ntoskrnl
|
||||||
Ok(gpa) => println!("[+] translate(CR3 as GVA) -> GPA {:#018x}", gpa),
|
// and is typically within the first few MiB of the image.
|
||||||
Err(e) => println!("[-] translate: {}", e),
|
let idt_handler = kernel::read_idt_handler(&guest, cr3, sregs.idt.base, 0);
|
||||||
|
println!("[+] IDT base={:#018x} entry[0]={:#018x}",
|
||||||
|
sregs.idt.base,
|
||||||
|
idt_handler.unwrap_or(0));
|
||||||
|
|
||||||
|
let hints: &[u64] = &[
|
||||||
|
regs.rip,
|
||||||
|
idt_handler.unwrap_or(0),
|
||||||
|
];
|
||||||
|
let base_opt = hints.iter()
|
||||||
|
.filter(|&&h| h >= 0xffff_8000_0000_0000)
|
||||||
|
.find_map(|&h| kernel::find_kernel_base(&guest, cr3, h));
|
||||||
|
|
||||||
|
match base_opt {
|
||||||
|
None => println!("[-] kernel base scan failed"),
|
||||||
|
Some(base) => {
|
||||||
|
println!("[+] kernel base = {:#018x}", base);
|
||||||
|
match eprocess::find_initial_eprocess_via_export(&guest, cr3, base) {
|
||||||
|
Err(e) => println!("[-] PsInitialSystemProcess: {}", e),
|
||||||
|
Ok(ep) => {
|
||||||
|
println!("[+] System EPROCESS = {:#018x}", ep);
|
||||||
|
let procs = eprocess::walk_eprocess_list(&guest, cr3, ep);
|
||||||
|
println!("[+] {} processes:", procs.len());
|
||||||
|
println!(" {:>6} {:>18} {:>18} name",
|
||||||
|
"PID", "EPROCESS", "DTB");
|
||||||
|
for p in &procs {
|
||||||
|
println!(" {:>6} {:#018x} {:#018x} {}",
|
||||||
|
p.pid, p.eprocess_va, p.dtb, p.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
src/offsets.rs
Normal file
18
src/offsets.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Windows 11 22H2 build 22621.x x64
|
||||||
|
// Derived from public PDB symbols via WinDbg:
|
||||||
|
// dt nt!_KPCR; dt nt!_KPRCB; dt nt!_KTHREAD; dt nt!_EPROCESS
|
||||||
|
|
||||||
|
// KPCR → embedded KPRCB → CurrentThread
|
||||||
|
pub const KPCR_PRCB: u64 = 0x180; // _KPCR.Prcb (embedded _KPRCB)
|
||||||
|
pub const KPRCB_CURRENT_THREAD: u64 = 0x008; // _KPRCB.CurrentThread
|
||||||
|
|
||||||
|
// KTHREAD → owning process
|
||||||
|
// _KTHREAD.Process (Ptr64 → _KPROCESS = _EPROCESS base on x64)
|
||||||
|
pub const KTHREAD_PROCESS: u64 = 0x220;
|
||||||
|
|
||||||
|
// _EPROCESS / embedded _KPROCESS fields
|
||||||
|
pub const EPROC_DTB: u64 = 0x028; // _KPROCESS.DirectoryTableBase
|
||||||
|
pub const EPROC_PID: u64 = 0x440; // _EPROCESS.UniqueProcessId
|
||||||
|
pub const EPROC_LINKS: u64 = 0x448; // _EPROCESS.ActiveProcessLinks (Flink)
|
||||||
|
pub const EPROC_NAME: u64 = 0x5A8; // _EPROCESS.ImageFileName[15]
|
||||||
|
pub const EPROC_VAD_ROOT: u64 = 0x7D8; // _EPROCESS.VadRoot (RTL_AVL_TREE)
|
||||||
|
|
@ -26,8 +26,59 @@ fn mem_read(pid: u32, addr: u64, buf: &mut [u8]) -> std::io::Result<()> {
|
||||||
f.read_exact(buf)
|
f.read_exact(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search the target process's executable mappings for a `syscall` instruction
|
||||||
|
/// (0x0f 0x05) and return its virtual address.
|
||||||
|
///
|
||||||
|
/// Writing shellcode to the stack is blocked by the NX (no-execute) bit:
|
||||||
|
/// attempting to execute it raises SIGSEGV, which QEMU's signal handler converts
|
||||||
|
/// to SIGKILL (status=0x9 from waitpid). Using an existing gadget in already-
|
||||||
|
/// executable memory avoids this entirely.
|
||||||
|
///
|
||||||
|
/// The vdso is tried first — it's tiny, always present, and always contains
|
||||||
|
/// `syscall` — then all other executable regions are searched.
|
||||||
|
fn find_syscall_gadget(pid: u32) -> Option<u64> {
|
||||||
|
let maps = std::fs::read_to_string(format!("/proc/{}/maps", pid)).ok()?;
|
||||||
|
let mut mem = File::open(format!("/proc/{}/mem", pid)).ok()?;
|
||||||
|
|
||||||
|
// Collect executable regions; float the vdso to the front.
|
||||||
|
let mut vdso: Vec<(u64, u64)> = Vec::new();
|
||||||
|
let mut rest: Vec<(u64, u64)> = Vec::new();
|
||||||
|
|
||||||
|
for line in maps.lines() {
|
||||||
|
let cols: Vec<&str> = line.splitn(6, ' ').collect();
|
||||||
|
if cols.len() < 2 { continue; }
|
||||||
|
if !cols[1].contains('x') { continue; }
|
||||||
|
|
||||||
|
let mut parts = cols[0].split('-');
|
||||||
|
let Ok(start) = u64::from_str_radix(parts.next().unwrap_or(""), 16) else { continue };
|
||||||
|
let Ok(end) = u64::from_str_radix(parts.next().unwrap_or(""), 16) else { continue };
|
||||||
|
if end <= start { continue; }
|
||||||
|
|
||||||
|
if cols.get(5).map_or(false, |p| p.trim() == "[vdso]") {
|
||||||
|
vdso.push((start, end));
|
||||||
|
} else {
|
||||||
|
rest.push((start, end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (start, end) in vdso.into_iter().chain(rest) {
|
||||||
|
// Cap per-region scan to 4 MiB so startup stays fast.
|
||||||
|
let size = ((end - start) as usize).min(4 << 20);
|
||||||
|
let mut buf = vec![0u8; size];
|
||||||
|
if mem.seek(SeekFrom::Start(start)).is_err() { continue; }
|
||||||
|
if mem.read_exact(&mut buf).is_err() { continue; }
|
||||||
|
|
||||||
|
for i in 0..buf.len().saturating_sub(1) {
|
||||||
|
if buf[i] == 0x0f && buf[i + 1] == 0x05 {
|
||||||
|
return Some(start + i as u64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Inject `ioctl(remote_fd, ioctl_nr, ptr)` into the main QEMU thread (TID=PID)
|
/// Inject `ioctl(remote_fd, ioctl_nr, ptr)` into the main QEMU thread (TID=PID)
|
||||||
/// using a `syscall; int3` gadget placed below the SysV red zone.
|
/// using an existing `syscall` gadget found in QEMU's already-executable memory.
|
||||||
///
|
///
|
||||||
/// **Caller must call freeze() (SIGSTOP) before this function.**
|
/// **Caller must call freeze() (SIGSTOP) before this function.**
|
||||||
/// SIGSTOP guarantees every vCPU thread has exited KVM_RUN and called vcpu_put(),
|
/// SIGSTOP guarantees every vCPU thread has exited KVM_RUN and called vcpu_put(),
|
||||||
|
|
@ -35,13 +86,17 @@ fn mem_read(pid: u32, addr: u64, buf: &mut [u8]) -> std::io::Result<()> {
|
||||||
/// The injected ioctl then runs inside QEMU's mm so the kernel mm-check passes,
|
/// The injected ioctl then runs inside QEMU's mm so the kernel mm-check passes,
|
||||||
/// and can acquire vcpu->mutex (which is free) to read the register state.
|
/// and can acquire vcpu->mutex (which is free) to read the register state.
|
||||||
///
|
///
|
||||||
/// PTRACE_ATTACH is used instead of PTRACE_SEIZE+PTRACE_INTERRUPT because:
|
/// PTRACE_SINGLESTEP is used instead of a `syscall; int3` shellcode gadget because:
|
||||||
/// - The vCPU threads block SIGTRAP in their signal mask, making int3 unreliable.
|
/// - The stack is NX (no-execute); writing code there and jumping to it raises
|
||||||
/// - PTRACE_CONT on PTRACE_SEIZE'd threads in group-stop immediately re-stops.
|
/// SIGSEGV, which QEMU's signal handler escalates to SIGKILL.
|
||||||
/// - PTRACE_ATTACH on the main thread (which has a normal signal mask and is
|
/// - PTRACE_SINGLESTEP sets the TF (Trap Flag) and executes exactly one
|
||||||
/// already group-stopped) gives a clean ptrace-stop that PTRACE_CONT resumes
|
/// instruction — the `syscall` — then stops, giving the same "stop after ioctl"
|
||||||
/// correctly, escaping the group-stop for that one thread while the rest stay
|
/// semantics without touching executable memory.
|
||||||
/// stopped.
|
///
|
||||||
|
/// PTRACE_SEIZE is used instead of PTRACE_ATTACH for the same reasons as before:
|
||||||
|
/// it reports the existing group-stop immediately so waitpid returns right away,
|
||||||
|
/// and PTRACE_CONT data=0 on the seized thread escapes group-stop for that one
|
||||||
|
/// thread while the rest stay stopped.
|
||||||
pub fn inject_ioctl(
|
pub fn inject_ioctl(
|
||||||
pid: u32,
|
pid: u32,
|
||||||
remote_fd: RawFd,
|
remote_fd: RawFd,
|
||||||
|
|
@ -52,10 +107,9 @@ pub fn inject_ioctl(
|
||||||
let null = 0usize as *mut c_void;
|
let null = 0usize as *mut c_void;
|
||||||
let tid = pid as pid_t;
|
let tid = pid as pid_t;
|
||||||
|
|
||||||
// PTRACE_ATTACH sends SIGSTOP internally; on an already group-stopped thread
|
let gadget_addr = find_syscall_gadget(pid)
|
||||||
// this does NOT generate a new ptrace-stop event, so the subsequent waitpid
|
.ok_or("no syscall gadget found in target executable mappings")?;
|
||||||
// blocks forever. PTRACE_SEIZE instead reports the *existing* group-stop as
|
|
||||||
// an immediate notification — waitpid returns right away.
|
|
||||||
let r = unsafe { libc::ptrace(PTRACE_SEIZE as _, tid, null, null) };
|
let r = unsafe { libc::ptrace(PTRACE_SEIZE as _, tid, null, null) };
|
||||||
if r != 0 {
|
if r != 0 {
|
||||||
return Err(format!("PTRACE_SEIZE: {}", std::io::Error::last_os_error()).into());
|
return Err(format!("PTRACE_SEIZE: {}", std::io::Error::last_os_error()).into());
|
||||||
|
|
@ -77,14 +131,9 @@ pub fn inject_ioctl(
|
||||||
return Err(format!("PTRACE_GETREGS: {}", std::io::Error::last_os_error()).into());
|
return Err(format!("PTRACE_GETREGS: {}", std::io::Error::last_os_error()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scratch layout below the 128-byte SysV AMD64 red zone (descending):
|
// Place the ioctl output struct below the SysV AMD64 red zone.
|
||||||
// gadget_addr : 0f 05 cc (syscall; int3) — 8-byte aligned
|
// The stack is writable (just not executable), so this is fine for data.
|
||||||
// struct_addr : zeroed ioctl struct — 16-byte aligned
|
let struct_addr = (orig.rsp - 128 - struct_size as u64) & !15u64;
|
||||||
// rsp is left unchanged: neither `syscall` nor `int3` touch the user stack.
|
|
||||||
let gadget_addr = (orig.rsp - 128 - 8) & !7u64;
|
|
||||||
let struct_addr = (gadget_addr - struct_size as u64) & !15u64;
|
|
||||||
|
|
||||||
mem_write(pid, gadget_addr, &[0x0f, 0x05, 0xcc])?;
|
|
||||||
mem_write(pid, struct_addr, &vec![0u8; struct_size])?;
|
mem_write(pid, struct_addr, &vec![0u8; struct_size])?;
|
||||||
|
|
||||||
let mut regs = orig;
|
let mut regs = orig;
|
||||||
|
|
@ -92,7 +141,7 @@ pub fn inject_ioctl(
|
||||||
regs.rdi = remote_fd as u64;
|
regs.rdi = remote_fd as u64;
|
||||||
regs.rsi = ioctl_nr;
|
regs.rsi = ioctl_nr;
|
||||||
regs.rdx = struct_addr;
|
regs.rdx = struct_addr;
|
||||||
regs.rip = gadget_addr;
|
regs.rip = gadget_addr; // existing executable `syscall` instruction
|
||||||
|
|
||||||
let r = unsafe {
|
let r = unsafe {
|
||||||
libc::ptrace(PTRACE_SETREGS as _, tid, null, ®s as *const _ as *mut c_void)
|
libc::ptrace(PTRACE_SETREGS as _, tid, null, ®s as *const _ as *mut c_void)
|
||||||
|
|
@ -102,19 +151,25 @@ pub fn inject_ioctl(
|
||||||
return Err(format!("PTRACE_SETREGS: {}", std::io::Error::last_os_error()).into());
|
return Err(format!("PTRACE_SETREGS: {}", std::io::Error::last_os_error()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// PTRACE_CONT with data=0 suppresses the pending SIGSTOP for this thread,
|
// PTRACE_SINGLESTEP sets TF and executes exactly one instruction.
|
||||||
// resuming it while every other thread stays in group-stop.
|
// data=0 on the first call suppresses the pending SIGSTOP, escaping group-stop.
|
||||||
// Loop until rip passes the 2-byte `syscall` instruction (gadget_addr+2).
|
// After `syscall` completes and sysret restores RFLAGS (with TF set), the
|
||||||
// Stops at gadget_addr itself are signal-delivery-stops before the syscall;
|
// CPU raises #DB and the thread stops with rip = gadget_addr + 2.
|
||||||
// suppress each and retry.
|
// If a different signal interrupts before the instruction executes (rip still
|
||||||
|
// at gadget_addr), suppress it and retry.
|
||||||
let mut ret_regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
|
let mut ret_regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
|
||||||
loop {
|
loop {
|
||||||
unsafe { libc::ptrace(libc::PTRACE_CONT, tid, null, null) };
|
unsafe { libc::ptrace(libc::PTRACE_SINGLESTEP, tid, null, null) };
|
||||||
unsafe { libc::waitpid(tid, &mut status, libc::__WALL) };
|
unsafe { libc::waitpid(tid, &mut status, libc::__WALL) };
|
||||||
|
if libc::WIFEXITED(status) || libc::WIFSIGNALED(status) {
|
||||||
|
return Err(format!("QEMU exited during injection (status={:#x})", status).into());
|
||||||
|
}
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::ptrace(PTRACE_GETREGS as _, tid, null, &mut ret_regs as *mut _ as *mut c_void)
|
libc::ptrace(PTRACE_GETREGS as _, tid, null, &mut ret_regs as *mut _ as *mut c_void)
|
||||||
};
|
};
|
||||||
if ret_regs.rip >= gadget_addr + 2 { break; }
|
if ret_regs.rip == gadget_addr + 2 {
|
||||||
|
break; // syscall completed; rip now points past the 2-byte `syscall`
|
||||||
|
}
|
||||||
if ret_regs.rip != gadget_addr {
|
if ret_regs.rip != gadget_addr {
|
||||||
unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, null, null) };
|
unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, null, null) };
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|
@ -122,6 +177,8 @@ pub fn inject_ioctl(
|
||||||
ret_regs.rip, gadget_addr
|
ret_regs.rip, gadget_addr
|
||||||
).into());
|
).into());
|
||||||
}
|
}
|
||||||
|
// rip == gadget_addr: a signal fired before the instruction; suppressed
|
||||||
|
// by passing data=0 in the next PTRACE_SINGLESTEP.
|
||||||
}
|
}
|
||||||
|
|
||||||
let syscall_ret = ret_regs.rax as i64;
|
let syscall_ret = ret_regs.rax as i64;
|
||||||
|
|
|
||||||
32
src/qemu.rs
32
src/qemu.rs
|
|
@ -27,19 +27,31 @@ impl Drop for QemuVm {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QemuVm {
|
impl QemuVm {
|
||||||
/// SIGSTOP the QEMU process and spin until /proc/<pid>/status shows State: T.
|
/// SIGSTOP the QEMU process and spin until every thread in /proc/<pid>/task/
|
||||||
/// Required before KVM_GET_REGS / KVM_GET_SREGS — the kernel returns EIO
|
/// shows State: T (stopped) or t (tracing stop).
|
||||||
/// if the vCPU thread is actively inside KVM_RUN when you call those ioctls.
|
///
|
||||||
/// SIGSTOP is async; the threads only exit KVM_RUN once the kernel delivers it.
|
/// Checking the main thread alone is insufficient: vCPU threads run inside
|
||||||
|
/// KVM_RUN (a blocking ioctl) and must exit before they enter group-stop and
|
||||||
|
/// release vcpu->mutex. Spinning until all TIDs are stopped ensures the mutex
|
||||||
|
/// is free before the injected ioctl tries to acquire it.
|
||||||
|
///
|
||||||
|
/// Previous bug: the check used `l.contains('t')` which is always true because
|
||||||
|
/// "State:" itself contains the letter 't' — freeze() returned immediately.
|
||||||
pub fn freeze(&self) {
|
pub fn freeze(&self) {
|
||||||
unsafe { libc::kill(self.pid as libc::pid_t, libc::SIGSTOP); }
|
unsafe { libc::kill(self.pid as libc::pid_t, libc::SIGSTOP); }
|
||||||
let status_path = format!("/proc/{}/status", self.pid);
|
let task_dir = format!("/proc/{}/task", self.pid);
|
||||||
loop {
|
loop {
|
||||||
let Ok(s) = fs::read_to_string(&status_path) else { break };
|
let Ok(dir) = fs::read_dir(&task_dir) else { break };
|
||||||
// "State:\tT (stopped)" or "State:\tt (tracing stop)"
|
let all_stopped = dir
|
||||||
if s.lines().any(|l| l.starts_with("State:") && (l.contains('T') || l.contains('t'))) {
|
.filter_map(|e| e.ok())
|
||||||
break;
|
.all(|e| {
|
||||||
}
|
let path = format!("{}/{}/status",
|
||||||
|
task_dir, e.file_name().to_string_lossy());
|
||||||
|
let Ok(s) = fs::read_to_string(&path) else { return true };
|
||||||
|
// Match "State:\tT ..." or "State:\tt ..." only — not the word "State" itself.
|
||||||
|
s.lines().any(|l| l.starts_with("State:\tT") || l.starts_with("State:\tt"))
|
||||||
|
});
|
||||||
|
if all_stopped { break; }
|
||||||
std::hint::spin_loop();
|
std::hint::spin_loop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
src/stubs.s
37
src/stubs.s
|
|
@ -62,3 +62,40 @@ wrmsr:
|
||||||
shrq $32, %rdx # high 32 bits
|
shrq $32, %rdx # high 32 bits
|
||||||
wrmsr
|
wrmsr
|
||||||
ret
|
ret
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
##
|
||||||
|
##
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
##
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# kurva
|
||||||
Loading…
Add table
Reference in a new issue