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