From 15c58a5f78e17588f83a6fa35f57a5d2561bee75 Mon Sep 17 00:00:00 2001 From: 486c Date: Thu, 10 Jul 2025 01:09:06 +0300 Subject: [PATCH 1/2] feat!: support a generic address type - Support generic `TryInto` for addresses instead of hardcoded i32 & same for signature scanning - Test for exclude words - Various clippy warnings fixes - Example for the osu! stable & osu! lazer clients - Linux: check for the memory regions permissions --- examples/lazer.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++ examples/stable.rs | 39 ++++++++++++++++++++++++++++++++++++ src/address.rs | 22 +++++++++++++++++++++ src/error.rs | 9 +++++++-- src/linux.rs | 39 +++++++++++++++++++++++------------- src/process.rs | 20 ++++++++++++------- src/windows.rs | 16 +++++++++++---- tests/process.rs | 21 ++++++++++++++++++-- tests/types.rs | 12 +++++++----- 9 files changed, 193 insertions(+), 34 deletions(-) create mode 100644 examples/lazer.rs create mode 100644 examples/stable.rs create mode 100644 src/address.rs diff --git a/examples/lazer.rs b/examples/lazer.rs new file mode 100644 index 0000000..3383f40 --- /dev/null +++ b/examples/lazer.rs @@ -0,0 +1,49 @@ +use rosu_mem::{ + error::ProcessError, + process::{Process, ProcessTraits}, + signature::Signature, +}; +use std::str::FromStr; + +fn main() -> Result<(), ProcessError> { + // Initialize a process first + let osu_process = Process::initialize("osu!", &[])?; + + println!("Found a osu! lazer process"); + + // Initialize a signatures + let session_signature = + Signature::from_str("00 00 00 00 80 4F 12 41").unwrap(); + + // Scan process for pre-initialized signatures + // Be aware that osu! lazer uses usize for addresses, so we also + // explicitly defining that + // Also reading a values for lazer is bit more tidious than stable :) + let mut session_addr: usize = + osu_process.read_signature(&session_signature)?; //- 0x208; + + // Read values! For simplicity willl read only a current game time + session_addr -= 0x208; + + let one = (osu_process.read_i64(session_addr + 0x90)? + 0x90) as usize; + let two = (osu_process.read_i64(one)? + 0x90) as usize; + let three = (osu_process.read_i64(two)? + 0x90) as usize; + let four = (osu_process.read_i64(three)? + 0x90) as usize; + let five = (osu_process.read_i64(four)? + 0x90) as usize; + let six = (osu_process.read_i64(five)? + 0x90) as usize; + let seven = (osu_process.read_i64(six)? + 0x340) as usize; + + let game_base = osu_process.read_i64(seven)? as usize; + + println!("Read a game base!"); + + let beatmap_clock_ptr = osu_process.read_i64(game_base + 0x4d0)? as usize; + let final_clock_ptr = + osu_process.read_i64(beatmap_clock_ptr + 0x210)? as usize; + + let current_time = osu_process.read_f64(final_clock_ptr + 0x30)?; + + println!("Current osu time: {current_time}"); + + Ok(()) +} diff --git a/examples/stable.rs b/examples/stable.rs new file mode 100644 index 0000000..d23e5c4 --- /dev/null +++ b/examples/stable.rs @@ -0,0 +1,39 @@ +use rosu_mem::{ + error::ProcessError, + process::{Process, ProcessTraits}, + signature::Signature, +}; +use std::str::FromStr; + +// Exclude words, basically a hack to properly find a osu! process when using wine +static EXCLUDE_WORDS: [&str; 2] = ["umu-run", "waitforexitandrun"]; + +fn main() -> Result<(), ProcessError> { + // Initialize a process first + let osu_process = Process::initialize("osu!.exe", &EXCLUDE_WORDS)?; + + println!("Found a osu! process"); + + // Initialize a signatures + let base_signature = Signature::from_str("F8 01 74 04 83 65").unwrap(); + let status_signature = Signature::from_str("48 83 F8 04 73 1E").unwrap(); + + // Scan process for pre-initialized signatures + // Be aware that osu! stable uses i32 for addresses, so we also + // explicitly defining that + + // Reading a base signature just to be sure that we are in the correct osu! process + let _base: i32 = osu_process.read_signature(&base_signature)?; + let status: i32 = osu_process.read_signature(&status_signature)?; + + println!("Found all required signatures!"); + + // Now read the values that you are intrested in :) + // For the sake of keeping example simple we will read a current game state + let status_ptr = osu_process.read_i32(status - 0x4)?; + let osu_state_status = osu_process.read_u32(status_ptr)?; + + println!("Current osu! game status: {osu_state_status}"); + + Ok(()) +} diff --git a/src/address.rs b/src/address.rs new file mode 100644 index 0000000..f17ad30 --- /dev/null +++ b/src/address.rs @@ -0,0 +1,22 @@ +pub enum Address { + Native(usize), + I64(i64), + I32(i32) +} + +fn test_convert>(value: T) { + + let end_value: usize = value.try_into().unwrap(); + + println!("") +} + +fn test_xd() { + let value_i32 = 1337i32; + let value_i64 = 1338i64; + let value_usize = 1339usize; + + + test_convert(value_i64); + +} diff --git a/src/error.rs b/src/error.rs index ffead38..4a65de5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,6 +21,7 @@ pub enum ProcessError { #[cfg(target_os = "windows")] inner: windows::core::Error, }, + ConvertError, } impl From for ProcessError { @@ -85,7 +86,7 @@ impl Display for ProcessError { write!(f, "Got error during type convertion") } ProcessError::SignatureNotFound(v) => { - write!(f, "Cannot find signature {}", v) + write!(f, "Cannot find signature {v}") } ProcessError::OsError { .. } => write!(f, "Got OS error"), ProcessError::NotEnoughPermissions => { @@ -93,11 +94,14 @@ impl Display for ProcessError { } ProcessError::BadAddress(addr, len) => { let _ = writeln!(f, "Trying to read bad address"); - writeln!(f, "Address: {:X}, Length: {:X}", addr, len) + writeln!(f, "Address: {addr:X}, Length: {len:X}") } ProcessError::ExecutablePathNotFound => { write!(f, "Executable path not found!") } + ProcessError::ConvertError => { + write!(f, "Failed to converted passed address to usize") + } } } } @@ -114,6 +118,7 @@ impl std::error::Error for ProcessError { ProcessError::SignatureNotFound(_) => None, ProcessError::OsError { inner } => Some(inner), ProcessError::BadAddress(..) => None, + ProcessError::ConvertError => None, } } } diff --git a/src/linux.rs b/src/linux.rs index 649bf9e..d5d965f 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -94,26 +94,35 @@ impl ProcessTraits for Process { let mut split = line.split_whitespace(); let range_raw = split.next().unwrap(); + let mut permissions_raw_chars = split.next().unwrap().chars(); + let mut range_split = range_raw.split('-'); let from_str = range_split.next().unwrap(); let to_str = range_split.next().unwrap(); let from = usize::from_str_radix(from_str, 16)?; - let to = usize::from_str_radix(to_str, 16)?; - v.push(MemoryRegion { - from, - size: to - from, - }); + let read = permissions_raw_chars.next().unwrap(); + let write = permissions_raw_chars.next().unwrap(); + + if read == 'r' && write == 'w' { + v.push(MemoryRegion { + from, + size: to - from, + }); + } } self.maps = v; Ok(self) } - fn read_signature(&self, sign: &Signature) -> Result { + fn read_signature>( + &self, + sign: &Signature, + ) -> Result { let mut buff = Vec::new(); for region in &self.maps { @@ -140,23 +149,25 @@ impl ProcessTraits for Process { } if let Some(offset) = find_signature(buff.as_slice(), sign) { - return Ok((remote.base + offset) as i32); + return (remote.base + offset) + .try_into() + .map_err(|_| ProcessError::ConvertError); } } Err(ProcessError::SignatureNotFound(sign.to_string())) } - fn read( + fn read>( &self, - addr: i32, + addr: T, len: usize, buff: &mut [u8], ) -> Result<(), ProcessError> { - let remote = RemoteIoVec { - base: addr as usize, - len, - }; + let addr: usize = + addr.try_into().map_err(|_| ProcessError::ConvertError)?; + + let remote = RemoteIoVec { base: addr, len }; let slice = IoSliceMut::new(buff); @@ -167,7 +178,7 @@ impl ProcessTraits for Process { Ok(_) => (), Err(e) => match e { nix::errno::Errno::EFAULT => { - return Err(ProcessError::BadAddress(addr as usize, len)) + return Err(ProcessError::BadAddress(addr, len)) } _ => return Err(e.into()), }, diff --git a/src/process.rs b/src/process.rs index c6b34f4..775fbf2 100644 --- a/src/process.rs +++ b/src/process.rs @@ -15,9 +15,9 @@ pub struct MemoryRegion { macro_rules! prim_read_impl { ($t: ident) => { paste! { - fn []( + fn []>( &self, - addr: i32 + addr: T ) -> Result<$t, ProcessError> { let mut bytes = [0u8; std::mem::size_of::<$t>()]; self.read(addr, std::mem::size_of::<$t>(), &mut bytes)?; @@ -31,11 +31,14 @@ macro_rules! prim_read_impl { macro_rules! prim_read_array_impl { ($t: ident) => { paste! { - fn []( + fn []>( &self, - addr: i32, + addr: T, buff: &mut Vec<$t> ) -> Result<(), ProcessError> { + let addr: usize = addr.try_into() + .map_err(|_| ProcessError::ConvertError)?; + let items_ptr = self.read_i32(addr + 4)?; let size = self.read_i32(addr + 12)? as usize; @@ -106,11 +109,14 @@ where fn read_regions(self) -> Result; - fn read_signature(&self, sign: &Signature) -> Result; + fn read_signature>( + &self, + sign: &Signature, + ) -> Result; - fn read( + fn read>( &self, - addr: i32, + addr: T, len: usize, buff: &mut [u8], ) -> Result<(), ProcessError>; diff --git a/src/windows.rs b/src/windows.rs index 9e0d72c..ab91b81 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -130,7 +130,10 @@ impl ProcessTraits for Process { Ok(self) } - fn read_signature(&self, sign: &Signature) -> Result { + fn read_signature>( + &self, + sign: &Signature, + ) -> Result { let mut buf = Vec::new(); let mut bytesread: usize = 0; @@ -159,19 +162,24 @@ impl ProcessTraits for Process { } if let Some(offset) = find_signature(&buf[..bytesread], sign) { - return Ok((region.from + offset) as i32); + return (region.from + offset) + .try_into() + .map_err(|_| ProcessError::ConvertError); } } Err(ProcessError::SignatureNotFound(sign.to_string())) } - fn read( + fn read>( &self, - addr: i32, + addr: T, len: usize, buff: &mut [u8], ) -> Result<(), ProcessError> { + let addr: usize = + addr.try_into().map_err(|_| ProcessError::ConvertError)?; + let mut n = 0; let res = unsafe { diff --git a/tests/process.rs b/tests/process.rs index 9d58911..8c83e9a 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -1,4 +1,7 @@ -use rosu_mem::process::{Process, ProcessTraits}; +use rosu_mem::{ + error::ProcessError, + process::{Process, ProcessTraits}, +}; cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { @@ -49,7 +52,7 @@ fn get_process_name(id: u32) -> String { #[cfg(target_os = "linux")] fn get_process_name(id: u32) -> String { - std::fs::read_to_string(format!("/proc/{}/cmdline", id)).unwrap() + std::fs::read_to_string(format!("/proc/{id}/cmdline")).unwrap() } /// Trying to find current process (process in which this test is running) @@ -65,3 +68,17 @@ fn test_process_finder() { let proc = proc.read_regions().unwrap(); assert!(!proc.maps.is_empty()) } + +#[test] +fn test_process_finder_exclude() { + let proc_id = std::process::id(); + let name = get_process_name(proc_id); + + let proc = Process::find_process(&name, &[&name]); + + assert!(proc.is_err()); + + let Err(err) = proc else { panic!("not a Err") }; + + assert!(matches!(err, ProcessError::ProcessNotFound)); +} diff --git a/tests/types.rs b/tests/types.rs index ce81325..a16cde7 100644 --- a/tests/types.rs +++ b/tests/types.rs @@ -113,24 +113,26 @@ impl ProcessTraits for FakeProccess { todo!() } - fn read_signature( + fn read_signature>( &self, _sign: &rosu_mem::signature::Signature, - ) -> Result { + ) -> Result { todo!() } - fn read( + fn read>( &self, - addr: i32, + addr: T, len: usize, buff: &mut [u8], ) -> Result<(), ProcessError> { + let addr: usize = + addr.try_into().map_err(|_| ProcessError::ConvertError)?; // Addr - starting index // self.buff.set_position(addr as u64); // self.buff.read(buff); - let mut slice = &self.buff[addr as usize..addr as usize + len]; + let mut slice = &self.buff[addr..addr + len]; let _ = slice.read(buff); Ok(()) From 97586cf68a88ba2e65cca80f11d46f82c1d82a92 Mon Sep 17 00:00:00 2001 From: 486c Date: Fri, 11 Jul 2025 16:29:29 +0300 Subject: [PATCH 2/2] feat: switch to thiserror for errors Instead of implementing them manually --- Cargo.lock | 21 +++++++ Cargo.toml | 1 + src/error.rs | 152 ++++++++----------------------------------------- src/linux.rs | 7 ++- src/process.rs | 2 +- src/windows.rs | 7 ++- tests/types.rs | 5 +- 7 files changed, 59 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f3c778..d519940 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,7 @@ dependencies = [ "nix", "paste", "rand", + "thiserror", "windows", ] @@ -140,6 +141,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.14" diff --git a/Cargo.toml b/Cargo.toml index c49b739..2f34a7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ path = "src/lib.rs" [dependencies] cfg-if = "1.0.0" paste = "1.0.15" +thiserror = "2.0.12" [dev-dependencies] rand = "0.8.5" diff --git a/src/error.rs b/src/error.rs index 4a65de5..55ec64f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,33 +1,32 @@ -use std::{ - error::Error, fmt::Display, num::ParseIntError, str::Utf8Error, - string::FromUtf8Error, -}; +use std::{num::ParseIntError, string::FromUtf8Error}; -#[derive(Debug)] +#[derive(thiserror::Error, Debug)] pub enum ProcessError { + #[error("process not found")] ProcessNotFound, + #[error("executable path not found")] ExecutablePathNotFound, + #[error("not enough permissions to run, please run as admin/sudo")] NotEnoughPermissions, - IoError { - inner: std::io::Error, - }, + #[error("io error")] + IoError(#[from] std::io::Error), + #[error("failed to convert bytes to string")] FromUtf8Error, + #[error("failed to convert type")] ConvertionError, + #[error("trying to read bad address, addr: {0:X}, len: {1:X}")] BadAddress(usize, usize), + #[error("cannot find signature: {0}")] SignatureNotFound(String), - OsError { - #[cfg(target_os = "linux")] - inner: nix::errno::Errno, - #[cfg(target_os = "windows")] - inner: windows::core::Error, - }, - ConvertError, -} + #[error("failed to convert address to usize")] + AddressConvertError, -impl From for ProcessError { - fn from(value: std::io::Error) -> Self { - Self::IoError { inner: value } - } + #[cfg(target_os = "linux")] + #[error("os error `{0}`")] + OsError(#[from] nix::errno::Errno), + #[cfg(target_os = "windows")] + #[error("os error `{0}`")] + OsError(#[from] windows::core::Error), } impl From for ProcessError { @@ -36,123 +35,22 @@ impl From for ProcessError { } } -impl From for ProcessError { - fn from(_: std::num::TryFromIntError) -> Self { - Self::ConvertionError - } -} - impl From for ProcessError { fn from(_: FromUtf8Error) -> Self { Self::FromUtf8Error } } -impl From for ProcessError { - fn from(_: Utf8Error) -> Self { +impl From for ProcessError { + fn from(_: std::str::Utf8Error) -> Self { Self::FromUtf8Error } } -// Linux only -#[cfg(target_os = "linux")] -impl From for ProcessError { - fn from(inner: nix::errno::Errno) -> Self { - match inner { - nix::errno::Errno::EPERM => Self::NotEnoughPermissions, - nix::errno::Errno::ESRCH => Self::ProcessNotFound, - _ => Self::OsError { inner }, - } - } -} - -// Windows only -#[cfg(target_os = "windows")] -impl From for ProcessError { - fn from(inner: windows::core::Error) -> Self { - Self::OsError { inner } // TODO add code value - } -} - -impl Display for ProcessError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ProcessError::ProcessNotFound => write!(f, "Process not found!"), - ProcessError::IoError { .. } => write!(f, "Got I/O error!"), - ProcessError::FromUtf8Error => { - write!(f, "Got Error when converting bytes to string!") - } - ProcessError::ConvertionError => { - write!(f, "Got error during type convertion") - } - ProcessError::SignatureNotFound(v) => { - write!(f, "Cannot find signature {v}") - } - ProcessError::OsError { .. } => write!(f, "Got OS error"), - ProcessError::NotEnoughPermissions => { - write!(f, "Not enough permissions to run, please run as sudo") - } - ProcessError::BadAddress(addr, len) => { - let _ = writeln!(f, "Trying to read bad address"); - writeln!(f, "Address: {addr:X}, Length: {len:X}") - } - ProcessError::ExecutablePathNotFound => { - write!(f, "Executable path not found!") - } - ProcessError::ConvertError => { - write!(f, "Failed to converted passed address to usize") - } - } - } -} - -impl std::error::Error for ProcessError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - ProcessError::ProcessNotFound => None, - ProcessError::ExecutablePathNotFound => None, - ProcessError::NotEnoughPermissions => None, - ProcessError::IoError { inner } => Some(inner), - ProcessError::FromUtf8Error => None, - ProcessError::ConvertionError => None, - ProcessError::SignatureNotFound(_) => None, - ProcessError::OsError { inner } => Some(inner), - ProcessError::BadAddress(..) => None, - ProcessError::ConvertError => None, - } - } -} - -#[derive(Debug)] +#[derive(thiserror::Error, Debug)] pub enum ParseSignatureError { + #[error("invalid string length `{0}`")] InvalidLength(usize), - InvalidInt { inner: ParseIntError }, -} - -impl From for ParseSignatureError { - fn from(inner: ParseIntError) -> Self { - Self::InvalidInt { inner } - } -} - -impl Error for ParseSignatureError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - ParseSignatureError::InvalidLength(_) => None, - ParseSignatureError::InvalidInt { inner } => Some(inner), - } - } -} - -impl Display for ParseSignatureError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ParseSignatureError::InvalidLength(len) => { - write!(f, "Invalid string length {len}") - } - ParseSignatureError::InvalidInt { .. } => { - f.write_str("Failed to parse integer") - } - } - } + #[error("failed to parse integer")] + InvalidInt(#[from] ParseIntError), } diff --git a/src/linux.rs b/src/linux.rs index d5d965f..2e14fed 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -151,7 +151,7 @@ impl ProcessTraits for Process { if let Some(offset) = find_signature(buff.as_slice(), sign) { return (remote.base + offset) .try_into() - .map_err(|_| ProcessError::ConvertError); + .map_err(|_| ProcessError::AddressConvertError); } } @@ -164,8 +164,9 @@ impl ProcessTraits for Process { len: usize, buff: &mut [u8], ) -> Result<(), ProcessError> { - let addr: usize = - addr.try_into().map_err(|_| ProcessError::ConvertError)?; + let addr: usize = addr + .try_into() + .map_err(|_| ProcessError::AddressConvertError)?; let remote = RemoteIoVec { base: addr, len }; diff --git a/src/process.rs b/src/process.rs index 775fbf2..81901b4 100644 --- a/src/process.rs +++ b/src/process.rs @@ -37,7 +37,7 @@ macro_rules! prim_read_array_impl { buff: &mut Vec<$t> ) -> Result<(), ProcessError> { let addr: usize = addr.try_into() - .map_err(|_| ProcessError::ConvertError)?; + .map_err(|_| ProcessError::AddressConvertError)?; let items_ptr = self.read_i32(addr + 4)?; let size = self.read_i32(addr + 12)? as usize; diff --git a/src/windows.rs b/src/windows.rs index ab91b81..a1ea5dc 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -164,7 +164,7 @@ impl ProcessTraits for Process { if let Some(offset) = find_signature(&buf[..bytesread], sign) { return (region.from + offset) .try_into() - .map_err(|_| ProcessError::ConvertError); + .map_err(|_| ProcessError::AddressConvertError); } } @@ -177,8 +177,9 @@ impl ProcessTraits for Process { len: usize, buff: &mut [u8], ) -> Result<(), ProcessError> { - let addr: usize = - addr.try_into().map_err(|_| ProcessError::ConvertError)?; + let addr: usize = addr + .try_into() + .map_err(|_| ProcessError::AddressConvertError)?; let mut n = 0; diff --git a/tests/types.rs b/tests/types.rs index a16cde7..8e98091 100644 --- a/tests/types.rs +++ b/tests/types.rs @@ -126,8 +126,9 @@ impl ProcessTraits for FakeProccess { len: usize, buff: &mut [u8], ) -> Result<(), ProcessError> { - let addr: usize = - addr.try_into().map_err(|_| ProcessError::ConvertError)?; + let addr: usize = addr + .try_into() + .map_err(|_| ProcessError::AddressConvertError)?; // Addr - starting index // self.buff.set_position(addr as u64); // self.buff.read(buff);