diff --git a/CLAUDE.MD b/CLAUDE.MD new file mode 100644 index 0000000..07743be --- /dev/null +++ b/CLAUDE.MD @@ -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. diff --git a/Cargo.lock b/Cargo.lock index e83e9c3..bcd22fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 6fc3739..0546634 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,5 +9,8 @@ kvm-bindings = "0.11" vmm-sys-util = "0.12" libc = "0.2" +[build-dependencies] +cc = "1" + [profile.release] debug = true diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..9171c32 --- /dev/null +++ b/build.rs @@ -0,0 +1,6 @@ +fn main() { + cc::Build::new() + .file("src/stubs.s") + .compile("stubs"); + println!("cargo:rerun-if-changed=src/stubs.s"); +} diff --git a/src/asm.rs b/src/asm.rs index 15e8e81..4ae76d0 100644 --- a/src/asm.rs +++ b/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); } diff --git a/src/kvm.rs b/src/kvm.rs index d25c821..f969c12 100644 --- a/src/kvm.rs +++ b/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 { +pub fn get_regs(vcpu_fd: RawFd) -> Result { 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 { +/// 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> { + let nr = kvm_ior(0x83, std::mem::size_of::() as u64); + let bytes = crate::ptrace_inject::inject_ioctl(pid, remote_fd, nr, std::mem::size_of::())?; + // SAFETY: bytes has exactly size_of::() 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> { + let nr = kvm_ior(0x81, std::mem::size_of::() as u64); + let bytes = crate::ptrace_inject::inject_ioctl(pid, remote_fd, nr, std::mem::size_of::())?; + // SAFETY: bytes has exactly size_of::() 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 { 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) } diff --git a/src/main.rs b/src/main.rs index 261ff8f..d27bd1d 100644 --- a/src/main.rs +++ b/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) { diff --git a/src/mem.rs b/src/mem.rs index 4412485..18f40f3 100644 --- a/src/mem.rs +++ b/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=). +// 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=) pub struct MemRegion { pub gpa_base: u64, pub hva_base: u64, @@ -16,10 +16,8 @@ pub struct GuestMem { } impl GuestMem { - // Open /proc//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 { 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//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(()) } diff --git a/src/ptrace_inject.rs b/src/ptrace_inject.rs new file mode 100644 index 0000000..adbf24a --- /dev/null +++ b/src/ptrace_inject.rs @@ -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, Box> { + 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) +} diff --git a/src/qemu.rs b/src/qemu.rs index fb459bf..12037b6 100644 --- a/src/qemu.rs +++ b/src/qemu.rs @@ -11,6 +11,8 @@ pub struct QemuVm { pub pid: u32, pub vm_fd: RawFd, pub vcpu_fds: Vec, + /// fd numbers as they appear in the QEMU process — needed for ptrace injection + pub vcpu_fds_remote: Vec, } impl Drop for QemuVm { @@ -24,11 +26,35 @@ impl Drop for QemuVm { } } +impl QemuVm { + /// SIGSTOP the QEMU process and spin until /proc//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 { 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::() else { continue }; if !is_qemu_windows_process(pid) { continue; @@ -66,11 +92,12 @@ fn steal_kvm_fds(pid: u32) -> Option { let mut vm_fd: Option = None; let mut vcpu_fds: Vec = Vec::new(); + let mut vcpu_fds_remote: Vec = 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::() 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 { 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 { 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 { diff --git a/src/stubs.s b/src/stubs.s new file mode 100644 index 0000000..fecc8dc --- /dev/null +++ b/src/stubs.s @@ -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