wraith/manipulation/inline_hook/hook/
eat.rs

1//! EAT (Export Address Table) hooking
2//!
3//! EAT hooks work by modifying entries in a module's Export Address Table.
4//! When a module exports a function, its address is stored in the EAT.
5//! By replacing an EAT entry with an RVA pointing to a detour, all calls
6//! that resolve the export (via GetProcAddress or loader resolution) are redirected.
7//!
8//! # Advantages
9//! - Affects all future resolutions of the export
10//! - No code modification (safer for integrity checks)
11//! - Works across the entire process
12//!
13//! # Limitations
14//! - Only affects future GetProcAddress calls (not already-resolved pointers)
15//! - Does not affect direct calls to known addresses
16//! - Requires the detour to be within ±2GB of the module (for RVA encoding)
17
18#[cfg(all(not(feature = "std"), feature = "alloc"))]
19use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
20
21#[cfg(feature = "std")]
22use std::{collections::HashMap, format, string::String, vec::Vec};
23
24use crate::error::{Result, WraithError};
25use crate::navigation::{Module, ModuleQuery};
26use crate::structures::pe::{DataDirectoryType, ExportDirectory};
27use crate::structures::Peb;
28use crate::util::memory::ProtectionGuard;
29
30const PAGE_READWRITE: u32 = 0x04;
31const MAX_EXPORT_COUNT: usize = 0x10000;
32
33/// information about a single EAT entry
34#[derive(Debug, Clone)]
35pub struct EatEntry {
36    /// address of the EAT entry (pointer to the function RVA)
37    pub entry_address: usize,
38    /// current RVA value in the EAT
39    pub current_rva: u32,
40    /// current absolute address (base + rva)
41    pub current_address: usize,
42    /// function name (if exported by name)
43    pub function_name: Option<String>,
44    /// ordinal
45    pub ordinal: u32,
46    /// whether this is a forwarded export
47    pub is_forwarded: bool,
48    /// forwarder string (if forwarded)
49    pub forwarder: Option<String>,
50}
51
52/// EAT hook instance
53pub struct EatHook {
54    /// address of the EAT entry (RVA pointer)
55    eat_entry: usize,
56    /// module base (needed to convert RVA <-> VA)
57    module_base: usize,
58    /// original RVA value
59    original_rva: u32,
60    /// detour RVA value
61    detour_rva: u32,
62    /// detour absolute address
63    detour: usize,
64    /// whether the hook is currently active
65    active: bool,
66    /// whether to restore on drop
67    auto_restore: bool,
68}
69
70impl EatHook {
71    /// create and install an EAT hook
72    ///
73    /// # Arguments
74    /// * `module_name` - the module containing the export (e.g., "kernel32.dll")
75    /// * `function_name` - the function name to hook
76    /// * `detour` - address of the detour function
77    ///
78    /// # Note
79    /// The detour function must be within ±2GB of the module base for the RVA
80    /// to be encodable. For distant detours, consider using a trampoline.
81    ///
82    /// # Example
83    /// ```ignore
84    /// let hook = EatHook::new("kernel32.dll", "GetProcAddress", my_detour as usize)?;
85    /// // future GetProcAddress("kernel32.dll", "GetProcAddress") calls return my_detour
86    /// ```
87    pub fn new(module_name: &str, function_name: &str, detour: usize) -> Result<Self> {
88        let peb = Peb::current()?;
89        let query = ModuleQuery::new(&peb);
90        let module = query.find_by_name(module_name)?;
91
92        Self::new_in_module(&module, function_name, detour)
93    }
94
95    /// create and install an EAT hook in a specific module
96    pub fn new_in_module(module: &Module, function_name: &str, detour: usize) -> Result<Self> {
97        let eat_entry = find_eat_entry(module, function_name)?;
98
99        if eat_entry.is_forwarded {
100            return Err(WraithError::ForwardedExport {
101                forwarder: eat_entry.forwarder.unwrap_or_default(),
102            });
103        }
104
105        Self::new_at_address(eat_entry.entry_address, module.base(), detour)
106    }
107
108    /// create and install an EAT hook at a specific EAT entry address
109    pub fn new_at_address(eat_entry: usize, module_base: usize, detour: usize) -> Result<Self> {
110        if eat_entry == 0 {
111            return Err(WraithError::NullPointer { context: "eat_entry" });
112        }
113
114        // read original RVA
115        // SAFETY: eat_entry points to valid EAT entry
116        let original_rva = unsafe { *(eat_entry as *const u32) };
117
118        // calculate detour RVA
119        let detour_rva = address_to_rva(module_base, detour)?;
120
121        let mut hook = Self {
122            eat_entry,
123            module_base,
124            original_rva,
125            detour_rva,
126            detour,
127            active: false,
128            auto_restore: true,
129        };
130
131        hook.install()?;
132        Ok(hook)
133    }
134
135    /// install the hook (write detour RVA to EAT)
136    pub fn install(&mut self) -> Result<()> {
137        if self.active {
138            return Ok(());
139        }
140
141        write_eat_entry(self.eat_entry, self.detour_rva)?;
142        self.active = true;
143
144        Ok(())
145    }
146
147    /// remove the hook (restore original RVA)
148    pub fn uninstall(&mut self) -> Result<()> {
149        if !self.active {
150            return Ok(());
151        }
152
153        write_eat_entry(self.eat_entry, self.original_rva)?;
154        self.active = false;
155
156        Ok(())
157    }
158
159    /// check if hook is active
160    pub fn is_active(&self) -> bool {
161        self.active
162    }
163
164    /// get the original function address
165    pub fn original(&self) -> usize {
166        self.module_base + self.original_rva as usize
167    }
168
169    /// get the original RVA
170    pub fn original_rva(&self) -> u32 {
171        self.original_rva
172    }
173
174    /// get the detour function address
175    pub fn detour(&self) -> usize {
176        self.detour
177    }
178
179    /// get the EAT entry address
180    pub fn eat_entry(&self) -> usize {
181        self.eat_entry
182    }
183
184    /// set whether to auto-restore on drop
185    pub fn set_auto_restore(&mut self, restore: bool) {
186        self.auto_restore = restore;
187    }
188
189    /// leak the hook (keep active after drop)
190    pub fn leak(mut self) {
191        self.auto_restore = false;
192        core::mem::forget(self);
193    }
194
195    /// consume the hook and restore the original
196    pub fn restore(mut self) -> Result<()> {
197        self.uninstall()?;
198        self.auto_restore = false;
199        Ok(())
200    }
201}
202
203impl Drop for EatHook {
204    fn drop(&mut self) {
205        if self.auto_restore && self.active {
206            let _ = self.uninstall();
207        }
208    }
209}
210
211// SAFETY: EAT hook operates on process-wide memory
212unsafe impl Send for EatHook {}
213unsafe impl Sync for EatHook {}
214
215/// RAII guard for an EAT hook
216pub type EatHookGuard = EatHook;
217
218/// find an EAT entry for a specific export
219pub fn find_eat_entry(module: &Module, function_name: &str) -> Result<EatEntry> {
220    let entries = enumerate_eat_entries(module)?;
221
222    for entry in entries {
223        if let Some(ref name) = entry.function_name {
224            if name == function_name {
225                return Ok(entry);
226            }
227        }
228    }
229
230    Err(WraithError::ModuleNotFound {
231        name: format!("EAT entry for {}", function_name),
232    })
233}
234
235/// find an EAT entry by ordinal
236pub fn find_eat_entry_by_ordinal(module: &Module, ordinal: u32) -> Result<EatEntry> {
237    let entries = enumerate_eat_entries(module)?;
238
239    for entry in entries {
240        if entry.ordinal == ordinal {
241            return Ok(entry);
242        }
243    }
244
245    Err(WraithError::ModuleNotFound {
246        name: format!("EAT entry for ordinal {}", ordinal),
247    })
248}
249
250/// enumerate all EAT entries in a module
251pub fn enumerate_eat_entries(module: &Module) -> Result<Vec<EatEntry>> {
252    let nt = module.nt_headers()?;
253    let export_dir = nt
254        .data_directory(DataDirectoryType::Export.index())
255        .ok_or_else(|| WraithError::InvalidPeFormat {
256            reason: "no export directory".into(),
257        })?;
258
259    if !export_dir.is_present() {
260        return Ok(Vec::new());
261    }
262
263    let base = module.base();
264    let export_va = base + export_dir.virtual_address as usize;
265    let export_end = export_dir.virtual_address + export_dir.size;
266
267    // SAFETY: export_va points to valid export directory
268    let exports = unsafe { &*(export_va as *const ExportDirectory) };
269
270    let num_functions = exports.number_of_functions as usize;
271    let num_names = exports.number_of_names as usize;
272    let ordinal_base = exports.base;
273
274    if num_functions > MAX_EXPORT_COUNT || num_names > MAX_EXPORT_COUNT {
275        return Err(WraithError::InvalidPeFormat {
276            reason: format!("unreasonable export count: {} functions", num_functions),
277        });
278    }
279
280    let functions_va = base + exports.address_of_functions as usize;
281    let names_va = base + exports.address_of_names as usize;
282    let ordinals_va = base + exports.address_of_name_ordinals as usize;
283
284    let mut entries = Vec::with_capacity(num_functions);
285
286    // build name -> ordinal mapping
287    #[cfg(feature = "std")]
288    let mut name_map = HashMap::new();
289    #[cfg(not(feature = "std"))]
290    let mut name_map = BTreeMap::new();
291    for i in 0..num_names {
292        // SAFETY: reading within export table bounds
293        let ordinal = unsafe { *((ordinals_va + i * 2) as *const u16) };
294        let name_rva = unsafe { *((names_va + i * 4) as *const u32) };
295        let name_va = base + name_rva as usize;
296        if let Ok(name) = read_cstring(name_va, 256) {
297            name_map.insert(ordinal as usize, name);
298        }
299    }
300
301    // enumerate all functions
302    for i in 0..num_functions {
303        let entry_addr = functions_va + i * 4;
304        // SAFETY: reading within export table bounds
305        let func_rva = unsafe { *(entry_addr as *const u32) };
306
307        if func_rva == 0 {
308            continue; // empty entry
309        }
310
311        let ordinal = ordinal_base + i as u32;
312
313        // check if forwarded
314        let is_forwarded = func_rva >= export_dir.virtual_address && func_rva < export_end;
315        let forwarder = if is_forwarded {
316            let forwarder_va = base + func_rva as usize;
317            read_cstring(forwarder_va, 256).ok()
318        } else {
319            None
320        };
321
322        entries.push(EatEntry {
323            entry_address: entry_addr,
324            current_rva: func_rva,
325            current_address: base + func_rva as usize,
326            function_name: name_map.get(&i).cloned(),
327            ordinal,
328            is_forwarded,
329            forwarder,
330        });
331    }
332
333    Ok(entries)
334}
335
336/// convert absolute address to RVA
337fn address_to_rva(module_base: usize, address: usize) -> Result<u32> {
338    if address < module_base {
339        return Err(WraithError::InvalidPeFormat {
340            reason: format!(
341                "address {:#x} is below module base {:#x}",
342                address, module_base
343            ),
344        });
345    }
346
347    let offset = address - module_base;
348
349    if offset > u32::MAX as usize {
350        return Err(WraithError::InvalidPeFormat {
351            reason: format!(
352                "offset {:#x} exceeds u32 max for RVA encoding",
353                offset
354            ),
355        });
356    }
357
358    Ok(offset as u32)
359}
360
361/// write a value to an EAT entry
362fn write_eat_entry(entry: usize, rva: u32) -> Result<()> {
363    let _guard = ProtectionGuard::new(entry, core::mem::size_of::<u32>(), PAGE_READWRITE)?;
364
365    // SAFETY: entry is valid EAT address, protection changed to RW
366    unsafe {
367        *(entry as *mut u32) = rva;
368    }
369
370    Ok(())
371}
372
373/// read a null-terminated C string
374fn read_cstring(addr: usize, max_len: usize) -> Result<String> {
375    let mut bytes = Vec::new();
376
377    for i in 0..max_len {
378        // SAFETY: reading bytes within max_len
379        let byte = unsafe { *((addr + i) as *const u8) };
380        if byte == 0 {
381            break;
382        }
383        bytes.push(byte);
384    }
385
386    String::from_utf8(bytes).map_err(|_| WraithError::InvalidPeFormat {
387        reason: "invalid string encoding".into(),
388    })
389}
390
391/// helper to create an EAT hook with a trampoline for distant detours
392pub struct EatHookBuilder {
393    module_name: Option<String>,
394    function_name: Option<String>,
395    detour: Option<usize>,
396}
397
398impl EatHookBuilder {
399    /// create a new builder
400    pub fn new() -> Self {
401        Self {
402            module_name: None,
403            function_name: None,
404            detour: None,
405        }
406    }
407
408    /// set the module name
409    pub fn module(mut self, name: &str) -> Self {
410        self.module_name = Some(name.to_string());
411        self
412    }
413
414    /// set the function name
415    pub fn function(mut self, name: &str) -> Self {
416        self.function_name = Some(name.to_string());
417        self
418    }
419
420    /// set the detour address
421    pub fn detour(mut self, addr: usize) -> Self {
422        self.detour = Some(addr);
423        self
424    }
425
426    /// build and install the hook
427    pub fn build(self) -> Result<EatHook> {
428        let module_name = self.module_name.ok_or(WraithError::NullPointer {
429            context: "module_name not set",
430        })?;
431        let function_name = self.function_name.ok_or(WraithError::NullPointer {
432            context: "function_name not set",
433        })?;
434        let detour = self.detour.ok_or(WraithError::NullPointer {
435            context: "detour not set",
436        })?;
437
438        EatHook::new(&module_name, &function_name, detour)
439    }
440}
441
442impl Default for EatHookBuilder {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn test_enumerate_eat() {
454        let peb = Peb::current().expect("should get PEB");
455        let query = ModuleQuery::new(&peb);
456        let ntdll = query.ntdll().expect("should get ntdll");
457
458        let entries = enumerate_eat_entries(&ntdll).expect("should enumerate EAT");
459        assert!(!entries.is_empty(), "ntdll should have exports");
460    }
461
462    #[test]
463    fn test_find_ntclose() {
464        let peb = Peb::current().expect("should get PEB");
465        let query = ModuleQuery::new(&peb);
466        let ntdll = query.ntdll().expect("should get ntdll");
467
468        let entry = find_eat_entry(&ntdll, "NtClose").expect("should find NtClose");
469        assert!(entry.function_name.as_deref() == Some("NtClose"));
470        assert!(!entry.is_forwarded);
471    }
472
473    #[test]
474    fn test_address_to_rva() {
475        let base = 0x10000usize;
476        let addr = 0x10500usize;
477
478        let rva = address_to_rva(base, addr).expect("should convert");
479        assert_eq!(rva, 0x500);
480    }
481}