Skip to main content

windows_erg/process/
inject.rs

1//! DLL injection into a target process.
2
3use std::ffi::CString;
4use std::path::Path;
5
6use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE, WAIT_OBJECT_0};
7use windows::Win32::System::Diagnostics::Debug::WriteProcessMemory;
8use windows::Win32::System::Diagnostics::ToolHelp::{
9    CreateToolhelp32Snapshot, MODULEENTRY32W, Module32FirstW, Module32NextW, TH32CS_SNAPMODULE,
10    TH32CS_SNAPMODULE32,
11};
12use windows::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
13use windows::Win32::System::Memory::{
14    MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, VirtualAllocEx, VirtualFreeEx,
15};
16use windows::Win32::System::Threading::{
17    CreateRemoteThread, OpenProcess, PROCESS_CREATE_THREAD, PROCESS_DUP_HANDLE,
18    PROCESS_VM_OPERATION, PROCESS_VM_WRITE, WaitForSingleObject,
19};
20
21use super::processes::Process;
22use crate::error::{
23    AlreadyInjectedError, Error, InjectionFailedError, InvalidParameterError, ProcessError, Result,
24};
25
26/// Injection timeout in milliseconds.
27const INJECT_TIMEOUT_MS: u32 = 5000;
28
29impl Process {
30    /// Inject a DLL into this process.
31    ///
32    /// Opens the process with the necessary rights, allocates memory for the DLL path,
33    /// and creates a remote thread running `LoadLibraryA` in the target process.
34    ///
35    /// Returns `Err(ProcessError::AlreadyInjected)` if the DLL filename is already
36    /// loaded by the target process.
37    ///
38    /// # Remarks
39    /// - The DLL path must be valid UTF-8 and contain no embedded null characters.
40    /// - The calling process must have the privileges required to open the target process
41    ///   with `PROCESS_CREATE_THREAD | PROCESS_VM_WRITE | PROCESS_VM_OPERATION`.
42    /// - Injecting a 32-bit DLL into a 64-bit process (or vice versa) will silently fail;
43    ///   ensure bitness matches.
44    ///
45    /// # Example
46    /// ```no_run
47    /// use std::path::Path;
48    /// use windows_erg::process::{Process, ProcessId};
49    ///
50    /// # fn main() -> windows_erg::Result<()> {
51    /// let process = Process::open(ProcessId::new(1234))?;
52    /// process.inject_dll(Path::new("C:\\path\\to\\my.dll"))?;
53    /// # Ok(())
54    /// # }
55    /// ```
56    pub fn inject_dll(&self, dll_path: &Path) -> Result<()> {
57        let path_str = dll_path.to_str().ok_or_else(|| {
58            Error::InvalidParameter(InvalidParameterError::new(
59                "dll_path",
60                "DLL path must be valid UTF-8",
61            ))
62        })?;
63
64        let dll_path_cstr = CString::new(path_str).map_err(|_| {
65            Error::InvalidParameter(InvalidParameterError::new(
66                "dll_path",
67                "DLL path must not contain embedded null characters",
68            ))
69        })?;
70
71        // Check whether the DLL (by filename) is already loaded.
72        let dll_filename = dll_path
73            .file_name()
74            .and_then(|n| n.to_str())
75            .unwrap_or(path_str)
76            .to_ascii_lowercase();
77
78        if self.is_dll_loaded(&dll_filename)? {
79            return Err(Error::Process(ProcessError::AlreadyInjected(
80                AlreadyInjectedError::new(self.pid().as_u32(), dll_filename),
81            )));
82        }
83
84        // Open the target process with the rights required for injection.
85        let pid = self.pid().as_u32();
86        let process_handle = unsafe {
87            OpenProcess(
88                PROCESS_CREATE_THREAD
89                    | PROCESS_VM_WRITE
90                    | PROCESS_VM_OPERATION
91                    | PROCESS_DUP_HANDLE,
92                false,
93                pid,
94            )
95        }
96        .map_err(|e| {
97            Error::Process(ProcessError::InjectionFailed(
98                InjectionFailedError::with_code(
99                    pid,
100                    "Failed to open process with injection rights",
101                    e.code().0,
102                ),
103            ))
104        })?;
105
106        // Perform injection; ensure we always close the handle.
107        let result = inject_impl(process_handle, pid, &dll_path_cstr);
108        unsafe {
109            let _ = CloseHandle(process_handle);
110        }
111        result
112    }
113
114    /// Check whether a DLL with the given name (case-insensitive, filename only) is
115    /// currently loaded in this process.
116    ///
117    /// # Example
118    /// ```no_run
119    /// use windows_erg::process::{Process, ProcessId};
120    ///
121    /// # fn main() -> windows_erg::Result<()> {
122    /// let process = Process::open(ProcessId::new(1234))?;
123    /// if process.is_dll_loaded("kernel32.dll")? {
124    ///     println!("kernel32 is loaded");
125    /// }
126    /// # Ok(())
127    /// # }
128    /// ```
129    pub fn is_dll_loaded(&self, dll_name: &str) -> Result<bool> {
130        let pid = self.pid().as_u32();
131        let dll_name_lower = dll_name.to_ascii_lowercase();
132
133        // TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32 captures both 32- and 64-bit modules.
134        let snapshot =
135            unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid) }
136                .map_err(|e| {
137                    Error::Process(ProcessError::InjectionFailed(
138                        InjectionFailedError::with_code(
139                            pid,
140                            "Failed to snapshot process modules",
141                            e.code().0,
142                        ),
143                    ))
144                })?;
145
146        let mut entry = MODULEENTRY32W {
147            dwSize: std::mem::size_of::<MODULEENTRY32W>() as u32,
148            ..Default::default()
149        };
150
151        let found = unsafe {
152            if Module32FirstW(snapshot, &mut entry).is_err() {
153                let _ = CloseHandle(snapshot);
154                return Ok(false);
155            }
156            let mut result = false;
157            loop {
158                // szModule is the module filename (no path), null-terminated u16 slice.
159                let name_end = entry
160                    .szModule
161                    .iter()
162                    .position(|&c| c == 0)
163                    .unwrap_or(entry.szModule.len());
164                let module_name =
165                    String::from_utf16_lossy(&entry.szModule[..name_end]).to_ascii_lowercase();
166                if module_name == dll_name_lower {
167                    result = true;
168                    break;
169                }
170                if Module32NextW(snapshot, &mut entry).is_err() {
171                    break;
172                }
173            }
174            result
175        };
176
177        unsafe {
178            let _ = CloseHandle(snapshot);
179        }
180        Ok(found)
181    }
182
183    /// Convenience accessor used within this module.
184    fn pid(&self) -> super::types::ProcessId {
185        self.id()
186    }
187}
188
189/// Core injection logic. `process_handle` must already be open with the required rights.
190/// The caller is responsible for closing `process_handle`.
191fn inject_impl(process_handle: HANDLE, pid: u32, dll_path: &CString) -> Result<()> {
192    let path_bytes = dll_path.as_bytes_with_nul();
193
194    // Allocate memory in the target process for the DLL path.
195    let remote_mem = unsafe {
196        VirtualAllocEx(
197            process_handle,
198            None,
199            path_bytes.len(),
200            MEM_RESERVE | MEM_COMMIT,
201            PAGE_READWRITE,
202        )
203    };
204    if remote_mem.is_null() {
205        return Err(Error::Process(ProcessError::InjectionFailed(
206            InjectionFailedError::new(pid, "Failed to allocate memory in target process"),
207        )));
208    }
209
210    // Write the DLL path into the target process.
211    let write_result = unsafe {
212        WriteProcessMemory(
213            process_handle,
214            remote_mem,
215            path_bytes.as_ptr() as *const _,
216            path_bytes.len(),
217            None,
218        )
219    };
220    if let Err(e) = write_result {
221        unsafe {
222            let _ = VirtualFreeEx(process_handle, remote_mem, 0, MEM_RELEASE);
223        }
224        return Err(Error::Process(ProcessError::InjectionFailed(
225            InjectionFailedError::with_code(
226                pid,
227                "Failed to write DLL path into target process",
228                e.code().0,
229            ),
230        )));
231    }
232
233    // Resolve LoadLibraryA in kernel32.
234    let load_library_addr = unsafe {
235        let k32 = GetModuleHandleA(windows::core::s!("kernel32.dll")).map_err(
236            |e: windows::core::Error| {
237                let _ = VirtualFreeEx(process_handle, remote_mem, 0, MEM_RELEASE);
238                Error::Process(ProcessError::InjectionFailed(
239                    InjectionFailedError::with_code(
240                        pid,
241                        "Failed to get kernel32.dll handle",
242                        e.code().0,
243                    ),
244                ))
245            },
246        )?;
247        GetProcAddress(k32, windows::core::s!("LoadLibraryA")).ok_or_else(|| {
248            let _ = VirtualFreeEx(process_handle, remote_mem, 0, MEM_RELEASE);
249            Error::Process(ProcessError::InjectionFailed(InjectionFailedError::new(
250                pid,
251                "Failed to resolve LoadLibraryA",
252            )))
253        })?
254    };
255
256    // Create a remote thread executing LoadLibraryA(dll_path).
257    let start_routine: unsafe extern "system" fn(*mut std::ffi::c_void) -> u32 =
258        unsafe { std::mem::transmute(load_library_addr) };
259
260    let remote_thread = unsafe {
261        CreateRemoteThread(
262            process_handle,
263            None,
264            0,
265            Some(start_routine),
266            Some(remote_mem),
267            0,
268            None,
269        )
270    }
271    .map_err(|e| {
272        unsafe {
273            let _ = VirtualFreeEx(process_handle, remote_mem, 0, MEM_RELEASE);
274        }
275        Error::Process(ProcessError::InjectionFailed(
276            InjectionFailedError::with_code(
277                pid,
278                "Failed to create remote thread in target process",
279                e.code().0,
280            ),
281        ))
282    })?;
283
284    if remote_thread == INVALID_HANDLE_VALUE {
285        unsafe {
286            let _ = VirtualFreeEx(process_handle, remote_mem, 0, MEM_RELEASE);
287        }
288        return Err(Error::Process(ProcessError::InjectionFailed(
289            InjectionFailedError::new(pid, "CreateRemoteThread returned invalid handle"),
290        )));
291    }
292
293    // Wait for the remote thread to complete.
294    let wait_result = unsafe { WaitForSingleObject(remote_thread, INJECT_TIMEOUT_MS) };
295
296    if wait_result == WAIT_OBJECT_0 {
297        // Retrieve the exit code to detect obvious LoadLibraryA failures.
298        // GetExitCodeThread is u32, so we do not interpret it as a pointer-sized
299        // remote module handle on 64-bit targets.
300        let mut exit_code: u32 = 0;
301        if unsafe {
302            windows::Win32::System::Threading::GetExitCodeThread(remote_thread, &mut exit_code)
303                .is_ok()
304        } && exit_code == 0
305        {
306            // LoadLibraryA returned NULL — injection failed inside the target process.
307            unsafe {
308                let _ = CloseHandle(remote_thread);
309                let _ = VirtualFreeEx(process_handle, remote_mem, 0, MEM_RELEASE);
310            }
311            return Err(Error::Process(ProcessError::InjectionFailed(
312                InjectionFailedError::new(pid, "LoadLibraryA returned NULL in target process"),
313            )));
314        }
315    }
316    // If wait timed out the thread may still be running; we leave it running
317    // (the DLL will load asynchronously) but do not return an error.
318
319    unsafe {
320        let _ = CloseHandle(remote_thread);
321        let _ = VirtualFreeEx(process_handle, remote_mem, 0, MEM_RELEASE);
322    }
323    Ok(())
324}