Skip to content
Closed
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", features = ["winnt", "memoryapi", "minwindef", "processthreadsapi", "handleapi"] }
53 changes: 4 additions & 49 deletions examples/fastyboy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,64 +15,19 @@ fn main() {
println!("FastyBoy can only be run on systems supporting the game Mirror's Edge Catalyst, which as of writing is only Windows.")
}

/// A helper function to get a Pid from the name of a process
#[cfg(windows)]
pub fn get_pid(process_name: &str) -> process_memory::Pid {
/// 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()
}
}
let mut entry = winapi::um::tlhelp32::PROCESSENTRY32 {
dwSize: std::mem::size_of::<winapi::um::tlhelp32::PROCESSENTRY32>() as u32,
cntUsage: 0,
th32ProcessID: 0,
th32DefaultHeapID: 0,
th32ModuleID: 0,
cntThreads: 0,
th32ParentProcessID: 0,
pcPriClassBase: 0,
dwFlags: 0,
szExeFile: [0; winapi::shared::minwindef::MAX_PATH],
};
let snapshot: winapi::um::winnt::HANDLE;
unsafe {
snapshot = winapi::um::tlhelp32::CreateToolhelp32Snapshot(
winapi::um::tlhelp32::TH32CS_SNAPPROCESS,
0,
);
if winapi::um::tlhelp32::Process32First(snapshot, &mut entry)
== winapi::shared::minwindef::TRUE
{
while winapi::um::tlhelp32::Process32Next(snapshot, &mut entry)
== winapi::shared::minwindef::TRUE
{
if utf8_to_string(&entry.szExeFile) == process_name {
return entry.th32ProcessID;
}
}
}
}
0
}

#[cfg(windows)]
fn main() -> std::io::Result<()> {
use process_memory::*;
let process_handle = get_pid("MirrorsEdgeCatalyst.exe").try_into_process_handle()?;
let process_handle = get_pid("MirrorsEdgeCatalyst.exe")?.try_into_process_handle()?;

let mut spawn_timer = DataMember::<f32>::new(process_handle);
spawn_timer.set_offset(vec![0x1_42_14_2a_d8, 0xac]);
spawn_timer.set_offset(0x1_42_14_2a_d8, vec![0xac]);

let mut level_warmup = DataMember::<f32>::new(process_handle);
level_warmup.set_offset(vec![0x1_42_14_2a_d8, 0x9c]);
level_warmup.set_offset(0x1_42_14_2a_d8, vec![0x9c]);

let mut emitters_enabled = DataMember::<bool>::new(process_handle);
emitters_enabled.set_offset(vec![0x1_42_3e_44_78, 0xac]);
emitters_enabled.set_offset(0x1_42_3e_44_78, vec![0xac]);

spawn_timer.write(&1.0)?;
level_warmup.write(&1.0)?;
Expand Down
76 changes: 64 additions & 12 deletions src/data_member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{CopyAddress, Memory, ProcessHandle, PutAddress};
/// // We need to make sure that we get a handle to a process, in this case, ourselves
/// let handle = (std::process::id() as Pid).try_into_process_handle().unwrap();
/// // We make a `DataMember` that has an offset referring to its location in memory
/// let member = DataMember::new_offset(handle, vec![&x as *const _ as usize]);
/// let member = DataMember::<u32>::new_addr(handle, &x as *const _ as usize);
/// // The memory refered to is now the same
/// println!("Memory location: &x: {}, member: {}", &x as *const _ as usize,
/// member.get_offset().unwrap());
Expand All @@ -29,7 +29,8 @@ use crate::{CopyAddress, Memory, ProcessHandle, PutAddress};
/// ```
#[derive(Clone, Debug)]
pub struct DataMember<T> {
offsets: Vec<usize>,
base: usize,
offsets: Vec<isize>,
process: ProcessHandle,
_phantom: std::marker::PhantomData<*mut T>,
}
Expand All @@ -51,40 +52,90 @@ impl<T: Sized + Copy> DataMember<T> {
#[must_use]
pub fn new(handle: ProcessHandle) -> Self {
Self {
base: 0,
offsets: Vec::new(),
process: handle,
_phantom: std::marker::PhantomData,
}
}

/// Create a new `DataMember` from a [`ProcessHandle`] and some number of offsets. You must
// /// Create a new `DataMember` from a [`ProcessHandle`] and some number of offsets. You must
// /// remember to call [`try_into_process_handle`] on a [`Pid`] as sometimes the `Pid` can have
// /// the same backing type as a [`ProcessHandle`], resulting in an error.
// ///
// /// [`try_into_process_handle`]: trait.TryIntoProcessHandle.html#tymethod.try_into_process_handle
// /// [`ProcessHandle`]: type.ProcessHandle.html
// /// [`Pid`]: type.Pid.html
// #[must_use]
// pub fn new_offset(handle: ProcessHandle, offsets: Vec<isize>) -> Self {
// Self {
// base: 0,
// offsets,
// process: handle,
// _phantom: std::marker::PhantomData,
// }
// }

/// Create a new `DataMember` from a [`ProcessHandle`], a base address, and some number of offsets. You must
/// remember to call [`try_into_process_handle`] on a [`Pid`] as sometimes the `Pid` can have
/// the same backing type as a [`ProcessHandle`], resulting in an error.
///
/// [`try_into_process_handle`]: trait.TryIntoProcessHandle.html#tymethod.try_into_process_handle
/// [`ProcessHandle`]: type.ProcessHandle.html
/// [`Pid`]: type.Pid.html
#[must_use]
pub fn new_offset(handle: ProcessHandle, offsets: Vec<usize>) -> Self {
pub fn new_addr_offset(handle: ProcessHandle, base: usize, offsets: Vec<isize>) -> Self {
Self {
base,
offsets,
process: handle,
_phantom: std::marker::PhantomData,
}
}

/// Create a new `DataMember` from a [`ProcessHandle`], and an absolute address (just one offset, so to speak). You must
/// remember to call [`try_into_process_handle`] on a [`Pid`] as sometimes the `Pid` can have
/// the same backing type as a [`ProcessHandle`], resulting in an error.
///
/// [`try_into_process_handle`]: trait.TryIntoProcessHandle.html#tymethod.try_into_process_handle
/// [`ProcessHandle`]: type.ProcessHandle.html
/// [`Pid`]: type.Pid.html
#[must_use]
pub fn new_addr(handle: ProcessHandle, base: usize) -> Self {
Self {
base,
offsets: Vec::new(),
process: handle,
_phantom: std::marker::PhantomData,
}
}

/// Clones an existing `DataMember`, and appends some offsets. Mostly provided for convenience.
#[must_use]
pub fn extend<TNew>(&self, more_offsets: Vec<isize>) -> DataMember<TNew> {
let mut clone = DataMember {
base: self.base,
offsets: self.offsets.clone(),
process: self.process,
_phantom: std::marker::PhantomData,
};
clone.offsets.extend(more_offsets);
clone
}
}

impl<T: Sized + Copy> Memory<T> for DataMember<T> {
fn set_offset(&mut self, new_offsets: Vec<usize>) {
fn set_offset(&mut self, new_base: usize, new_offsets: Vec<isize>) {
self.base = new_base;
self.offsets = new_offsets;
}

fn get_offset(&self) -> std::io::Result<usize> {
self.process.get_offset(&self.offsets)
self.process.get_offset(self.base, &self.offsets)
}

fn read(&self) -> std::io::Result<T> {
let offset = self.process.get_offset(&self.offsets)?;
let offset = self.process.get_offset(self.base, &self.offsets)?;
// This can't be [0_u8;size_of::<T>()] because no const generics.
// It will be freed at the end of the function because no references are held to it.
let mut buffer = vec![0_u8; std::mem::size_of::<T>()];
Expand All @@ -94,7 +145,7 @@ impl<T: Sized + Copy> Memory<T> for DataMember<T> {

fn write(&self, value: &T) -> std::io::Result<()> {
use std::slice;
let offset = self.process.get_offset(&self.offsets)?;
let offset = self.process.get_offset(self.base, &self.offsets)?;
let buffer: &[u8] =
unsafe { slice::from_raw_parts(value as *const _ as _, std::mem::size_of::<T>()) };
self.process.put_address(offset, &buffer)
Expand All @@ -114,7 +165,7 @@ mod test {
.unwrap();
println!("Process Handle: {:?}", handle);
let mut member = DataMember::<i32>::new(handle);
member.set_offset(vec![&test as *const _ as usize]);
member.set_offset(&test as *const _ as usize, vec![]);
assert_eq!(test, member.read().unwrap());
member.write(&5_i32).unwrap();
assert_eq!(test, 5_i32);
Expand All @@ -128,7 +179,7 @@ mod test {
.unwrap();
println!("Process Handle: {:?}", handle);
let mut member = DataMember::<i64>::new(handle);
member.set_offset(vec![&test as *const _ as usize]);
member.set_offset(&test as *const _ as usize, vec![]);
assert_eq!(test, member.read().unwrap());
member.write(&-1_i64).unwrap();
assert_eq!(test, -1);
Expand All @@ -141,8 +192,9 @@ mod test {
.try_into_process_handle()
.unwrap();
println!("Process Handle: {:?}", handle);
let mut member = DataMember::<usize>::new(handle);
member.set_offset(vec![&test as *const _ as usize]);
// let mut member = DataMember::<usize>::new(handle);
// member.set_offset(&test as *const _ as usize, vec![]);
let member = DataMember::<usize>::new_addr(handle, &test as *const _ as usize);
assert_eq!(test, member.read().unwrap());
member.write(&0xffff).unwrap();
assert_eq!(test, 0xffff);
Expand Down
108 changes: 92 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
//! // We need to make sure that we get a handle to a process, in this case, ourselves
//! let handle = (std::process::id() as Pid).try_into_process_handle().unwrap();
//! // We make a `DataMember` that has an offset referring to its location in memory
//! let member = DataMember::new_offset(handle, vec![&x as *const _ as usize]);
//! let member = DataMember::new_addr(handle, &x as *const _ as usize);
//! // The memory refered to is now the same
//! println!("Memory location: &x: {}, member: {}", &x as *const _ as usize,
//! println!("Memory location: &x: {:X}, member: {}", &x as *const _ as usize,
//! member.get_offset().unwrap());
//! assert_eq!(&x as *const _ as usize, member.get_offset().unwrap());
//! // The value of the member is the same as the variable
Expand All @@ -35,9 +35,9 @@
//! println!("Original x-value: {}", x);
//!
//! // We make a `LocalMember` that has an offset referring to its location in memory
//! let member = LocalMember::new_offset(vec![&x as *const _ as usize]);
//! let member = LocalMember::new_addr(&x as *const _ as usize);
//! // The memory refered to is now the same
//! println!("Memory location: &x: {}, member: {}", &x as *const _ as usize,
//! println!("Memory location: &x: {:X}, member: {}", &x as *const _ as usize,
//! member.get_offset().unwrap());
//! assert_eq!(&x as *const _ as usize, member.get_offset().unwrap());
//! // The value of the member is the same as the variable
Expand All @@ -58,7 +58,7 @@
//! .set_arch(Architecture::Arch32Bit);
//! // We make a `DataMember` that has a series of offsets refering to a known value in
//! // the target processes memory
//! let member = DataMember::new_offset(handle, vec![0x01_02_03_04, 0x04, 0x08, 0x10]);
//! let member = DataMember::new_addr_offset(handle, 0x01020304, vec![0x04, 0x08, 0x10]);
//! // The memory offset can now be correctly calculated:
//! println!("Target memory location: {}", member.get_offset().unwrap());
//! // The memory offset can now be used to retrieve and modify values:
Expand Down Expand Up @@ -92,6 +92,9 @@ mod platform;
#[path = "windows.rs"]
mod platform;

#[cfg(windows)]
pub use platform::get_pid;

/// A trait that defines that it is possible to copy some memory from something represented by a
/// type into a buffer.
pub trait CopyAddress {
Expand All @@ -109,19 +112,26 @@ pub trait CopyAddress {
///
/// # Errors
/// `std::io::Error` if an error occurs copying the address.
fn get_offset(&self, offsets: &[usize]) -> std::io::Result<usize> {
#[allow(clippy::cast_sign_loss)]
fn get_offset(&self, base: usize, offsets: &[isize]) -> std::io::Result<usize> {
// Look ma! No unsafes!
let mut offset: usize = 0;

let noffsets: usize = offsets.len();
let mut copy = vec![0_u8; self.get_pointer_width() as usize];
for next_offset in offsets.iter().take(noffsets - 1) {
offset += next_offset;
self.copy_address(offset, &mut copy)?;
offset = self.get_pointer_width().pointer_from_ne_bytes(&copy);
}
if noffsets > 0 {
let mut offset: usize = base;
let mut copy = vec![0_u8; self.get_pointer_width() as usize];
for next_offset in offsets.iter().take(noffsets - 1) {
// should work because of 2s-complement.
offset = offset.wrapping_add(*next_offset as usize);
self.copy_address(offset, &mut copy)?;
offset = self.get_pointer_width().pointer_from_ne_bytes(&copy);
}

offset += offsets[noffsets - 1];
Ok(offset)
// should work because of 2s-complement.
Ok(offset.wrapping_add(offsets[noffsets - 1] as usize))
} else {
Ok(base)
}
}

/// Get the the pointer width of the underlying process.
Expand Down Expand Up @@ -190,6 +200,22 @@ pub trait ProcessHandleExt {
fn set_arch(self, arch: Architecture) -> Self;
}

/// Handling modules (e.g. DLLs) in a process.
pub trait ModuleInfo {
/// Gets the base address of a module in a process. For example, "GameAssembly.dll" when on Windows.
Copy link
Owner

Choose a reason for hiding this comment

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

The equivalent for dlls on Windows on Linux and OSX are "Shared Libraries" (which usually have the extension .so). Would you mind also mentioning that in docs?

/// You can then use the address in the `base` parameter of [`set_offset`] for example.
///
/// # Errors
/// Returns `std::io::ErrorKind::NotFound` when no such module name exists.
/// Returns OS Error when something else went wrong.
///
/// # Panics
/// Panics when closing the handle fails (e.g. double close).
///
/// [`set_offset`]: trait.Memory.html#tymethod.set_offset
fn get_module_base(&self, name: &str) -> std::io::Result<usize>;
}

/// 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 All @@ -198,7 +224,7 @@ pub trait Memory<T> {
/// For those sorts of data structures, to access data you need to go via multiple pointers, so
/// that if an inner region reallocates its size, the variable that is being modified will be
/// correctly modified.
fn set_offset(&mut self, new_offsets: Vec<usize>);
fn set_offset(&mut self, new_base: usize, new_offsets: Vec<isize>);

/// Gets the actual total offset from the offsets given by [`Memory::set_offset`].
///
Expand Down Expand Up @@ -259,3 +285,53 @@ where
source.copy_address(addr, &mut copy)?;
Ok(copy)
}

#[cfg(test)]
mod tests {
use super::*;

#[repr(C)]
#[derive(Clone, Copy)]
struct Player {
x: u32,
y: u32,
}

#[repr(C)]
struct GameState {
garbage: u32,
garbage2: u32,
players: [Box<Player>; 2], // note that this array is in-place, since it's fixed size.
}

#[test]
fn multilevel_pointers() {
let game = GameState {
garbage: 42,
garbage2: 1337,
players: [
Box::new(Player { x: 1, y: 2 }),
Box::new(Player { x: 3, y: 4 }),
],
};
let handle = (std::process::id() as Pid)
.try_into_process_handle()
.unwrap();

let garbage2 =
DataMember::<u32>::new_addr_offset(handle, &game as *const _ as usize, vec![4]);
assert_eq!(1337, garbage2.read().unwrap());

let second_player = DataMember::<Player>::new_addr_offset(
handle,
&game as *const _ as usize,
vec![8 + (handle.get_pointer_width() as u8 as isize) * 1],
); // skip u32 + i64 + first player.

let second_player_x = second_player.extend::<u32>(vec![0]);
let second_player_y = second_player.extend::<u32>(vec![4]); // sizeof u32

assert_eq!(3, second_player_x.read().unwrap());
assert_eq!(4, second_player_y.read().unwrap());
}
}
Loading