use crate::memory::ffi::mach_exc::mach_vm_remap;
use crate::memory::platform::thread;
#[cfg(debug_assertions)]
use crate::utils::logger;
use jit_assembler::aarch64::Aarch64InstructionBuilder;
use jit_assembler::common::InstructionBuilder;
use mach2::kern_return::KERN_SUCCESS;
use mach2::traps::mach_task_self;
use mach2::vm::{mach_vm_deallocate, mach_vm_protect};
use mach2::vm_prot::{VM_PROT_COPY, VM_PROT_EXECUTE, VM_PROT_READ, VM_PROT_WRITE};
use std::arch::asm;
use std::ffi::c_void;
use std::ptr;
use thiserror::Error;
const CACHE_LINE_SIZE: usize = 64;
const B_RANGE: isize = 128 * 1024 * 1024;
const VM_FLAGS_ANYWHERE: i32 = 0x1;
const VM_INHERIT_NONE: u32 = 2;
#[derive(Error, Debug)]
pub enum PatchError {
#[error("Invalid hex: {0}")]
InvalidHex(#[from] hex::FromHexError),
#[error("Image not found: {0}")]
ImageBaseNotFound(#[from] crate::memory::info::image::ImageError),
#[error("Protection failed: {0}")]
ProtectionFailed(i32),
#[error("Thread error: {0}")]
ThreadError(#[from] crate::memory::platform::thread::ThreadError),
#[error("Empty instructions")]
EmptyInstructions,
#[error("Code cave error: {0}")]
CaveError(#[from] crate::memory::info::code_cave::CodeCaveError),
#[error("Branch target out of range")]
BranchOutOfRange,
#[error("Write verification failed")]
VerificationFailed,
}
pub struct Patch {
address: usize,
original_bytes: Vec<u8>,
patch_bytes: Vec<u8>,
cave: Option<crate::memory::info::code_cave::CodeCave>,
}
impl Patch {
pub fn revert(&self) {
unsafe {
let suspended = match thread::suspend_other_threads() {
Ok(s) => s,
Err(_e) => {
#[cfg(debug_assertions)]
logger::error(&format!("Revert suspend failed: {}", _e));
return;
}
};
if let Err(_e) = stealth_write(self.address, &self.original_bytes) {
#[cfg(debug_assertions)]
logger::error(&format!("Revert write failed: {}", _e));
}
if let Some(cave) = &self.cave
&& let Err(_e) = crate::memory::info::code_cave::free_cave(cave.address)
{
#[cfg(debug_assertions)]
logger::error(&format!("Cave free failed: {}", _e));
}
thread::resume_threads(&suspended);
#[cfg(debug_assertions)]
logger::debug("Patch reverted");
}
}
pub fn address(&self) -> usize {
self.address
}
pub fn original_bytes(&self) -> &[u8] {
&self.original_bytes
}
pub fn patch_bytes(&self) -> &[u8] {
&self.patch_bytes
}
pub fn size(&self) -> usize {
self.original_bytes.len()
}
pub fn current_bytes(&self) -> Vec<u8> {
unsafe { read_bytes(self.address, self.original_bytes.len()) }
}
}
pub fn apply(rva: usize, hex_str: &str) -> Result<Patch, PatchError> {
let clean: String = hex_str.chars().filter(|c| !c.is_whitespace()).collect();
let bytes = hex::decode(&clean)?;
let image_name = crate::config::get_target_image_name().ok_or_else(|| {
crate::memory::info::image::ImageError::NotFound("call mem_init first".to_string())
})?;
let base = crate::memory::info::image::get_image_base(&image_name)?;
let address = base + rva;
unsafe {
let suspended = thread::suspend_other_threads()?;
let original_bytes = read_bytes(address, bytes.len());
let result = stealth_write(address, &bytes);
if let Err(e) = result {
thread::resume_threads(&suspended);
return Err(e);
}
if !verify_write(address, &bytes) {
thread::resume_threads(&suspended);
return Err(PatchError::VerificationFailed);
}
thread::resume_threads(&suspended);
#[cfg(debug_assertions)]
logger::debug("Patch applied");
Ok(Patch {
address,
original_bytes,
patch_bytes: bytes,
cave: None,
})
}
}
pub fn apply_asm<F>(rva: usize, build: F) -> Result<Patch, PatchError>
where
F: FnOnce(&mut Aarch64InstructionBuilder) -> &mut Aarch64InstructionBuilder,
{
let mut builder = Aarch64InstructionBuilder::new();
build(&mut builder);
let instructions = builder.instructions();
if instructions.is_empty() {
return Err(PatchError::EmptyInstructions);
}
let bytes: Vec<u8> = instructions
.iter()
.flat_map(|instr| instr.0.to_le_bytes())
.collect();
let image_name = crate::config::get_target_image_name().ok_or_else(|| {
crate::memory::info::image::ImageError::NotFound("call mem_init first".to_string())
})?;
let base = crate::memory::info::image::get_image_base(&image_name)?;
let address = base + rva;
unsafe {
let suspended = thread::suspend_other_threads()?;
let original_bytes = read_bytes(address, bytes.len());
let result = stealth_write(address, &bytes);
if let Err(e) = result {
thread::resume_threads(&suspended);
return Err(e);
}
if !verify_write(address, &bytes) {
thread::resume_threads(&suspended);
return Err(PatchError::VerificationFailed);
}
thread::resume_threads(&suspended);
#[cfg(debug_assertions)]
logger::debug("ASM patch applied");
Ok(Patch {
address,
original_bytes,
patch_bytes: bytes,
cave: None,
})
}
}
pub fn apply_asm_in_cave<F>(rva: usize, build: F) -> Result<Patch, PatchError>
where
F: FnOnce(&mut Aarch64InstructionBuilder) -> &mut Aarch64InstructionBuilder,
{
let mut builder = Aarch64InstructionBuilder::new();
build(&mut builder);
let instructions = builder.instructions();
if instructions.is_empty() {
return Err(PatchError::EmptyInstructions);
}
let bytes: Vec<u8> = instructions
.iter()
.flat_map(|instr| instr.0.to_le_bytes())
.collect();
let image_name = crate::config::get_target_image_name().ok_or_else(|| {
crate::memory::info::image::ImageError::NotFound("call mem_init first".to_string())
})?;
let base = crate::memory::info::image::get_image_base(&image_name)?;
let address = base + rva;
let cave = crate::memory::info::code_cave::allocate_cave_near(address, bytes.len())?;
let aligned_address = (cave.address + 3) & !3;
if aligned_address + bytes.len() > cave.address + cave.size {
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(PatchError::CaveError(
crate::memory::info::code_cave::CodeCaveError::Custom(
"Cave too small for alignment".to_string(),
),
));
}
let offset = (aligned_address as isize) - (address as isize);
if !(-B_RANGE..B_RANGE).contains(&offset) {
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(PatchError::BranchOutOfRange);
}
let b_instr = 0x14000000 | (((offset >> 2) as u32) & 0x03FFFFFF);
let b_bytes = b_instr.to_le_bytes();
unsafe {
let suspended = thread::suspend_other_threads()?;
if let Err(e) = stealth_write(aligned_address, &bytes) {
thread::resume_threads(&suspended);
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(e);
}
if !verify_write(aligned_address, &bytes) {
thread::resume_threads(&suspended);
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(PatchError::VerificationFailed);
}
let original_bytes = read_bytes(address, 4);
if let Err(e) = stealth_write(address, &b_bytes) {
thread::resume_threads(&suspended);
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(e);
}
if !verify_write(address, &b_bytes) {
thread::resume_threads(&suspended);
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(PatchError::VerificationFailed);
}
thread::resume_threads(&suspended);
#[cfg(debug_assertions)]
logger::debug("Cave ASM patch applied");
Ok(Patch {
address,
original_bytes,
patch_bytes: b_bytes.to_vec(),
cave: Some(cave),
})
}
}
pub fn apply_in_cave(rva: usize, hex_str: &str) -> Result<Patch, PatchError> {
let clean: String = hex_str.chars().filter(|c| !c.is_whitespace()).collect();
let bytes = hex::decode(&clean)?;
if bytes.is_empty() {
return Err(PatchError::EmptyInstructions);
}
let image_name = crate::config::get_target_image_name().ok_or_else(|| {
crate::memory::info::image::ImageError::NotFound("call mem_init first".to_string())
})?;
let base = crate::memory::info::image::get_image_base(&image_name)?;
let address = base + rva;
let cave = crate::memory::info::code_cave::allocate_cave_near(address, bytes.len())?;
let aligned_address = (cave.address + 3) & !3;
if aligned_address + bytes.len() > cave.address + cave.size {
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(PatchError::CaveError(
crate::memory::info::code_cave::CodeCaveError::Custom(
"Cave too small for alignment".to_string(),
),
));
}
let offset = (aligned_address as isize) - (address as isize);
if !(-B_RANGE..B_RANGE).contains(&offset) {
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(PatchError::BranchOutOfRange);
}
let b_instr = 0x14000000 | (((offset >> 2) as u32) & 0x03FFFFFF);
let b_bytes = b_instr.to_le_bytes();
unsafe {
let suspended = thread::suspend_other_threads()?;
if let Err(e) = stealth_write(aligned_address, &bytes) {
thread::resume_threads(&suspended);
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(e);
}
if !verify_write(aligned_address, &bytes) {
thread::resume_threads(&suspended);
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(PatchError::VerificationFailed);
}
let original_bytes = read_bytes(address, 4);
if let Err(e) = stealth_write(address, &b_bytes) {
thread::resume_threads(&suspended);
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(e);
}
if !verify_write(address, &b_bytes) {
thread::resume_threads(&suspended);
crate::memory::info::code_cave::free_cave(cave.address).ok();
return Err(PatchError::VerificationFailed);
}
thread::resume_threads(&suspended);
#[cfg(debug_assertions)]
logger::debug("Cave patch applied");
Ok(Patch {
address,
original_bytes,
patch_bytes: b_bytes.to_vec(),
cave: Some(cave),
})
}
}
pub fn apply_at_address(address: usize, bytes: &[u8]) -> Result<Patch, PatchError> {
unsafe {
let suspended = thread::suspend_other_threads()?;
let original_bytes = read_bytes(address, bytes.len());
let result = stealth_write(address, bytes);
if let Err(e) = result {
thread::resume_threads(&suspended);
return Err(e);
}
if !verify_write(address, bytes) {
thread::resume_threads(&suspended);
return Err(PatchError::VerificationFailed);
}
thread::resume_threads(&suspended);
#[cfg(debug_assertions)]
logger::debug("Address patch applied");
Ok(Patch {
address,
original_bytes,
patch_bytes: bytes.to_vec(),
cave: None,
})
}
}
unsafe fn read_bytes(address: usize, len: usize) -> Vec<u8> {
unsafe {
(0..len)
.map(|i| super::rw::read::<u8>(address + i).unwrap_or(0))
.collect()
}
}
pub(crate) unsafe fn stealth_write(address: usize, data: &[u8]) -> Result<(), PatchError> {
unsafe {
let page_size = libc::sysconf(libc::_SC_PAGESIZE) as usize;
let page_mask = !(page_size - 1);
let page_start = address & page_mask;
let page_len = ((address + data.len() + page_size - 1) & page_mask) - page_start;
let offset_in_page = address - page_start;
let task = mach_task_self();
let mut remap_addr: u64 = 0;
let mut cur_prot: i32 = 0;
let mut max_prot: i32 = 0;
let kr = mach_vm_remap(
task,
&mut remap_addr,
page_len as u64,
0,
VM_FLAGS_ANYWHERE,
task,
page_start as u64,
0, &mut cur_prot,
&mut max_prot,
VM_INHERIT_NONE,
);
if kr != KERN_SUCCESS {
#[cfg(debug_assertions)]
logger::debug("Remap unavailable, fallback");
return fallback_write(address, data);
}
if (max_prot & VM_PROT_WRITE) == 0 {
mach_vm_deallocate(task, remap_addr, page_len as u64);
#[cfg(debug_assertions)]
logger::debug("Remap max prot insufficient, fallback");
return fallback_write(address, data);
}
let kr = mach_vm_protect(
task,
remap_addr,
page_len as u64,
0,
VM_PROT_READ | VM_PROT_WRITE,
);
if kr != KERN_SUCCESS {
mach_vm_deallocate(task, remap_addr, page_len as u64);
#[cfg(debug_assertions)]
logger::debug("Remap protect failed, fallback");
return fallback_write(address, data);
}
let write_addr = remap_addr as usize + offset_in_page;
ptr::copy_nonoverlapping(data.as_ptr(), write_addr as *mut u8, data.len());
mach_vm_deallocate(task, remap_addr, page_len as u64);
invalidate_icache(address as *mut c_void, data.len());
Ok(())
}
}
unsafe fn fallback_write(address: usize, data: &[u8]) -> Result<(), PatchError> {
unsafe {
use crate::memory::info::protection;
let page_size = libc::sysconf(libc::_SC_PAGESIZE) as usize;
let page_mask = !(page_size - 1);
let page_start = address & page_mask;
let page_len = ((address + data.len() + page_size - 1) & page_mask) - page_start;
let original_prot = protection::get_protection(address)
.map(|p| p.raw())
.unwrap_or(VM_PROT_READ | VM_PROT_EXECUTE);
protection::protect(
page_start,
page_len,
protection::PageProtection::from_raw(VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY),
)
.map_err(|e| match e {
protection::ProtectionError::ProtectionFailed(k) => PatchError::ProtectionFailed(k),
_ => PatchError::ProtectionFailed(0),
})?;
ptr::copy_nonoverlapping(data.as_ptr(), address as *mut u8, data.len());
let _ = protection::protect(
page_start,
page_len,
protection::PageProtection::from_raw(original_prot),
);
invalidate_icache(address as *mut c_void, data.len());
Ok(())
}
}
unsafe fn verify_write(address: usize, expected: &[u8]) -> bool {
unsafe {
for (i, &byte) in expected.iter().enumerate() {
if super::rw::read::<u8>(address + i).unwrap_or(!byte) != byte {
return false;
}
}
true
}
}
#[inline]
pub unsafe fn invalidate_icache(start: *mut c_void, len: usize) {
unsafe {
let start_addr = start as usize;
let end_addr = start_addr + len;
let mut addr = start_addr & !(CACHE_LINE_SIZE - 1);
while addr < end_addr {
asm!("dc cvau, {x}", x = in(reg) addr, options(nostack, preserves_flags));
addr += CACHE_LINE_SIZE;
}
asm!("dsb ish", options(nostack, preserves_flags));
addr = start_addr & !(CACHE_LINE_SIZE - 1);
while addr < end_addr {
asm!("ic ivau, {x}", x = in(reg) addr, options(nostack, preserves_flags));
addr += CACHE_LINE_SIZE;
}
asm!("dsb ish", options(nostack, preserves_flags));
asm!("isb", options(nostack, preserves_flags));
}
}