#![allow(unsafe_code)]
use std::ffi::OsStr;
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows_sys::Win32::Foundation::{
CloseHandle, GetLastError, FALSE, HANDLE, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::System::Diagnostics::Debug::WriteProcessMemory;
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
use windows_sys::Win32::System::Memory::{
VirtualAllocEx, VirtualFreeEx, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE,
};
use windows_sys::Win32::System::Threading::{
CreateRemoteThread, GetExitCodeThread, OpenProcess, WaitForSingleObject,
LPTHREAD_START_ROUTINE, PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION,
PROCESS_VM_WRITE,
};
const WAIT_OBJECT_0_LOCAL: u32 = 0;
const WAIT_TIMEOUT_LOCAL: u32 = 0x0000_0102;
const DEFAULT_INJECT_WAIT_TIMEOUT_MS: u32 = 30_000;
const INJECT_WAIT_TIMEOUT_ENV: &str = "RUNNING_PROCESS_INJECT_WAIT_TIMEOUT_MS";
fn parse_inject_wait_timeout_ms(raw: Option<&str>) -> u32 {
raw.and_then(|raw| raw.trim().parse::<u32>().ok())
.filter(|&ms| ms > 0)
.unwrap_or(DEFAULT_INJECT_WAIT_TIMEOUT_MS)
}
fn inject_wait_timeout_ms() -> u32 {
parse_inject_wait_timeout_ms(std::env::var(INJECT_WAIT_TIMEOUT_ENV).ok().as_deref())
}
pub fn inject_into_pid(pid: u32, dll_path: &Path) -> io::Result<usize> {
let path_wide = encode_wide(dll_path);
if path_wide.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"dll_path encoded to empty UTF-16 — refusing to inject",
));
}
let path_bytes = (path_wide.len() * 2) as u32;
let process = unsafe {
OpenProcess(
PROCESS_CREATE_THREAD
| PROCESS_QUERY_INFORMATION
| PROCESS_VM_OPERATION
| PROCESS_VM_WRITE,
FALSE,
pid,
)
};
if process.is_null() || process == INVALID_HANDLE_VALUE {
return Err(last_error("OpenProcess"));
}
struct Resources {
process: HANDLE,
remote_alloc: *mut core::ffi::c_void,
thread: HANDLE,
}
impl Drop for Resources {
fn drop(&mut self) {
unsafe {
if !self.thread.is_null() && self.thread != INVALID_HANDLE_VALUE {
CloseHandle(self.thread);
}
if !self.remote_alloc.is_null() && !self.process.is_null() {
VirtualFreeEx(self.process, self.remote_alloc, 0, MEM_RELEASE);
}
if !self.process.is_null() && self.process != INVALID_HANDLE_VALUE {
CloseHandle(self.process);
}
}
}
}
let mut resources = Resources {
process,
remote_alloc: core::ptr::null_mut(),
thread: core::ptr::null_mut(),
};
let remote_alloc = unsafe {
VirtualAllocEx(
process,
core::ptr::null(),
path_bytes as usize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
)
};
if remote_alloc.is_null() {
return Err(last_error("VirtualAllocEx"));
}
resources.remote_alloc = remote_alloc;
let mut bytes_written: usize = 0;
let write_ok = unsafe {
WriteProcessMemory(
process,
remote_alloc,
path_wide.as_ptr() as *const _,
path_bytes as usize,
&mut bytes_written,
)
};
if write_ok == FALSE || bytes_written != path_bytes as usize {
return Err(last_error("WriteProcessMemory"));
}
let kernel32_w: [u16; 13] = [
b'k' as u16,
b'e' as u16,
b'r' as u16,
b'n' as u16,
b'e' as u16,
b'l' as u16,
b'3' as u16,
b'2' as u16,
b'.' as u16,
b'd' as u16,
b'l' as u16,
b'l' as u16,
0,
];
let kernel32 = unsafe { GetModuleHandleW(kernel32_w.as_ptr()) };
if kernel32.is_null() {
return Err(last_error("GetModuleHandleW(kernel32.dll)"));
}
let load_library_w = unsafe { GetProcAddress(kernel32, c"LoadLibraryW".as_ptr() as *const u8) };
let Some(load_library_w) = load_library_w else {
return Err(last_error("GetProcAddress(LoadLibraryW)"));
};
let start_routine: LPTHREAD_START_ROUTINE = Some(unsafe {
core::mem::transmute::<
unsafe extern "system" fn() -> isize,
unsafe extern "system" fn(*mut core::ffi::c_void) -> u32,
>(load_library_w)
});
let mut thread_id: u32 = 0;
let thread = unsafe {
CreateRemoteThread(
process,
core::ptr::null(),
0,
start_routine,
remote_alloc,
0,
&mut thread_id,
)
};
if thread.is_null() || thread == INVALID_HANDLE_VALUE {
return Err(last_error("CreateRemoteThread"));
}
resources.thread = thread;
let timeout_ms = inject_wait_timeout_ms();
let wait = unsafe { WaitForSingleObject(thread, timeout_ms) };
if wait == WAIT_TIMEOUT_LOCAL {
resources.remote_alloc = core::ptr::null_mut();
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!(
"remote LoadLibraryW({}) did not return within {timeout_ms} ms; \
the target may be stalled on the loader lock",
dll_path.display()
),
));
}
if wait != WAIT_OBJECT_0_LOCAL {
return Err(last_error("WaitForSingleObject"));
}
let mut exit_code: u32 = 0;
let get_ok = unsafe { GetExitCodeThread(thread, &mut exit_code) };
if get_ok == FALSE {
return Err(last_error("GetExitCodeThread"));
}
if exit_code == 0 {
return Err(io::Error::other(format!(
"remote LoadLibraryW({}) returned NULL (exit_code=0); \
DLL not loadable in target",
dll_path.display()
)));
}
Ok(exit_code as usize)
}
fn encode_wide(path: &Path) -> Vec<u16> {
let mut v: Vec<u16> = OsStr::new(path).encode_wide().collect();
v.push(0);
v
}
fn last_error(op: &'static str) -> io::Error {
let code = unsafe { GetLastError() };
let inner = io::Error::from_raw_os_error(code as i32);
io::Error::new(inner.kind(), format!("{op} failed: {inner}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inject_wait_timeout_defaults_when_unset_or_invalid() {
assert_eq!(
parse_inject_wait_timeout_ms(None),
DEFAULT_INJECT_WAIT_TIMEOUT_MS
);
assert_eq!(
parse_inject_wait_timeout_ms(Some("not-a-number")),
DEFAULT_INJECT_WAIT_TIMEOUT_MS
);
assert_eq!(
parse_inject_wait_timeout_ms(Some("0")),
DEFAULT_INJECT_WAIT_TIMEOUT_MS
);
}
#[test]
fn inject_wait_timeout_honors_valid_override() {
assert_eq!(parse_inject_wait_timeout_ms(Some("500")), 500);
assert_eq!(parse_inject_wait_timeout_ms(Some(" 1500 ")), 1500);
}
#[test]
fn wait_timeout_constant_matches_win32() {
assert_eq!(WAIT_TIMEOUT_LOCAL, 258);
}
}