act on what the binary declares: callable Pulse shims, ConVars, string anchors; gen v2.1
This commit is contained in:
parent
3de955c4ff
commit
71ce34edd2
14 changed files with 1507 additions and 54 deletions
138
src/live.rs
138
src/live.rs
|
|
@ -207,6 +207,64 @@ impl LiveProcess {
|
|||
pub struct CallResult {
|
||||
pub rax: u64,
|
||||
pub clean_return: bool,
|
||||
/// Where the scratch blob was placed, so the caller can read back what the callee wrote into it.
|
||||
/// Zero when the call carried no scratch.
|
||||
pub scratch_base: u64,
|
||||
}
|
||||
|
||||
/// One argument to a remote call.
|
||||
///
|
||||
/// [`Arg::Scratch`] exists because a callee that takes a POINTER needs a structure to point at, and the
|
||||
/// address of that structure is not known until the call frame is laid out. Naming it relative to the
|
||||
/// scratch base lets the caller describe "argument 5 points at my blob" without knowing where the blob
|
||||
/// will land.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Arg {
|
||||
Val(u64),
|
||||
/// `scratch_base + addend`.
|
||||
Scratch(i64),
|
||||
}
|
||||
|
||||
/// A blob placed in the target's stack scratch before the call.
|
||||
pub struct Scratch<'a> {
|
||||
pub bytes: &'a [u8],
|
||||
/// `(offset, addend)` — write `scratch_base + addend` as a little-endian u64 at `offset` in the blob.
|
||||
/// This is how a pointer INSIDE the blob becomes absolute; an array-of-pointers argument is otherwise
|
||||
/// impossible to build, since every element has to name an address that does not exist yet.
|
||||
pub relocs: &'a [(usize, i64)],
|
||||
}
|
||||
|
||||
/// Write `data` into the target at `addr`, a word at a time.
|
||||
///
|
||||
/// A trailing partial word is read back and merged rather than zero-filled: `PTRACE_POKEDATA` writes a
|
||||
/// whole word, so writing the tail without preserving the bytes past it would clobber memory the caller
|
||||
/// never asked to touch.
|
||||
unsafe fn poke_bytes(pid: i32, addr: u64, data: &[u8]) -> Result<()> {
|
||||
use anyhow::bail;
|
||||
let mut i = 0usize;
|
||||
while i < data.len() {
|
||||
let at = addr + i as u64;
|
||||
let n = (data.len() - i).min(8);
|
||||
let mut word = if n == 8 {
|
||||
[0u8; 8]
|
||||
} else {
|
||||
// PEEKDATA returns -1 both for an error and for a word whose value IS -1, so errno is the
|
||||
// only way to tell them apart and it must be cleared first.
|
||||
unsafe { *libc::__errno_location() = 0 };
|
||||
let cur = unsafe { libc::ptrace(libc::PTRACE_PEEKDATA, pid, at as usize, 0usize) };
|
||||
if cur == -1 && errno() != 0 {
|
||||
bail!("PEEKDATA at {at:#x} failed (errno {})", errno());
|
||||
}
|
||||
(cur as u64).to_le_bytes()
|
||||
};
|
||||
word[..n].copy_from_slice(&data[i..i + n]);
|
||||
let w = u64::from_le_bytes(word) as usize;
|
||||
if unsafe { libc::ptrace(libc::PTRACE_POKEDATA, pid, at as usize, w) } < 0 {
|
||||
bail!("POKEDATA at {at:#x} failed (errno {})", errno());
|
||||
}
|
||||
i += n;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Call the function at runtime address `func` inside process `pid` with `args` (SysV: up to 6 in
|
||||
|
|
@ -215,8 +273,42 @@ pub struct CallResult {
|
|||
/// the thread exactly — the SIGSEGV from the return trap is suppressed. Needs ptrace permission
|
||||
/// (owned child, or same-user with ptrace_scope=0). UNSAFE: only call leaf-ish functions with valid args.
|
||||
pub fn call_remote(pid: i32, func: u64, args: &[u64]) -> Result<CallResult> {
|
||||
let regs: Vec<Arg> = args.iter().map(|&v| Arg::Val(v)).collect();
|
||||
call_remote_ex(pid, func, ®s, &[], None)
|
||||
}
|
||||
|
||||
/// [`call_remote`] plus stack arguments and a scratch blob placed in the target.
|
||||
///
|
||||
/// Needed for callees that take more than six integer arguments or a pointer to a structure the caller has
|
||||
/// to build — neither of which the register-only form can express.
|
||||
///
|
||||
/// **Stack geometry**, descending from the interrupted `rsp`, chosen so three regions cannot collide:
|
||||
/// the 128-byte red zone is left alone (the interrupted frame lives there); the scratch blob sits at
|
||||
/// `rsp-1024`; the call frame starts at `rsp-2048`, so the callee's own stack — which grows DOWN from
|
||||
/// there — can never reach the scratch ABOVE it. Entry keeps SysV's `rsp % 16 == 8`, with the return
|
||||
/// address at `[rsp]` and stack argument *i* at `[rsp + 8 + 8i]`.
|
||||
pub fn call_remote_ex(
|
||||
pid: i32,
|
||||
func: u64,
|
||||
regs_in: &[Arg],
|
||||
stack_in: &[Arg],
|
||||
scratch: Option<Scratch<'_>>,
|
||||
) -> Result<CallResult> {
|
||||
use anyhow::bail;
|
||||
let dbg = std::env::var("SOURCE2ROSETTA_DBG").is_ok();
|
||||
if regs_in.len() > 6 {
|
||||
bail!("{} register arguments; SysV has 6", regs_in.len());
|
||||
}
|
||||
if let Some(s) = &scratch {
|
||||
// The blob lives in the 1 KiB between the frame and the red zone. Refuse rather than silently
|
||||
// overlap the call frame, which would corrupt the return address mid-call.
|
||||
if s.bytes.len() > 768 {
|
||||
bail!(
|
||||
"scratch blob is {} bytes; the reserved window is 768",
|
||||
s.bytes.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
if libc::ptrace(libc::PTRACE_ATTACH, pid, 0usize, 0usize) < 0 {
|
||||
bail!(
|
||||
|
|
@ -249,6 +341,31 @@ pub fn call_remote(pid: i32, func: u64, args: &[u64]) -> Result<CallResult> {
|
|||
// If we attached mid-syscall, orig_rax holds the syscall number and the kernel would run its
|
||||
// syscall-restart logic on our injected rip. Setting it to -1 says "no syscall in progress".
|
||||
regs.orig_rax = u64::MAX;
|
||||
|
||||
// Place the scratch blob first: every Arg::Scratch resolves against its base.
|
||||
let scratch_base = (saved.rsp - 1024) & !0xfu64;
|
||||
if let Some(s) = &scratch {
|
||||
let mut blob = s.bytes.to_vec();
|
||||
for &(off, addend) in s.relocs {
|
||||
let Some(dst) = blob.get_mut(off..off + 8) else {
|
||||
restore(&saved);
|
||||
bail!(
|
||||
"scratch reloc at {off} runs past the {}-byte blob",
|
||||
s.bytes.len()
|
||||
);
|
||||
};
|
||||
dst.copy_from_slice(&scratch_base.wrapping_add(addend as u64).to_le_bytes());
|
||||
}
|
||||
if let Err(e) = poke_bytes(pid, scratch_base, &blob) {
|
||||
restore(&saved);
|
||||
return Err(e.context(format!("placing scratch at {scratch_base:#x}")));
|
||||
}
|
||||
}
|
||||
let resolve = |a: Arg| match a {
|
||||
Arg::Val(v) => v,
|
||||
Arg::Scratch(addend) => scratch_base.wrapping_add(addend as u64),
|
||||
};
|
||||
|
||||
let slots = [
|
||||
&mut regs.rdi as *mut u64,
|
||||
&mut regs.rsi,
|
||||
|
|
@ -257,12 +374,12 @@ pub fn call_remote(pid: i32, func: u64, args: &[u64]) -> Result<CallResult> {
|
|||
&mut regs.r8,
|
||||
&mut regs.r9,
|
||||
];
|
||||
for (i, &a) in args.iter().take(6).enumerate() {
|
||||
*slots[i] = a;
|
||||
for (i, &a) in regs_in.iter().enumerate() {
|
||||
*slots[i] = resolve(a);
|
||||
}
|
||||
// Scratch stack BELOW the 128-byte redzone so we never corrupt the interrupted frame; write a
|
||||
// Call frame well below the scratch, so the callee's downward stack growth cannot reach it. Write a
|
||||
// return address of 0 and keep SysV's `rsp % 16 == 8` at function entry.
|
||||
let mut sp = (saved.rsp - 512) & !0xfu64;
|
||||
let mut sp = (saved.rsp - 2048) & !0xfu64;
|
||||
sp -= 8;
|
||||
if libc::ptrace(libc::PTRACE_POKEDATA, pid, sp as usize, 0usize) < 0 {
|
||||
restore(&saved);
|
||||
|
|
@ -271,6 +388,17 @@ pub fn call_remote(pid: i32, func: u64, args: &[u64]) -> Result<CallResult> {
|
|||
errno()
|
||||
);
|
||||
}
|
||||
// Stack arguments sit immediately above the return address, which is where the callee reads them.
|
||||
for (i, &a) in stack_in.iter().enumerate() {
|
||||
let at = sp + 8 + 8 * i as u64;
|
||||
if libc::ptrace(libc::PTRACE_POKEDATA, pid, at as usize, resolve(a) as usize) < 0 {
|
||||
restore(&saved);
|
||||
bail!(
|
||||
"POKEDATA(stack arg {i}) at {at:#x} failed (errno {})",
|
||||
errno()
|
||||
);
|
||||
}
|
||||
}
|
||||
let wrote = libc::ptrace(libc::PTRACE_PEEKDATA, pid, sp as usize, 0usize);
|
||||
regs.rsp = sp;
|
||||
regs.rip = func;
|
||||
|
|
@ -305,6 +433,7 @@ pub fn call_remote(pid: i32, func: u64, args: &[u64]) -> Result<CallResult> {
|
|||
let r = CallResult {
|
||||
rax: cur.rax,
|
||||
clean_return: true,
|
||||
scratch_base,
|
||||
};
|
||||
restore(&saved);
|
||||
return Ok(r);
|
||||
|
|
@ -313,6 +442,7 @@ pub fn call_remote(pid: i32, func: u64, args: &[u64]) -> Result<CallResult> {
|
|||
let r = CallResult {
|
||||
rax: cur.rax,
|
||||
clean_return: false,
|
||||
scratch_base,
|
||||
};
|
||||
restore(&saved);
|
||||
return Ok(r);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue