Skip to main content

cradle_hooks/
engine.rs

1use crate::breakpoints::{Breakpoint, BreakpointMap, ModuleBaseCache};
2use crate::defs::{HookAction, HookContext, HookHandler};
3use crate::utils::{
4    get_thread_context, normalize_name, read_remote_or, read_single_byte, resolve_export,
5    set_thread_context, write_process_memory, write_single_byte,
6};
7use cradle_shared::{CradleError, CradleResult};
8use windows_sys::Win32::Foundation::HANDLE;
9
10/// Result of the hooking operation
11pub enum DispatchResult {
12    /// The hooked event was handled correctly
13    Handled,
14    /// No registered hook handled this event
15    Skip,
16}
17
18/// The core hooking engine
19/// Responsible for dispatching, registering, and removing hooks from the target process
20pub struct HookEngine {
21    breakpoints: BreakpointMap,
22    module_cache: ModuleBaseCache,
23    process: HANDLE,
24    pending_restore: Option<PendingRestore>,
25}
26
27struct PendingRestore {
28    addr: usize,
29    thread: HANDLE,
30}
31
32impl HookEngine {
33    /// Creates an empty [`HookEngine`] object
34    pub fn empty() -> HookEngine {
35        Self {
36            breakpoints: BreakpointMap::new(),
37            module_cache: ModuleBaseCache::new(),
38            process: HANDLE::default(),
39            pending_restore: None,
40        }
41    }
42}
43
44impl HookEngine {
45    /// Creates a new [`HookEngine`] object with the specified process handle
46    pub fn new(process: HANDLE) -> Self {
47        Self {
48            breakpoints: BreakpointMap::new(),
49            module_cache: ModuleBaseCache::new(),
50            process,
51            pending_restore: None,
52        }
53    }
54
55    /// Registers a module to hook given its name and base address
56    pub fn register_module(&mut self, name: &str, base: usize) {
57        self.module_cache.insert(normalize_name(name), base);
58    }
59
60    /// Dispatches a hook on the given handle
61    ///
62    /// # Safety
63    /// Calls `get_thread_context` to get the current registers. This function is unsafe and users should be aware of it
64    pub unsafe fn dispatch(&mut self, thread: HANDLE) -> CradleResult<DispatchResult> {
65        unsafe {
66            let mut ctx = get_thread_context(thread)?;
67            let bp_addr = (ctx.Rip - 1) as usize;
68            let bp = match self.breakpoints.get(&bp_addr) {
69                Some(bp) => bp,
70                None => return Ok(DispatchResult::Skip),
71            };
72            let mut hook_ctx = HookContext {
73                registers: &mut ctx,
74                process: self.process,
75                ret_value: 0,
76                export_name: &bp.export_name,
77                module_name: &bp.module_name,
78                #[cfg(feature = "unstable")]
79                return_hook: None,
80            };
81            let mut action = HookAction::Continue;
82            for handler in &bp.handlers {
83                match handler(&mut hook_ctx) {
84                    Ok(HookAction::Continue) => {}
85                    Ok(HookAction::Modify) => action = HookAction::Modify,
86                    Ok(block @ HookAction::Block(_)) => {
87                        action = block;
88                        break;
89                    }
90                    Err(_) => {}
91                }
92            }
93            let ret = hook_ctx.ret_value;
94            match action {
95                HookAction::Continue | HookAction::Modify => {
96                    write_process_memory(self.process, bp_addr, &[bp.original_byte])?;
97                    ctx.Rip = bp_addr as u64;
98                    ctx.EFlags |= 0x100;
99                    set_thread_context(thread, &ctx)?;
100                    self.pending_restore = Some(PendingRestore {
101                        addr: bp_addr,
102                        thread,
103                    });
104                }
105                HookAction::Block(u) => {
106                    let ret_addr: u64 = read_remote_or(self.process, ctx.Rsp as usize, u);
107                    ctx.Rip = ret_addr;
108                    ctx.Rsp += 8;
109                    ctx.Rax = ret;
110                    set_thread_context(thread, &ctx)?;
111                }
112            }
113            Ok(DispatchResult::Handled)
114        }
115    }
116
117    /// Dispatches a single breakpoint by writing it to the process
118    ///
119    /// # Safety
120    /// Calls `WriteProcessMemory` under the hood (via FFI). This may be unsafe
121    pub unsafe fn dispatch_single_step(&mut self, thread: HANDLE) -> CradleResult<DispatchResult> {
122        match self.pending_restore.take() {
123            Some(restore) if restore.thread == thread => {
124                if self.breakpoints.contains_key(&restore.addr) {
125                    unsafe {
126                        write_process_memory(self.process, restore.addr, &[0xCC])?;
127                    }
128                }
129                Ok(DispatchResult::Handled)
130            }
131            Some(restore) => {
132                self.pending_restore = Some(restore);
133                Ok(DispatchResult::Skip)
134            }
135            None => Ok(DispatchResult::Skip),
136        }
137    }
138
139    /// Attempts to hook an exported function in the specified module
140    /// Will return an error if a hook is already installed there
141    ///
142    /// # Safety
143    /// Reads and writes data to the process' memory, which is unsafe and should be treated as such
144    pub unsafe fn hook_export(
145        &mut self,
146        module: &str,
147        export: &str,
148        handler: HookHandler,
149    ) -> CradleResult {
150        if self.module_cache.is_empty() {
151            return Err(CradleError::ModuleNotFound(
152                "module cache is empty".to_owned(),
153            ));
154        }
155        let module_lower = normalize_name(module);
156        let base = self
157            .module_cache
158            .get(&module_lower)
159            .copied()
160            .ok_or_else(|| CradleError::ModuleNotFound(module.to_string()))?;
161        let addr = resolve_export(self.process, base, export)?;
162        if let Some(bp) = self.breakpoints.get_mut(&addr) {
163            bp.push_handler(handler);
164            return Ok(());
165        }
166        let mut orig = [0u8; 1];
167        unsafe {
168            read_single_byte(self.process, addr, &mut orig)?;
169            write_single_byte(self.process, addr, &[0xCC])?;
170        }
171        self.breakpoints.insert(
172            addr,
173            Breakpoint::new(orig[0], handler, module_lower, export.to_string()),
174        );
175
176        Ok(())
177    }
178    /// Attempts to unhook the breakpoint at the given address
179    ///
180    /// # Safety
181    /// Calls `WriteProcessMemory` under the hood (via FFI). This may be unsafe
182    pub unsafe fn unhook(&mut self, addr: usize) -> CradleResult {
183        if let Some(bp) = self.breakpoints.remove(&addr) {
184            unsafe {
185                write_process_memory(self.process, addr, &[bp.original_byte])?;
186            }
187        }
188        Ok(())
189    }
190
191    /// Attempts to unhook all existing hooks
192    ///
193    /// # Safety
194    /// Calls `self.unhook(addr)` on each hook. This function is unsafe
195    pub unsafe fn unhook_all(&mut self) -> CradleResult {
196        let addrs: Vec<usize> = self.breakpoints.keys().copied().collect();
197        for addr in addrs {
198            unsafe {
199                self.unhook(addr)?;
200            }
201        }
202        Ok(())
203    }
204
205    /// Returns the amount of breakpoints are currently installed
206    pub fn hooked_count(&self) -> usize {
207        self.breakpoints.len()
208    }
209}
210unsafe impl Send for HookEngine {}