wraith/manipulation/hooks/
unhook.rs

1//! Surgical function unhooking
2//!
3//! Restores hooked functions to their original state by copying
4//! original bytes from a clean copy of the module (loaded from disk).
5
6use super::detector::{HookDetector, HookInfo};
7use crate::error::{Result, WraithError};
8use crate::manipulation::manual_map::ParsedPe;
9use crate::navigation::Module;
10use crate::util::memory::ProtectionGuard;
11
12/// protection constant for RWX memory
13const PAGE_EXECUTE_READWRITE: u32 = 0x40;
14
15/// result of unhook operation
16#[derive(Debug)]
17pub struct UnhookResult {
18    /// number of functions successfully unhooked
19    pub unhooked_count: usize,
20    /// functions that were unhooked
21    pub unhooked_functions: Vec<String>,
22    /// functions that failed to unhook (name, reason)
23    pub failed_functions: Vec<(String, String)>,
24}
25
26impl UnhookResult {
27    /// check if all hooks were removed
28    pub fn all_successful(&self) -> bool {
29        self.failed_functions.is_empty()
30    }
31
32    /// total number of hooks processed
33    pub fn total(&self) -> usize {
34        self.unhooked_count + self.failed_functions.len()
35    }
36}
37
38/// module unhooker
39pub struct Unhooker<'a> {
40    module: &'a Module<'a>,
41    clean_copy: Vec<u8>,
42    parsed_pe: ParsedPe,
43}
44
45impl<'a> Unhooker<'a> {
46    /// create unhooker for module (loads clean copy from disk)
47    pub fn new(module: &'a Module<'a>) -> Result<Self> {
48        let path = module.full_path();
49        let clean_copy = std::fs::read(&path).map_err(|_| WraithError::CleanCopyUnavailable)?;
50
51        let parsed_pe = ParsedPe::parse(&clean_copy)?;
52
53        Ok(Self {
54            module,
55            clean_copy,
56            parsed_pe,
57        })
58    }
59
60    /// create unhooker with explicit clean copy
61    pub fn with_clean_copy(module: &'a Module<'a>, clean_copy: Vec<u8>) -> Result<Self> {
62        let parsed_pe = ParsedPe::parse(&clean_copy)?;
63
64        Ok(Self {
65            module,
66            clean_copy,
67            parsed_pe,
68        })
69    }
70
71    /// unhook a single function by restoring original bytes
72    pub fn unhook_function(&self, hook: &HookInfo) -> Result<()> {
73        if hook.original_bytes.is_empty() {
74            return Err(WraithError::UnhookFailed {
75                function: hook.function_name.clone(),
76                reason: "no original bytes available".into(),
77            });
78        }
79
80        let addr = hook.function_address;
81        let size = hook.original_bytes.len();
82
83        // change protection to RWX
84        let _guard = ProtectionGuard::new(addr, size, PAGE_EXECUTE_READWRITE)?;
85
86        // restore original bytes
87        // SAFETY: protection changed to RWX, original_bytes length is correct
88        unsafe {
89            core::ptr::copy_nonoverlapping(hook.original_bytes.as_ptr(), addr as *mut u8, size);
90        }
91
92        Ok(())
93    }
94
95    /// unhook all detected hooks
96    pub fn unhook_all(&self) -> Result<UnhookResult> {
97        let detector = HookDetector::with_clean_copy(self.module, self.clean_copy.clone());
98        let hooks = detector.scan_exports()?;
99
100        let mut result = UnhookResult {
101            unhooked_count: 0,
102            unhooked_functions: Vec::new(),
103            failed_functions: Vec::new(),
104        };
105
106        for hook in hooks {
107            match self.unhook_function(&hook) {
108                Ok(()) => {
109                    result.unhooked_count += 1;
110                    result.unhooked_functions.push(hook.function_name);
111                }
112                Err(e) => {
113                    result
114                        .failed_functions
115                        .push((hook.function_name, e.to_string()));
116                }
117            }
118        }
119
120        Ok(result)
121    }
122
123    /// unhook entire .text section by copying from clean copy
124    pub fn unhook_text_section(&self) -> Result<()> {
125        let text_section = self
126            .parsed_pe
127            .sections()
128            .iter()
129            .find(|s| s.name_str() == ".text")
130            .ok_or_else(|| WraithError::UnhookFailed {
131                function: ".text".into(),
132                reason: "no .text section found".into(),
133            })?;
134
135        let text_rva = text_section.virtual_address as usize;
136        let text_size = text_section.virtual_size as usize;
137        let text_file_offset = text_section.pointer_to_raw_data as usize;
138        let text_raw_size = text_section.size_of_raw_data as usize;
139
140        let target_addr = self.module.base() + text_rva;
141
142        // change protection
143        let _guard = ProtectionGuard::new(target_addr, text_size, PAGE_EXECUTE_READWRITE)?;
144
145        // get clean .text section data
146        let copy_size = text_raw_size.min(text_size);
147        if text_file_offset + copy_size > self.clean_copy.len() {
148            return Err(WraithError::UnhookFailed {
149                function: ".text".into(),
150                reason: "clean copy too small".into(),
151            });
152        }
153
154        let clean_text = &self.clean_copy[text_file_offset..text_file_offset + copy_size];
155
156        // copy clean .text section
157        // SAFETY: protection changed, bounds checked
158        unsafe {
159            core::ptr::copy_nonoverlapping(clean_text.as_ptr(), target_addr as *mut u8, copy_size);
160        }
161
162        Ok(())
163    }
164
165    /// unhook specific function by name
166    pub fn unhook_by_name(&self, function_name: &str) -> Result<()> {
167        let addr = self.module.get_export(function_name)?;
168
169        // get RVA
170        let rva = self.module.va_to_rva(addr).ok_or_else(|| WraithError::UnhookFailed {
171            function: function_name.into(),
172            reason: "address not in module".into(),
173        })?;
174
175        // get original bytes from clean copy
176        let original = self.get_original_bytes(rva as usize, 32)?;
177
178        // change protection and restore
179        let _guard = ProtectionGuard::new(addr, original.len(), PAGE_EXECUTE_READWRITE)?;
180
181        // SAFETY: protection changed, bounds verified
182        unsafe {
183            core::ptr::copy_nonoverlapping(original.as_ptr(), addr as *mut u8, original.len());
184        }
185
186        Ok(())
187    }
188
189    /// unhook multiple functions by name
190    pub fn unhook_by_names(&self, names: &[&str]) -> UnhookResult {
191        let mut result = UnhookResult {
192            unhooked_count: 0,
193            unhooked_functions: Vec::new(),
194            failed_functions: Vec::new(),
195        };
196
197        for &name in names {
198            match self.unhook_by_name(name) {
199                Ok(()) => {
200                    result.unhooked_count += 1;
201                    result.unhooked_functions.push(name.to_string());
202                }
203                Err(e) => {
204                    result.failed_functions.push((name.to_string(), e.to_string()));
205                }
206            }
207        }
208
209        result
210    }
211
212    /// get original bytes at RVA from clean copy
213    fn get_original_bytes(&self, rva: usize, len: usize) -> Result<Vec<u8>> {
214        for section in self.parsed_pe.sections() {
215            let sec_rva = section.virtual_address as usize;
216            let sec_size = section.virtual_size as usize;
217
218            if rva >= sec_rva && rva < sec_rva + sec_size {
219                let offset_in_section = rva - sec_rva;
220                let file_offset = section.pointer_to_raw_data as usize + offset_in_section;
221
222                if file_offset + len <= self.clean_copy.len() {
223                    return Ok(self.clean_copy[file_offset..file_offset + len].to_vec());
224                }
225            }
226        }
227
228        Err(WraithError::UnhookFailed {
229            function: format!("RVA {rva:#x}"),
230            reason: "RVA not in any section".into(),
231        })
232    }
233}
234
235/// restore a single function to original state
236pub fn restore_function(module: &Module, function_name: &str) -> Result<()> {
237    let unhooker = Unhooker::new(module)?;
238    unhooker.unhook_by_name(function_name)
239}
240
241/// restore entire .text section of a module
242pub fn restore_text_section(module: &Module) -> Result<()> {
243    let unhooker = Unhooker::new(module)?;
244    unhooker.unhook_text_section()
245}