winhooker/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3use core::{
4    cell::UnsafeCell,
5    marker::PhantomData,
6    mem::MaybeUninit,
7    ops::{Deref, DerefMut},
8    sync::atomic::AtomicBool,
9};
10
11use arrayvec::ArrayVec;
12use hooker::{gen_hook_info, HookInfo, JumperBytes, MAX_JUMPER_LEN};
13use thiserror_no_std::Error;
14use windows_sys::{
15    core::PCSTR,
16    Win32::{
17        Foundation::{FreeLibrary, GetLastError, HMODULE},
18        System::{
19            Diagnostics::Debug::WriteProcessMemory,
20            LibraryLoader::{GetProcAddress, LoadLibraryA},
21            Memory::{
22                VirtualAlloc, VirtualFree, VirtualProtect, MEM_COMMIT, MEM_RELEASE, PAGE_EXECUTE,
23                PAGE_PROTECTION_FLAGS, PAGE_READWRITE,
24            },
25            ProcessStatus::{GetModuleInformation, MODULEINFO},
26            Threading::GetCurrentProcess,
27        },
28    },
29};
30
31/// a lock which just fails when trying to lock it while it is already locked.
32struct SingleLock<T> {
33    value: UnsafeCell<T>,
34    is_locked: AtomicBool,
35}
36impl<T> SingleLock<T> {
37    /// creates a new lock with the given value
38    const fn new(value: T) -> Self {
39        Self {
40            value: UnsafeCell::new(value),
41            is_locked: AtomicBool::new(false),
42        }
43    }
44    /// locks the lock and returns a guard. if the lock is already locked, returns `None`.
45    fn lock(&self) -> Option<SingleLockGuard<T>> {
46        if self
47            .is_locked
48            .swap(true, core::sync::atomic::Ordering::AcqRel)
49        {
50            return None;
51        }
52        Some(SingleLockGuard { lock: self })
53    }
54}
55unsafe impl<T> Send for SingleLock<T> {}
56unsafe impl<T> Sync for SingleLock<T> {}
57
58/// a single lock guard which unlocks the lock when dropped.
59struct SingleLockGuard<'a, T> {
60    lock: &'a SingleLock<T>,
61}
62impl<'a, T> Drop for SingleLockGuard<'a, T> {
63    fn drop(&mut self) {
64        self.lock
65            .is_locked
66            .store(false, core::sync::atomic::Ordering::Release)
67    }
68}
69impl<'a, T> Deref for SingleLockGuard<'a, T> {
70    type Target = T;
71
72    fn deref(&self) -> &Self::Target {
73        unsafe { &*self.lock.value.get() }
74    }
75}
76impl<'a, T> DerefMut for SingleLockGuard<'a, T> {
77    fn deref_mut(&mut self) -> &mut Self::Target {
78        unsafe { &mut *self.lock.value.get() }
79    }
80}
81
82/// a static hook which can be used to store hook information as a static variable so that it can easily be accessed from anywhere.
83/// the generic argument `F` should be a function pointer signature of the hooked function (e.g `extern "C" fn(i32) -> i32`).
84pub struct StaticHook<F: Copy> {
85    hook: SingleLock<Option<Hook>>,
86    phantom: PhantomData<F>,
87}
88impl<F: Copy> StaticHook<F> {
89    const HOOK_USED_MULTIPLE_TIMES_ERR_MSG: &'static str = "static hook used multiple times";
90    const HOOK_USED_BEFORE_HOOKING_ERR_MSG: &'static str =
91        "static hook used before hooking any function";
92
93    /// creates a new, empty, static hook.
94    pub const fn new() -> Self {
95        Self {
96            hook: SingleLock::new(None),
97            phantom: PhantomData,
98        }
99    }
100    /// locks the hook and returns a lock guard for it.
101    ///
102    /// # Panics
103    ///
104    /// panics if the lock is already held
105    fn lock_hook(&self) -> SingleLockGuard<Option<Hook>> {
106        self.hook
107            .lock()
108            .expect(Self::HOOK_USED_MULTIPLE_TIMES_ERR_MSG)
109    }
110
111    /// locks the hook, makes sure that it is empty, and returns a lock guard for it.
112    ///
113    /// # Panics
114    ///
115    /// panics if the lock is already held or if the hook is not empty
116    fn lock_hook_and_assert_empty(&self) -> SingleLockGuard<Option<Hook>> {
117        let hook = self.lock_hook();
118        if hook.is_some() {
119            panic!("{}", Self::HOOK_USED_MULTIPLE_TIMES_ERR_MSG)
120        }
121        hook
122    }
123
124    /// hooks the function with the given `fn_addr` from the given `module` such that when the function is called it instead jumps
125    /// to the given `hook_to_addr`.
126    ///
127    /// # Safety
128    ///
129    /// the signature of the provided function must match the signature of this static hook.
130    ///
131    /// # Panics
132    ///
133    /// panics if this static hook was already used to hook some function.
134    pub unsafe fn hook_function(
135        &self,
136        module: HMODULE,
137        fn_addr: usize,
138        hook_to_addr: usize,
139    ) -> Result<()> {
140        let mut hook = self.lock_hook_and_assert_empty();
141        let created_hook = hook_function(module, fn_addr, hook_to_addr)?;
142        *hook = Some(created_hook);
143        Ok(())
144    }
145
146    /// hooks the function with the `fn_name` from the library with the provided `library_name` such that when the function is called it instead jumps
147    /// to the given `hook_to_addr`.
148    ///
149    /// # Safety
150    ///
151    /// the signature of the provided function must match the signature of this static hook.
152    ///
153    /// # Panics
154    ///
155    /// panics if this static hook was already used to hook some function.
156    pub fn hook_function_by_name(
157        &self,
158        library_name: PCSTR,
159        fn_name: PCSTR,
160        hook_to_addr: usize,
161    ) -> Result<()> {
162        let mut hook = self.lock_hook_and_assert_empty();
163        let created_hook = hook_function_by_name(library_name, fn_name, hook_to_addr)?;
164        *hook = Some(created_hook);
165        Ok(())
166    }
167
168    /// returns a reference to the hook.
169    ///
170    /// # Panics
171    ///
172    /// panics if the static hook was not yet used to hook any function.
173    pub fn get_hook(&self) -> &Hook {
174        let hook_guard = self.lock_hook();
175        let hook_opt = unsafe { &*(hook_guard.deref() as *const Option<Hook>) };
176        hook_opt
177            .as_ref()
178            .expect(Self::HOOK_USED_BEFORE_HOOKING_ERR_MSG)
179    }
180
181    /// provides an interface for calling a function which will simulate the original function behaviour without the hook.
182    ///
183    /// # Panics
184    ///
185    /// panics if the static hook was not yet used to hook any function.
186    pub fn original(&self) -> F {
187        let hook = self.get_hook();
188        unsafe { hook.original() }
189    }
190
191    /// removes this hook. if the static hook was not yet used to hook any function, this does nothing.
192    pub fn remove(&self) {
193        let mut hook_guard = self.lock_hook();
194        let hook_opt = unsafe { &mut *(hook_guard.deref_mut() as *mut Option<Hook>) };
195        if let Some(hook) = hook_opt.take() {
196            hook.remove()
197        }
198    }
199}
200
201/// a guard which calls `FreeLibrary` on the module handle when dropped.
202struct ModuleHandleGuard(HMODULE);
203impl Drop for ModuleHandleGuard {
204    fn drop(&mut self) {
205        let _ = unsafe { FreeLibrary(self.0) };
206    }
207}
208
209/// hooks the function with the `fn_name` from the library with the provided `library_name` such that when the function is called it instead jumps
210/// to the given `hook_to_addr`.
211pub fn hook_function_by_name(
212    library_name: PCSTR,
213    fn_name: PCSTR,
214    hook_to_addr: usize,
215) -> Result<Hook> {
216    let load_library_res = unsafe { LoadLibraryA(library_name) };
217    if load_library_res == 0 {
218        return Err(Error::FailedToLoadLibrary(WinapiError::last_error()));
219    }
220    let module_guard = ModuleHandleGuard(load_library_res);
221    let fn_addr =
222        unsafe { GetProcAddress(module_guard.0, fn_name).ok_or(Error::NoFunctionWithThatName)? };
223    hook_function(module_guard.0, fn_addr as usize, hook_to_addr)
224}
225
226/// returns a slice which represents the possible content of the function at the given address in the given module.
227///
228/// # Safety
229///
230/// the returned byte slice is only valid while the module is in memory
231pub unsafe fn module_get_fn_possible_content(
232    module: HMODULE,
233    fn_addr: usize,
234) -> Result<&'static [u8]> {
235    let mut module_info_uninit: MaybeUninit<MODULEINFO> = MaybeUninit::uninit();
236    let res = unsafe {
237        GetModuleInformation(
238            GetCurrentProcess(),
239            module,
240            module_info_uninit.as_mut_ptr(),
241            core::mem::size_of::<MODULEINFO>() as u32,
242        )
243    };
244    if res == 0 {
245        return Err(Error::FailedToGetModuleInformation(
246            WinapiError::last_error(),
247        ));
248    }
249    let module_info = unsafe { module_info_uninit.assume_init() };
250    let module_end_addr = module_info.lpBaseOfDll as usize + module_info.SizeOfImage as usize;
251    let fn_max_possible_size = module_end_addr - fn_addr;
252
253    Ok(unsafe { core::slice::from_raw_parts(fn_addr as *const u8, fn_max_possible_size) })
254}
255
256/// allocates and builds the trampoline to a heap allocated executable memory region.
257pub fn alloc_and_build_trampoline(hook_info: &HookInfo) -> Allocation {
258    // allocate the trampoline
259    let mut trampiline_alloc = Allocation::new(hook_info.trampoline_size());
260
261    // write the trampoline code
262    let trampoline_code = hook_info.build_trampoline(trampiline_alloc.ptr as u64);
263    let trampoline_alloc_slice = unsafe { trampiline_alloc.as_mut_slice() };
264    trampoline_alloc_slice[..trampoline_code.len()].copy_from_slice(&trampoline_code);
265
266    // done writing the trampoline, now make it executable
267    trampiline_alloc.make_executable_and_read_only();
268
269    trampiline_alloc
270}
271
272/// prepares a hook to be placed on the given `fn_addr` from the given `module`, but doesn't actually put the hook on the function.
273pub fn prepare_hook(module: HMODULE, fn_addr: usize, hook_to_addr: usize) -> Result<PreparedHook> {
274    let fn_possible_content = unsafe { module_get_fn_possible_content(module, fn_addr) }?;
275    let hook_info = gen_hook_info(fn_possible_content, fn_addr as u64, hook_to_addr as u64)?;
276
277    // create the trampoline
278    let trampiline_alloc = alloc_and_build_trampoline(&hook_info);
279
280    // copy the original bytes
281    let overwritten_bytes = fn_possible_content[..hook_info.jumper().len()]
282        .try_into()
283        .unwrap();
284
285    Ok(PreparedHook {
286        trampoline: trampiline_alloc,
287        fn_addr,
288        hook_to_addr,
289        overwritten_bytes,
290        jumper_code: hook_info.jumper().clone(),
291    })
292}
293
294/// hooks the function with the given `fn_addr` from the given `module` such that when the function is called it instead jumps
295/// to the given `hook_to_addr`.
296pub fn hook_function(module: HMODULE, fn_addr: usize, hook_to_addr: usize) -> Result<Hook> {
297    let prepared_hook = prepare_hook(module, fn_addr, hook_to_addr)?;
298    Ok(prepared_hook.hook())
299}
300
301/// writes the given bytes to the given address in the memory of the current process, but allows writing to non-writable memory.
302fn write_current_process_memory(addr: usize, bytes: &[u8]) {
303    let mut bytes_written = 0;
304    let res = unsafe {
305        WriteProcessMemory(
306            GetCurrentProcess(),
307            addr as *const _,
308            bytes.as_ptr().cast(),
309            bytes.len(),
310            &mut bytes_written,
311        )
312    };
313    assert_ne!(
314        res,
315        0,
316        "failed to write {} bytes to address 0x{:x}",
317        bytes.len(),
318        addr
319    );
320    // make sure that all bytes were written
321    assert_eq!(
322        bytes_written,
323        bytes.len(),
324        "only {} out of {} bytes were written when trying to write to address 0x{:x}",
325        bytes_written,
326        bytes.len(),
327        addr
328    );
329}
330
331/// a memory allocation
332pub struct Allocation {
333    ptr: *mut u8,
334    size: usize,
335}
336impl Allocation {
337    pub fn new(size: usize) -> Self {
338        let ptr = unsafe { VirtualAlloc(core::ptr::null(), size, MEM_COMMIT, PAGE_READWRITE) };
339        if ptr.is_null() {
340            // should never happen except for OOM, in which case the default behaviour is to panic anyways.
341            panic!("failed to allocate read-write memory using VirtualAlloc");
342        }
343        Self {
344            ptr: ptr.cast(),
345            size,
346        }
347    }
348    pub fn make_executable_and_read_only(&mut self) {
349        let mut old_prot: MaybeUninit<PAGE_PROTECTION_FLAGS> = MaybeUninit::uninit();
350        let res = unsafe {
351            VirtualProtect(
352                self.ptr.cast(),
353                self.size,
354                PAGE_EXECUTE,
355                old_prot.as_mut_ptr(),
356            )
357        };
358        assert_ne!(res, 0, "failed to change memory protection to executable");
359    }
360    /// # Safety
361    /// must be called only if the memory still has write permissions
362    pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] {
363        unsafe { core::slice::from_raw_parts_mut(self.ptr, self.size) }
364    }
365    /// returns a pointer to the allocation
366    pub fn as_ptr(&self) -> *const u8 {
367        self.ptr
368    }
369    /// returns a mutable pointer to the allocation
370    pub fn as_mut_ptr(&mut self) -> *mut u8 {
371        self.ptr
372    }
373    /// returns the size of the allocation
374    pub fn size(&self) -> usize {
375        self.size
376    }
377}
378impl Drop for Allocation {
379    fn drop(&mut self) {
380        unsafe {
381            let _ = VirtualFree(self.ptr.cast(), 0, MEM_RELEASE);
382        }
383    }
384}
385
386/// a hook that was prepared to be placed on some function
387pub struct PreparedHook {
388    trampoline: Allocation,
389    overwritten_bytes: ArrayVec<u8, MAX_JUMPER_LEN>,
390    fn_addr: usize,
391    hook_to_addr: usize,
392    jumper_code: JumperBytes,
393}
394impl PreparedHook {
395    /// returns a reference to the trampoline prepared for this hook.
396    pub fn trampoline(&self) -> &Allocation {
397        &self.trampoline
398    }
399    /// actually hooks the function.
400    pub fn hook(self) -> Hook {
401        write_current_process_memory(self.fn_addr, &self.jumper_code);
402        Hook {
403            trampoline: self.trampoline,
404            overwritten_bytes: self.overwritten_bytes,
405            fn_addr: self.fn_addr,
406            hook_to_addr: self.hook_to_addr,
407        }
408    }
409}
410
411/// a hook that was placed on some function
412pub struct Hook {
413    trampoline: Allocation,
414    overwritten_bytes: ArrayVec<u8, MAX_JUMPER_LEN>,
415    fn_addr: usize,
416    hook_to_addr: usize,
417}
418impl Hook {
419    /// returns an address of a function which when called will simulate the original function behaviour without the hook.
420    pub fn original_addr(&self) -> usize {
421        self.trampoline.ptr as usize
422    }
423    /// provides an interface for calling a function which will simulate the original function behaviour without the hook.
424    /// the generic argument `F` should be a function pointer signature of the original function (e.g `extern "C" fn(i32) -> i32`).
425    ///
426    /// # Safety
427    ///
428    /// the generic argument `F` must be a function pointer, and must have the correct signature of the original function.
429    pub unsafe fn original<F: Copy>(&self) -> F {
430        // make sure that the provided fn signature indeed looks like a function pointer
431        assert!(
432            core::mem::size_of::<F>() == core::mem::size_of::<usize>()
433                && core::mem::align_of::<F>() == core::mem::align_of::<usize>(),
434            "provided function signature type {} is not a function pointer",
435            core::any::type_name::<F>()
436        );
437        let trampoline_ptr = self.trampoline.ptr;
438        core::mem::transmute_copy(&trampoline_ptr)
439    }
440    /// returns the address of the hooked function
441    pub fn fn_addr(&self) -> usize {
442        self.fn_addr
443    }
444    /// returns the address that the function was hooked to
445    pub fn hook_to_addr(&self) -> usize {
446        self.hook_to_addr
447    }
448    /// returns a reference to the hook's trampoline
449    pub fn trampoline(&self) -> &Allocation {
450        &self.trampoline
451    }
452    /// returns the hook's trampoline
453    pub fn into_trampoline(self) -> Allocation {
454        self.trampoline
455    }
456    /// remove this hook
457    pub fn remove(self) {
458        write_current_process_memory(self.fn_addr, &self.overwritten_bytes)
459    }
460}
461
462/// a winapi error
463#[derive(Debug, Error)]
464#[error("winapi error code 0x{0:x}")]
465pub struct WinapiError(pub u32);
466impl WinapiError {
467    /// returns the last error that occured.
468    pub fn last_error() -> Self {
469        Self(unsafe { GetLastError() })
470    }
471}
472
473/// an error that occurs while hooking a function
474#[derive(Debug, Error)]
475pub enum Error {
476    #[error("failed to get module information")]
477    FailedToGetModuleInformation(#[source] WinapiError),
478
479    #[error("failed to load library")]
480    FailedToLoadLibrary(#[source] WinapiError),
481
482    #[error("no function with the provided name exists in the specified library")]
483    NoFunctionWithThatName,
484
485    #[error("failed to generate hook info")]
486    FailedToGenHookInfo(
487        #[source]
488        #[from]
489        hooker::HookError,
490    ),
491}
492
493/// the result of hooking a function
494pub type Result<T> = core::result::Result<T, Error>;