Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
59 changes: 59 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<LibraryInfo>>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One potential option instead of returning Result<Vec<LibraryInfo>> would be creating an iterating class (say LibraryIter<'a>, which implements Iterator<Item=LibraryInfo>) and implementing it for each OS. That's probably the most "rust-like" method of doing what you want.


/// 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<usize> {
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<T> {
/// Set the offsets to the location in memory. This is used for things such as multi-level
Expand Down Expand Up @@ -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,
}
181 changes: 180 additions & 1 deletion src/windows.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<ProcessHandle> {
self.pid.try_into_process_handle()
}
}

/// Use `ReadProcessMemory` to read memory from another process on Windows.
impl CopyAddress for ProcessHandle {
#[allow(clippy::inline_always)]
Expand Down Expand Up @@ -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<impl Iterator<Item = ProcessInfo>> {
let mut entry = tlhelp32::PROCESSENTRY32 {
dwSize: std::mem::size_of::<tlhelp32::PROCESSENTRY32>() 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);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked how this deals with processes with unicode in them? The impression I get from https://docs.microsoft.com/en-us/windows/win32/intl/conventions-for-function-prototypes is that the ...W functions should be used, and then converted to utf-8 using std::os::windows::ffi::OsStringExt::from_wide().

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<Vec<LibraryInfo>> {
// 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::<tlhelp32::MODULEENTRY32>() 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 => {}
}
}
}