Skip to main content

ib_hook/inject/dll/
app.rs

1/*!
2Inject DLL into target processes with an opinioned RPC schema.
3
4## API
5- [`DllApp`]: DLL RPC schema.
6- [`DllInjection::inject()`]: Inject a DLL into a process and optionally appliy it (call `APPLY()`).
7- [`DllInjection::eject()`]: Eject a DLL (automatically unapply first if applied before).
8- [`DllInjection::leak()`]: Prevent automatic ejection on drop.
9- [`DllInjection::drop()`]: Automatically unapply and eject if not already ejected (or [`DllInjection::leak`]ed).
10- [`DllInjectionVec`]: Manages multiple injections with batch eject support.
11  - [`DllInjectionVecWithInput`]: With owned `dll_path` and `input` for `apply()`.
12
13## Example: Single process injection
14
15```no_run
16use ib_hook::inject::dll::app::{DllApp, DllInjection, OwnedProcess};
17
18// Define your DLL app trait implementation
19struct MyDll;
20impl DllApp for MyDll {
21    const APPLY: &str = "apply_hook";
22    type Input = String;
23    type Output = ();
24}
25
26// Inject into a single process
27let process = OwnedProcess::find_first_by_name("Notepad.exe").unwrap();
28let mut injection = DllInjection::<MyDll>::inject(process)
29    .dll_path(std::path::Path::new("hook.dll"))
30    .apply(&"input".into())
31    .call()
32    .unwrap();
33
34// Eject manually or let drop handle it
35injection.eject().unwrap();
36```
37
38## Example: Multiple processes by name
39
40```no_run
41use ib_hook::inject::dll::app::{DllApp, DllInjectionVec};
42
43struct MyDll;
44impl DllApp for MyDll {
45    const APPLY: &str = "apply_hook";
46    type Input = String;
47    type Output = ();
48}
49
50// Inject into all processes named Notepad.exe
51let mut injections = DllInjectionVec::<MyDll>::new();
52injections.inject_with_process_name("Notepad.exe")
53    .dll_path(std::path::Path::new("hook.dll"))
54    .apply(&"input".into())
55    .on_error(|pid, err| ())
56    .call()
57    .unwrap();
58
59// Eject all manually or let drop handle it
60injections.eject().on_error(|pid, err| ()).call();
61```
62
63## Example: Custom process iterator
64
65```no_run
66use ib_hook::inject::dll::app::{DllApp, DllInjectionVec, OwnedProcess};
67
68struct MyDll;
69impl DllApp for MyDll {
70    const APPLY: &str = "apply_hook";
71    type Input = ();
72    type Output = ();
73}
74
75let processes = vec![
76    OwnedProcess::find_first_by_name("Notepad.exe").unwrap(),
77    OwnedProcess::find_first_by_name("Time.exe").unwrap(),
78];
79
80let mut injections = DllInjectionVec::<MyDll>::new();
81injections.inject(processes.into_iter())
82    .dll_path(std::path::Path::new("hook.dll"))
83    .on_error(|pid, err| ())
84    .call()
85    .unwrap();
86```
87
88## Disclaimer
89This is currently implemented as a wrapper of [`dll_syringe`],
90for object ownership (avoiding self-references), RAII (drop guard) and `Send`.
91
92Ref: https://github.com/Chaoses-Ib/ib-shell/blob/7dc099ea07a9c0a0e2db6aea10a74b2b53c9373e/ib-shell-item/src/hook/inject.rs
93*/
94use std::{
95    mem::transmute,
96    path::{Path, PathBuf},
97};
98
99use bon::bon;
100use derive_more::{Deref, DerefMut};
101use dll_syringe::{
102    Syringe,
103    process::{BorrowedProcessModule, Process},
104    rpc::RemotePayloadProcedure,
105};
106use thiserror::Error;
107
108use crate::{log::*, process::Pid};
109
110#[doc(hidden)]
111pub use dll_syringe::payload_utils::__payload_procedure_helper;
112pub use dll_syringe::process::OwnedProcess;
113
114#[derive(Error, Debug)]
115pub enum InjectError {
116    #[error("dll not found: {0}")]
117    DllNotFound(PathBuf),
118    #[error("cannot find any {0} process")]
119    ProcessNotFound(String),
120    #[error("inject failed: {0}")]
121    InjectFailed(#[from] dll_syringe::error::InjectError),
122    #[error("get apply failed: {0}")]
123    GetApplyFailed(#[from] dll_syringe::error::LoadProcedureError),
124    #[error("apply not found")]
125    ApplyNotFound,
126    #[error("apply: {0}")]
127    ApplyFailed(#[from] dll_syringe::rpc::PayloadRpcError),
128    #[error("eject failed: {0}")]
129    EjectFailed(#[from] dll_syringe::error::EjectError),
130}
131
132/// DLL RPC schema.
133///
134/// Call `APPLY(Some(Input))` on [`inject`](DllInjection::inject),
135/// and `APPLY(None)` on [`eject`](DllInjection::eject)
136/// (and on drop if not [`leak`](DllInjection::leak)ed).
137/**
138<div class="warning">
139
140`APPLY(None)` should clean up all the references to the DLL's code,
141including hooks and threads. Otherwise, when the DLL is unloaded
142the process will crash due to memory access violation.
143</div>
144*/
145pub trait DllApp {
146    /// The name of the exported function for RPC.
147    const APPLY: &str;
148
149    type Input: serde::Serialize + 'static;
150    type Output: serde::de::DeserializeOwned + 'static;
151}
152
153/**
154Usage:
155```ignore
156ib_hook::inject::dll::app::export_apply!(apply_hook, "apply_hook");
157```
158
159## Implementation
160For example:
161```ignore
162#[derive(Debug, Clone, Copy)]
163#[repr(C)]
164pub(crate) struct ArgAndResultBufInfo {
165    pub data: u64,
166    pub len: u64,
167    pub is_error: bool,
168}
169
170const _: () = {
171    #[unsafe(export_name = "ib_shell_apply")]
172    pub unsafe extern "system" fn _ib_hook_inject_dll_app_apply(
173        __args_and_params: *mut ::core::ffi::c_void,
174    ) {
175        let buf_info_ptr = __args_and_params;
176        let buf_info_ptr = buf_info_ptr.cast::<ArgAndResultBufInfo>();
177        let buf_info = unsafe { &mut *buf_info_ptr };
178        // buf_info.len = buf_info.len - 1;
179        let buf =
180            unsafe { slice::from_raw_parts_mut(buf_info.data as *mut u8, buf_info.len as usize) };
181        // dbg!(&buf_info);
182
183        let config = bincode::config::standard();
184
185        // eprintln!("decode_from_slice {:02X?}", buf);
186        let args: Result<((Option<Input>,), usize), bincode::error::DecodeError> =
187            bincode::serde::decode_from_slice::<(Option<Input>,), _>(buf, config);
188        // eprintln!("{:X}\n{:02X?}\nargs: {:?}", buf_info.data, buf, &args);
189
190        ib_hook::inject::dll::app::__payload_procedure_helper(__args_and_params, |__args| {
191            let (input,) = __args;
192
193            // panic!(
194            //     "{:X}\n{:02X?}\n{:?}\n{:?}",
195            //     buf_info.data, buf, &input, &args
196            // );
197            apply_hook(input)
198        });
199    }
200};
201```
202
203TODO: https://github.com/rust-lang/rust/issues/52393
204TODO: https://github.com/rust-lang/rust/issues/143547 or proc macro
205*/
206#[macro_export]
207macro_rules! export_apply {
208    ($apply:ident, $export_name:literal) => {
209        const _: () = {
210            #[unsafe(export_name = $export_name)]
211            pub unsafe extern "system" fn _ib_hook_inject_dll_app_apply(
212                __args_and_params: *mut ::core::ffi::c_void,
213            ) {
214                $crate::inject::dll::app::__payload_procedure_helper(__args_and_params, |__args| {
215                    let (input,) = __args;
216                    $apply(input)
217                });
218            }
219        };
220    };
221}
222pub use export_apply;
223
224/// Represents an injected DLL with its syringe, payload, and remote apply function.
225pub struct DllInjection<D: DllApp> {
226    syringe: Syringe,
227    /// The injected DLL module (borrowed from the syringe).
228    payload: BorrowedProcessModule<'static>,
229    /// Remote procedure to call apply on the injected DLL.
230    remote_apply: RemotePayloadProcedure<fn(Option<&'static D::Input>) -> D::Output>,
231    /// PID of the target process.
232    pid: Pid,
233    /// Whether APPLY was successfully called.
234    applied: bool,
235    /// Whether the injection has been ejected (prevents cleanup on drop).
236    ejected: bool,
237}
238
239/**
240[`Syringe`] contains [`RemoteBoxAllocator`] which is [`Rc`] inner and thus `!Send`.
241But [`Syringe`] itself is `!Clone`, it's actually `Send`.
242*/
243unsafe impl<D: DllApp> Send for DllInjection<D> {}
244
245/*
246Only [`apply()`] works with `&self`.
247Unfortunately, it uses a `!Sync` allocator.
248*/
249// unsafe impl<D: DllApp> Sync for DllInjection<D> {}
250
251#[bon]
252impl<D: DllApp> DllInjection<D> {
253    /// Inject the DLL into the given process and optionally appliy it (call `APPLY()`).
254    #[builder]
255    pub fn inject(
256        #[builder(start_fn)] process: OwnedProcess,
257        dll_path: &Path,
258        apply: Option<&D::Input>,
259    ) -> Result<Self, InjectError> {
260        let pid = Pid(process.pid().unwrap().get());
261        let syringe = Syringe::for_process(process);
262
263        info!(%pid, ?dll_path, "Injecting");
264        let payload = syringe.find_or_inject(dll_path)?;
265        let eject = || {
266            if let Err(e) = syringe.eject(payload) {
267                warn!(?e, "eject");
268            }
269        };
270
271        // Eject if ApplyNotFound
272        let remote_apply = unsafe { syringe.get_payload_procedure(payload, D::APPLY) }
273            .map_err(InjectError::from)
274            .inspect_err(|_| eject())?
275            .ok_or(InjectError::ApplyNotFound)
276            .inspect_err(|_| eject())?;
277
278        // Transmute payload to 'static since syringe (owner of process) is returned
279        let payload = unsafe { transmute(payload) };
280
281        let mut injection = Self {
282            payload,
283            syringe,
284            remote_apply,
285            pid,
286            applied: false,
287            ejected: false,
288        };
289
290        if let Some(input) = apply {
291            // Drop & eject on error
292            injection.apply(input)?;
293            injection.applied = true;
294        }
295
296        info!(%pid, "Successfully injected");
297
298        Ok(injection)
299    }
300
301    pub fn pid(&self) -> Pid {
302        self.pid
303    }
304
305    /// Call [`DllApp::APPLY`] with the given input.
306    pub fn maybe_apply(
307        &self,
308        input: Option<&D::Input>,
309    ) -> Result<D::Output, dll_syringe::rpc::PayloadRpcError> {
310        if let Some(input) = input {
311            self.apply(input)
312        } else {
313            self.unapply()
314        }
315    }
316
317    /// Call [`DllApp::APPLY`] with the given input.
318    /**
319    ## Implementation
320    ```ignore
321    let args = (input,);
322    let config = bincode::config::standard();
323
324    let mut size_writer = bincode::enc::write::SizeWriter::default();
325    bincode::serde::encode_into_writer(&args, &mut size_writer, config)?;
326    let arg_bytes = size_writer.bytes_written;
327    let mut local_arg_buf = Vec::with_capacity(arg_bytes);
328    bincode::serde::encode_into_std_write(&args, &mut local_arg_buf, config)?;
329    unsafe { local_arg_buf.set_len(arg_bytes) };
330    // println!("{:02X?}", local_arg_buf);
331
332    /*
333    let buf = &local_arg_buf[..arg_bytes].to_owned();
334    println!("decode_from_slice {:02X?}", buf);
335    let args: Result<((Option<Input>,), usize), bincode::error::DecodeError> =
336        bincode::serde::decode_from_slice::<(Option<Input>,), _>(buf, config);
337    println!("{:?}", args);
338    */
339    ...
340    ```
341    */
342    pub fn apply(&self, input: &D::Input) -> Result<D::Output, dll_syringe::rpc::PayloadRpcError> {
343        // call() doen't really need 'static.
344        // &Option<D::Input> can be used instead and was used before,
345        // but it made life pathetic.
346        let input: &'static D::Input = unsafe { transmute(input) };
347        self.remote_apply.call(&Some(input))
348    }
349
350    pub fn unapply(&self) -> Result<D::Output, dll_syringe::rpc::PayloadRpcError> {
351        self.remote_apply.call(&None)
352    }
353
354    /// Eject the DLL from the target process.
355    ///
356    /// This will first unapply if it was applied before.
357    pub fn eject(&mut self) -> Result<(), InjectError> {
358        if self.ejected {
359            return Ok(());
360        }
361        if self.applied {
362            self.unapply()?;
363        }
364        // Panics if the given module was not loaded in the target process.
365        self.syringe.eject(self.payload)?;
366        self.ejected = true;
367        Ok(())
368    }
369
370    /// Leak the injection, preventing both manual and automatic `eject()`.
371    pub fn leak(&mut self) {
372        self.ejected = true;
373    }
374}
375
376impl<D: DllApp> Drop for DllInjection<D> {
377    fn drop(&mut self) {
378        if let Err(e) = self.eject() {
379            error!(pid = self.pid.0, ?e, "Failed to eject on drop");
380        }
381    }
382}
383
384/// A collection of injected processes that can be ejected together.
385pub struct DllInjectionVec<D: DllApp> {
386    injections: Vec<DllInjection<D>>,
387    ejected: bool,
388}
389
390/// #[derive(Debug)] will require D: Default
391impl<D: DllApp> Default for DllInjectionVec<D> {
392    fn default() -> Self {
393        Self {
394            injections: Default::default(),
395            ejected: Default::default(),
396        }
397    }
398}
399
400#[bon]
401impl<D: DllApp> DllInjectionVec<D> {
402    pub fn new() -> Self {
403        Self::default()
404    }
405
406    pub fn injections(&self) -> &[DllInjection<D>] {
407        &self.injections
408    }
409
410    pub fn injections_mut(&mut self) -> &mut [DllInjection<D>] {
411        &mut self.injections
412    }
413
414    /// Inject the DLL into the given processes.
415    ///
416    /// Before [`DllInjectionVec::eject()`], the DLL file will be locked and can't be deleted.
417    ///
418    /// # Returns
419    /// - `Ok(DllInjectionVec)`: Successfully injected processes
420    /// - `Err(InjectError)`: Error during injection
421    #[builder]
422    pub fn inject(
423        &mut self,
424        /// Processes to inject into.
425        #[builder(start_fn)]
426        processes: impl Iterator<Item = OwnedProcess>,
427        /// Path to the DLL
428        dll_path: &Path,
429        /// Optionally apply with the given input after injection.
430        apply: Option<&D::Input>,
431        /// Optional callback for errors during injection (called in the middle of the loop).
432        ///
433        /// Errors will always be logged.
434        mut on_error: Option<impl FnMut(Pid, InjectError)>,
435    ) -> Result<&mut Self, InjectError> {
436        if !dll_path.exists() {
437            return Err(InjectError::DllNotFound(dll_path.to_path_buf()));
438        }
439
440        // Store injected processes for later eject
441        for target_process in processes {
442            let pid = Pid(target_process.pid().unwrap().get());
443            match DllInjection::inject(target_process)
444                .dll_path(&dll_path)
445                .maybe_apply(apply)
446                .call()
447            {
448                Ok(injection) => {
449                    self.injections.push(injection);
450                }
451                Err(e) => {
452                    error!(%pid, ?e, "Failed to inject");
453                    if let Some(cb) = on_error.as_mut() {
454                        cb(pid, e);
455                    }
456                }
457            }
458        }
459
460        Ok(self)
461    }
462
463    /// Inject the DLL into all processes with the given name.
464    ///
465    /// Before [`DllInjectionVec::eject()`], the DLL file will be locked and can't be deleted.
466    ///
467    /// # Returns
468    /// - `Ok(DllInjectionVec)`: Successfully injected processes
469    /// - `Err(InjectError)`: Error during injection
470    #[builder]
471    pub fn inject_with_process_name(
472        &mut self,
473        /// Name of the process to inject into.
474        #[builder(start_fn)]
475        process_name: &str,
476        /// Path to the DLL
477        dll_path: &Path,
478        /// Optionally apply with the given input after injection.
479        apply: Option<&D::Input>,
480        /// Optional callback for errors during injection (called in the middle of the loop).
481        ///
482        /// Errors will always be logged.
483        on_error: Option<impl FnMut(Pid, InjectError)>,
484    ) -> Result<&mut Self, InjectError> {
485        // Find all processes with the given name
486        let processes = OwnedProcess::find_all_by_name(process_name);
487        if processes.is_empty() {
488            return Err(InjectError::ProcessNotFound(process_name.to_string()));
489        }
490        info!("Found {} {} processes", processes.len(), process_name);
491
492        self.inject(processes.into_iter())
493            .dll_path(dll_path)
494            .maybe_apply(apply)
495            .maybe_on_error(on_error)
496            .call()
497    }
498
499    /// Call [`apply`](DllInjection::apply) on all injections.
500    ///
501    /// Errors are reported via the `on_error` callback.
502    #[builder]
503    pub fn apply(
504        &self,
505        #[builder(start_fn)] input: &D::Input,
506        mut on_error: Option<impl FnMut(Pid, &dll_syringe::rpc::PayloadRpcError)>,
507    ) {
508        for injection in &self.injections {
509            if let Err(e) = injection.apply(input) {
510                if let Some(on_error) = on_error.as_mut() {
511                    on_error(injection.pid(), &e);
512                }
513            }
514        }
515    }
516
517    /// Call [`unapply`](DllInjection::unapply) on all injections.
518    ///
519    /// Errors are reported via the `on_error` callback.
520    #[builder]
521    pub fn unapply(
522        &self,
523        mut on_error: Option<impl FnMut(Pid, &dll_syringe::rpc::PayloadRpcError)>,
524    ) {
525        for injection in &self.injections {
526            if let Err(e) = injection.unapply() {
527                if let Some(on_error) = on_error.as_mut() {
528                    on_error(injection.pid(), &e);
529                }
530            }
531        }
532    }
533
534    /// Eject all DLL injections.
535    #[builder]
536    pub fn eject(
537        &mut self,
538        /// Optional callback for errors during ejection (called in the middle of the loop).
539        ///
540        /// Errors will always be logged.
541        mut on_error: Option<impl FnMut(Pid, InjectError)>,
542    ) {
543        for mut injection in self.injections.drain(..) {
544            let pid = injection.pid;
545            if let Err(e) = injection.eject() {
546                warn!(%pid, ?e, "Failed to eject");
547                if let Some(cb) = on_error.as_mut() {
548                    cb(pid, e);
549                }
550            }
551        }
552
553        info!("Successfully ejected");
554    }
555
556    /// Leak existing and new injections, preventing automatic cleanup on drop.
557    /// (Unlike [`DllInjection::leak`], this doesn't prevent manual `eject()`)
558    pub fn leak(&mut self) {
559        self.ejected = true;
560    }
561}
562
563impl<D: DllApp> Drop for DllInjectionVec<D> {
564    fn drop(&mut self) {
565        if self.ejected {
566            for injection in &mut self.injections {
567                injection.leak();
568            }
569            return;
570        }
571        self.eject().on_error(|_, _| ()).call();
572    }
573}
574
575/// A collection of injected processes that can be ejected together, with a shared input for apply.
576///
577/// Unlike [`DllInjectionVec`], this stores the input and applies it automatically during inject.
578#[derive(Deref, DerefMut)]
579pub struct DllInjectionVecWithInput<D: DllApp> {
580    dll_path: PathBuf,
581    input: Option<D::Input>,
582    #[deref]
583    #[deref_mut]
584    inner: DllInjectionVec<D>,
585}
586
587impl<D: DllApp> DllInjectionVecWithInput<D> {
588    /// Creates a new `DllInjectionVecWithInput` with the given DLL path.
589    ///
590    /// The DLL path is checked to ensure it exists.
591    pub fn new(dll_path: PathBuf) -> Result<Self, InjectError> {
592        Self::with_input(dll_path, None)
593    }
594
595    pub fn with_input(dll_path: PathBuf, input: Option<D::Input>) -> Result<Self, InjectError> {
596        if !dll_path.exists() {
597            return Err(InjectError::DllNotFound(dll_path));
598        }
599        Ok(Self {
600            dll_path,
601            input,
602            inner: Default::default(),
603        })
604    }
605
606    pub fn dll_path(&self) -> &PathBuf {
607        &self.dll_path
608    }
609
610    pub fn input(&self) -> Option<&D::Input> {
611        self.input.as_ref()
612    }
613}
614
615#[bon]
616impl<D: DllApp> DllInjectionVecWithInput<D> {
617    /// Inject the DLL into the given processes with the stored input and dll_path.
618    ///
619    /// Before [`DllInjectionVecWithInput::eject()`], the DLL file will be locked and can't be deleted.
620    ///
621    /// # Returns
622    /// - `Ok(DllInjectionVecWithInput)`: Successfully injected processes
623    /// - `Err(InjectError)`: Error during injection
624    #[builder]
625    pub fn inject(
626        &mut self,
627        /// Processes to inject into.
628        #[builder(start_fn)]
629        processes: impl Iterator<Item = OwnedProcess>,
630        /// Optional callback for errors during injection (called in the middle of the loop).
631        ///
632        /// Errors will always be logged.
633        on_error: Option<impl FnMut(Pid, InjectError)>,
634    ) -> Result<&mut Self, InjectError> {
635        self.inner
636            .inject(processes)
637            .dll_path(&self.dll_path)
638            .maybe_apply(self.input.as_ref())
639            .maybe_on_error(on_error)
640            .call()?;
641        Ok(self)
642    }
643
644    /// Inject the DLL into all processes with the given name.
645    ///
646    /// Before [`DllInjectionVecWithInput::eject()`], the DLL file will be locked and can't be deleted.
647    ///
648    /// # Returns
649    /// - `Ok(DllInjectionVecWithInput)`: Successfully injected processes
650    /// - `Err(InjectError)`: Error during injection
651    #[builder]
652    pub fn inject_with_process_name(
653        &mut self,
654        /// Name of the process to inject into.
655        #[builder(start_fn)]
656        process_name: &str,
657        /// Optional callback for errors during injection (called in the middle of the loop).
658        ///
659        /// Errors will always be logged.
660        on_error: Option<impl FnMut(Pid, InjectError)>,
661    ) -> Result<&mut Self, InjectError> {
662        self.inner
663            .inject_with_process_name(process_name)
664            .dll_path(&self.dll_path)
665            .maybe_apply(self.input.as_ref())
666            .maybe_on_error(on_error)
667            .call()?;
668        Ok(self)
669    }
670
671    /// Call [`apply`](DllInjection::apply) on all injections with the stored input.
672    ///
673    /// Updates the stored input to the new value.
674    #[builder]
675    pub fn apply(
676        &mut self,
677        #[builder(start_fn)] input: D::Input,
678        on_error: Option<impl FnMut(Pid, &dll_syringe::rpc::PayloadRpcError)>,
679    ) {
680        let input = self.input.insert(input);
681        self.inner.apply(input).maybe_on_error(on_error).call();
682    }
683
684    /// Call [`unapply`](DllInjection::unapply) on all injections.
685    ///
686    /// Updates the stored input to `None`.
687    #[builder]
688    pub fn unapply(
689        &mut self,
690        on_error: Option<impl FnMut(Pid, &dll_syringe::rpc::PayloadRpcError)>,
691    ) {
692        self.input = None;
693        self.inner.unapply().maybe_on_error(on_error).call()
694    }
695}