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
7 changes: 7 additions & 0 deletions src/architecture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,11 @@ impl Architecture {
Architecture::Arch128Bit => u128::from_ne_bytes(bytes.try_into().unwrap()) as usize,
}
}

/// Returns the amount of bytes which a pointer takes up on this architecture.
/// E.g. 4 for 32bit, 8 for 64bit, etc.
#[must_use]
pub fn pointer_width_bytes(self) -> usize {
Copy link
Owner

Choose a reason for hiding this comment

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

You can already cast Architecture to usize with architecture as usize. Perhaps what's needed instead is making that availability more clear in documentation?

Copy link
Author

Choose a reason for hiding this comment

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

I thought casting an enum was pretty unintuitive, and even if documentation exists but someone stumbles upon it in the wild, they will have no clue what's going on. Especially when that person is new to rust too, and have no idea that it returns the Arch64Bit = 8 enum value.

I would instead rename get_pointer_width to get_arch, and keep the function I added (or rename it to pointer_width).

Copy link
Owner

Choose a reason for hiding this comment

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

That's the entire point of the enum being repr(u8), although I guess you'd probably need to read the nomicon to know that.

I'd probably prefer not adding another function, do you think it'd be okay to just add a doc comment to the Architecture struct with something along the lines of:

This enum is `repr(u8)`, which means that it can be treated exactly the same as a `u8` with the same value as the number of bytes in the architecture.

self as u8 as usize
}
}
155 changes: 155 additions & 0 deletions src/data_member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,108 @@ impl<T: Sized + Copy> DataMember<T> {
_phantom: std::marker::PhantomData,
}
}

/// Create a new `DataMember` from a [`ProcessHandle`] and some number of (potentially negative) 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.htm
#[must_use]
#[allow(clippy::cast_sign_loss)]
pub fn new_offset_relative(handle: ProcessHandle, offsets: Vec<isize>) -> Self {
Copy link
Owner

Choose a reason for hiding this comment

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

Could you copy these functions to LocalMember as well?

Self {
// Yes, we are casting to usize. This will not touch any bits, but due to 2s complement,
// we still get the correct result when adding offsets.
offsets: offsets.into_iter().map(|x| x as usize).collect(),
process: handle,
_phantom: std::marker::PhantomData,
}
}

/// Create a new `DataMember` from a [`ProcessHandle`], pointing to the memory address in the
/// remote process. Equivalent to `new_offset(handle, vec![addr])`. 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, addr: usize) -> Self {
Self {
offsets: vec![addr],
process: handle,
_phantom: std::marker::PhantomData,
}
}

/// Create a new `DataMember` from a [`ProcessHandle`], pointing to the memory address in the
/// process, then following a bunch of pointers and offsets, which may be negative.
/// If you use CheatEngine and get a pointer of form "MyModule.dll + 0x12345678", plus a bunch
/// of offsets, then you want to put the base address of the module as `addr`, `0x12345678` as
/// the first offset, then any further offsets etc.
Copy link
Owner

Choose a reason for hiding this comment

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

Could you add a newline after this? I think that when it gets rendered into HTML, single linebreaks are removed.

/// This function is merely a convenience function, and is equivalent to
/// `new_offset_relative(handle, vec![addr as isize, offsets[0], offsets[1], ...])`.
/// 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]
#[allow(clippy::doc_markdown)]
#[allow(clippy::cast_sign_loss)]
pub fn new_addr_offset(handle: ProcessHandle, addr: usize, offsets: Vec<isize>) -> Self {
let mut vec = vec![addr];
// Yes, we are casting to usize. This will not touch any bits, and due to 2s complement,
// we still get the correct result when adding offsets.
vec.extend(offsets.into_iter().map(|x| x as usize));
Self {
offsets: vec,
process: handle,
_phantom: std::marker::PhantomData,
}
}

/// Creates a new `DataMember` by appending some more offets. Useful when you have a data
/// structure, and want to refer to multiple fields in it, or use it as a starting point
/// for chasing down more pointers.
Copy link
Owner

Choose a reason for hiding this comment

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

This one as well.

/// Since the pointed-to data type might have changed, this function is generic. It is your
/// responsibility to make sure you know what you point to.
#[allow(clippy::cast_sign_loss)]
#[must_use]
pub fn extend<TNew>(&self, more_offsets: Vec<isize>) -> DataMember<TNew> {
Copy link
Owner

Choose a reason for hiding this comment

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

Can these functions be put on the Memory trait instead? I think that they would also be useful on LocalMember.

let mut clone = DataMember {
offsets: self.offsets.clone(),
process: self.process,
_phantom: std::marker::PhantomData,
};
// Yes, we are casting to usize. This will not touch any bits, and due to 2s complement,
// we still get the correct result when adding offsets.
clone
.offsets
.extend(more_offsets.into_iter().map(|x| x as usize));
clone
}

/// Creates a new `DataMember`, based on self, by shifting the last offset by a number of
/// bytes. Does not append new offsets. This is useful if you have a pointer to a struct
/// and want to address different fields, or access elements in an array.
#[allow(clippy::cast_sign_loss)]
#[must_use]
pub fn shift<TNew>(&self, n_bytes: isize) -> DataMember<TNew> {
let mut clone = DataMember {
offsets: self.offsets.clone(),
process: self.process,
_phantom: std::marker::PhantomData,
};
let new = clone.offsets[self.offsets.len() - 1].wrapping_add(n_bytes as usize);
Copy link
Owner

Choose a reason for hiding this comment

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

The number of offsets needs to be checked here, or else you'll panic if someone just called DataMember::new() and passed it to this function.

I'm not sure of the best way to handle this. Maybe returning an Option<DataMember<...>> and doing something like let new = clone.offsets.last_mut()?.wrapping_add(n_bytes as usize);?

Copy link
Owner

Choose a reason for hiding this comment

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

Another potential option would be turning it into extend() if there are no offsets in the existing data member.

clone.offsets[self.offsets.len() - 1] = new;
clone
}
}

impl<T: Sized + Copy> Memory<T> for DataMember<T> {
Expand Down Expand Up @@ -147,4 +249,57 @@ mod test {
member.write(&0xffff).unwrap();
assert_eq!(test, 0xffff);
}

#[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 crate::Pid)
.try_into_process_handle()
.unwrap();

// point to `game`, then our data is +4 from the base of `game`.
let garbage2 = DataMember::<u32>::new_addr(handle, &game as *const _ as usize + 4);
assert_eq!(1337, garbage2.read().unwrap());

let garbage1 = garbage2.shift(-4);
assert_eq!(42u32, garbage1.read().unwrap());

// At `game + 2*sizeof(u32) + 1*sizeof(Player*) is where we find
// a pointer to the second player.
// So second_player.read() right now would just get you the pointer to the player.
let second_player = DataMember::<*mut Player>::new_addr(
handle,
(&game as *const _ as usize) + 8 + handle.get_pointer_width().pointer_width_bytes(),
Copy link
Owner

Choose a reason for hiding this comment

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

I think you can do handle.get_pointer_width() as usize here.

);

// But when we add an offset, in this case an offset of 0, we follow the pointer,
// and thus second_player_x points to the beginning of the second player, which in this
// case is also the x coordinate.
let second_player_x = second_player.extend::<u32>(vec![0]);
let second_player_y = second_player.extend::<u32>(vec![4]); // sizeof u32 = 4

assert_eq!(3, second_player_x.read().unwrap());
assert_eq!(4, second_player_y.read().unwrap());
}
}
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,12 @@ pub trait CopyAddress {
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;
offset = offset.wrapping_add(*next_offset);
self.copy_address(offset, &mut copy)?;
offset = self.get_pointer_width().pointer_from_ne_bytes(&copy);
}

offset += offsets[noffsets - 1];
offset = offset.wrapping_add(offsets[noffsets - 1]);
Ok(offset)
}

Expand Down