#![cfg(target_arch = "x86_64")]
use std::{
debug_assert_matches,
mem::{self, offset_of},
ptr,
range::RangeInclusive,
sync::atomic::Ordering,
};
use closure_ffi::traits::FnPtr;
use diversion_abi::{context::process::ProcessContext, fn_ptr::AtomicErasedFnPtr};
use crate::{
Result,
error::Error as E,
installer::{
Installer,
arch::{
BoundedRangeAllocatorExt,
atomic::U8SliceExt,
os::{memory::Protection, thread::IpReloc},
x86_64::hook_site::HookSite,
},
},
};
mod hook_site;
mod intrinsics;
const MAX_INSN_LEN: usize = 15;
const DISASM_LEN: usize = JmpRel::LEN - 1 + MAX_INSN_LEN;
#[derive(Debug)]
struct JmpChain<'a> {
context: &'a mut ProcessContext,
thunk: &'static mut Thunk,
jmp_rel: JmpRel,
trampoline_bytes: &'static mut [u8],
relocs: Vec<IpReloc>,
}
#[derive(Debug)]
#[repr(C)]
struct Thunk {
jmp_abs: JmpAbs,
ud2: [u8; 2],
ptr: AtomicErasedFnPtr,
}
#[derive(Clone, Copy, Debug)]
#[repr(C, packed(1))]
struct JmpRel {
opcode: u8,
disp: i32,
}
#[derive(Clone, Copy, Debug)]
#[repr(C, packed(1))]
struct JmpAbs {
opcode: u8,
modrm: u8,
disp: i32,
}
struct ErasedInstaller {
target: *const (),
thunk: &'static AtomicErasedFnPtr,
}
enum InstallError {
Error(E),
TryAgain,
}
pub unsafe fn install<T>(target: T) -> Result<Installer<T>>
where
T: FnPtr + 'static,
{
let mut context = ProcessContext::acquire().map_err(E::ProcessContext)?;
let slot = match context.get_thunk(target) {
Ok(thunk) => return Ok(Installer { target, thunk }),
Err(slot) => slot,
};
loop {
let target_ptr = target.to_ptr();
let ptr = ptr::slice_from_raw_parts_mut(target_ptr as *mut u8, DISASM_LEN);
let prot_guard = unsafe {
Protection::make_rwx(ptr).map_err(|err| E::Protection {
err,
addr: ptr.addr(),
})?
};
let site_bytes = unsafe {
let mut bytes = [0; DISASM_LEN];
bytes.atomic_copy_from_ptr(ptr, Ordering::Acquire, Ordering::SeqCst);
bytes
};
let site = HookSite::analyze(ptr.addr(), &site_bytes)?;
let installer = match unsafe { install_fast(target_ptr, &mut context, &site) } {
Ok(installer) => installer,
Err(InstallError::TryAgain) => continue,
Err(InstallError::Error(e)) => return Err(e),
};
let installer = match installer {
Some(installer) => installer,
None => match unsafe { install_slow(target_ptr, &mut context, &site) } {
Ok(installer) => installer,
Err(InstallError::TryAgain) => continue,
Err(InstallError::Error(e)) => return Err(e),
},
};
let installer = unsafe { installer.downcast::<T>() };
context.insert_thunk(slot, installer.thunk);
debug_assert_matches!(
prot_guard.restore(),
Ok(()),
"could not restore memory protection"
);
return Ok(installer);
}
}
unsafe fn install_fast(
target: *const (),
context: &mut ProcessContext,
site: &HookSite<'_>,
) -> ::std::result::Result<Option<ErasedInstaller>, InstallError> {
let first_len = site.insns[0].len();
let range = JmpRel::short_encoding_range(site.bytes, first_len);
let Some(jmp_chain) = JmpChain::build(target, context, site, range)? else {
if first_len < JmpRel::LEN {
return Ok(None);
} else {
return Err(E::oom(target.addr()).into());
}
};
if unsafe { !site.detour(target, jmp_chain.jmp_rel) } {
jmp_chain.reclaim();
return Err(InstallError::TryAgain);
}
Ok(Some(ErasedInstaller {
target,
thunk: &mut jmp_chain.thunk.ptr,
}))
}
#[cold]
unsafe fn install_slow(
target: *const (),
context: &mut ProcessContext,
site: &HookSite<'_>,
) -> ::std::result::Result<ErasedInstaller, InstallError> {
let Some(mut jmp_chain) = JmpChain::build(target, context, site, JmpRel::RANGE)? else {
return Err(E::oom(target.addr()).into());
};
if let Err(e) = unsafe { site.suspend_and_detour(target, &mut jmp_chain) } {
jmp_chain.reclaim();
return Err(e);
}
Ok(ErasedInstaller {
target,
thunk: &mut jmp_chain.thunk.ptr,
})
}
impl<'a> JmpChain<'a> {
fn build(
target: *const (),
context: &'a mut ProcessContext,
site: &HookSite<'_>,
jmp_rel_range: RangeInclusive<isize>,
) -> Result<Option<Self>> {
let alloc = context.bounded_range_alloc();
let Some(thunk) = alloc.os_alloc_near::<Thunk>(target, jmp_rel_range)? else {
return Ok(None);
};
let relocated = match site.relocate(alloc) {
Ok(relocated) => relocated,
Err(e) => {
alloc.reclaim(thunk);
return Err(e);
}
};
let thunk = thunk.write(Thunk::new(relocated.trampoline_ptr));
let jmp_rel = JmpRel::encode(target, thunk);
Ok(Some(Self {
context,
thunk,
jmp_rel,
trampoline_bytes: relocated.bytes,
relocs: relocated.relocs,
}))
}
#[cold]
fn reclaim(self) {
let alloc = self.context.bounded_range_alloc();
alloc.reclaim(self.trampoline_bytes);
alloc.reclaim(self.thunk);
}
}
impl Thunk {
fn new(ptr: *const ()) -> Self {
let disp32 = const {
offset_of!(Self, ptr) as i32 - (offset_of!(Self, jmp_abs) + JmpAbs::LEN) as i32
};
Self {
jmp_abs: JmpAbs::new(disp32),
ud2: [0x0f, 0x0b],
ptr: AtomicErasedFnPtr::new(ptr),
}
}
}
impl JmpRel {
const LEN: usize = size_of::<Self>();
const RANGE: RangeInclusive<isize> = disp32_range(Self::LEN);
fn new(disp: i32) -> Self {
Self { opcode: 0xe9, disp }
}
#[track_caller]
fn encode<Ip: ?Sized, Tgt: ?Sized>(ip: *const Ip, target: *const Tgt) -> Self {
let disp = disp32_between(ip.addr() + size_of::<Self>(), target.addr());
Self::new(disp)
}
fn as_bytes(&self) -> &[u8] {
unsafe { mem::transmute::<&Self, &[u8; size_of::<Self>()]>(self) }
}
#[track_caller]
fn short_encoding_range(bytes: &[u8; DISASM_LEN], len: usize) -> RangeInclusive<isize> {
assert!(len > 0, "must be at least 1 byte long");
let mut min_disp32_bytes = i32::MIN.to_le_bytes();
let mut max_disp32_bytes = i32::MAX.to_le_bytes();
for i in len..Self::LEN {
let byte = bytes[i];
min_disp32_bytes[i - 1] = byte;
max_disp32_bytes[i - 1] = byte;
}
let min = i32::from_le_bytes(min_disp32_bytes) as isize + Self::LEN as isize;
let max = i32::from_le_bytes(max_disp32_bytes) as isize + Self::LEN as isize;
RangeInclusive {
start: min,
last: max,
}
}
}
impl JmpAbs {
const LEN: usize = size_of::<Self>();
fn new(disp: i32) -> Self {
Self {
opcode: 0xff,
modrm: 0x25,
disp,
}
}
}
#[track_caller]
fn disp32_between(ip: usize, target: usize) -> i32 {
i32::try_from(target as isize - ip as isize).expect("pointer is not in range")
}
const fn disp32_range(insn_len: usize) -> RangeInclusive<isize> {
RangeInclusive {
start: i32::MIN as isize + insn_len as isize,
last: i32::MAX as isize + insn_len as isize,
}
}
impl ErasedInstaller {
unsafe fn downcast<T>(self) -> Installer<T>
where
T: FnPtr + 'static,
{
unsafe {
Installer {
target: T::from_ptr(self.target),
thunk: self.thunk.downcast(),
}
}
}
}
impl From<E> for InstallError {
fn from(err: E) -> Self {
Self::Error(err)
}
}