wraith/manipulation/inline_hook/hook/
vmt.rs

1//! VMT (Virtual Method Table) hooking
2//!
3//! VMT hooks work by modifying entries in a C++ object's virtual method table.
4//! In C++, virtual functions are called through a vtable pointer stored at the
5//! beginning of each polymorphic object. By replacing vtable entries, we can
6//! intercept virtual function calls.
7//!
8//! # Approaches
9//!
10//! 1. **Direct VMT hook**: Modify the vtable entry directly (affects all instances)
11//! 2. **Shadow VMT**: Create a copy of the vtable and swap the object's vptr
12//!    (affects only specific instances, safer)
13//!
14//! # Advantages
15//! - No code modification
16//! - Easy to enable/disable
17//! - Works on any virtual function
18//!
19//! # Limitations
20//! - Only works with virtual functions (not final/non-virtual)
21//! - Requires knowing vtable layout
22//! - Direct hooks affect all objects of that class
23//! - May conflict with RTTI or other vtable-dependent features
24
25#[cfg(all(not(feature = "std"), feature = "alloc"))]
26use alloc::{boxed::Box, format, string::String, vec, vec::Vec};
27
28#[cfg(feature = "std")]
29use std::{boxed::Box, format, string::String, vec, vec::Vec};
30
31use crate::error::{Result, WraithError};
32use crate::util::memory::ProtectionGuard;
33use core::marker::PhantomData;
34
35const PAGE_READWRITE: u32 = 0x04;
36
37/// a single VMT entry hook (modifies vtable directly)
38///
39/// this affects all objects of the class. use `ShadowVmt` for
40/// instance-specific hooking.
41pub struct VmtHook {
42    /// address of the vtable entry
43    vtable_entry: usize,
44    /// original function pointer
45    original: usize,
46    /// detour function pointer
47    detour: usize,
48    /// whether the hook is active
49    active: bool,
50    /// whether to restore on drop
51    auto_restore: bool,
52}
53
54impl VmtHook {
55    /// create and install a VMT hook
56    ///
57    /// # Arguments
58    /// * `object` - pointer to the C++ object (or any pointer to a vptr)
59    /// * `index` - index of the virtual function in the vtable
60    /// * `detour` - address of the detour function
61    ///
62    /// # Safety
63    /// The object pointer must point to a valid C++ object with a vtable.
64    /// The index must be a valid vtable index for that class.
65    ///
66    /// # Example
67    /// ```ignore
68    /// // hook the 3rd virtual function (index 2)
69    /// let hook = unsafe { VmtHook::new(object_ptr, 2, my_detour as usize)? };
70    /// let original: fn() = unsafe { std::mem::transmute(hook.original()) };
71    /// ```
72    pub unsafe fn new(object: *const (), index: usize, detour: usize) -> Result<Self> {
73        if object.is_null() {
74            return Err(WraithError::NullPointer { context: "object" });
75        }
76
77        // read vptr (first pointer in object)
78        let vptr = unsafe { *(object as *const usize) };
79        if vptr == 0 {
80            return Err(WraithError::NullPointer { context: "vptr" });
81        }
82
83        Self::new_at_vtable(vptr, index, detour)
84    }
85
86    /// create and install a VMT hook at a known vtable address
87    ///
88    /// # Arguments
89    /// * `vtable` - address of the vtable
90    /// * `index` - index of the virtual function
91    /// * `detour` - address of the detour function
92    pub fn new_at_vtable(vtable: usize, index: usize, detour: usize) -> Result<Self> {
93        if vtable == 0 {
94            return Err(WraithError::NullPointer { context: "vtable" });
95        }
96
97        let ptr_size = core::mem::size_of::<usize>();
98        let vtable_entry = vtable + index * ptr_size;
99
100        // read original function pointer
101        // SAFETY: vtable_entry points to valid vtable entry
102        let original = unsafe { *(vtable_entry as *const usize) };
103
104        let mut hook = Self {
105            vtable_entry,
106            original,
107            detour,
108            active: false,
109            auto_restore: true,
110        };
111
112        hook.install()?;
113        Ok(hook)
114    }
115
116    /// install the hook
117    pub fn install(&mut self) -> Result<()> {
118        if self.active {
119            return Ok(());
120        }
121
122        write_vtable_entry(self.vtable_entry, self.detour)?;
123        self.active = true;
124
125        Ok(())
126    }
127
128    /// remove the hook
129    pub fn uninstall(&mut self) -> Result<()> {
130        if !self.active {
131            return Ok(());
132        }
133
134        write_vtable_entry(self.vtable_entry, self.original)?;
135        self.active = false;
136
137        Ok(())
138    }
139
140    /// check if hook is active
141    pub fn is_active(&self) -> bool {
142        self.active
143    }
144
145    /// get the original function pointer
146    pub fn original(&self) -> usize {
147        self.original
148    }
149
150    /// get the detour function pointer
151    pub fn detour(&self) -> usize {
152        self.detour
153    }
154
155    /// get the vtable entry address
156    pub fn vtable_entry(&self) -> usize {
157        self.vtable_entry
158    }
159
160    /// set whether to auto-restore on drop
161    pub fn set_auto_restore(&mut self, restore: bool) {
162        self.auto_restore = restore;
163    }
164
165    /// leak the hook
166    pub fn leak(mut self) {
167        self.auto_restore = false;
168        core::mem::forget(self);
169    }
170
171    /// restore and consume
172    pub fn restore(mut self) -> Result<()> {
173        self.uninstall()?;
174        self.auto_restore = false;
175        Ok(())
176    }
177}
178
179impl Drop for VmtHook {
180    fn drop(&mut self) {
181        if self.auto_restore && self.active {
182            let _ = self.uninstall();
183        }
184    }
185}
186
187// SAFETY: VmtHook operates on process-wide memory
188unsafe impl Send for VmtHook {}
189unsafe impl Sync for VmtHook {}
190
191/// shadow VMT for instance-specific hooking
192///
193/// creates a copy of the original vtable and replaces the object's
194/// vptr to point to the shadow copy. this allows hooking specific
195/// instances without affecting other objects of the same class.
196pub struct ShadowVmt<T: ?Sized = ()> {
197    /// pointer to the object
198    object: *mut (),
199    /// pointer to the original vtable
200    original_vtable: usize,
201    /// pointer to our shadow vtable copy
202    shadow_vtable: Box<[usize]>,
203    /// list of hooked indices and their original values
204    hooks: Vec<(usize, usize)>,
205    /// whether auto-restore is enabled
206    auto_restore: bool,
207    /// marker for the object type
208    _marker: PhantomData<T>,
209}
210
211impl<T: ?Sized> ShadowVmt<T> {
212    /// create a shadow VMT for an object
213    ///
214    /// # Arguments
215    /// * `object` - pointer to the C++ object
216    /// * `vtable_size` - number of entries in the vtable
217    ///
218    /// # Safety
219    /// The object must be a valid C++ object with a vtable.
220    /// vtable_size must be accurate (too small = missing functions, too large = garbage).
221    ///
222    /// # Example
223    /// ```ignore
224    /// // create shadow for an object with 10 virtual functions
225    /// let mut shadow = unsafe { ShadowVmt::new(object_ptr, 10)? };
226    ///
227    /// // hook the 3rd virtual function
228    /// shadow.hook(2, my_detour as usize)?;
229    ///
230    /// // get original to call
231    /// let original: fn() = unsafe { std::mem::transmute(shadow.original(2)) };
232    /// ```
233    pub unsafe fn new(object: *mut (), vtable_size: usize) -> Result<Self> {
234        if object.is_null() {
235            return Err(WraithError::NullPointer { context: "object" });
236        }
237
238        if vtable_size == 0 {
239            return Err(WraithError::InvalidPeFormat {
240                reason: "vtable_size cannot be 0".into(),
241            });
242        }
243
244        // read original vptr
245        let original_vtable = unsafe { *(object as *const usize) };
246        if original_vtable == 0 {
247            return Err(WraithError::NullPointer { context: "vptr" });
248        }
249
250        // copy the vtable
251        let mut shadow = Vec::with_capacity(vtable_size);
252        for i in 0..vtable_size {
253            let entry_addr = original_vtable + i * core::mem::size_of::<usize>();
254            // SAFETY: reading within vtable bounds
255            let entry = unsafe { *(entry_addr as *const usize) };
256            shadow.push(entry);
257        }
258        let shadow_vtable = shadow.into_boxed_slice();
259
260        // replace object's vptr with our shadow
261        // SAFETY: object pointer is valid, we're replacing vptr
262        unsafe {
263            *(object as *mut usize) = shadow_vtable.as_ptr() as usize;
264        }
265
266        Ok(Self {
267            object,
268            original_vtable,
269            shadow_vtable,
270            hooks: Vec::new(),
271            auto_restore: true,
272            _marker: PhantomData,
273        })
274    }
275
276    /// hook a virtual function by index
277    ///
278    /// # Arguments
279    /// * `index` - vtable index to hook
280    /// * `detour` - address of the detour function
281    pub fn hook(&mut self, index: usize, detour: usize) -> Result<()> {
282        if index >= self.shadow_vtable.len() {
283            return Err(WraithError::InvalidPeFormat {
284                reason: format!(
285                    "vtable index {} out of bounds (size {})",
286                    index,
287                    self.shadow_vtable.len()
288                ),
289            });
290        }
291
292        // save original if not already hooked at this index
293        if !self.hooks.iter().any(|(i, _)| *i == index) {
294            self.hooks.push((index, self.shadow_vtable[index]));
295        }
296
297        // replace with detour
298        self.shadow_vtable[index] = detour;
299
300        Ok(())
301    }
302
303    /// unhook a specific index
304    pub fn unhook(&mut self, index: usize) -> Result<()> {
305        if let Some(pos) = self.hooks.iter().position(|(i, _)| *i == index) {
306            let (_, original) = self.hooks.remove(pos);
307            if index < self.shadow_vtable.len() {
308                self.shadow_vtable[index] = original;
309            }
310        }
311        Ok(())
312    }
313
314    /// unhook all
315    pub fn unhook_all(&mut self) {
316        for (index, original) in self.hooks.drain(..) {
317            if index < self.shadow_vtable.len() {
318                self.shadow_vtable[index] = original;
319            }
320        }
321    }
322
323    /// get the original function at an index
324    pub fn original(&self, index: usize) -> Option<usize> {
325        // check if we have a saved original
326        for (i, original) in &self.hooks {
327            if *i == index {
328                return Some(*original);
329            }
330        }
331        // otherwise return current value
332        self.shadow_vtable.get(index).copied()
333    }
334
335    /// get the original vtable address
336    pub fn original_vtable(&self) -> usize {
337        self.original_vtable
338    }
339
340    /// get the shadow vtable address
341    pub fn shadow_vtable(&self) -> usize {
342        self.shadow_vtable.as_ptr() as usize
343    }
344
345    /// get the vtable size
346    pub fn vtable_size(&self) -> usize {
347        self.shadow_vtable.len()
348    }
349
350    /// check if an index is hooked
351    pub fn is_hooked(&self, index: usize) -> bool {
352        self.hooks.iter().any(|(i, _)| *i == index)
353    }
354
355    /// get number of active hooks
356    pub fn hook_count(&self) -> usize {
357        self.hooks.len()
358    }
359
360    /// set whether to auto-restore on drop
361    pub fn set_auto_restore(&mut self, restore: bool) {
362        self.auto_restore = restore;
363    }
364
365    /// restore and consume
366    pub fn restore(mut self) -> Result<()> {
367        self.restore_internal()?;
368        self.auto_restore = false;
369        Ok(())
370    }
371
372    fn restore_internal(&mut self) -> Result<()> {
373        // restore original vptr
374        // SAFETY: object pointer is still valid
375        unsafe {
376            *(self.object as *mut usize) = self.original_vtable;
377        }
378        Ok(())
379    }
380}
381
382impl<T: ?Sized> Drop for ShadowVmt<T> {
383    fn drop(&mut self) {
384        if self.auto_restore {
385            let _ = self.restore_internal();
386        }
387    }
388}
389
390// SAFETY: ShadowVmt operates on the object's vtable
391unsafe impl<T: ?Sized> Send for ShadowVmt<T> {}
392unsafe impl<T: ?Sized> Sync for ShadowVmt<T> {}
393
394/// RAII guard for VMT hook
395pub type VmtHookGuard = VmtHook;
396
397/// helper to get vtable pointer from an object
398///
399/// # Safety
400/// Object must be a valid C++ polymorphic object.
401pub unsafe fn get_vtable(object: *const ()) -> Result<usize> {
402    if object.is_null() {
403        return Err(WraithError::NullPointer { context: "object" });
404    }
405
406    // SAFETY: caller guarantees valid object
407    let vptr = unsafe { *(object as *const usize) };
408    if vptr == 0 {
409        return Err(WraithError::NullPointer { context: "vptr" });
410    }
411
412    Ok(vptr)
413}
414
415/// helper to get a vtable entry
416///
417/// # Safety
418/// vtable must be a valid vtable pointer.
419pub unsafe fn get_vtable_entry(vtable: usize, index: usize) -> Result<usize> {
420    if vtable == 0 {
421        return Err(WraithError::NullPointer { context: "vtable" });
422    }
423
424    let entry_addr = vtable + index * core::mem::size_of::<usize>();
425    // SAFETY: caller guarantees valid vtable
426    let entry = unsafe { *(entry_addr as *const usize) };
427
428    Ok(entry)
429}
430
431/// estimate vtable size by scanning for entries
432///
433/// # Safety
434/// vtable must be a valid vtable pointer.
435pub unsafe fn estimate_vtable_size(vtable: usize, max_scan: usize) -> usize {
436    if vtable == 0 {
437        return 0;
438    }
439
440    let mut count = 0;
441    for i in 0..max_scan {
442        let entry_addr = vtable + i * core::mem::size_of::<usize>();
443
444        // try to read the entry
445        let entry = unsafe { *(entry_addr as *const usize) };
446
447        // heuristic: vtable entries should be non-null and look like code addresses
448        // this is a rough estimate and may not be accurate for all cases
449        if entry == 0 {
450            break;
451        }
452
453        // on Windows x64, code is typically in high memory
454        #[cfg(target_arch = "x86_64")]
455        {
456            if entry < 0x10000 || entry > 0x7FFF_FFFF_FFFF {
457                break;
458            }
459        }
460
461        #[cfg(target_arch = "x86")]
462        {
463            if entry < 0x10000 {
464                break;
465            }
466        }
467
468        count = i + 1;
469    }
470
471    count
472}
473
474/// write a value to a vtable entry
475fn write_vtable_entry(entry: usize, value: usize) -> Result<()> {
476    let _guard = ProtectionGuard::new(entry, core::mem::size_of::<usize>(), PAGE_READWRITE)?;
477
478    // SAFETY: entry is valid vtable address, protection changed
479    unsafe {
480        *(entry as *mut usize) = value;
481    }
482
483    Ok(())
484}
485
486/// helper trait to get vtable for typed objects
487pub trait VmtObject {
488    /// get the vtable pointer
489    fn vtable(&self) -> usize {
490        // SAFETY: self is a valid object
491        unsafe { *(self as *const Self as *const usize) }
492    }
493}
494
495/// builder for VMT hooks
496pub struct VmtHookBuilder {
497    object: Option<*const ()>,
498    vtable: Option<usize>,
499    index: Option<usize>,
500    detour: Option<usize>,
501}
502
503impl VmtHookBuilder {
504    /// create a new builder
505    pub fn new() -> Self {
506        Self {
507            object: None,
508            vtable: None,
509            index: None,
510            detour: None,
511        }
512    }
513
514    /// set the object to hook
515    ///
516    /// # Safety
517    /// Object must be a valid C++ polymorphic object.
518    pub unsafe fn object(mut self, object: *const ()) -> Self {
519        self.object = Some(object);
520        self
521    }
522
523    /// set the vtable directly
524    pub fn vtable(mut self, vtable: usize) -> Self {
525        self.vtable = Some(vtable);
526        self
527    }
528
529    /// set the function index
530    pub fn index(mut self, index: usize) -> Self {
531        self.index = Some(index);
532        self
533    }
534
535    /// set the detour
536    pub fn detour(mut self, detour: usize) -> Self {
537        self.detour = Some(detour);
538        self
539    }
540
541    /// build and install the hook
542    pub fn build(self) -> Result<VmtHook> {
543        let vtable = if let Some(vt) = self.vtable {
544            vt
545        } else if let Some(obj) = self.object {
546            unsafe { get_vtable(obj)? }
547        } else {
548            return Err(WraithError::NullPointer {
549                context: "neither object nor vtable set",
550            });
551        };
552
553        let index = self.index.ok_or(WraithError::NullPointer {
554            context: "index not set",
555        })?;
556
557        let detour = self.detour.ok_or(WraithError::NullPointer {
558            context: "detour not set",
559        })?;
560
561        VmtHook::new_at_vtable(vtable, index, detour)
562    }
563}
564
565impl Default for VmtHookBuilder {
566    fn default() -> Self {
567        Self::new()
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574
575    // create a test object with a vtable
576    #[repr(C)]
577    struct TestVtable {
578        func1: usize,
579        func2: usize,
580        func3: usize,
581    }
582
583    #[repr(C)]
584    struct TestObject {
585        vptr: *const TestVtable,
586    }
587
588    extern "C" fn test_func1() -> i32 {
589        1
590    }
591    extern "C" fn test_func2() -> i32 {
592        2
593    }
594    extern "C" fn test_func3() -> i32 {
595        3
596    }
597
598    #[test]
599    fn test_get_vtable() {
600        static VTABLE: TestVtable = TestVtable {
601            func1: test_func1 as usize,
602            func2: test_func2 as usize,
603            func3: test_func3 as usize,
604        };
605
606        let obj = TestObject {
607            vptr: &VTABLE,
608        };
609
610        let vptr = unsafe { get_vtable(&obj as *const _ as *const ()) }
611            .expect("should get vtable");
612
613        assert_eq!(vptr, &VTABLE as *const _ as usize);
614    }
615
616    #[test]
617    fn test_get_vtable_entry() {
618        static VTABLE: TestVtable = TestVtable {
619            func1: test_func1 as usize,
620            func2: test_func2 as usize,
621            func3: test_func3 as usize,
622        };
623
624        let vtable = &VTABLE as *const _ as usize;
625
626        let entry0 = unsafe { get_vtable_entry(vtable, 0) }.expect("should get entry");
627        let entry1 = unsafe { get_vtable_entry(vtable, 1) }.expect("should get entry");
628
629        assert_eq!(entry0, test_func1 as usize);
630        assert_eq!(entry1, test_func2 as usize);
631    }
632
633    #[test]
634    fn test_estimate_vtable_size() {
635        static VTABLE: [usize; 5] = [
636            test_func1 as usize,
637            test_func2 as usize,
638            test_func3 as usize,
639            0, // null terminates
640            0,
641        ];
642
643        let size = unsafe { estimate_vtable_size(VTABLE.as_ptr() as usize, 10) };
644        assert_eq!(size, 3);
645    }
646}