more changes
This commit is contained in:
parent
2649fd48f4
commit
e8c96a4f00
11 changed files with 384 additions and 127 deletions
32
CLAUDE.MD
Normal file
32
CLAUDE.MD
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# KVM Hypervisor Introspector
|
||||
|
||||
## Project
|
||||
Rust-based KVM hypervisor introspector targeting a quickemu Windows 11 guest VM.
|
||||
Performs live memory forensics from outside the guest OS (ring -1).
|
||||
|
||||
## Goals
|
||||
- Walk guest EPROCESS list via CR3 + DirectoryTableBase
|
||||
- Cross-view diff EPROCESS vs PspCidTable to detect DKOM-hidden processes
|
||||
- Find rwx / non-image-backed VADs
|
||||
- Inline asm for VMREAD/VMWRITE, CR3 manipulation, VMX instruction wrappers
|
||||
- Or asm outside of inline
|
||||
- possibly finding a vuln in qemu or quickemu however this would be difficult to do
|
||||
|
||||
## Stack
|
||||
- Rust, `kvm-ioctls`, `vmm-sys-util`, `iced-x86`
|
||||
- Target guest: quickemu Windows 11 VM on arch-uwu
|
||||
- KVM via /dev/kvm ioctls
|
||||
|
||||
## Code Style
|
||||
- No unnecessary comments inside of the code
|
||||
- Unsafe blocks are expected and fine — document WHY not WHAT
|
||||
- Inline asm preferred over wrappers where it's cleaner
|
||||
- No clippy noise about unsafe
|
||||
|
||||
## Key Paths
|
||||
- Guest VM socket/pid: resolve from quickemu at runtime
|
||||
- Windows kernel offsets: hardcode initially for a known Win11 build, document the build
|
||||
|
||||
## Context
|
||||
This is hackathon work. Prioritize working PoC over perfect architecture.
|
||||
Security research / digital forensics framing.
|
||||
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -6,6 +6,7 @@ version = 4
|
|||
name = "KVM-hypervisor-introspector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"kvm-bindings",
|
||||
"kvm-ioctls",
|
||||
"libc",
|
||||
|
|
@ -24,6 +25,22 @@ version = "2.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "kvm-bindings"
|
||||
version = "0.11.1"
|
||||
|
|
@ -51,6 +68,12 @@ version = "0.2.184"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "vmm-sys-util"
|
||||
version = "0.12.1"
|
||||
|
|
|
|||
|
|
@ -9,5 +9,8 @@ kvm-bindings = "0.11"
|
|||
vmm-sys-util = "0.12"
|
||||
libc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
|
|
|||
6
build.rs
Normal file
6
build.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
fn main() {
|
||||
cc::Build::new()
|
||||
.file("src/stubs.s")
|
||||
.compile("stubs");
|
||||
println!("cargo:rerun-if-changed=src/stubs.s");
|
||||
}
|
||||
121
src/asm.rs
121
src/asm.rs
|
|
@ -2,11 +2,11 @@
|
|||
//
|
||||
// Userspace-callable: rdtsc, cpuid, svm_supported
|
||||
// Ring-0 stubs: read_cr3, write_cr3, vmload, vmsave, vmrun, rdmsr, wrmsr
|
||||
// — these are here for when this code runs in a kernel context.
|
||||
// Calling them from userspace will #GP or #UD.
|
||||
// these are here for when this code runs in a kernel context
|
||||
// Calling them from userspace will #GP or #UD
|
||||
|
||||
/// Read the hardware timestamp counter.
|
||||
/// Used for timing guest memory access patterns (cache-timing side channels).
|
||||
/// Read the hardware timestamp counter (TSC)
|
||||
/// Used for timing guest memory access patterns (cache-timing side channels)
|
||||
#[inline(always)]
|
||||
pub fn rdtsc() -> u64 {
|
||||
let lo: u32;
|
||||
|
|
@ -42,107 +42,28 @@ pub fn cpuid(leaf: u32, subleaf: u32) -> (u32, u32, u32, u32) {
|
|||
(eax, ebx, ecx, edx)
|
||||
}
|
||||
|
||||
/// Possible delete only used to check for AMD SVM
|
||||
/// Check whether the host CPU supports AMD SVM (CPUID 0x80000001 ECX bit 2).
|
||||
pub fn svm_supported() -> bool {
|
||||
let (_, _, ecx, _) = cpuid(0x8000_0001, 0);
|
||||
ecx & (1 << 2) != 0
|
||||
}
|
||||
|
||||
// ── ring-0 stubs ─────────────────────────────────────────────────────────────
|
||||
// All of these should fault at CPL > 0 (#GP or #UD).
|
||||
|
||||
/// Read host CR3. Ring-0 only — #GP in CPL > 0.
|
||||
/// Returns the physical address of the host PML4.
|
||||
#[inline(always)]
|
||||
pub unsafe fn read_cr3() -> u64 {
|
||||
let val: u64;
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"mov {}, cr3",
|
||||
out(reg) val,
|
||||
options(nomem, nostack, preserves_flags),
|
||||
);
|
||||
}
|
||||
val
|
||||
}
|
||||
|
||||
/// Write host CR3 — flushes all non-global TLB entries. Ring-0 only.
|
||||
#[inline(always)]
|
||||
pub unsafe fn write_cr3(cr3: u64) {
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"mov cr3, {}",
|
||||
in(reg) cr3,
|
||||
options(nomem, nostack, preserves_flags),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// VMLOAD — loads guest state from the VMCB at the given physical address.
|
||||
/// AMD SVM, ring-0 only.
|
||||
#[inline(always)]
|
||||
pub unsafe fn vmload(vmcb_pa: u64) {
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"vmload rax",
|
||||
in("rax") vmcb_pa,
|
||||
options(nostack),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// VMSAVE — saves guest state back to the VMCB at the given physical address.
|
||||
/// AMD SVM, ring-0 only.
|
||||
#[inline(always)]
|
||||
pub unsafe fn vmsave(vmcb_pa: u64) {
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"vmsave rax",
|
||||
in("rax") vmcb_pa,
|
||||
options(nostack),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// VMRUN — enters the guest. Does not return until a #VMEXIT occurs.
|
||||
/// AMD SVM, ring-0 only.
|
||||
#[inline(always)]
|
||||
pub unsafe fn vmrun(vmcb_pa: u64) {
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"vmrun rax",
|
||||
inout("rax") vmcb_pa => _,
|
||||
options(nostack),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// RDMSR — read model-specific register. Ring-0 only.
|
||||
#[inline(always)]
|
||||
pub unsafe fn rdmsr(msr: u32) -> u64 {
|
||||
let lo: u32;
|
||||
let hi: u32;
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"rdmsr",
|
||||
in("ecx") msr,
|
||||
out("eax") lo,
|
||||
out("edx") hi,
|
||||
options(nomem, nostack, preserves_flags),
|
||||
);
|
||||
}
|
||||
((hi as u64) << 32) | lo as u64
|
||||
}
|
||||
|
||||
/// WRMSR — write model-specific register. Ring-0 only.
|
||||
#[inline(always)]
|
||||
pub unsafe fn wrmsr(msr: u32, val: u64) {
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"wrmsr",
|
||||
in("ecx") msr,
|
||||
in("eax") val as u32,
|
||||
in("edx") (val >> 32) as u32,
|
||||
options(nomem, nostack, preserves_flags),
|
||||
);
|
||||
}
|
||||
unsafe extern "C" {
|
||||
/// Read host CR3. Returns the physical address of the host PML4.
|
||||
pub fn read_cr3() -> u64;
|
||||
/// Write host CR3 — flushes all non-global TLB entries.
|
||||
pub fn write_cr3(cr3: u64);
|
||||
/// VMLOAD — loads guest state from the VMCB at the given physical address. AMD SVM.
|
||||
pub fn vmload(vmcb_pa: u64);
|
||||
/// VMSAVE — saves guest state back to the VMCB at the given physical address. AMD SVM.
|
||||
pub fn vmsave(vmcb_pa: u64);
|
||||
/// VMRUN — enters the guest. Does not return until a #VMEXIT occurs. AMD SVM.
|
||||
pub fn vmrun(vmcb_pa: u64);
|
||||
/// RDMSR — read model-specific register.
|
||||
pub fn rdmsr(msr: u32) -> u64;
|
||||
/// WRMSR — write model-specific register.
|
||||
pub fn wrmsr(msr: u32, val: u64);
|
||||
}
|
||||
|
|
|
|||
36
src/kvm.rs
36
src/kvm.rs
|
|
@ -4,6 +4,11 @@ use kvm_bindings::{kvm_regs, kvm_sregs, KVMIO};
|
|||
use vmm_sys_util::ioctl::ioctl_with_mut_ref;
|
||||
use vmm_sys_util::ioctl_ioc_nr;
|
||||
|
||||
// _IOR(type, nr, size): direction=READ(2), type, nr, size encoded into 32-bit ioctl number
|
||||
fn kvm_ior(nr: u64, size: u64) -> u64 {
|
||||
(2u64 << 30) | ((KVMIO as u64) << 8) | nr | (size << 16)
|
||||
}
|
||||
|
||||
// vmm_sys_util macros need AsRawFd; wrap stolen fds cheaply.
|
||||
// SAFETY: caller must ensure the fd stays valid for the lifetime of Fd.
|
||||
struct Fd(RawFd);
|
||||
|
|
@ -16,18 +21,41 @@ impl std::os::unix::io::AsRawFd for Fd {
|
|||
vmm_sys_util::ioctl_ior_nr!(KVM_GET_REGS, KVMIO, 0x81, kvm_regs);
|
||||
vmm_sys_util::ioctl_ior_nr!(KVM_GET_SREGS, KVMIO, 0x83, kvm_sregs);
|
||||
|
||||
pub fn get_regs(vcpu_fd: RawFd) -> Result<kvm_regs, i32> {
|
||||
pub fn get_regs(vcpu_fd: RawFd) -> Result<kvm_regs, std::io::Error> {
|
||||
let mut regs = kvm_regs::default();
|
||||
// SAFETY: vcpu_fd is a valid KVM vcpu fd; kvm_regs is the correct type for this ioctl.
|
||||
let ret = unsafe { ioctl_with_mut_ref(&Fd(vcpu_fd), KVM_GET_REGS(), &mut regs) };
|
||||
if ret < 0 { return Err(ret); }
|
||||
if ret < 0 { return Err(std::io::Error::last_os_error()); }
|
||||
Ok(regs)
|
||||
}
|
||||
|
||||
pub fn get_sregs(vcpu_fd: RawFd) -> Result<kvm_sregs, i32> {
|
||||
/// Inject KVM_GET_SREGS into the vCPU thread via ptrace so it runs inside
|
||||
/// QEMU's mm — the direct ioctl path returns EIO because the kernel checks
|
||||
/// vcpu->kvm->mm != current->mm when called from a foreign process.
|
||||
pub fn get_sregs_injected(
|
||||
pid: u32,
|
||||
remote_fd: RawFd,
|
||||
) -> Result<kvm_sregs, Box<dyn std::error::Error>> {
|
||||
let nr = kvm_ior(0x83, std::mem::size_of::<kvm_sregs>() as u64);
|
||||
let bytes = crate::ptrace_inject::inject_ioctl(pid, remote_fd, nr, std::mem::size_of::<kvm_sregs>())?;
|
||||
// SAFETY: bytes has exactly size_of::<kvm_sregs>() bytes from the kernel-filled struct
|
||||
Ok(unsafe { std::ptr::read(bytes.as_ptr() as *const kvm_sregs) })
|
||||
}
|
||||
|
||||
pub fn get_regs_injected(
|
||||
pid: u32,
|
||||
remote_fd: RawFd,
|
||||
) -> Result<kvm_regs, Box<dyn std::error::Error>> {
|
||||
let nr = kvm_ior(0x81, std::mem::size_of::<kvm_regs>() as u64);
|
||||
let bytes = crate::ptrace_inject::inject_ioctl(pid, remote_fd, nr, std::mem::size_of::<kvm_regs>())?;
|
||||
// SAFETY: bytes has exactly size_of::<kvm_regs>() bytes from the kernel-filled struct
|
||||
Ok(unsafe { std::ptr::read(bytes.as_ptr() as *const kvm_regs) })
|
||||
}
|
||||
|
||||
pub fn get_sregs(vcpu_fd: RawFd) -> Result<kvm_sregs, std::io::Error> {
|
||||
let mut sregs = kvm_sregs::default();
|
||||
// SAFETY: vcpu_fd is a valid KVM vcpu fd; kvm_sregs is the correct type for this ioctl.
|
||||
let ret = unsafe { ioctl_with_mut_ref(&Fd(vcpu_fd), KVM_GET_SREGS(), &mut sregs) };
|
||||
if ret < 0 { return Err(ret); }
|
||||
if ret < 0 { return Err(std::io::Error::last_os_error()); }
|
||||
Ok(sregs)
|
||||
}
|
||||
|
|
|
|||
17
src/main.rs
17
src/main.rs
|
|
@ -2,16 +2,21 @@ mod asm;
|
|||
mod kvm;
|
||||
mod mem;
|
||||
mod paging;
|
||||
mod ptrace_inject;
|
||||
mod qemu;
|
||||
|
||||
fn main() {
|
||||
let vm = qemu::find_windows_vm().expect("no quickemu Windows VM found");
|
||||
println!("[+] pid={} vm_fd={} vcpu_fds={:?}", vm.pid, vm.vm_fd, vm.vcpu_fds);
|
||||
println!("[+] pid={} vm_fd={} vcpu_fds={:?} remote={:?}",
|
||||
vm.pid, vm.vm_fd, vm.vcpu_fds, vm.vcpu_fds_remote);
|
||||
|
||||
let sregs = kvm::get_sregs(vm.vcpu_fds[0]).expect("KVM_GET_SREGS failed");
|
||||
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);
|
||||
|
||||
let regs = kvm::get_regs(vm.vcpu_fds[0]).expect("KVM_GET_REGS failed");
|
||||
println!("[+] vcpu0 RIP={:#018x} RSP={:#018x}", regs.rip, regs.rsp);
|
||||
|
||||
let (eax, _, _, _) = asm::cpuid(0, 0);
|
||||
|
|
@ -24,8 +29,8 @@ fn main() {
|
|||
}
|
||||
|
||||
// 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.
|
||||
// a GVA, so we translate an arbitrary kernel VA to prove the walk works)
|
||||
// Real EPROCESS walk comes next
|
||||
let cr3 = sregs.cr3;
|
||||
println!("[+] CR3 (guest PML4 GPA) = {:#018x}", cr3);
|
||||
match paging::translate(&guest, cr3, sregs.cr3) {
|
||||
|
|
|
|||
16
src/mem.rs
16
src/mem.rs
|
|
@ -2,8 +2,8 @@ use std::fs::{self, File};
|
|||
use std::os::unix::fs::FileExt;
|
||||
|
||||
// A contiguous range of guest physical memory backed by a region in QEMU's
|
||||
// address space. gpa_base is where this slot starts in guest physical space.
|
||||
// For most single-slot quickemu VMs this will be (gpa_base=0, hva_base=<mmap addr>).
|
||||
// address space. gpa_base is where this slot starts in guest physical space
|
||||
// For most single-slot quickemu VMs this will be (gpa_base=0, hva_base=<mmap addr>)
|
||||
pub struct MemRegion {
|
||||
pub gpa_base: u64,
|
||||
pub hva_base: u64,
|
||||
|
|
@ -16,10 +16,8 @@ pub struct GuestMem {
|
|||
}
|
||||
|
||||
impl GuestMem {
|
||||
// Open /proc/<qemu_pid>/mem and locate guest RAM by scanning QEMU's
|
||||
// address space for large (>=256 MiB) rw-p anonymous / memfd mappings.
|
||||
// For a standard quickemu Windows 11 VM, guest physical memory starts at
|
||||
// GPA 0 and is backed by one or two such mappings.
|
||||
// GPA 0 and is backed by one or two such mappings
|
||||
pub fn attach(qemu_pid: u32) -> Option<GuestMem> {
|
||||
let maps_path = format!("/proc/{}/maps", qemu_pid);
|
||||
let mem_path = format!("/proc/{}/mem", qemu_pid);
|
||||
|
|
@ -31,9 +29,9 @@ impl GuestMem {
|
|||
if regions.is_empty() { return None; }
|
||||
|
||||
// Assign guest physical addresses in ascending HVA order — the first
|
||||
// region gets GPA 0, successive ones are packed behind it.
|
||||
// This is only heuristically correct; will be replaced with proper
|
||||
// KVM_SET_USER_MEMORY_REGION slot enumeration once we need NUMA / holes.
|
||||
// region gets GPA 0, successive ones are packed behind it
|
||||
// This is only heuristically correct will be replaced with proper
|
||||
// KVM_SET_USER_MEMORY_REGION slot enumeration once we need NUMA / holes
|
||||
regions.sort_by_key(|r| r.hva_base);
|
||||
let mut cursor: u64 = 0;
|
||||
for r in &mut regions {
|
||||
|
|
@ -52,7 +50,7 @@ impl GuestMem {
|
|||
let hva = region.hva_base + (gpa - region.gpa_base);
|
||||
// SAFETY: pread64 on /proc/<pid>/mem at the HVA offset is how
|
||||
// external processes read another process's virtual memory without
|
||||
// ptrace — the kernel validates the address in the target process.
|
||||
// ptrace — the kernel validates the address in the target process
|
||||
self.mem_fd.read_at(buf, hva).map_err(|e| format!("pread GPA {:#x}: {}", gpa, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
144
src/ptrace_inject.rs
Normal file
144
src/ptrace_inject.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::os::unix::io::RawFd;
|
||||
use libc::pid_t;
|
||||
|
||||
/// Refrence: https://attack.mitre.org/techniques/T1055/008/
|
||||
/// https://github.com/akamai/Linux-Process-Injection
|
||||
/// https://docs.rs/ptrace-inject/latest/ptrace_inject/
|
||||
/// https://github.com/Artemis21/ptrace-inject
|
||||
///
|
||||
|
||||
const PTRACE_SEIZE: u32 = 0x4206;
|
||||
const PTRACE_GETREGS: u32 = 12;
|
||||
const PTRACE_SETREGS: u32 = 13;
|
||||
const SYS_IOCTL: u64 = 16;
|
||||
|
||||
fn mem_write(pid: u32, addr: u64, data: &[u8]) -> std::io::Result<()> {
|
||||
let mut f = OpenOptions::new().write(true).open(format!("/proc/{}/mem", pid))?;
|
||||
f.seek(SeekFrom::Start(addr))?;
|
||||
f.write_all(data)
|
||||
}
|
||||
|
||||
fn mem_read(pid: u32, addr: u64, buf: &mut [u8]) -> std::io::Result<()> {
|
||||
let mut f = File::open(format!("/proc/{}/mem", pid))?;
|
||||
f.seek(SeekFrom::Start(addr))?;
|
||||
f.read_exact(buf)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// **Caller must call freeze() (SIGSTOP) before this function.**
|
||||
/// SIGSTOP guarantees every vCPU thread has exited KVM_RUN and called vcpu_put(),
|
||||
/// releasing vcpu->mutex and syncing the VMCB/VMCS to kvm_vcpu->arch.
|
||||
/// 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.
|
||||
///
|
||||
/// PTRACE_ATTACH is used instead of PTRACE_SEIZE+PTRACE_INTERRUPT because:
|
||||
/// - The vCPU threads block SIGTRAP in their signal mask, making int3 unreliable.
|
||||
/// - PTRACE_CONT on PTRACE_SEIZE'd threads in group-stop immediately re-stops.
|
||||
/// - PTRACE_ATTACH on the main thread (which has a normal signal mask and is
|
||||
/// already group-stopped) gives a clean ptrace-stop that PTRACE_CONT resumes
|
||||
/// correctly, escaping the group-stop for that one thread while the rest stay
|
||||
/// stopped.
|
||||
pub fn inject_ioctl(
|
||||
pid: u32,
|
||||
remote_fd: RawFd,
|
||||
ioctl_nr: u64,
|
||||
struct_size: usize,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
use libc::c_void;
|
||||
let null = 0usize as *mut c_void;
|
||||
let tid = pid as pid_t;
|
||||
|
||||
// PTRACE_ATTACH sends SIGSTOP internally; on an already group-stopped thread
|
||||
// this does NOT generate a new ptrace-stop event, so the subsequent waitpid
|
||||
// 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) };
|
||||
if r != 0 {
|
||||
return Err(format!("PTRACE_SEIZE: {}", std::io::Error::last_os_error()).into());
|
||||
}
|
||||
|
||||
let mut status = 0i32;
|
||||
unsafe { libc::waitpid(tid, &mut status, libc::__WALL) };
|
||||
if !libc::WIFSTOPPED(status) {
|
||||
unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, null, null) };
|
||||
return Err(format!("PTRACE_SEIZE waitpid: unexpected status {:#x}", status).into());
|
||||
}
|
||||
|
||||
let mut orig: libc::user_regs_struct = unsafe { std::mem::zeroed() };
|
||||
let r = unsafe {
|
||||
libc::ptrace(PTRACE_GETREGS as _, tid, null, &mut orig as *mut _ as *mut c_void)
|
||||
};
|
||||
if r != 0 {
|
||||
unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, null, null) };
|
||||
return Err(format!("PTRACE_GETREGS: {}", std::io::Error::last_os_error()).into());
|
||||
}
|
||||
|
||||
// Scratch layout below the 128-byte SysV AMD64 red zone (descending):
|
||||
// gadget_addr : 0f 05 cc (syscall; int3) — 8-byte aligned
|
||||
// struct_addr : zeroed ioctl struct — 16-byte aligned
|
||||
// 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])?;
|
||||
|
||||
let mut regs = orig;
|
||||
regs.rax = SYS_IOCTL;
|
||||
regs.rdi = remote_fd as u64;
|
||||
regs.rsi = ioctl_nr;
|
||||
regs.rdx = struct_addr;
|
||||
regs.rip = gadget_addr;
|
||||
|
||||
let r = unsafe {
|
||||
libc::ptrace(PTRACE_SETREGS as _, tid, null, ®s as *const _ as *mut c_void)
|
||||
};
|
||||
if r != 0 {
|
||||
unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, null, null) };
|
||||
return Err(format!("PTRACE_SETREGS: {}", std::io::Error::last_os_error()).into());
|
||||
}
|
||||
|
||||
// PTRACE_CONT with data=0 suppresses the pending SIGSTOP for this thread,
|
||||
// resuming it while every other thread stays in group-stop.
|
||||
// Loop until rip passes the 2-byte `syscall` instruction (gadget_addr+2).
|
||||
// Stops at gadget_addr itself are signal-delivery-stops before the syscall;
|
||||
// suppress each and retry.
|
||||
let mut ret_regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
|
||||
loop {
|
||||
unsafe { libc::ptrace(libc::PTRACE_CONT, tid, null, null) };
|
||||
unsafe { libc::waitpid(tid, &mut status, libc::__WALL) };
|
||||
unsafe {
|
||||
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 {
|
||||
unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, null, null) };
|
||||
return Err(format!(
|
||||
"thread escaped gadget: rip={:#x} gadget={:#x}",
|
||||
ret_regs.rip, gadget_addr
|
||||
).into());
|
||||
}
|
||||
}
|
||||
|
||||
let syscall_ret = ret_regs.rax as i64;
|
||||
eprintln!("[dbg] inject_ioctl ioctl={:#x} ret={} gadget={:#x} rip_after={:#x}",
|
||||
ioctl_nr, syscall_ret, gadget_addr, ret_regs.rip);
|
||||
|
||||
let mut result = vec![0u8; struct_size];
|
||||
if syscall_ret >= 0 {
|
||||
mem_read(pid, struct_addr, &mut result)?;
|
||||
}
|
||||
|
||||
unsafe { libc::ptrace(PTRACE_SETREGS as _, tid, null, &orig as *const _ as *mut c_void) };
|
||||
unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, null, null) };
|
||||
|
||||
if syscall_ret < 0 {
|
||||
return Err(format!("injected ioctl errno {}", -syscall_ret).into());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
49
src/qemu.rs
49
src/qemu.rs
|
|
@ -11,6 +11,8 @@ pub struct QemuVm {
|
|||
pub pid: u32,
|
||||
pub vm_fd: RawFd,
|
||||
pub vcpu_fds: Vec<RawFd>,
|
||||
/// fd numbers as they appear in the QEMU process — needed for ptrace injection
|
||||
pub vcpu_fds_remote: Vec<RawFd>,
|
||||
}
|
||||
|
||||
impl Drop for QemuVm {
|
||||
|
|
@ -24,11 +26,35 @@ impl Drop for QemuVm {
|
|||
}
|
||||
}
|
||||
|
||||
impl QemuVm {
|
||||
/// SIGSTOP the QEMU process and spin until /proc/<pid>/status shows State: T.
|
||||
/// Required before KVM_GET_REGS / KVM_GET_SREGS — the kernel returns EIO
|
||||
/// 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.
|
||||
pub fn freeze(&self) {
|
||||
unsafe { libc::kill(self.pid as libc::pid_t, libc::SIGSTOP); }
|
||||
let status_path = format!("/proc/{}/status", self.pid);
|
||||
loop {
|
||||
let Ok(s) = fs::read_to_string(&status_path) else { break };
|
||||
// "State:\tT (stopped)" or "State:\tt (tracing stop)"
|
||||
if s.lines().any(|l| l.starts_with("State:") && (l.contains('T') || l.contains('t'))) {
|
||||
break;
|
||||
}
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// SIGCONT to resume.
|
||||
pub fn thaw(&self) {
|
||||
unsafe { libc::kill(self.pid as libc::pid_t, libc::SIGCONT); }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_windows_vm() -> Option<QemuVm> {
|
||||
for entry in fs::read_dir("/proc").ok()? {
|
||||
let entry = entry.ok()?;
|
||||
let Ok(entry) = entry else { continue };
|
||||
let pid_str = entry.file_name();
|
||||
let pid: u32 = pid_str.to_str()?.parse().ok()?;
|
||||
let Ok(pid) = pid_str.to_str().unwrap_or("").parse::<u32>() else { continue };
|
||||
|
||||
if !is_qemu_windows_process(pid) {
|
||||
continue;
|
||||
|
|
@ -66,11 +92,12 @@ fn steal_kvm_fds(pid: u32) -> Option<QemuVm> {
|
|||
|
||||
let mut vm_fd: Option<RawFd> = None;
|
||||
let mut vcpu_fds: Vec<RawFd> = Vec::new();
|
||||
let mut vcpu_fds_remote: Vec<RawFd> = Vec::new();
|
||||
|
||||
let fd_dir = format!("/proc/{}/fd", pid);
|
||||
for entry in fs::read_dir(&fd_dir).ok()? {
|
||||
let entry = entry.ok()?;
|
||||
let fd_num: RawFd = entry.file_name().to_str()?.parse().ok()?;
|
||||
let Ok(entry) = entry else { continue };
|
||||
let Ok(fd_num) = entry.file_name().to_str().unwrap_or("").parse::<RawFd>() else { continue };
|
||||
|
||||
let link_path = format!("/proc/{}/fd/{}", pid, fd_num);
|
||||
let Ok(target) = fs::read_link(&link_path) else { continue };
|
||||
|
|
@ -81,7 +108,10 @@ fn steal_kvm_fds(pid: u32) -> Option<QemuVm> {
|
|||
if dup >= 0 { vm_fd = Some(dup); }
|
||||
} else if target.starts_with("anon_inode:kvm-vcpu:") {
|
||||
let dup = dup_fd(pidfd, fd_num);
|
||||
if dup >= 0 { vcpu_fds.push(dup); }
|
||||
if dup >= 0 {
|
||||
vcpu_fds.push(dup);
|
||||
vcpu_fds_remote.push(fd_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,10 +123,13 @@ fn steal_kvm_fds(pid: u32) -> Option<QemuVm> {
|
|||
return None;
|
||||
}
|
||||
|
||||
// sort vcpu fds by vcpu index so vcpu_fds[0] == vcpu 0
|
||||
vcpu_fds.sort();
|
||||
// Sort both vecs together by remote fd number so index 0 == vcpu 0
|
||||
// (QEMU opens vcpu fds in vcpu-index order, so lowest fd == vcpu 0)
|
||||
let mut pairs: Vec<(RawFd, RawFd)> = vcpu_fds_remote.into_iter().zip(vcpu_fds).collect();
|
||||
pairs.sort_by_key(|&(remote, _)| remote);
|
||||
let (vcpu_fds_remote, vcpu_fds): (Vec<_>, Vec<_>) = pairs.into_iter().unzip();
|
||||
|
||||
Some(QemuVm { pid, vm_fd, vcpu_fds })
|
||||
Some(QemuVm { pid, vm_fd, vcpu_fds, vcpu_fds_remote })
|
||||
}
|
||||
|
||||
fn dup_fd(pidfd: RawFd, target_fd: RawFd) -> RawFd {
|
||||
|
|
|
|||
64
src/stubs.s
Normal file
64
src/stubs.s
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Ring-0 stubs — AMD64 System V ABI.
|
||||
# Args: rdi, rsi, rdx, rcx, r8, r9. Return: rax (rdx:rax for 128-bit).
|
||||
# All of these fault at CPL > 0 (#GP or #UD).
|
||||
|
||||
.text
|
||||
|
||||
# read_cr3() -> u64
|
||||
# returns the physical address of the host PML4.
|
||||
.globl read_cr3
|
||||
read_cr3:
|
||||
movq %cr3, %rax
|
||||
ret
|
||||
|
||||
# write_cr3(cr3: u64)
|
||||
# flushes all non-global TLB entries
|
||||
.globl write_cr3
|
||||
write_cr3:
|
||||
movq %rdi, %cr3
|
||||
ret
|
||||
|
||||
# vmload(vmcb_pa: u64)
|
||||
# AMD SVM: loads guest state from the VMCB at the given physical address
|
||||
.globl vmload
|
||||
vmload:
|
||||
movq %rdi, %rax
|
||||
vmload %rax
|
||||
ret
|
||||
|
||||
# vmsave(vmcb_pa: u64)
|
||||
# AMD SVM: saves guest state back to the VMCB at the given physical address
|
||||
.globl vmsave
|
||||
vmsave:
|
||||
movq %rdi, %rax
|
||||
vmsave %rax
|
||||
ret
|
||||
|
||||
# vmrun(vmcb_pa: u64)
|
||||
# AMD SVM: enters the guest. Does not return until a #VMEXIT
|
||||
.globl vmrun
|
||||
vmrun:
|
||||
movq %rdi, %rax
|
||||
vmrun %rax
|
||||
ret
|
||||
|
||||
# rdmsr(msr: u32) -> u64
|
||||
# Reads the MSR number in edi; returns edx:eax packed into rax
|
||||
.globl rdmsr
|
||||
rdmsr:
|
||||
movl %edi, %ecx
|
||||
rdmsr
|
||||
shlq $32, %rdx
|
||||
orq %rdx, %rax
|
||||
ret
|
||||
|
||||
# wrmsr(msr: u32, val: u64)
|
||||
# msr in edi, 64-bit value in rsi
|
||||
.globl wrmsr
|
||||
wrmsr:
|
||||
movl %edi, %ecx
|
||||
movl %esi, %eax # low 32 bits
|
||||
movq %rsi, %rdx
|
||||
shrq $32, %rdx # high 32 bits
|
||||
wrmsr
|
||||
ret
|
||||
Loading…
Add table
Reference in a new issue