use std::io;
use crate::checkpoint::MemoryMap;
#[cfg(target_arch = "x86_64")]
use std::os::raw::c_void;
#[cfg(target_arch = "x86_64")]
fn ptrace_peektext(pid: i32, addr: u64) -> io::Result<u64> {
unsafe {
*libc::__errno_location() = 0;
let word = libc::ptrace(
libc::PTRACE_PEEKTEXT,
pid,
addr as *mut c_void,
std::ptr::null_mut::<c_void>(),
);
if word == -1 {
let errno = *libc::__errno_location();
if errno != 0 {
return Err(io::Error::from_raw_os_error(errno));
}
}
Ok(word as u64)
}
}
#[cfg(target_arch = "x86_64")]
fn ptrace_poketext(pid: i32, addr: u64, data: u64) -> io::Result<()> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_POKETEXT,
pid,
addr as *mut c_void,
data as *mut c_void,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(target_arch = "x86_64")]
fn ptrace_getregs(pid: i32) -> io::Result<libc::user_regs_struct> {
let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let ret = unsafe {
libc::ptrace(
libc::PTRACE_GETREGS,
pid,
std::ptr::null_mut::<c_void>(),
&mut regs as *mut libc::user_regs_struct as *mut c_void,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(regs)
}
#[cfg(target_arch = "x86_64")]
fn ptrace_setregs(pid: i32, regs: &libc::user_regs_struct) -> io::Result<()> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SETREGS,
pid,
std::ptr::null_mut::<c_void>(),
regs as *const libc::user_regs_struct as *mut c_void,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(target_arch = "x86_64")]
fn ptrace_singlestep(pid: i32) -> io::Result<()> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SINGLESTEP,
pid,
std::ptr::null_mut::<c_void>(),
std::ptr::null_mut::<c_void>(),
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[allow(dead_code)]
#[cfg(target_arch = "x86_64")]
pub(crate) fn inject_syscall(pid: i32, nr: u64, args: [u64; 6]) -> io::Result<i64> {
let saved_regs = ptrace_getregs(pid)?;
let rip = saved_regs.rip;
let orig_word = ptrace_peektext(pid, rip)?;
let planted = (orig_word & !0xffffu64) | 0x050fu64;
let result = (|| -> io::Result<i64> {
ptrace_poketext(pid, rip, planted)?;
let mut regs = saved_regs;
regs.rax = nr;
regs.rdi = args[0];
regs.rsi = args[1];
regs.rdx = args[2];
regs.r10 = args[3];
regs.r8 = args[4];
regs.r9 = args[5];
regs.rip = rip; ptrace_setregs(pid, ®s)?;
ptrace_singlestep(pid)?;
let mut status: i32 = 0;
let w = unsafe { libc::waitpid(pid, &mut status, 0) };
if w < 0 {
return Err(io::Error::last_os_error());
}
let stopped = (status & 0xff) == 0x7f;
let stopsig = (status >> 8) & 0xff;
if !stopped || stopsig != libc::SIGTRAP {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("injected syscall did not complete: status={status:#x}"),
));
}
let after = ptrace_getregs(pid)?;
Ok(after.rax as i64)
})();
let restore_word = ptrace_poketext(pid, rip, orig_word);
let restore_regs = ptrace_setregs(pid, &saved_regs);
let ret = result?;
restore_word?;
restore_regs?;
Ok(ret)
}
#[cfg(not(target_arch = "x86_64"))]
#[allow(dead_code)]
pub(crate) fn inject_syscall(_pid: i32, _nr: u64, _args: [u64; 6]) -> io::Result<i64> {
Err(io::Error::new(io::ErrorKind::Unsupported,
"syscall injection is only implemented on x86_64"))
}
#[allow(dead_code)]
pub(crate) fn find_free_page(maps: &[MemoryMap]) -> Option<u64> {
const PAGE: u64 = 4096;
const WINDOW_LO: u64 = 0x1_0000;
const WINDOW_HI: u64 = 0x7fff_0000_0000;
let mut ranges: Vec<(u64, u64)> = maps.iter().map(|m| (m.start, m.end)).collect();
ranges.sort_by_key(|r| r.0);
let mut cursor = WINDOW_LO;
for (start, end) in &ranges {
if *end <= cursor {
continue;
}
if *start <= cursor {
cursor = *end;
if cursor >= WINDOW_HI {
return None;
}
continue;
}
if start.saturating_sub(cursor) >= PAGE && cursor + PAGE <= WINDOW_HI {
return Some(cursor);
}
cursor = *end;
if cursor >= WINDOW_HI {
return None;
}
}
if cursor + PAGE <= WINDOW_HI {
Some(cursor)
} else {
None
}
}
#[allow(dead_code)]
#[cfg(target_arch = "x86_64")]
pub(crate) fn inject_syscall_at(pid: i32, gadget: u64, nr: u64, args: [u64; 6]) -> io::Result<i64> {
let saved_regs = ptrace_getregs(pid)?;
let result = (|| -> io::Result<i64> {
let mut regs = saved_regs;
regs.rax = nr;
regs.rdi = args[0];
regs.rsi = args[1];
regs.rdx = args[2];
regs.r10 = args[3];
regs.r8 = args[4];
regs.r9 = args[5];
regs.rip = gadget; ptrace_setregs(pid, ®s)?;
ptrace_singlestep(pid)?;
let mut status: i32 = 0;
let w = unsafe { libc::waitpid(pid, &mut status, 0) };
if w < 0 {
return Err(io::Error::last_os_error());
}
let stopped = (status & 0xff) == 0x7f;
let stopsig = (status >> 8) & 0xff;
if !stopped || stopsig != libc::SIGTRAP {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("injected syscall did not complete: status={status:#x}"),
));
}
let after = ptrace_getregs(pid)?;
Ok(after.rax as i64)
})();
let restore_regs = ptrace_setregs(pid, &saved_regs);
let ret = result?;
restore_regs?;
Ok(ret)
}
#[cfg(not(target_arch = "x86_64"))]
#[allow(dead_code)]
pub(crate) fn inject_syscall_at(_pid: i32, _gadget: u64, _nr: u64, _args: [u64; 6]) -> io::Result<i64> {
Err(io::Error::new(io::ErrorKind::Unsupported,
"syscall injection is only implemented on x86_64"))
}
pub(crate) fn write_child_mem(pid: i32, addr: u64, bytes: &[u8]) -> io::Result<()> {
let local_iov = libc::iovec {
iov_base: bytes.as_ptr() as *mut libc::c_void,
iov_len: bytes.len(),
};
let remote_iov = libc::iovec {
iov_base: addr as *mut libc::c_void,
iov_len: bytes.len(),
};
let ret = unsafe {
libc::process_vm_writev(pid, &local_iov, 1, &remote_iov, 1, 0)
};
if ret < 0 {
Err(io::Error::last_os_error())
} else if (ret as usize) < bytes.len() {
Err(io::Error::new(
io::ErrorKind::WriteZero,
format!("short write: {} of {} bytes", ret, bytes.len()),
))
} else {
Ok(())
}
}
#[allow(dead_code)]
#[cfg(target_arch = "x86_64")]
pub(crate) fn setup_trampoline(pid: i32, maps: &[MemoryMap]) -> io::Result<u64> {
let addr = find_free_page(maps)
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no free page for restore trampoline"))?;
let prot = (libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC) as u64;
let flags = (libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED) as u64;
let ret = inject_syscall(pid, 9, [addr, 4096, prot, flags, (-1i64) as u64, 0])?;
if ret < 0 || (ret as u64) != addr {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("trampoline mmap at {addr:#x} failed (ret={ret:#x})"),
));
}
write_child_mem(pid, addr, &[0x0f, 0x05])?;
Ok(addr)
}
#[cfg(not(target_arch = "x86_64"))]
#[allow(dead_code)]
pub(crate) fn setup_trampoline(_pid: i32, _maps: &[MemoryMap]) -> io::Result<u64> {
Err(io::Error::new(io::ErrorKind::Unsupported,
"trampoline setup is only implemented on x86_64"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(target_arch = "x86_64")]
fn inject_getpid_returns_child_pid() {
use std::os::unix::process::CommandExt;
let mut child = unsafe {
std::process::Command::new("sleep").arg("30")
.pre_exec(|| {
libc::ptrace(libc::PTRACE_TRACEME, 0, 0, 0);
Ok(())
})
.spawn().unwrap()
};
let pid = child.id() as i32;
let mut st = 0i32;
unsafe { libc::waitpid(pid, &mut st, 0); }
let ret = inject_syscall(pid, 39, [0; 6]).expect("inject getpid");
let _ = child.kill();
let _ = child.wait();
assert_eq!(ret as i32, pid, "injected getpid should return the child's pid");
}
#[test]
fn find_free_page_finds_gap_between_maps() {
let maps = vec![
MemoryMap {
start: 0x1_0000,
end: 0x20_0000,
perms: "r-xp".into(),
offset: 0,
path: None,
},
MemoryMap {
start: 0x30_0000,
end: 0x40_0000,
perms: "rw-p".into(),
offset: 0,
path: None,
},
];
let addr = find_free_page(&maps).expect("a gap should be found");
assert_eq!(addr % 4096, 0, "result must be page aligned");
assert!(
addr >= 0x20_0000 && addr + 4096 <= 0x30_0000,
"result {addr:#x} should sit inside the gap"
);
}
#[test]
fn find_free_page_none_when_packed() {
let maps = vec![MemoryMap {
start: 0x1_0000,
end: 0x7fff_0000_0000,
perms: "rw-p".into(),
offset: 0,
path: None,
}];
assert!(find_free_page(&maps).is_none(), "packed window has no gap");
}
#[test]
#[cfg(target_arch = "x86_64")]
fn trampoline_executes_getpid() {
use std::os::unix::process::CommandExt;
let mut child = unsafe {
std::process::Command::new("sleep").arg("30")
.pre_exec(|| { libc::ptrace(libc::PTRACE_TRACEME, 0, 0, 0); Ok(()) })
.spawn().unwrap()
};
let pid = child.id() as i32;
let mut st = 0i32;
unsafe { libc::waitpid(pid, &mut st, 0); }
let maps = crate::checkpoint::capture::parse_proc_maps(pid).expect("read child maps");
let tramp = setup_trampoline(pid, &maps).expect("setup trampoline");
let ret = inject_syscall_at(pid, tramp, 39, [0; 6]).expect("getpid via trampoline");
let _ = child.kill(); let _ = child.wait();
assert_eq!(ret as i32, pid, "getpid via trampoline should return child pid");
}
}