diff --git a/Cargo.toml b/Cargo.toml index bd19522..874e6e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,4 @@ libc = "0.2" mach = "0.3" [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3", features = ["winnt", "memoryapi", "minwindef", "processthreadsapi"] } +winapi = { version = "0.3.9", features = ["winnt", "memoryapi", "minwindef", "processthreadsapi", "tlhelp32", "wow64apiset", "handleapi"] } diff --git a/src/lib.rs b/src/lib.rs index 27024cb..0c5ada3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,6 +190,42 @@ pub trait ProcessHandleExt { fn set_arch(self, arch: Architecture) -> Self; } +/// Getting some information (such as base address) of loaded libraries in a process. +pub trait GetLibraryInfo { + /// Lists all loaded libraries in a given process. + /// You can then use the address in [`set_offset`] for example. + /// On Windows, libraries are also called "modules", and usually end in `*.dll`. + /// On Linux and macOS, libraries are also called "shared library", and usually end in `*.so`. + /// + /// # Errors + /// Returns OS Error when something else went wrong. + /// Returns other error when closing the handle fails. + /// + /// [`set_offset`]: trait.Memory.html#tymethod.set_offset + fn libs_iter(&self) -> std::io::Result>; + + /// Finds the base address of a loaded library in a process. + /// You can then use the address in [`set_offset`] for example. + /// On Windows, libraries are also called "modules", and usually end in `*.dll`. + /// On Linux and macOS, libraries are also called "shared library", and usually end in `*.so`. + /// + /// # Errors + /// Returns OS Error when something else went wrong. + /// Returns other error when closing the handle fails. + /// Returns a `NotFound` when no such library is loaded. + /// + /// [`set_offset`]: trait.Memory.html#tymethod.set_offset + fn get_library_base(&self, name: &str) -> std::io::Result { + match self.libs_iter()?.iter().find(|p| p.name == name) { + None => Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Count not find base address of library \"{}\"", name), + )), + Some(lib) => Ok(lib.base), + } + } +} + /// A trait that refers to and allows writing to a region of memory in a running program. pub trait Memory { /// Set the offsets to the location in memory. This is used for things such as multi-level @@ -259,3 +295,26 @@ where source.copy_address(addr, &mut copy)?; Ok(copy) } + +/// A minimal amount of information about a system process. +#[derive(Debug)] +pub struct ProcessInfo { + /// Process ID of this process. + pub pid: Pid, + /// Name of this process. For example "MyGame.exe" on Windows. + pub name: String, +} + +#[cfg(windows)] +pub use platform::processes_iter; + +/// A minimal amount of information about a library inside another process. +/// On Windows, libraries are also called "modules", and usually end in `*.dll`. +/// On Linux and macOS, libraries are also called "shared library", and usually end in `*.so`. +#[derive(Debug)] +pub struct LibraryInfo { + /// Name of the library. + pub name: String, + /// Base address of the module in the remote process. + pub base: usize, +} diff --git a/src/windows.rs b/src/windows.rs index 1ed2a39..711f344 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,10 +1,14 @@ use winapi::shared::minwindef; +use winapi::um::{handleapi::CloseHandle, tlhelp32}; use std::os::windows::io::AsRawHandle; use std::process::Child; use std::ptr; -use super::{Architecture, CopyAddress, ProcessHandleExt, PutAddress, TryIntoProcessHandle}; +use super::{ + Architecture, CopyAddress, GetLibraryInfo, LibraryInfo, ProcessHandleExt, ProcessInfo, + PutAddress, TryIntoProcessHandle, +}; /// On Windows a `Pid` is a `DWORD`. pub type Pid = minwindef::DWORD; @@ -55,6 +59,13 @@ impl TryIntoProcessHandle for Child { } } +/// A `ProcessInfo` has a pid (= `u32`, = `minwindef::DWORD`), which can be turned into a `ProcessHandle`. +impl TryIntoProcessHandle for ProcessInfo { + fn try_into_process_handle(&self) -> std::io::Result { + self.pid.try_into_process_handle() + } +} + /// Use `ReadProcessMemory` to read memory from another process on Windows. impl CopyAddress for ProcessHandle { #[allow(clippy::inline_always)] @@ -107,3 +118,171 @@ impl PutAddress for ProcessHandle { } } } + +/// List all processes on the system. +/// +/// Example: +/// ```rust +/// match process_memory::processes_iter().unwrap().find(|p| p.name == "MyGame.exe") { +/// None => println!("No such process currently exists"), +/// Some(p) => println!("Pid of {} is {}", p.name, p.pid) +/// } +/// ``` +/// +/// # Errors +/// Returns an `std::io::Error` with last operating system error when fetching process +/// list fails for some reason. +/// Return an `std::io::Error` with `Other` kind when failing to close the Windows `HANDLE`. +#[allow(clippy::cast_possible_truncation)] +pub fn processes_iter() -> std::io::Result> { + let mut entry = tlhelp32::PROCESSENTRY32 { + dwSize: std::mem::size_of::() as u32, // need to allow clippy truncation because of this + cntUsage: 0, + th32ProcessID: 0, + th32DefaultHeapID: 0, + th32ModuleID: 0, + cntThreads: 0, + th32ParentProcessID: 0, + pcPriClassBase: 0, + dwFlags: 0, + szExeFile: [0; minwindef::MAX_PATH], + }; + + let mut processes = Vec::new(); + + let snapshot: winapi::um::winnt::HANDLE; + unsafe { + snapshot = tlhelp32::CreateToolhelp32Snapshot(tlhelp32::TH32CS_SNAPPROCESS, 0); + if snapshot.is_null() { + return Err(std::io::Error::last_os_error()); + } + + if tlhelp32::Process32First(snapshot, &mut entry) == minwindef::TRUE { + // makeshift do-while + loop { + processes.push(ProcessInfo { + pid: entry.th32ProcessID, + name: utf8_to_string(&entry.szExeFile), + }); + + if tlhelp32::Process32Next(snapshot, &mut entry) == minwindef::FALSE { + break; + } + } + } + + if CloseHandle(snapshot) == minwindef::FALSE { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Could not close handle.", + )); + }; + Ok(processes.into_iter()) + } +} + +/// Use `CreateToolhelp32Snapshot` to get and filter list of loaded libraries (also called modules) +/// of this process, returning the base address of it. +#[allow(clippy::cast_possible_truncation)] +impl GetLibraryInfo for Pid { + fn libs_iter(&self) -> std::io::Result> { + // taken from https://stackoverflow.com/questions/41552466/how-do-i-get-the-physical-baseaddress-of-an-dll-used-in-a-process + let mut module_entry = tlhelp32::MODULEENTRY32 { + dwSize: std::mem::size_of::() as u32, // need the clippy exception because of this + th32ModuleID: 0, + th32ProcessID: 0, + GlblcntUsage: 0, + ProccntUsage: 0, + modBaseAddr: std::ptr::null_mut(), + modBaseSize: 0, + hModule: std::ptr::null_mut(), + szModule: [0; tlhelp32::MAX_MODULE_NAME32 + 1], + szExePath: [0; minwindef::MAX_PATH], + }; + + let mut libs = Vec::new(); + + unsafe { + let snapshot = tlhelp32::CreateToolhelp32Snapshot( + tlhelp32::TH32CS_SNAPMODULE | tlhelp32::TH32CS_SNAPMODULE32, + *self, + ); + if snapshot.is_null() { + return Err(std::io::Error::last_os_error()); + } + + if tlhelp32::Module32First(snapshot, &mut module_entry) == minwindef::TRUE { + // makeshift do-while + loop { + libs.push(LibraryInfo { + name: utf8_to_string(&module_entry.szModule), + base: module_entry.modBaseAddr as usize, + }); + + if tlhelp32::Module32Next(snapshot, &mut module_entry) == minwindef::FALSE { + break; + } + } + } + + // We searched everything, nothing found + if CloseHandle(snapshot) == minwindef::FALSE { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Could not close handle.", + )); + }; + Ok(libs) + } + } +} + +/// A helper function to turn a `c_char` array to a String +fn utf8_to_string(bytes: &[i8]) -> String { + use std::ffi::CStr; + unsafe { + CStr::from_ptr(bytes.as_ptr()) + .to_string_lossy() + .into_owned() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn module_info() { + let pid = std::process::id() as Pid; + let base = pid.get_library_base("ntdll.dll").unwrap(); + assert_ne!(0, base); + println!("ntdll.dll address: 0x{:X}", base); + + match pid + .libs_iter() + .unwrap() + .iter() + .find(|lib| lib.name == "this_dll_doesnt_exist.dll") + { + Some(_) => panic!(), + None => {} + } + } + + #[test] + fn getpid() { + let proc = processes_iter() + .unwrap() + .find(|p| p.name == "svchost.exe") + .unwrap(); + assert_eq!("svchost.exe", proc.name); + + match processes_iter() + .unwrap() + .find(|p| p.name == "this_process_doesnt_exist.exe") + { + Some(_) => panic!(), + None => {} + } + } +}