Skip to main content

prehook_macros/
lib.rs

1//! Procedural macros for the prehook library.
2//!
3//! This crate provides the `#[hook]` and `#[interpose]` attribute macros,
4//! which are the primary ways to define interceptions in the `prehook` ecosystem.
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::{FnArg, ItemFn, Lit, Token, parse_macro_input, punctuated::Punctuated, spanned::Spanned};
9
10mod interpose;
11
12const SUPPORTED_HOOK_TYPES: &str = "retn (FunctionHook), jmp_back (InlineHook), jmp_to_ret (DynamicRedirect) and jmp_to_addr (Static Redirect)";
13
14impl TargetKind {}
15
16/// The primary macro for defining a hook.
17///
18/// This attribute macro transforms a standard Rust function into a
19/// hook handler and automatically generates a constructor to register
20/// the hook with the global `HOOK_REGISTRY`.
21///
22/// ### Parameters
23/// - `kind`: (Optional) The type of hook to apply.
24///     - `"retn"` (Default): A function-level hook. Injects `call_original!(...)`.
25///     - `"jmp_back"`: An inline hook that returns to the next instruction.
26///     - `"jmp_to_ret"`: An inline hook that jumps to a dynamic address.
27///     - `"jmp_to_addr"`: An inline hook that jumps to a fixed address.
28/// - `symbol`: (Optional) The name of the symbol to hook.
29/// - `offset`: (Optional) The address offset from the binary's base address.
30/// - `dest`: (Required for `"jmp_to_addr"`) The destination offset to jump to.
31///
32/// ### Examples
33///
34/// #### Function Hook (`retn`)
35/// ```rust,ignore
36/// use prehook::hook;
37///
38/// #[hook(kind = "retn", symbol = "check_license")]
39/// fn my_hook(key: i32) -> i32 {
40///     let result = call_original!(key);
41///     if result == 0 { 1 } else { result }
42/// }
43/// ```
44///
45/// #### Inline Hook (`jmp_back`)
46/// ```rust,ignore
47/// use prehook::hook;
48///
49/// #[hook(kind = "jmp_back", offset = 0x1234)]
50/// fn my_inline_hook(reg: &mut Registers) {
51///     println!("RAX: 0x{:x}", reg.rax);
52/// }
53/// ```
54#[proc_macro_attribute]
55pub fn hook(attr: TokenStream, item: TokenStream) -> TokenStream {
56    let args = parse_macro_input!(attr as HookArgs);
57    let mut func = parse_macro_input!(item as ItemFn);
58
59    let func_name = func.sig.ident.clone();
60    let func_vis = &func.vis;
61    let func_attrs = &func.attrs;
62    let is_void = match &func.sig.output {
63        syn::ReturnType::Default => true,
64        syn::ReturnType::Type(_, ty) => {
65            if let syn::Type::Tuple(t) = &**ty {
66                t.elems.is_empty()
67            } else {
68                false
69            }
70        }
71    };
72
73    let target_expr = args.target.emit();
74    let handler_name = syn::Ident::new(&format!("__hook_{}", func_name), func.sig.span());
75
76    let hook_name_str = func_name.to_string();
77
78    let result = match args.kind {
79        HookKind::Retn => {
80            let func_args = func.sig.inputs.clone();
81            let mut arg_decls = Vec::new();
82            let mut arg_names = Vec::new();
83            let mut arg_types = Vec::new();
84
85            for (i, arg) in func_args.iter().enumerate() {
86                match arg {
87                    FnArg::Receiver(_) => continue,
88                    FnArg::Typed(typed) => {
89                        let name = &typed.pat;
90                        let ty = &typed.ty;
91                        arg_names.push(name);
92                        arg_types.push(ty);
93
94                        let extraction = match i {
95                            0 => quote! { let #name = (*reg).rdi as #ty; },
96                            1 => quote! { let #name = (*reg).rsi as #ty; },
97                            2 => quote! { let #name = (*reg).rdx as #ty; },
98                            3 => quote! { let #name = (*reg).rcx as #ty; },
99                            4 => quote! { let #name = (*reg).r8 as #ty; },
100                            5 => quote! { let #name = (*reg).r9 as #ty; },
101                            _ => {
102                                let stack_index = i - 6;
103                                quote! { let #name = unsafe { (*reg).get_stack(#stack_index) } as #ty; }
104                            }
105                        };
106                        arg_decls.push(extraction);
107                    }
108                }
109            }
110
111            let return_type = match &func.sig.output {
112                syn::ReturnType::Default => quote! { () },
113                syn::ReturnType::Type(_, ty) => quote! { #ty },
114            };
115
116            func.sig
117                .inputs
118                .push(syn::parse_quote! { __orig_ptr: usize });
119            let body = &func.block;
120            let sig = &func.sig;
121
122            let result = if arg_names.is_empty() {
123                quote! {
124                    let result = #func_name(orig_func_ptr);
125                }
126            } else {
127                quote! {
128                   let result = #func_name(#(#arg_names),*, orig_func_ptr);
129                }
130            };
131
132            let return_val = if is_void {
133                quote! { 0 }
134            } else {
135                quote! { result as usize }
136            };
137            let guard_name = format!("__guard_{}", hook_name_str);
138
139            let m_orig_call = if is_void {
140                quote! {
141                    orig(#(#arg_names),*);
142                    return 0;
143                }
144            } else {
145                quote! {
146                    return orig(#(#arg_names),*) as usize;
147                }
148            };
149            let hook_guard_entry = if args.guard {
150                quote! {
151                    if !prehook::hook::HookGuard::try_enter(#guard_name) {
152                        let orig: unsafe extern "C" fn(#(#arg_types),*) -> #return_type = std::mem::transmute(orig_func_ptr);
153                        #m_orig_call
154                    }
155                }
156            } else {
157                quote! {}
158            };
159
160            let hook_guard_exit = if args.guard {
161                quote! {
162                    prehook::hook::HookGuard::exit(#guard_name);
163                }
164            } else {
165                quote! {}
166            };
167
168            quote! {
169                #(#func_attrs)*
170                #func_vis #sig {
171                    #[allow(unused_macros)]
172                    macro_rules! call_original {
173                        ($($arg:expr),*) => {
174                            unsafe {
175                                let orig: unsafe extern "C" fn(#(#arg_types),*) -> #return_type = std::mem::transmute(__orig_ptr);
176                                orig($($arg),*)
177                            }
178                        }
179                    }
180                    #body
181                }
182
183                #[ctor::ctor]
184                fn #handler_name() {
185                    use prehook::hook::{HOOK_REGISTRY, HookInstance};
186                    use prehook::hook::RetnRoutine;
187
188                    let handler: RetnRoutine = {
189                        unsafe extern "win64" fn __hook_handler(
190                            reg: *mut prehook::hook::Registers,
191                            orig_func_ptr: usize,
192                            _stack: usize,
193                        ) -> usize {
194                            #( #arg_decls )*
195                            #hook_guard_entry
196
197                            #result
198
199                            #hook_guard_exit
200                            #return_val
201                        }
202                        __hook_handler
203                    };
204
205                    let hook = HookInstance::new_retn(
206                        #target_expr,
207                        stringify!(#func_name).to_string(),
208                        handler,
209                    );
210                    HOOK_REGISTRY.register(hook);
211                }
212            }
213        }
214        HookKind::JmpBack => {
215            quote! {
216                #func
217
218                #[ctor::ctor]
219                fn #handler_name() {
220                    use prehook::hook::{HOOK_REGISTRY, HookInstance};
221                    use prehook::hook::JmpBackRoutine;
222
223                    unsafe extern "win64" fn __hook_handler(
224                        reg: *mut prehook::hook::Registers,
225                        _user_data: usize,
226                    ) {
227                        #func_name(&mut *reg);
228                    }
229
230                    let hook = HookInstance::new_jmpback(
231                        #target_expr,
232                        stringify!(#func_name).to_string(),
233                        __hook_handler,
234                    );
235                    HOOK_REGISTRY.register(hook);
236                }
237            }
238        }
239        HookKind::JmpToRet => {
240            quote! {
241                #func
242
243                #[ctor::ctor]
244                fn #handler_name() {
245                    use prehook::hook::{HOOK_REGISTRY, HookInstance};
246                    use prehook::hook::JmpToRetRoutine;
247
248                    static HOOK_NAME: &str = #hook_name_str;
249
250                    unsafe extern "win64" fn __hook_handler(
251                        reg: *mut prehook::hook::Registers,
252                        _orig_func_ptr: usize,
253                        _user_data: usize,
254                    ) -> usize {
255                        let base = prehook::init::MAIN_EXE_BASE.load(std::sync::atomic::Ordering::SeqCst);
256                        let hook_point = #target_expr.resolve(base, 0).expect("Failed to resolve hook point address");
257                        let result: prehook::hook::Target = #func_name(&mut *reg, _orig_func_ptr);
258
259                        result.resolve(base, hook_point).expect("Failed to resolve return address")
260                    }
261
262                    let hook = HookInstance::new_jmptoret(
263                        #target_expr,
264                        stringify!(#func_name).to_string(),
265                        __hook_handler,
266                    );
267                    HOOK_REGISTRY.register(hook);
268                }
269            }
270        }
271        HookKind::JmpToAddr => {
272            let target_expr = args.target.emit();
273            let dest_expr = args
274                .dest
275                .expect("dest attribute is required for jmp_to_addr hook")
276                .emit();
277            quote! {
278                #func
279
280                #[ctor::ctor]
281                fn #handler_name() {
282                    use prehook::hook::{HOOK_REGISTRY, HookInstance};
283                    use prehook::hook::JmpToAddrRoutine;
284
285                    unsafe extern "win64" fn __hook_handler(
286                        reg: *mut prehook::hook::Registers,
287                        _orig_func_ptr: usize,
288                        _user_data: usize,
289                    ) {
290                        #func_name(&mut *reg);
291                    }
292
293                    let hook = HookInstance::new_jmptoaddr(
294                        #target_expr,
295                        #dest_expr,
296                        stringify!(#func_name).to_string(),
297                        __hook_handler,
298                    );
299                    HOOK_REGISTRY.register(hook);
300                }
301            }
302        }
303    };
304
305    result.into()
306}
307
308/// The macro for defining a symbol interposer.
309///
310/// This attribute macro transforms a standard Rust function into an
311/// exported symbol (via `#[no_mangle]`) and automatically resolves
312/// the original function from the next available library using `dlsym(RTLD_NEXT, ...)`.
313///
314/// ### Parameters
315/// - `symbol`: (Optional) The name of the symbol to interpose. Defaults to the function name.
316///
317/// ### Example
318/// ```rust,ignore
319/// use prehook::interpose;
320///
321/// #[interpose(symbol = "malloc")]
322/// fn my_malloc(size: usize) -> *mut c_void {
323///     println!("Allocating {} bytes", size);
324///     call_original!(size)
325/// }
326/// ```
327#[proc_macro_attribute]
328pub fn interpose(attr: TokenStream, item: TokenStream) -> TokenStream {
329    interpose::interpose_impl(attr, item)
330}
331
332#[derive(Clone, Debug)]
333enum TargetKind {
334    Raw(usize),
335    Symbol(String),
336    SymbolOffset(String, isize),
337    Relative(isize),
338}
339
340impl TargetKind {
341    fn parse_numeric(s: &str) -> isize {
342        let s = s.trim();
343        let (sign, s) = if let Some(stripped) = s.strip_prefix('+') {
344            (1, stripped)
345        } else if let Some(stripped) = s.strip_prefix('-') {
346            (-1, stripped)
347        } else {
348            (1, s)
349        };
350
351        let val = if let Some(hex_val) = s.strip_prefix("0x") {
352            usize::from_str_radix(hex_val, 16).unwrap_or(0)
353        } else {
354            s.parse::<usize>().unwrap_or(0)
355        };
356
357        (val as isize) * sign
358    }
359    fn emit(&self) -> proc_macro2::TokenStream {
360        match self {
361            TargetKind::Raw(v) => quote! { prehook::hook::Target::Raw(#v) },
362            TargetKind::Symbol(s) => quote! { prehook::hook::Target::Symbol(#s.to_string()) },
363            TargetKind::SymbolOffset(s, off) => {
364                quote! { prehook::hook::Target::SymbolOffset(#s.to_string(), #off) }
365            }
366            TargetKind::Relative(off) => quote! { prehook::hook::Target::Relative(#off) },
367        }
368    }
369}
370
371impl syn::parse::Parse for TargetKind {
372    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
373        let lit: Lit = input.parse()?;
374
375        match &lit {
376            // Handle raw integer literals like 0x1234
377            Lit::Int(i) => Ok(TargetKind::Raw(i.base10_parse()?)),
378
379            // Handle complex string logic
380            Lit::Str(s) => {
381                let val = s.value();
382                let input_str = val.trim();
383
384                if input_str.is_empty() {
385                    return Err(syn::Error::new(lit.span(), "Target string cannot be empty"));
386                }
387
388                // Handle Explicit Relative: "+0x10", "-16", "+50"
389                // We check if it starts with a sign but ISN'T just a hex prefix (like +0x...)
390                if (input_str.starts_with('+') || input_str.starts_with('-'))
391                    && !input_str.starts_with("0x")
392                    && !input_str.starts_with("-0x")
393                {
394                    return Ok(TargetKind::Relative(Self::parse_numeric(input_str)));
395                }
396
397                // Handle Hex String: "0xdeadbeef"
398                if let Some(v) = input_str.strip_prefix("0x")
399                    && let Ok(val) = usize::from_str_radix(v, 16)
400                {
401                    return Ok(TargetKind::Raw(val));
402                }
403
404                // Handle Symbol + Offset: "main+0x20"
405                if let Some((sym, off)) = input_str.rsplit_once('+') {
406                    return Ok(TargetKind::SymbolOffset(
407                        sym.to_string(),
408                        Self::parse_numeric(off),
409                    ));
410                }
411
412                // Handle Symbol - Offset: "main-0x20"
413                // We verify the right side looks like a number to avoid breaking mangled-names-with-hyphens
414                if let Some((sym, off)) = input_str.rsplit_once('-') {
415                    let off_trimmed = off.trim();
416                    if off_trimmed.starts_with("0x")
417                        || off_trimmed.chars().all(|c| c.is_ascii_digit())
418                    {
419                        return Ok(TargetKind::SymbolOffset(
420                            sym.to_string(),
421                            -Self::parse_numeric(off_trimmed),
422                        ));
423                    }
424                }
425
426                // Default to pure Symbol: "add"
427                Ok(TargetKind::Symbol(input_str.to_string()))
428            }
429            _ => Err(syn::Error::new(
430                lit.span(),
431                "Expected string or integer for hook target",
432            )),
433        }
434    }
435}
436
437enum HookArg {
438    Kind(HookKind),
439    Target(TargetKind),
440    Dest(TargetKind),
441    Offset(usize),
442    Guard(bool),
443}
444
445impl syn::parse::Parse for HookArg {
446    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
447        let ident: syn::Ident = input.parse()?;
448        let _: syn::Token![=] = input.parse()?;
449
450        match ident.to_string().as_str() {
451            "kind" => Ok(Self::Kind(input.parse()?)),
452            "symbol" | "target" => Ok(Self::Target(input.parse()?)),
453            "dest" => Ok(Self::Dest(input.parse()?)),
454            "offset" => Ok(Self::Offset(input.parse::<syn::LitInt>()?.base10_parse()?)),
455            "guard" => Ok(Self::Guard(input.parse::<syn::LitBool>()?.value())),
456            _ => Err(syn::Error::new(
457                ident.span(),
458                format!("Unknown argument: {}", ident),
459            )),
460        }
461    }
462}
463
464#[derive(Debug, Clone, PartialEq, Eq)]
465enum HookKind {
466    Retn,
467    JmpBack,
468    JmpToRet,
469    JmpToAddr,
470}
471
472impl syn::parse::Parse for HookKind {
473    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
474        let lit: Lit = input.parse()?;
475
476        let Lit::Str(s) = &lit else {
477            return Err(syn::Error::new(
478                lit.span(),
479                "Hook kind only expects strings",
480            ));
481        };
482
483        let hook_kind_str = s.value();
484
485        Ok(match hook_kind_str.as_str() {
486            "retn" | "FunctionHook" => Self::Retn,
487            "jmp_back" | "InlineHook" => Self::JmpBack,
488            "jmp_to_ret" | "DynamicRedirect" => Self::JmpToRet,
489            "jmp_to_addr" | "StaticRedirect" => Self::JmpToAddr,
490            _ => {
491                return Err(syn::Error::new(
492                    lit.span(),
493                    format!("Unsupported hook type. Supported types are: {SUPPORTED_HOOK_TYPES}"),
494                ));
495            }
496        })
497    }
498}
499
500struct HookArgs {
501    target: TargetKind,
502    kind: HookKind,
503    dest: Option<TargetKind>,
504    guard: bool,
505}
506
507impl syn::parse::Parse for HookArgs {
508    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
509        let args: Punctuated<HookArg, Token![,]> =
510            input.parse_terminated(HookArg::parse, Token![,])?;
511
512        let mut kind = None;
513        let mut target = None;
514        let mut dest = None;
515        let mut offset = None;
516        let mut guard = None;
517
518        // Distribute the parsed arguments into your struct fields
519        for arg in args {
520            match arg {
521                HookArg::Kind(k) => kind = Some(k),
522                HookArg::Target(t) => target = Some(t),
523                HookArg::Dest(d) => dest = Some(d),
524                HookArg::Offset(o) => offset = Some(o),
525                HookArg::Guard(g) => guard = Some(g),
526            }
527        }
528
529        if kind.is_none() {
530            return Err(syn::Error::new(
531                input.span(),
532                format!(
533                    "You need to specify a hook type. Supported types are: {SUPPORTED_HOOK_TYPES}"
534                ),
535            ));
536        }
537        let kind = kind.unwrap();
538
539        let final_target = match (target, offset) {
540            (Some(TargetKind::Symbol(s)), Some(off)) => {
541                // Handle the "symbol + offset" case
542                TargetKind::SymbolOffset(s, off as isize)
543            }
544            (Some(t), _) => t, // Use provided target or symbol
545            (None, Some(off)) => TargetKind::Raw(off), // Fallback to raw offset
546            (None, None) => {
547                // This is the "Hard Guard" you wanted
548                return Err(syn::Error::new(
549                    input.span(),
550                    "No hook target provided. You must specify 'symbol', 'offset', or 'target'.",
551                ));
552            }
553        };
554
555        // you can not use relative under target as it is likely to lead to segfault
556        if let TargetKind::Relative(_) = final_target {
557            return Err(syn::Error::new(
558                input.span(),
559                "You cannot use a relative offset on a hook point. This will likely lead to segfaults. Use raw offset instead",
560            ));
561        }
562
563        if kind == HookKind::JmpToAddr && dest.is_none() {
564            return Err(syn::Error::new(
565                input.span(),
566                "The 'jmp_to_addr' hook requires a 'dest' attribute (e.g., dest = 0x1c0 or dest = '+0x10').",
567            ));
568        }
569
570        Ok(HookArgs {
571            target: final_target,
572            kind,
573            dest,
574            guard: guard.unwrap_or_default(),
575        })
576    }
577}