Skip to main content

specter/memory/manipulation/
hook.rs

1//! # ARM64 Inline Hooking
2//!
3//! This module implements a robust inline hooking engine for ARM64 architecture.
4//! It supports:
5//! - Standard trampoline-based hooking
6//! - Code cave-based hooking (for stealth)
7//! - Call tracing and original function invocation
8//! - Automatic instruction relocation (fixing PC-relative instructions)
9//! - Thread safety during hook installation/removal
10//! - Instruction obfuscation (junk instructions, opaque predicates)
11//! - Self-checksumming for tamper detection
12
13use super::checksum;
14use crate::config;
15use crate::memory::info::{code_cave, symbol};
16use crate::memory::{image, patch, protection, thread};
17#[cfg(feature = "dev_release")]
18use crate::utils::logger;
19use once_cell::sync::Lazy;
20use parking_lot::Mutex;
21use std::collections::HashMap;
22use std::ffi::c_void;
23use std::ptr;
24use thiserror::Error;
25
26const PAGE_SIZE: usize = 0x4000;
27const TRAMPOLINE_SIZE: usize = 4096;
28const MAX_STOLEN_BYTES: usize = 16;
29const B_RANGE: isize = 128 * 1024 * 1024;
30
31/// Caller-saved registers safe to clobber at function entry
32const SAFE_REGS: [u32; 9] = [9, 10, 11, 12, 13, 14, 15, 16, 17];
33
34/// Maximum junk instructions to insert (randomized 1-4)
35const MAX_JUNK_INSTRS: usize = 4;
36
37/// Internal entry for a registered hook
38struct HookEntry {
39    /// Target address of the hook
40    target: usize,
41    /// Original bytes stolen from the target
42    original: Vec<u8>,
43    /// Address of the allocated trampoline
44    trampoline: usize,
45    /// Size of the stolen bytes (usually 16 or 4 bytes)
46    stolen_size: usize,
47}
48
49static REGISTRY: Lazy<Mutex<HashMap<usize, HookEntry>>> = Lazy::new(|| Mutex::new(HashMap::new()));
50
51#[derive(Error, Debug)]
52/// Errors that can occur during hook operations
53pub enum HookError {
54    /// A hook already exists at the specified address
55    #[error("Hook exists: {0:#x}")]
56    AlreadyExists(usize),
57    /// Failed to find the target image base
58    #[error("Image not found: {0}")]
59    ImageBaseNotFound(#[from] crate::memory::info::image::ImageError),
60    /// Failed to allocate memory for the trampoline
61    #[error("Alloc failed")]
62    AllocationFailed,
63    /// Failed to change memory protection
64    #[error("Protection failed: {0}")]
65    ProtectionFailed(i32),
66    /// Failed to patch the target memory
67    #[error("Patch failed")]
68    PatchFailed,
69    /// Failed to relocate instructions
70    #[error("Relocation failed")]
71    RelocationFailed,
72    /// Thread manipulation error
73    #[error("Thread error: {0}")]
74    ThreadError(#[from] crate::memory::platform::thread::ThreadError),
75    /// Failed to resolve symbol
76    #[error("Symbol error: {0}")]
77    SymbolError(#[from] crate::memory::info::symbol::SymbolError),
78}
79
80/// Represents an installed hook
81pub struct Hook {
82    /// The address where the hook was installed
83    target: usize,
84    /// The address of the trampoline (original function wrapper)
85    trampoline: usize,
86}
87
88impl Hook {
89    #[inline]
90    /// Returns the address of the trampoline
91    ///
92    /// # Returns
93    /// * `usize` - The memory address of the trampoline
94    pub fn trampoline(&self) -> usize {
95        self.trampoline
96    }
97    #[inline]
98    /// Returns the trampoline as a function pointer of the specified type
99    ///
100    /// # Type Parameters
101    /// * `F` - The function pointer type
102    ///
103    /// # Returns
104    /// * `F` - The trampoline cast to the function pointer type
105    pub unsafe fn trampoline_as<F>(&self) -> F
106    where
107        F: Copy,
108    {
109        unsafe { std::mem::transmute_copy(&self.trampoline) }
110    }
111    /// Removes the hook, restoring the original code
112    pub fn remove(self) {
113        unsafe {
114            remove_at_address(self.target);
115        }
116    }
117
118    #[inline]
119    /// Calls the original function (via trampoline)
120    ///
121    /// # Type Parameters
122    /// * `T` - The function pointer type for the trampoline
123    /// * `F` - The callback closure type
124    /// * `R` - The return type
125    ///
126    /// # Arguments
127    /// * `callback` - A closure that takes the trampoline and returns a result
128    ///
129    /// # Returns
130    /// * `R` - The result of the callback
131    pub unsafe fn call_original<T, F, R>(&self, callback: F) -> R
132    where
133        T: Copy,
134        F: FnOnce(T) -> R,
135    {
136        unsafe {
137            let orig: T = std::mem::transmute_copy(&self.trampoline);
138            callback(orig)
139        }
140    }
141
142    /// Verifies that the hook has not been tampered with
143    ///
144    /// # Returns
145    /// * `bool` - `true` if the hook is intact, `false` if it has been modified
146    #[inline]
147    pub fn verify_integrity(&self) -> bool {
148        checksum::verify(self.target).unwrap_or(false)
149    }
150
151    /// Returns the target address where the hook is installed
152    #[inline]
153    pub fn target(&self) -> usize {
154        self.target
155    }
156}
157
158/// Installs an inline hook at a relative virtual address (RVA)
159///
160/// # Arguments
161/// * `rva` - The relative virtual address to hook
162/// * `replacement` - The address of the replacement function
163///
164/// # Returns
165/// * `Result<Hook, HookError>` - The installed hook or an error
166pub unsafe fn install(rva: usize, replacement: usize) -> Result<Hook, HookError> {
167    unsafe {
168        let image_name = config::get_target_image_name().ok_or_else(|| {
169            crate::memory::info::image::ImageError::NotFound("call mem_init first".to_string())
170        })?;
171        let base = image::get_image_base(&image_name)?;
172        let target = base + rva;
173        let trampoline = install_at_address(target, replacement)?;
174        Ok(Hook { target, trampoline })
175    }
176}
177
178/// Installs an inline hook on a symbol resolved by name
179///
180/// # Arguments
181/// * `symbol_name` - The name of the symbol to hook (e.g., "_objc_msgSend")
182/// * `replacement` - The address of the replacement function
183///
184/// # Returns
185/// * `Result<Hook, HookError>` - The installed hook or an error
186pub unsafe fn hook_symbol(symbol_name: &str, replacement: usize) -> Result<Hook, HookError> {
187    unsafe {
188        let target = symbol::resolve_symbol(symbol_name)?;
189        let trampoline = install_at_address(target, replacement)?;
190        Ok(Hook { target, trampoline })
191    }
192}
193
194/// Installs an inline hook at an absolute address
195///
196/// # Arguments
197/// * `target` - The absolute address to hook
198/// * `replacement` - The address of the replacement function
199///
200/// # Returns
201/// * `Result<usize, HookError>` - The address of the trampoline or an error
202pub unsafe fn install_at_address(target: usize, replacement: usize) -> Result<usize, HookError> {
203    unsafe {
204        if REGISTRY.lock().contains_key(&target) {
205            #[cfg(feature = "dev_release")]
206            logger::warning("Hook already exists at target");
207            return Err(HookError::AlreadyExists(target));
208        }
209
210        let first_instr = super::rw::read::<u32>(target).map_err(|_| HookError::PatchFailed)?;
211        if is_b_instruction(first_instr) {
212            #[cfg(feature = "dev_release")]
213            logger::debug("Detected thunk, using short hook");
214            return install_thunk_hook(target, replacement, first_instr);
215        }
216        install_regular_hook(target, replacement)
217    }
218}
219
220/// Checks if instruction is a generic branch
221fn is_b_instruction(instr: u32) -> bool {
222    (instr >> 26) & 0x3F == 0x05
223}
224
225/// Decodes destination of a branch instruction
226fn decode_b_target(instr: u32, pc: usize) -> usize {
227    let imm26 = instr & 0x03FFFFFF;
228    let mut offset = (imm26 << 2) as i32;
229    if (offset & (1 << 27)) != 0 {
230        offset |= !0x0FFFFFFF_u32 as i32;
231    }
232    (pc as isize).wrapping_add(offset as isize) as usize
233}
234
235/// Encodes a branch instruction to target
236fn encode_b_instruction(from: usize, to: usize) -> Option<u32> {
237    let offset = (to as isize) - (from as isize);
238    if !(-B_RANGE..B_RANGE).contains(&offset) {
239        return None;
240    }
241    Some(0x14000000 | (((offset >> 2) as u32) & 0x03FFFFFF))
242}
243
244/// Generates a polymorphic 16-byte absolute branch to `dest`.
245/// Randomly selects between literal-pool and immediate-move encoding,
246/// and randomly rotates the scratch register per call.
247fn gen_branch_bytes(dest: usize) -> [u8; 16] {
248    let rand = unsafe { libc::arc4random() };
249    let reg = SAFE_REGS[(rand as usize) % SAFE_REGS.len()];
250    if (rand >> 16) & 1 == 0 {
251        gen_variant_ldr(dest, reg)
252    } else {
253        gen_variant_mov(dest, reg)
254    }
255}
256
257/// Variant A: LDR Xn, #8 ; BR Xn ; .quad dest (literal pool)
258fn gen_variant_ldr(dest: usize, reg: u32) -> [u8; 16] {
259    let mut buf = [0u8; 16];
260    let ldr = 0x58000040u32 | reg; // LDR Xn, #8
261    let br = 0xD61F0000u32 | (reg << 5); // BR Xn
262    buf[0..4].copy_from_slice(&ldr.to_le_bytes());
263    buf[4..8].copy_from_slice(&br.to_le_bytes());
264    buf[8..16].copy_from_slice(&dest.to_le_bytes());
265    buf
266}
267
268/// Variant B: MOVZ/MOVK Xn, #imm16 (x3) ; BR Xn (immediate moves, no embedded pointer)
269fn gen_variant_mov(dest: usize, reg: u32) -> [u8; 16] {
270    let mut buf = [0u8; 16];
271    let d = dest as u64;
272    let movz = 0xD2800000u32 | (((d & 0xFFFF) as u32) << 5) | reg;
273    let movk16 = 0xF2A00000u32 | ((((d >> 16) & 0xFFFF) as u32) << 5) | reg;
274    let movk32 = 0xF2C00000u32 | ((((d >> 32) & 0xFFFF) as u32) << 5) | reg;
275    let br = 0xD61F0000u32 | (reg << 5);
276    buf[0..4].copy_from_slice(&movz.to_le_bytes());
277    buf[4..8].copy_from_slice(&movk16.to_le_bytes());
278    buf[8..12].copy_from_slice(&movk32.to_le_bytes());
279    buf[12..16].copy_from_slice(&br.to_le_bytes());
280    buf
281}
282
283/// Generates a random junk instruction that has no semantic effect
284/// Uses caller-saved registers to avoid corrupting state
285#[allow(dead_code)]
286fn gen_junk_instruction() -> u32 {
287    let rand = unsafe { libc::arc4random() };
288    let reg = SAFE_REGS[(rand as usize) % SAFE_REGS.len()];
289
290    match (rand >> 8) % 6 {
291        // NOP
292        0 => 0xD503201F,
293        // MOV Xn, Xn (self-move via ORR)
294        1 => 0xAA0003E0 | reg | (reg << 16),
295        // ADD Xn, Xn, #0
296        2 => 0x91000000 | reg | (reg << 5),
297        // SUB Xn, Xn, #0
298        3 => 0xD1000000 | reg | (reg << 5),
299        // EOR Xn, Xn, Xn (XOR with self = 0, then we don't use result)
300        // Using AND Xn, Xn, Xn instead (preserves value)
301        4 => 0x8A000000 | reg | (reg << 5) | (reg << 16),
302        // ORR Xn, Xn, Xn (preserves value)
303        _ => 0xAA000000 | reg | (reg << 5) | (reg << 16),
304    }
305}
306
307/// Generates 1-4 random junk instructions
308/// Returns the bytes and the count of instructions generated
309#[allow(dead_code)]
310fn gen_junk_sled() -> (Vec<u8>, usize) {
311    let rand = unsafe { libc::arc4random() };
312    let count = 1 + ((rand as usize) % MAX_JUNK_INSTRS);
313    let mut bytes = Vec::with_capacity(count * 4);
314
315    for _ in 0..count {
316        bytes.extend_from_slice(&gen_junk_instruction().to_le_bytes());
317    }
318
319    (bytes, count)
320}
321
322/// Generates an opaque predicate that always evaluates to true
323/// Format: CMP XZR, XZR ; B.NE +8 (skip next instruction)
324/// The condition is never taken, so execution falls through
325#[allow(dead_code)]
326fn gen_opaque_always_true() -> [u8; 8] {
327    let mut buf = [0u8; 8];
328    // CMP XZR, XZR (SUBS XZR, XZR, XZR) - always sets Z flag
329    let cmp = 0xEB1F03FF_u32;
330    // B.NE +8 (skip 2 instructions) - never taken since Z is set
331    let bne = 0x54000041_u32;
332    buf[0..4].copy_from_slice(&cmp.to_le_bytes());
333    buf[4..8].copy_from_slice(&bne.to_le_bytes());
334    buf
335}
336
337/// Generates an opaque predicate that always evaluates to false
338/// Format: CMP XZR, XZR ; B.EQ +8 (always jumps forward)
339#[allow(dead_code)]
340fn gen_opaque_always_false() -> [u8; 8] {
341    let mut buf = [0u8; 8];
342    // CMP XZR, XZR - always sets Z flag
343    let cmp = 0xEB1F03FF_u32;
344    // B.EQ +8 (skip 2 instructions) - always taken since Z is set
345    let beq = 0x54000040_u32;
346    buf[0..4].copy_from_slice(&cmp.to_le_bytes());
347    buf[4..8].copy_from_slice(&beq.to_le_bytes());
348    buf
349}
350
351/// Generates an obfuscated branch sequence with junk instructions and opaque predicates
352/// Returns the full byte sequence for the obfuscated branch
353///
354/// Structure:
355/// 1. Random junk sled (1-4 NOPs/self-moves)
356/// 2. Optional opaque predicate (50% chance)
357/// 3. Actual branch instruction sequence
358#[allow(dead_code)]
359fn gen_obfuscated_branch(dest: usize) -> Vec<u8> {
360    let mut bytes = Vec::with_capacity(64);
361    let rand = unsafe { libc::arc4random() };
362
363    let (junk, _) = gen_junk_sled();
364    bytes.extend_from_slice(&junk);
365
366    if (rand >> 16) & 1 == 1 {
367        bytes.extend_from_slice(&gen_opaque_always_true());
368    }
369
370    let branch = gen_branch_bytes(dest);
371    bytes.extend_from_slice(&branch);
372
373    bytes
374}
375
376/// Writes an obfuscated absolute branch sequence to writable memory (trampolines)
377/// Returns the number of bytes written
378#[inline]
379unsafe fn emit_obfuscated_branch(addr: usize, dest: usize) -> usize {
380    unsafe {
381        let bytes = gen_obfuscated_branch(dest);
382        let len = bytes.len();
383        ptr::copy_nonoverlapping(bytes.as_ptr(), addr as *mut u8, len);
384        len
385    }
386}
387
388/// Installs a hook using existing branch/thunk
389unsafe fn install_thunk_hook(
390    target: usize,
391    replacement: usize,
392    first_instr: u32,
393) -> Result<usize, HookError> {
394    unsafe {
395        let suspended = thread::suspend_other_threads()?;
396        let original_target = decode_b_target(first_instr, target);
397        let trampoline = alloc_trampoline_near(target).ok_or(HookError::AllocationFailed)?;
398        let trampoline_base = trampoline as usize;
399
400        if encode_b_instruction(target, trampoline_base).is_none() {
401            libc::munmap(trampoline, TRAMPOLINE_SIZE);
402            thread::resume_threads(&suspended);
403            return install_regular_hook(target, replacement);
404        }
405
406        let can_direct = encode_b_instruction(target, replacement).is_some();
407        let first_len = emit_obfuscated_branch(trampoline_base, original_target);
408        if !can_direct {
409            emit_obfuscated_branch(trampoline_base + first_len, replacement);
410        }
411
412        if protection::protect(
413            trampoline_base,
414            TRAMPOLINE_SIZE,
415            protection::PageProtection::read_execute(),
416        )
417        .is_err()
418        {
419            libc::munmap(trampoline, TRAMPOLINE_SIZE);
420            thread::resume_threads(&suspended);
421            return Err(HookError::ProtectionFailed(0));
422        }
423        patch::invalidate_icache(trampoline, TRAMPOLINE_SIZE);
424
425        let mut original = vec![0u8; 4];
426        ptr::copy_nonoverlapping(target as *const u8, original.as_mut_ptr(), 4);
427
428        let b_instr = if can_direct {
429            encode_b_instruction(target, replacement).unwrap()
430        } else {
431            encode_b_instruction(target, trampoline_base + 16).unwrap()
432        };
433
434        if !patch_short(target, b_instr) {
435            libc::munmap(trampoline, TRAMPOLINE_SIZE);
436            thread::resume_threads(&suspended);
437            return Err(HookError::PatchFailed);
438        }
439
440        REGISTRY.lock().insert(
441            target,
442            HookEntry {
443                target,
444                original,
445                trampoline: trampoline_base,
446                stolen_size: 4,
447            },
448        );
449        let _ = checksum::register(target, 4);
450        thread::resume_threads(&suspended);
451        #[cfg(feature = "dev_release")]
452        logger::debug("Thunk hook installed");
453        Ok(trampoline_base)
454    }
455}
456
457/// Installs a standard inline hook with trampoline
458unsafe fn install_regular_hook(target: usize, replacement: usize) -> Result<usize, HookError> {
459    unsafe {
460        let suspended = thread::suspend_other_threads()?;
461        let trampoline = alloc_trampoline().ok_or(HookError::AllocationFailed)?;
462        if trampoline.is_null() {
463            thread::resume_threads(&suspended);
464            return Err(HookError::AllocationFailed);
465        }
466        let trampoline_base = trampoline as usize;
467
468        let mut original = vec![0u8; MAX_STOLEN_BYTES];
469        ptr::copy_nonoverlapping(target as *const u8, original.as_mut_ptr(), MAX_STOLEN_BYTES);
470
471        let mut trampoline_offset = 0;
472        for i in 0..4 {
473            let instr = super::rw::read::<u32>(target + i * 4).unwrap_or(0);
474            if let Some(size) =
475                relocate_instruction(instr, target + i * 4, trampoline_base + trampoline_offset)
476            {
477                trampoline_offset += size;
478            } else {
479                libc::munmap(trampoline, TRAMPOLINE_SIZE);
480                thread::resume_threads(&suspended);
481                return Err(HookError::RelocationFailed);
482            }
483        }
484
485        emit_obfuscated_branch(
486            trampoline_base + trampoline_offset,
487            target + MAX_STOLEN_BYTES,
488        );
489
490        if protection::protect(
491            trampoline_base,
492            TRAMPOLINE_SIZE,
493            protection::PageProtection::read_execute(),
494        )
495        .is_err()
496        {
497            libc::munmap(trampoline, TRAMPOLINE_SIZE);
498            thread::resume_threads(&suspended);
499            return Err(HookError::ProtectionFailed(0));
500        }
501        patch::invalidate_icache(trampoline, TRAMPOLINE_SIZE);
502
503        if !patch_code(target, replacement) {
504            libc::munmap(trampoline, TRAMPOLINE_SIZE);
505            thread::resume_threads(&suspended);
506            return Err(HookError::PatchFailed);
507        }
508
509        REGISTRY.lock().insert(
510            target,
511            HookEntry {
512                target,
513                original,
514                trampoline: trampoline_base,
515                stolen_size: MAX_STOLEN_BYTES,
516            },
517        );
518        let _ = checksum::register(target, MAX_STOLEN_BYTES);
519        thread::resume_threads(&suspended);
520        #[cfg(feature = "dev_release")]
521        logger::debug("Hook installed");
522        Ok(trampoline_base)
523    }
524}
525
526/// Removes a hook at a relative virtual address (RVA)
527///
528/// # Arguments
529/// * `rva` - The relative virtual address where the hook is installed
530///
531/// # Returns
532/// * `bool` - `true` if the hook was successfully removed, `false` otherwise
533pub unsafe fn remove(rva: usize) -> bool {
534    unsafe {
535        let image_name = match config::get_target_image_name() {
536            Some(n) => n,
537            None => return false,
538        };
539        match image::get_image_base(&image_name) {
540            Ok(base) => remove_at_address(base + rva),
541            Err(_) => false,
542        }
543    }
544}
545
546/// Removes a hook at an absolute address
547///
548/// # Arguments
549/// * `target` - The absolute address where the hook is installed
550///
551/// # Returns
552/// * `bool` - `true` if the hook was successfully removed, `false` otherwise
553pub unsafe fn remove_at_address(target: usize) -> bool {
554    unsafe {
555        let entry = match REGISTRY.lock().remove(&target) {
556            Some(e) => e,
557            None => return false,
558        };
559
560        let suspended = match thread::suspend_other_threads() {
561            Ok(s) => s,
562            Err(_) => return false,
563        };
564
565        let _ = patch::stealth_write(entry.target, &entry.original[..entry.stolen_size]);
566
567        libc::munmap(entry.trampoline as *mut c_void, TRAMPOLINE_SIZE);
568        checksum::unregister(target);
569        thread::resume_threads(&suspended);
570        #[cfg(feature = "dev_release")]
571        logger::debug("Hook removed");
572        true
573    }
574}
575
576/// Restores the hook redirect bytes at the target address
577///
578/// This is called by the integrity monitor when tampering is detected.
579/// It re-writes the hook redirect to restore functionality.
580///
581/// # Arguments
582/// * `target` - The hook target address
583///
584/// # Returns
585/// * `bool` - `true` if restored successfully, `false` otherwise
586pub fn restore_hook_bytes(target: usize) -> bool {
587    let registry = REGISTRY.lock();
588    let entry = match registry.get(&target) {
589        Some(e) => e,
590        None => return false,
591    };
592
593    let trampoline = entry.trampoline;
594    let stolen_size = entry.stolen_size;
595    drop(registry);
596
597    unsafe {
598        if stolen_size == 4 {
599            let offset = (trampoline as isize) - (target as isize);
600            if (-B_RANGE..B_RANGE).contains(&offset) {
601                let b_instr = 0x14000000 | (((offset >> 2) as u32) & 0x03FFFFFF);
602                return patch::stealth_write(target, &b_instr.to_le_bytes()).is_ok();
603            }
604        } else {
605            let bytes = gen_branch_bytes(trampoline + 16);
606            return patch::stealth_write(target, &bytes).is_ok();
607        }
608    }
609
610    false
611}
612
613/// Allocates executable memory for trampoline
614#[inline]
615unsafe fn alloc_trampoline() -> Option<*mut c_void> {
616    unsafe {
617        let ptr = libc::mmap(
618            ptr::null_mut(),
619            TRAMPOLINE_SIZE,
620            libc::PROT_READ | libc::PROT_WRITE,
621            libc::MAP_PRIVATE | libc::MAP_ANON,
622            -1,
623            0,
624        );
625        if ptr == libc::MAP_FAILED {
626            None
627        } else {
628            Some(ptr)
629        }
630    }
631}
632
633/// Allocates trampoline within relative branch range
634#[inline]
635unsafe fn alloc_trampoline_near(target: usize) -> Option<*mut c_void> {
636    unsafe {
637        let search_range = B_RANGE as usize - TRAMPOLINE_SIZE;
638        for offset in (0..search_range).step_by(0x100000) {
639            let hint = target.saturating_sub(offset);
640            let hint_aligned = (hint & !(PAGE_SIZE - 1)) as *mut c_void;
641            let ptr = libc::mmap(
642                hint_aligned,
643                TRAMPOLINE_SIZE,
644                libc::PROT_READ | libc::PROT_WRITE,
645                libc::MAP_PRIVATE | libc::MAP_ANON,
646                -1,
647                0,
648            );
649            if ptr != libc::MAP_FAILED && (ptr as isize - target as isize).abs() < B_RANGE {
650                return Some(ptr);
651            }
652            if ptr != libc::MAP_FAILED {
653                libc::munmap(ptr, TRAMPOLINE_SIZE);
654            }
655        }
656        for offset in (0..search_range).step_by(0x100000) {
657            let hint = target.saturating_add(offset);
658            let hint_aligned = (hint & !(PAGE_SIZE - 1)) as *mut c_void;
659            let ptr = libc::mmap(
660                hint_aligned,
661                TRAMPOLINE_SIZE,
662                libc::PROT_READ | libc::PROT_WRITE,
663                libc::MAP_PRIVATE | libc::MAP_ANON,
664                -1,
665                0,
666            );
667            if ptr != libc::MAP_FAILED && (ptr as isize - target as isize).abs() < B_RANGE {
668                return Some(ptr);
669            }
670            if ptr != libc::MAP_FAILED {
671                libc::munmap(ptr, TRAMPOLINE_SIZE);
672            }
673        }
674        alloc_trampoline()
675    }
676}
677
678/// Patches a single instruction via stealth write
679unsafe fn patch_short(target: usize, instr: u32) -> bool {
680    unsafe { patch::stealth_write(target, &instr.to_le_bytes()).is_ok() }
681}
682
683/// Patches code with polymorphic redirect branch via stealth write
684unsafe fn patch_code(target: usize, dest: usize) -> bool {
685    unsafe {
686        let bytes = gen_branch_bytes(dest);
687        patch::stealth_write(target, &bytes).is_ok()
688    }
689}
690
691/// Writes polymorphic absolute branch sequence (to writable memory like mmap'd trampolines)
692#[inline]
693unsafe fn emit_branch(addr: usize, dest: usize) {
694    unsafe {
695        let bytes = gen_branch_bytes(dest);
696        ptr::copy_nonoverlapping(bytes.as_ptr(), addr as *mut u8, 16);
697    }
698}
699
700/// Relocates PC-relative instructions to trampoline
701unsafe fn relocate_instruction(instr: u32, pc: usize, tramp: usize) -> Option<usize> {
702    unsafe {
703        let op24 = (instr >> 24) & 0x9F;
704        let op26 = (instr >> 26) & 0x3F;
705        let rd = (instr & 0x1F) as u8;
706
707        let is_adr = op24 == 0x10;
708        if is_adr || op24 == 0x90 {
709            let immlo = (instr >> 29) & 0x3;
710            let immhi = (instr >> 5) & 0x7FFFF;
711            let mut imm = (immhi << 2) | immlo;
712            if (imm & (1 << 20)) != 0 {
713                imm |= !0xFFFFF;
714            }
715            let target_val = if is_adr {
716                (pc as isize).wrapping_add(imm as isize) as usize
717            } else {
718                let pc_page = pc & !0xFFF;
719                (pc_page as isize).wrapping_add((imm as isize) << 12) as usize
720            };
721            ptr::write(tramp as *mut u32, 0x58000040 | (rd as u32));
722            ptr::write((tramp + 4) as *mut u32, 0x14000003);
723            ptr::write((tramp + 8) as *mut usize, target_val);
724            return Some(16);
725        }
726
727        let op_check = instr & 0x3B000000;
728        if op_check == 0x18000000 || op_check == 0x58000000 || op_check == 0x98000000 {
729            let imm19 = (instr >> 5) & 0x7FFFF;
730            let mut offset = imm19 << 2;
731            if (offset & (1 << 20)) != 0 {
732                offset |= !0xFFFFF;
733            }
734            let target_addr = (pc as isize).wrapping_add(offset as isize) as usize;
735            let ldr_reg_opcode = if op_check == 0x18000000 {
736                0xB9400000 | (rd as u32)
737            } else if op_check == 0x58000000 {
738                0xF9400000 | (rd as u32)
739            } else {
740                0xB9800000 | (rd as u32)
741            };
742            ptr::write(tramp as *mut u32, 0x58000071);
743            ptr::write((tramp + 4) as *mut u32, ldr_reg_opcode | (17 << 5));
744            ptr::write((tramp + 8) as *mut u32, 0x14000003);
745            ptr::write((tramp + 12) as *mut usize, target_addr);
746            return Some(20);
747        }
748
749        if op26 == 0x05 || op26 == 0x25 {
750            let imm26 = instr & 0x03FFFFFF;
751            let mut offset = imm26 << 2;
752            if (offset & (1 << 27)) != 0 {
753                offset |= !0x0FFFFFFF;
754            }
755            let target_addr = (pc as isize).wrapping_add(offset as isize) as usize;
756            if op26 == 0x25 {
757                ptr::write(tramp as *mut u32, 0x100000BE);
758                emit_branch(tramp + 4, target_addr);
759                return Some(20);
760            } else {
761                emit_branch(tramp, target_addr);
762                return Some(16);
763            }
764        }
765
766        let op_byte = (instr >> 24) & 0xFF;
767        let is_b_cond = op_byte == 0x54;
768        let is_cbz_cbnz = matches!(op_byte, 0x34 | 0xB4 | 0x35 | 0xB5);
769        let is_tbz_tbnz = matches!(op_byte, 0x36 | 0xB6 | 0x37 | 0xB7);
770
771        if is_b_cond || is_cbz_cbnz || is_tbz_tbnz {
772            let target_addr = if is_b_cond || is_cbz_cbnz {
773                let imm19 = (instr >> 5) & 0x7FFFF;
774                let offset = if (imm19 & (1 << 18)) != 0 {
775                    ((imm19 | 0xFFF80000) as i32) as isize
776                } else {
777                    imm19 as isize
778                };
779                (pc as isize).wrapping_add(offset * 4) as usize
780            } else {
781                let imm14 = (instr >> 5) & 0x3FFF;
782                let offset = if (imm14 & (1 << 13)) != 0 {
783                    ((imm14 | 0xFFFFC000) as i32) as isize
784                } else {
785                    imm14 as isize
786                };
787                (pc as isize).wrapping_add(offset * 4) as usize
788            };
789            let inverted = if is_b_cond {
790                ((instr & 0xFF00000F) ^ 1) | (5 << 5)
791            } else if is_cbz_cbnz {
792                ((instr & 0xFF00001F) ^ (1 << 24)) | (5 << 5)
793            } else {
794                ((instr & 0xFFF8001F) ^ (1 << 24)) | (5 << 5)
795            };
796            ptr::write(tramp as *mut u32, inverted);
797            ptr::write((tramp + 4) as *mut u32, 0x58000051);
798            ptr::write((tramp + 8) as *mut u32, 0xD61F0220);
799            ptr::write((tramp + 12) as *mut usize, target_addr);
800            return Some(20);
801        }
802
803        ptr::write(tramp as *mut u32, instr);
804        Some(4)
805    }
806}
807
808/// Installs a hook using a code cave for the trampoline (harder to detect)
809///
810/// # Arguments
811/// * `rva` - The relative virtual address to hook
812/// * `replacement` - The address of the replacement function
813///
814/// # Returns
815/// * `Result<Hook, HookError>` - The installed hook or an error
816pub unsafe fn install_in_cave(rva: usize, replacement: usize) -> Result<Hook, HookError> {
817    unsafe {
818        let image_name = config::get_target_image_name().ok_or_else(|| {
819            crate::memory::info::image::ImageError::NotFound("call mem_init first".to_string())
820        })?;
821        let base = image::get_image_base(&image_name)?;
822        let target = base + rva;
823        install_in_cave_at_address(target, replacement)
824    }
825}
826
827/// Installs a hook using a code cave at an absolute address
828///
829/// # Arguments
830/// * `target` - The absolute address to hook
831/// * `replacement` - The address of the replacement function
832///
833/// # Returns
834/// * `Result<Hook, HookError>` - The installed hook or an error
835pub unsafe fn install_in_cave_at_address(
836    target: usize,
837    replacement: usize,
838) -> Result<Hook, HookError> {
839    unsafe {
840        if REGISTRY.lock().contains_key(&target) {
841            return Err(HookError::AlreadyExists(target));
842        }
843
844        let cave =
845            code_cave::allocate_cave_near(target, 256).map_err(|_| HookError::AllocationFailed)?;
846
847        let trampoline_base = (cave.address + 3) & !3;
848        let suspended = thread::suspend_other_threads()?;
849
850        let mut original = vec![0u8; MAX_STOLEN_BYTES];
851        ptr::copy_nonoverlapping(target as *const u8, original.as_mut_ptr(), MAX_STOLEN_BYTES);
852
853        let mut tramp_buf = vec![0u8; 256];
854        let buf_base = tramp_buf.as_mut_ptr() as usize;
855
856        let mut cave_offset = 0;
857        for i in 0..4 {
858            let instr = super::rw::read::<u32>(target + i * 4).unwrap_or(0);
859            if let Some(size) = relocate_instruction(instr, target + i * 4, buf_base + cave_offset)
860            {
861                cave_offset += size;
862            } else {
863                code_cave::free_cave(cave.address).ok();
864                thread::resume_threads(&suspended);
865                return Err(HookError::RelocationFailed);
866            }
867        }
868
869        cave_offset += emit_obfuscated_branch(buf_base + cave_offset, target + MAX_STOLEN_BYTES);
870
871        if patch::stealth_write(trampoline_base, &tramp_buf[..cave_offset]).is_err() {
872            code_cave::free_cave(cave.address).ok();
873            thread::resume_threads(&suspended);
874            return Err(HookError::PatchFailed);
875        }
876
877        if !patch_code(target, replacement) {
878            code_cave::free_cave(cave.address).ok();
879            thread::resume_threads(&suspended);
880            return Err(HookError::PatchFailed);
881        }
882
883        REGISTRY.lock().insert(
884            target,
885            HookEntry {
886                target,
887                original,
888                trampoline: trampoline_base,
889                stolen_size: MAX_STOLEN_BYTES,
890            },
891        );
892        let _ = checksum::register(target, MAX_STOLEN_BYTES);
893
894        thread::resume_threads(&suspended);
895        #[cfg(feature = "dev_release")]
896        logger::debug("Cave hook installed");
897
898        Ok(Hook {
899            target,
900            trampoline: trampoline_base,
901        })
902    }
903}
904
905/// Returns the number of active hooks
906///
907/// # Returns
908/// * `usize` - The count of active hooks
909pub fn hook_count() -> usize {
910    REGISTRY.lock().len()
911}
912
913/// Lists all active hook target addresses
914///
915/// # Returns
916/// * `Vec<usize>` - A list of addresses where hooks are installed
917pub fn list_hooks() -> Vec<usize> {
918    REGISTRY.lock().keys().copied().collect()
919}
920
921/// Checks if an address has an active hook
922///
923/// # Arguments
924/// * `target` - The address to check
925///
926/// # Returns
927/// * `bool` - `true` if a hook exists at the address
928pub fn is_hooked(target: usize) -> bool {
929    REGISTRY.lock().contains_key(&target)
930}