first commit
This commit is contained in:
commit
aed7231b15
7 changed files with 348 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
CLAUDE.md
|
||||||
62
Cargo.lock
generated
Normal file
62
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "KVM-hypervisor-introspector"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"kvm-bindings",
|
||||||
|
"kvm-ioctls",
|
||||||
|
"libc",
|
||||||
|
"vmm-sys-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kvm-bindings"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b13baf7bdfda2e10bcb109fcb099ef40cff82374eb6b7cdcf4695bdec4e522c"
|
||||||
|
dependencies = [
|
||||||
|
"vmm-sys-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kvm-ioctls"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "083c460d5a272c2f22205973e319147b791d92a288d7d7a8d4c6194f95229440"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"kvm-bindings",
|
||||||
|
"libc",
|
||||||
|
"vmm-sys-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.184"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vmm-sys-util"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d1435039746e20da4f8d507a72ee1b916f7b4b05af7a91c093d2c6561934ede"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "KVM-hypervisor-introspector"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
kvm-ioctls = "0.21"
|
||||||
|
kvm-bindings = "0.11"
|
||||||
|
vmm-sys-util = "0.12"
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
33
src/kvm.rs
Normal file
33
src/kvm.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use std::os::unix::io::RawFd;
|
||||||
|
|
||||||
|
use kvm_bindings::{kvm_regs, kvm_sregs, KVMIO};
|
||||||
|
use vmm_sys_util::ioctl::ioctl_with_mut_ref;
|
||||||
|
use vmm_sys_util::ioctl_ioc_nr;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
impl std::os::unix::io::AsRawFd for Fd {
|
||||||
|
fn as_raw_fd(&self) -> RawFd { self.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// KVMIO = 0xAE (174). These mirror the definitions in kvm_ioctls but we
|
||||||
|
// can't use VcpuFd directly because it expects to own the fd it creates.
|
||||||
|
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> {
|
||||||
|
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); }
|
||||||
|
Ok(regs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sregs(vcpu_fd: RawFd) -> Result<kvm_sregs, i32> {
|
||||||
|
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); }
|
||||||
|
Ok(sregs)
|
||||||
|
}
|
||||||
20
src/main.rs
Normal file
20
src/main.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
mod kvm;
|
||||||
|
mod mem;
|
||||||
|
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);
|
||||||
|
|
||||||
|
let sregs = kvm::get_sregs(vm.vcpu_fds[0]).expect("KVM_GET_SREGS failed");
|
||||||
|
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 guest = mem::GuestMem::attach(vm.pid).expect("failed to map guest RAM");
|
||||||
|
for r in &guest.regions {
|
||||||
|
println!("[+] mem region GPA {:#010x}..{:#010x} (HVA {:#010x})",
|
||||||
|
r.gpa_base, r.gpa_base + r.size, r.hva_base);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/mem.rs
Normal file
113
src/mem.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
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>).
|
||||||
|
pub struct MemRegion {
|
||||||
|
pub gpa_base: u64,
|
||||||
|
pub hva_base: u64,
|
||||||
|
pub size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GuestMem {
|
||||||
|
pub regions: Vec<MemRegion>,
|
||||||
|
mem_fd: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
pub fn attach(qemu_pid: u32) -> Option<GuestMem> {
|
||||||
|
let maps_path = format!("/proc/{}/maps", qemu_pid);
|
||||||
|
let mem_path = format!("/proc/{}/mem", qemu_pid);
|
||||||
|
|
||||||
|
let maps = fs::read_to_string(&maps_path).ok()?;
|
||||||
|
let mem_fd = File::open(&mem_path).ok()?;
|
||||||
|
|
||||||
|
let mut regions: Vec<MemRegion> = parse_guest_ram_regions(&maps);
|
||||||
|
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.
|
||||||
|
regions.sort_by_key(|r| r.hva_base);
|
||||||
|
let mut cursor: u64 = 0;
|
||||||
|
for r in &mut regions {
|
||||||
|
r.gpa_base = cursor;
|
||||||
|
cursor += r.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(GuestMem { regions, mem_fd })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_phys(&self, gpa: u64, buf: &mut [u8]) -> Result<(), String> {
|
||||||
|
let region = self.regions.iter().find(|r| {
|
||||||
|
gpa >= r.gpa_base && gpa + buf.len() as u64 <= r.gpa_base + r.size
|
||||||
|
}).ok_or_else(|| format!("GPA {:#x} not covered by any known region", gpa))?;
|
||||||
|
|
||||||
|
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.
|
||||||
|
self.mem_fd.read_at(buf, hva).map_err(|e| format!("pread GPA {:#x}: {}", gpa, e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_u64(&self, gpa: u64) -> Result<u64, String> {
|
||||||
|
let mut buf = [0u8; 8];
|
||||||
|
self.read_phys(gpa, &mut buf)?;
|
||||||
|
Ok(u64::from_le_bytes(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_u32(&self, gpa: u64) -> Result<u32, String> {
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
self.read_phys(gpa, &mut buf)?;
|
||||||
|
Ok(u32::from_le_bytes(buf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_REGION_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB
|
||||||
|
|
||||||
|
fn parse_guest_ram_regions(maps: &str) -> Vec<MemRegion> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
for line in maps.lines() {
|
||||||
|
// Format: addr_start-addr_end perms offset dev inode [path]
|
||||||
|
let mut parts = line.splitn(6, ' ');
|
||||||
|
let addrs = parts.next().unwrap_or("");
|
||||||
|
let perms = parts.next().unwrap_or("");
|
||||||
|
let _offset = parts.next().unwrap_or("");
|
||||||
|
let _dev = parts.next().unwrap_or("");
|
||||||
|
let _inode = parts.next().unwrap_or("");
|
||||||
|
let path = parts.next().unwrap_or("").trim();
|
||||||
|
|
||||||
|
// Must be rw-p (private, read-write, not executable — guest RAM pages
|
||||||
|
// are not mapped executable in QEMU's address space).
|
||||||
|
if perms != "rw-p" { continue; }
|
||||||
|
|
||||||
|
// Anonymous (no backing file) or memfd-backed.
|
||||||
|
// Exclude vvar, vsyscall, stack, heap labels, and QEMU's own segments.
|
||||||
|
let is_anon = path.is_empty();
|
||||||
|
let is_memfd = path.starts_with("/memfd:");
|
||||||
|
if !is_anon && !is_memfd { continue; }
|
||||||
|
|
||||||
|
let (start, end) = parse_addr_range(addrs).unwrap_or((0, 0));
|
||||||
|
if end <= start { continue; }
|
||||||
|
|
||||||
|
let size = end - start;
|
||||||
|
if size < MIN_REGION_SIZE { continue; }
|
||||||
|
|
||||||
|
out.push(MemRegion { gpa_base: 0, hva_base: start, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_addr_range(s: &str) -> Option<(u64, u64)> {
|
||||||
|
let (a, b) = s.split_once('-')?;
|
||||||
|
Some((u64::from_str_radix(a, 16).ok()?, u64::from_str_radix(b, 16).ok()?))
|
||||||
|
}
|
||||||
105
src/qemu.rs
Normal file
105
src/qemu.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::io::RawFd;
|
||||||
|
|
||||||
|
// pidfd_open(2) — Linux 5.6+
|
||||||
|
// pidfd_getfd(2) — Linux 5.6+
|
||||||
|
// Neither is in libc yet so we syscall directly.
|
||||||
|
const SYS_PIDFD_OPEN: libc::c_long = 434;
|
||||||
|
const SYS_PIDFD_GETFD: libc::c_long = 438;
|
||||||
|
|
||||||
|
pub struct QemuVm {
|
||||||
|
pub pid: u32,
|
||||||
|
pub vm_fd: RawFd,
|
||||||
|
pub vcpu_fds: Vec<RawFd>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for QemuVm {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
libc::close(self.vm_fd);
|
||||||
|
for fd in &self.vcpu_fds {
|
||||||
|
libc::close(*fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_windows_vm() -> Option<QemuVm> {
|
||||||
|
for entry in fs::read_dir("/proc").ok()? {
|
||||||
|
let entry = entry.ok()?;
|
||||||
|
let pid_str = entry.file_name();
|
||||||
|
let pid: u32 = pid_str.to_str()?.parse().ok()?;
|
||||||
|
|
||||||
|
if !is_qemu_windows_process(pid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(vm) = steal_kvm_fds(pid) {
|
||||||
|
return Some(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_qemu_windows_process(pid: u32) -> bool {
|
||||||
|
let cmdline_path = format!("/proc/{}/cmdline", pid);
|
||||||
|
let Ok(cmdline) = fs::read(&cmdline_path) else { return false };
|
||||||
|
|
||||||
|
// cmdline is null-delimited; treat as bytes and look for qemu + windows indicators
|
||||||
|
let s = cmdline.split(|&b| b == 0)
|
||||||
|
.filter_map(|a| std::str::from_utf8(a).ok())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
// must be a qemu-system-x86_64 process running something Windows-flavoured
|
||||||
|
s.contains("qemu-system-x86_64") && (s.contains("windows") || s.contains("win11") || s.contains("win10"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn steal_kvm_fds(pid: u32) -> Option<QemuVm> {
|
||||||
|
// open a pidfd so we can duplicate fds out of the target process
|
||||||
|
// SAFETY: raw syscall, pid is a valid u32 we read from /proc
|
||||||
|
let pidfd = unsafe { libc::syscall(SYS_PIDFD_OPEN, pid as libc::pid_t, 0u32) };
|
||||||
|
if pidfd < 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let pidfd = pidfd as RawFd;
|
||||||
|
|
||||||
|
let mut vm_fd: Option<RawFd> = None;
|
||||||
|
let mut vcpu_fds: 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 link_path = format!("/proc/{}/fd/{}", pid, fd_num);
|
||||||
|
let Ok(target) = fs::read_link(&link_path) else { continue };
|
||||||
|
let target = target.to_string_lossy();
|
||||||
|
|
||||||
|
if target == "anon_inode:kvm-vm" {
|
||||||
|
let dup = dup_fd(pidfd, fd_num);
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe { libc::close(pidfd) };
|
||||||
|
|
||||||
|
let vm_fd = vm_fd?;
|
||||||
|
if vcpu_fds.is_empty() {
|
||||||
|
unsafe { libc::close(vm_fd) };
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort vcpu fds by vcpu index so vcpu_fds[0] == vcpu 0
|
||||||
|
vcpu_fds.sort();
|
||||||
|
|
||||||
|
Some(QemuVm { pid, vm_fd, vcpu_fds })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dup_fd(pidfd: RawFd, target_fd: RawFd) -> RawFd {
|
||||||
|
// SAFETY: pidfd and target_fd are valid fds obtained above
|
||||||
|
unsafe { libc::syscall(SYS_PIDFD_GETFD, pidfd, target_fd, 0u32) as RawFd }
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue