wraith/manipulation/inline_hook/hook/
hotpatch.rs

1//! Hot-patch style hooks
2//!
3//! Windows functions compiled with /hotpatch have a 2-byte NOP (mov edi, edi)
4//! at the entry point and 5 bytes of padding before. This allows atomic
5//! hook installation with minimal disruption.
6
7#[cfg(all(not(feature = "std"), feature = "alloc"))]
8use alloc::{format, string::String, vec::Vec};
9
10#[cfg(feature = "std")]
11use std::{format, string::String, vec::Vec};
12
13use crate::error::{Result, WraithError};
14use crate::util::memory::ProtectionGuard;
15use crate::manipulation::inline_hook::arch::Architecture;
16use crate::manipulation::inline_hook::guard::HookGuard;
17use crate::manipulation::inline_hook::trampoline::ExecutableMemory;
18use super::Hook;
19use core::marker::PhantomData;
20
21const PAGE_EXECUTE_READWRITE: u32 = 0x40;
22
23/// hot-patch style hook
24///
25/// uses the mov edi, edi / padding space present in Windows hot-patchable
26/// functions. only modifies 2 bytes at the function entry point atomically.
27pub struct HotPatchHook<A: Architecture> {
28    target: usize,
29    detour: usize,
30    _arch: PhantomData<A>,
31}
32
33impl<A: Architecture> HotPatchHook<A> {
34    /// create a new hot-patch hook
35    pub fn new(target: usize, detour: usize) -> Self {
36        Self {
37            target,
38            detour,
39            _arch: PhantomData,
40        }
41    }
42
43    /// check if the target function is hot-patchable
44    ///
45    /// looks for:
46    /// - 5 bytes of padding (CC or 90) at target-5
47    /// - 2-byte NOP (8B FF = mov edi,edi or 66 90) at target
48    pub fn is_patchable(target: usize) -> bool {
49        // read the bytes around the target
50        let pre_bytes = unsafe {
51            core::slice::from_raw_parts((target - 5) as *const u8, 5)
52        };
53        let entry_bytes = unsafe {
54            core::slice::from_raw_parts(target as *const u8, 2)
55        };
56
57        // check for padding before function (CC or 90)
58        let has_padding = pre_bytes.iter().all(|&b| b == 0xCC || b == 0x90);
59
60        // check for 2-byte NOP at entry
61        let has_nop_entry = entry_bytes == [0x8B, 0xFF]  // mov edi, edi
62            || entry_bytes == [0x66, 0x90]               // 2-byte nop
63            || entry_bytes == [0x89, 0xFF];              // mov edi, edi (alternate encoding)
64
65        has_padding && has_nop_entry
66    }
67
68    /// install the hot-patch hook
69    pub fn install(self) -> Result<HookGuard<A>> {
70        // verify target is hot-patchable
71        if !Self::is_patchable(self.target) {
72            return Err(WraithError::HookDetectionFailed {
73                function: format!("{:#x}", self.target),
74                reason: "function is not hot-patchable".into(),
75            });
76        }
77
78        // read original bytes (7 bytes: 5 padding + 2 entry)
79        let original_bytes = unsafe {
80            let ptr = (self.target - 5) as *const u8;
81            core::slice::from_raw_parts(ptr, 7).to_vec()
82        };
83
84        // allocate trampoline
85        // the trampoline just contains: original 2-byte nop + jump to target+2
86        let mut trampoline = ExecutableMemory::allocate_near(self.target, 32)?;
87
88        // build trampoline: copy the 2-byte nop + jump to target+2
89        let entry_bytes = &original_bytes[5..7];
90        let mut trampoline_code = Vec::with_capacity(16);
91        trampoline_code.extend_from_slice(entry_bytes);
92
93        // add jump to target+2 (continuation after the 2-byte nop)
94        let continuation = self.target + 2;
95        let trampoline_jmp_loc = trampoline.base() + trampoline_code.len();
96
97        if let Some(jmp_bytes) = A::encode_jmp_rel(trampoline_jmp_loc, continuation) {
98            trampoline_code.extend_from_slice(&jmp_bytes);
99        } else {
100            let jmp_bytes = A::encode_jmp_abs(continuation);
101            trampoline_code.extend_from_slice(&jmp_bytes);
102        }
103
104        trampoline.write(&trampoline_code)?;
105        trampoline.flush_icache()?;
106
107        // write the hook:
108        // 1. write long jump at target-5 (E9 rel32)
109        // 2. write short jump at target (EB F9 = jmp -7)
110
111        let long_jmp_addr = self.target - 5;
112
113        // change protection for the whole area
114        {
115            let _guard = ProtectionGuard::new(
116                long_jmp_addr,
117                7,
118                PAGE_EXECUTE_READWRITE,
119            )?;
120
121            // write long jump at target-5
122            let long_jmp = A::encode_jmp_rel(long_jmp_addr, self.detour)
123                .or_else(|| Some(A::encode_jmp_abs(self.detour)))
124                .unwrap();
125
126            // SAFETY: protection changed, writing to padding area
127            unsafe {
128                core::ptr::copy_nonoverlapping(
129                    long_jmp.as_ptr(),
130                    long_jmp_addr as *mut u8,
131                    5.min(long_jmp.len()),
132                );
133            }
134
135            // write short jump at entry point (EB F9 = jmp -7)
136            // this jumps back to the long jump we just wrote
137            // SAFETY: protection changed, writing 2 bytes atomically
138            unsafe {
139                let short_jmp: u16 = 0xF9EB; // little-endian: EB F9
140                core::ptr::write_volatile(self.target as *mut u16, short_jmp);
141            }
142        }
143
144        // flush instruction cache
145        flush_icache(long_jmp_addr, 7)?;
146
147        // create guard with original bytes (starting at target-5)
148        Ok(HookGuard::new(
149            long_jmp_addr,
150            self.detour,
151            original_bytes,
152            Some(trampoline),
153        ))
154    }
155}
156
157impl<A: Architecture> Hook for HotPatchHook<A> {
158    type Guard = HookGuard<A>;
159
160    fn install(self) -> Result<Self::Guard> {
161        HotPatchHook::install(self)
162    }
163
164    fn target(&self) -> usize {
165        self.target
166    }
167
168    fn detour(&self) -> usize {
169        self.detour
170    }
171}
172
173/// check if a function is hot-patchable
174pub fn is_hot_patchable(target: usize) -> bool {
175    // read the bytes
176    let pre_bytes = unsafe {
177        core::slice::from_raw_parts((target - 5) as *const u8, 5)
178    };
179    let entry_bytes = unsafe {
180        core::slice::from_raw_parts(target as *const u8, 2)
181    };
182
183    let has_padding = pre_bytes.iter().all(|&b| b == 0xCC || b == 0x90);
184    let has_nop_entry = entry_bytes == [0x8B, 0xFF]
185        || entry_bytes == [0x66, 0x90]
186        || entry_bytes == [0x89, 0xFF];
187
188    has_padding && has_nop_entry
189}
190
191/// convenience function to create and install a hot-patch hook
192pub fn hotpatch<A: Architecture>(target: usize, detour: usize) -> Result<HookGuard<A>> {
193    HotPatchHook::<A>::new(target, detour).install()
194}
195
196fn flush_icache(address: usize, size: usize) -> Result<()> {
197    let result = unsafe {
198        FlushInstructionCache(
199            GetCurrentProcess(),
200            address as *const _,
201            size,
202        )
203    };
204
205    if result == 0 {
206        Err(WraithError::from_last_error("FlushInstructionCache"))
207    } else {
208        Ok(())
209    }
210}
211
212#[link(name = "kernel32")]
213extern "system" {
214    fn FlushInstructionCache(
215        hProcess: *mut core::ffi::c_void,
216        lpBaseAddress: *const core::ffi::c_void,
217        dwSize: usize,
218    ) -> i32;
219
220    fn GetCurrentProcess() -> *mut core::ffi::c_void;
221}