use crate::error::{Error, Result};
use crate::module::Module;
use crate::region::{MemoryRegion, Protection};
use std::ffi::c_int;
type MachPortT = u32;
type KernReturnT = c_int;
type VmProtT = c_int;
type MachMsgTypeNumberT = u32;
const KERN_SUCCESS: KernReturnT = 0;
const KERN_INVALID_ADDRESS: KernReturnT = 1;
const VM_PROT_READ: VmProtT = 1;
const VM_PROT_WRITE: VmProtT = 2;
const VM_PROT_EXECUTE: VmProtT = 4;
const VM_REGION_BASIC_INFO_64: c_int = 9;
const VM_REGION_BASIC_INFO_64_COUNT: MachMsgTypeNumberT = 9;
const TASK_DYLD_INFO: u32 = 17;
const TASK_DYLD_INFO_COUNT: MachMsgTypeNumberT = 5;
const MH_MAGIC_64: u32 = 0xFEEDFACF;
const LC_SEGMENT_64: u32 = 0x19;
#[repr(C)]
struct VmRegionBasicInfo64 {
protection: VmProtT,
max_protection: VmProtT,
inheritance: u32,
shared: i32,
reserved: i32,
offset: u64,
behavior: i32,
user_wired_count: u16,
}
#[repr(C)]
struct TaskDyldInfo {
all_image_info_addr: u64,
all_image_info_size: u64,
all_image_info_format: c_int,
}
extern "C" {
fn mach_task_self() -> MachPortT;
fn task_for_pid(target: MachPortT, pid: c_int, task: *mut MachPortT) -> KernReturnT;
fn mach_vm_read_overwrite(
task: MachPortT,
address: u64,
size: u64,
data: u64,
out_size: *mut u64,
) -> KernReturnT;
fn mach_vm_write(task: MachPortT, address: u64, data: *const u8, count: u32) -> KernReturnT;
fn mach_vm_region(
task: MachPortT,
address: *mut u64,
size: *mut u64,
flavor: c_int,
info: *mut VmRegionBasicInfo64,
count: *mut MachMsgTypeNumberT,
object_name: *mut MachPortT,
) -> KernReturnT;
fn mach_port_deallocate(task: MachPortT, name: MachPortT) -> KernReturnT;
fn task_info(
target_task: MachPortT,
flavor: u32,
task_info_out: *mut TaskDyldInfo,
task_info_count: *mut MachMsgTypeNumberT,
) -> KernReturnT;
}
pub struct ProcessHandle {
task: MachPortT,
owns_port: bool,
}
impl Drop for ProcessHandle {
fn drop(&mut self) {
if self.owns_port {
unsafe {
mach_port_deallocate(mach_task_self(), self.task);
}
}
}
}
pub fn attach(pid: u32) -> Result<ProcessHandle> {
let self_port = unsafe { mach_task_self() };
let self_pid = std::process::id();
if pid == self_pid {
return Ok(ProcessHandle {
task: self_port,
owns_port: false,
});
}
let mut task: MachPortT = 0;
let kr = unsafe { task_for_pid(self_port, pid as c_int, &mut task) };
if kr != KERN_SUCCESS {
let err = std::io::Error::from_raw_os_error(kr);
if kr == 5 {
return Err(Error::PermissionDenied { pid });
}
return Err(Error::AttachFailed { pid, source: err });
}
Ok(ProcessHandle {
task,
owns_port: true,
})
}
pub fn read_bytes(handle: &ProcessHandle, address: usize, buf: &mut [u8]) -> Result<()> {
if buf.is_empty() {
return Ok(());
}
let mut out_size: u64 = 0;
let kr = unsafe {
mach_vm_read_overwrite(
handle.task,
address as u64,
buf.len() as u64,
buf.as_mut_ptr() as u64,
&mut out_size,
)
};
if kr != KERN_SUCCESS {
return Err(Error::ReadFailed {
address,
source: std::io::Error::from_raw_os_error(kr),
});
}
if (out_size as usize) != buf.len() {
return Err(Error::ReadFailed {
address,
source: std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"partial read: expected {} bytes, got {}",
buf.len(),
out_size
),
),
});
}
Ok(())
}
pub fn write_bytes(handle: &ProcessHandle, address: usize, buf: &[u8]) -> Result<()> {
if buf.is_empty() {
return Ok(());
}
let kr = unsafe { mach_vm_write(handle.task, address as u64, buf.as_ptr(), buf.len() as u32) };
if kr != KERN_SUCCESS {
return Err(Error::WriteFailed {
address,
source: std::io::Error::from_raw_os_error(kr),
});
}
Ok(())
}
pub fn regions(handle: &ProcessHandle, _pid: u32) -> Result<Vec<MemoryRegion>> {
let mut result = Vec::new();
let mut address: u64 = 0;
loop {
let mut size: u64 = 0;
let mut info = unsafe { std::mem::zeroed::<VmRegionBasicInfo64>() };
let mut count = VM_REGION_BASIC_INFO_64_COUNT;
let mut object_name: MachPortT = 0;
let kr = unsafe {
mach_vm_region(
handle.task,
&mut address,
&mut size,
VM_REGION_BASIC_INFO_64,
&mut info,
&mut count,
&mut object_name,
)
};
if kr != KERN_SUCCESS {
if kr == KERN_INVALID_ADDRESS {
break;
}
return Err(Error::RegionQueryFailed {
source: std::io::Error::from_raw_os_error(kr),
});
}
result.push(MemoryRegion {
base: address as usize,
size: size as usize,
protection: Protection {
read: info.protection & VM_PROT_READ != 0,
write: info.protection & VM_PROT_WRITE != 0,
execute: info.protection & VM_PROT_EXECUTE != 0,
},
});
address += size;
}
Ok(result)
}
pub fn modules(handle: &ProcessHandle, _pid: u32) -> Result<Vec<Module>> {
let mut dyld_info = unsafe { std::mem::zeroed::<TaskDyldInfo>() };
let mut count = TASK_DYLD_INFO_COUNT;
let kr = unsafe { task_info(handle.task, TASK_DYLD_INFO, &mut dyld_info, &mut count) };
if kr != KERN_SUCCESS {
return Err(Error::ModuleEnumFailed {
source: std::io::Error::from_raw_os_error(kr),
});
}
let mut header_buf = [0u8; 16];
read_bytes(
handle,
dyld_info.all_image_info_addr as usize,
&mut header_buf,
)?;
let info_array_count = u32::from_ne_bytes(header_buf[4..8].try_into().unwrap()) as usize;
let info_array_ptr = u64::from_ne_bytes(header_buf[8..16].try_into().unwrap());
let array_size = info_array_count * 24;
let mut array_buf = vec![0u8; array_size];
read_bytes(handle, info_array_ptr as usize, &mut array_buf)?;
let mut result = Vec::with_capacity(info_array_count);
for i in 0..info_array_count {
let offset = i * 24;
let load_address = u64::from_ne_bytes(array_buf[offset..offset + 8].try_into().unwrap());
let file_path_ptr =
u64::from_ne_bytes(array_buf[offset + 8..offset + 16].try_into().unwrap());
let path = read_c_string(handle, file_path_ptr as usize, 1024).unwrap_or_default();
let name = path.rsplit('/').next().unwrap_or(&path).to_string();
let size = read_macho_size(handle, load_address as usize).unwrap_or(0);
result.push(Module {
name,
base: load_address as usize,
size,
path,
});
}
Ok(result)
}
fn read_c_string(handle: &ProcessHandle, address: usize, max_len: usize) -> Result<String> {
let mut buf = vec![0u8; max_len];
read_bytes(handle, address, &mut buf)?;
let nul_pos = buf.iter().position(|&b| b == 0).unwrap_or(max_len);
Ok(String::from_utf8_lossy(&buf[..nul_pos]).into_owned())
}
fn read_macho_size(handle: &ProcessHandle, base: usize) -> Result<usize> {
let mut header_buf = [0u8; 32];
read_bytes(handle, base, &mut header_buf)?;
let magic = u32::from_ne_bytes(header_buf[0..4].try_into().unwrap());
if magic != MH_MAGIC_64 {
return Ok(0);
}
let ncmds = u32::from_ne_bytes(header_buf[16..20].try_into().unwrap()) as usize;
let sizeofcmds = u32::from_ne_bytes(header_buf[20..24].try_into().unwrap()) as usize;
let cmd_offset = base + 32;
let mut cmd_buf = vec![0u8; sizeofcmds];
read_bytes(handle, cmd_offset, &mut cmd_buf)?;
let mut min_addr: u64 = u64::MAX;
let mut max_addr: u64 = 0;
let mut offset = 0;
for _ in 0..ncmds {
if offset + 8 > cmd_buf.len() {
break;
}
let cmd = u32::from_ne_bytes(cmd_buf[offset..offset + 4].try_into().unwrap());
let cmdsize =
u32::from_ne_bytes(cmd_buf[offset + 4..offset + 8].try_into().unwrap()) as usize;
if cmdsize == 0 {
break;
}
if cmd == LC_SEGMENT_64 && offset + 48 <= cmd_buf.len() {
let vmaddr = u64::from_ne_bytes(cmd_buf[offset + 24..offset + 32].try_into().unwrap());
let vmsize = u64::from_ne_bytes(cmd_buf[offset + 32..offset + 40].try_into().unwrap());
if vmsize > 0 {
min_addr = min_addr.min(vmaddr);
max_addr = max_addr.max(vmaddr + vmsize);
}
}
offset += cmdsize;
}
if max_addr > min_addr {
Ok((max_addr - min_addr) as usize)
} else {
Ok(0)
}
}