diff --git a/src/ext/mod.rs b/src/ext/mod.rs index 76def6c..9db0cc4 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -7,3 +7,4 @@ mod mapping; pub mod read; pub mod search; +pub mod write; diff --git a/src/ext/read.rs b/src/ext/read.rs index 6898164..ecfef55 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -39,7 +39,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 1]; @@ -56,7 +56,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 2]; @@ -77,7 +77,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 4]; file.read_exact(&mut buffer)?; @@ -98,7 +98,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 8]; @@ -119,7 +119,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 1]; @@ -140,7 +140,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 2]; @@ -161,7 +161,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 4]; @@ -183,7 +183,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 8]; @@ -204,7 +204,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 4]; @@ -226,7 +226,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 8]; diff --git a/src/ext/search.rs b/src/ext/search.rs index b42641b..2b9a0ee 100644 --- a/src/ext/search.rs +++ b/src/ext/search.rs @@ -18,7 +18,7 @@ use crate::ext::mapping::MappingExtError; pub trait SearchExt { /// Search for a byte pattern and return memory addresses. fn search_bytes(&self, pattern: &[u8]) -> SearchExtResult>; - fn search_u8(&self, value: u8) -> SearchExtResult>; + fn search_u8(&self, value: u8, byte_order: Option) -> SearchExtResult>; fn search_u16(&self, value: u16, byte_order: Option) -> SearchExtResult>; fn search_u32(&self, value: u32, byte_order: Option) -> SearchExtResult>; #[cfg(target_pointer_width = "64")] @@ -47,7 +47,7 @@ impl SearchExt for crate::prelude::Process { } let mappings = self.mappings()?; - let mut file = self.mem_file()?; + let mut file = self.mem_file(false)?; const BUFFER_SIZE: usize = 8192; let mut buffer = [0u8; BUFFER_SIZE]; @@ -149,8 +149,14 @@ impl SearchExt for crate::prelude::Process { Ok(addresses) } - fn search_u8(&self, value: u8) -> SearchExtResult> { - self.search_bytes(&[value]) + fn search_u8(&self, value: u8, byte_order: Option) -> SearchExtResult> { + self.search_bytes( + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) } fn search_u16(&self, value: u16, byte_order: Option) -> SearchExtResult> { @@ -344,7 +350,9 @@ mod linux_tests { fn test_search_u8(dummy_process: DummyProcess) { let process = Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); - let addresses = process.search_u8(2).expect("Could not search u8 value."); + let addresses = process + .search_u8(2, None) + .expect("Could not search u8 value."); let target_address: usize = { let response = diff --git a/src/ext/write.rs b/src/ext/write.rs new file mode 100644 index 0000000..8722bba --- /dev/null +++ b/src/ext/write.rs @@ -0,0 +1,587 @@ +// Copyright (c) 2026 Eray Erdin +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::io::{self, Seek, Write}; + +use crate::prelude::ByteOrder; + +pub trait WriteExt { + fn write_bytes(&self, address: usize, bytes: &[u8]) -> WriteExtResult<()>; + fn write_u8( + &self, + address: usize, + value: u8, + byte_order: Option, + ) -> WriteExtResult<()>; + fn write_u16( + &self, + address: usize, + value: u16, + byte_order: Option, + ) -> WriteExtResult<()>; + fn write_u32( + &self, + address: usize, + value: u32, + byte_order: Option, + ) -> WriteExtResult<()>; + #[cfg(target_pointer_width = "64")] + fn write_u64( + &self, + address: usize, + value: u64, + byte_order: Option, + ) -> WriteExtResult<()>; + fn write_i8( + &self, + address: usize, + value: i8, + byte_order: Option, + ) -> WriteExtResult<()>; + fn write_i16( + &self, + address: usize, + value: i16, + byte_order: Option, + ) -> WriteExtResult<()>; + fn write_i32( + &self, + address: usize, + value: i32, + byte_order: Option, + ) -> WriteExtResult<()>; + #[cfg(target_pointer_width = "64")] + fn write_i64( + &self, + address: usize, + value: i64, + byte_order: Option, + ) -> WriteExtResult<()>; + fn write_f32( + &self, + address: usize, + value: f32, + byte_order: Option, + ) -> WriteExtResult<()>; + #[cfg(target_pointer_width = "64")] + fn write_f64( + &self, + address: usize, + value: f64, + byte_order: Option, + ) -> WriteExtResult<()>; +} + +#[cfg(target_os = "linux")] +impl WriteExt for crate::process::Process { + fn write_bytes(&self, address: usize, bytes: &[u8]) -> WriteExtResult<()> { + use nix::{sys::ptrace, unistd::Pid}; + + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mut file = self.mem_file(true)?; + file.seek(io::SeekFrom::Start(address as u64))?; + file.write_all(bytes)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(()) + } + + fn write_u8( + &self, + address: usize, + value: u8, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } + + fn write_u16( + &self, + address: usize, + value: u16, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } + + fn write_u32( + &self, + address: usize, + value: u32, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } + + #[cfg(target_pointer_width = "64")] + fn write_u64( + &self, + address: usize, + value: u64, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } + + fn write_i8( + &self, + address: usize, + value: i8, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } + + fn write_i16( + &self, + address: usize, + value: i16, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } + + fn write_i32( + &self, + address: usize, + value: i32, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } + + #[cfg(target_pointer_width = "64")] + fn write_i64( + &self, + address: usize, + value: i64, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } + + fn write_f32( + &self, + address: usize, + value: f32, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } + + #[cfg(target_pointer_width = "64")] + fn write_f64( + &self, + address: usize, + value: f64, + byte_order: Option, + ) -> WriteExtResult<()> { + self.write_bytes( + address, + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) + } +} + +#[derive(Debug, Error)] +pub enum WriteExtError { + #[error("A *nix error occured. {0:?}")] + Nix(#[from] nix::Error), + #[error("An IO error occured. {0:?}")] + IO(#[from] io::Error), +} + +pub type WriteExtResult = Result; + +#[cfg(target_os = "linux")] +#[cfg(test)] +mod tests { + use std::{env, io::BufRead, process}; + + use crate::process::Process; + + use super::*; + + struct DummyProcess { + child: process::Child, + pub pid: i32, + pub base_url: String, + } + + impl Drop for DummyProcess { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } + } + + #[fixture] + fn dummy_process() -> DummyProcess { + let cwd = env::current_dir().expect("Could not get the current working directory."); + let dummy_process_dir = cwd.join("dummy_process"); + + let build_status = process::Command::new("cargo") + .arg("build") + .current_dir(&dummy_process_dir) + .status() + .expect("Failed to run dummy_process compilation."); + assert!(build_status.success(), "Failed to build dummy_process"); + + let bin_path = dummy_process_dir.join("target/debug/dummy_process"); + let mut child = process::Command::new(bin_path) + .stdout(process::Stdio::piped()) + .spawn() + .expect("Could not spawn dummy_process."); + + let port = { + let stdout = child.stdout.as_mut().expect("Failed to get stdout."); + let reader = io::BufReader::new(stdout); + let port_line = reader + .lines() + .next() + .expect("Port line does not exist.") + .expect("Could not read port line of stdout."); + port_line + .trim() + .split(":") + .last() + .expect("Could not read port part from stdout.") + .parse::() + .expect("Could not parse port from stdout.") + }; + + let base_url = format!("http://127.0.0.1:{port}"); + + let pid = { + let body = reqwest::blocking::get(format!("{base_url}/pid")) + .expect("Could not request PID of dummy process."); + body.text() + .expect("Could not decode the charset of PID of dummy process.") + .parse::() + .expect("Could not parse PID of dummy process") + }; + + DummyProcess { + child, + pid, + base_url, + } + } + + #[rstest] + fn test_write_u8(dummy_process: DummyProcess) { + let process = Process::try_new(dummy_process.pid).expect("Could not init Process."); + let address: usize = { + let response = + reqwest::blocking::get(format!("{}/u8val/address", dummy_process.base_url)) + .expect("Could not request u8val address."); + response + .text() + .expect("Could not decode the charset of u8val address.") + .parse() + } + .expect("Could not parse address."); + + process + .write_u8(address, 20, None) + .expect("Could not write onto u8val."); + let value: u8 = { + let response = reqwest::blocking::get(format!("{}/u8val", dummy_process.base_url)) + .expect("Could not request u8val value."); + response + .text() + .expect("Could not decode the charset of u8val value.") + .parse() + } + .expect("Could not parse value."); + + assert_eq!(value, 20); + } + + #[rstest] + fn test_write_u16(dummy_process: DummyProcess) { + let process = Process::try_new(dummy_process.pid).expect("Could not init Process."); + let address: usize = { + let response = + reqwest::blocking::get(format!("{}/u16val/address", dummy_process.base_url)) + .expect("Could not request u16val address."); + response + .text() + .expect("Could not decode the charset of u16val address.") + .parse() + } + .expect("Could not parse address."); + + process + .write_u16(address, 30, None) + .expect("Could not write onto u16val."); + let value: u16 = { + let response = reqwest::blocking::get(format!("{}/u16val", dummy_process.base_url)) + .expect("Could not request u16val value."); + response + .text() + .expect("Could not decode the charset of u16val value.") + .parse() + } + .expect("Could not parse value."); + + assert_eq!(value, 30); + } + + #[rstest] + fn test_write_u32(dummy_process: DummyProcess) { + let process = Process::try_new(dummy_process.pid).expect("Could not init Process."); + let address: usize = { + let response = + reqwest::blocking::get(format!("{}/u32val/address", dummy_process.base_url)) + .expect("Could not request u32val address."); + response + .text() + .expect("Could not decode the charset of u32val address.") + .parse() + } + .expect("Could not parse address."); + + process + .write_u32(address, 40, None) + .expect("Could not write onto u32val."); + let value: u32 = { + let response = reqwest::blocking::get(format!("{}/u32val", dummy_process.base_url)) + .expect("Could not request u32val value."); + response + .text() + .expect("Could not decode the charset of u32val value.") + .parse() + } + .expect("Could not parse value."); + + assert_eq!(value, 40); + } + + #[cfg(target_pointer_width = "64")] + #[rstest] + fn test_write_u64(dummy_process: DummyProcess) { + let process = Process::try_new(dummy_process.pid).expect("Could not init Process."); + let address: usize = { + let response = + reqwest::blocking::get(format!("{}/u64val/address", dummy_process.base_url)) + .expect("Could not request u64val address."); + response + .text() + .expect("Could not decode the charset of u64val address.") + .parse() + } + .expect("Could not parse address."); + + process + .write_u64(address, 50, None) + .expect("Could not write onto u64val."); + let value: u64 = { + let response = reqwest::blocking::get(format!("{}/u64val", dummy_process.base_url)) + .expect("Could not request u64val value."); + response + .text() + .expect("Could not decode the charset of u64val value.") + .parse() + } + .expect("Could not parse value."); + + assert_eq!(value, 50); + } + + #[rstest] + fn test_write_i8(dummy_process: DummyProcess) { + let process = Process::try_new(dummy_process.pid).expect("Could not init Process."); + let address: usize = { + let response = + reqwest::blocking::get(format!("{}/i8val/address", dummy_process.base_url)) + .expect("Could not request i8val address."); + response + .text() + .expect("Could not decode the charset of i8val address.") + .parse() + } + .expect("Could not parse address."); + + process + .write_i8(address, -10, None) + .expect("Could not write onto i8val."); + let value: i8 = { + let response = reqwest::blocking::get(format!("{}/i8val", dummy_process.base_url)) + .expect("Could not request i8val value."); + response + .text() + .expect("Could not decode the charset of i8val value.") + .parse() + } + .expect("Could not parse value."); + + assert_eq!(value, -10); + } + + #[rstest] + fn test_write_i16(dummy_process: DummyProcess) { + let process = Process::try_new(dummy_process.pid).expect("Could not init Process."); + let address: usize = { + let response = + reqwest::blocking::get(format!("{}/i16val/address", dummy_process.base_url)) + .expect("Could not request i16val address."); + response + .text() + .expect("Could not decode the charset of i16val address.") + .parse() + } + .expect("Could not parse address."); + + process + .write_i16(address, -20, None) + .expect("Could not write onto i16val."); + let value: i16 = { + let response = reqwest::blocking::get(format!("{}/i16val", dummy_process.base_url)) + .expect("Could not request i16val value."); + response + .text() + .expect("Could not decode the charset of i16val value.") + .parse() + } + .expect("Could not parse value."); + + assert_eq!(value, -20); + } + + #[rstest] + fn test_write_i32(dummy_process: DummyProcess) { + let process = Process::try_new(dummy_process.pid).expect("Could not init Process."); + let address: usize = { + let response = + reqwest::blocking::get(format!("{}/i32val/address", dummy_process.base_url)) + .expect("Could not request i32val address."); + response + .text() + .expect("Could not decode the charset of i32val address.") + .parse() + } + .expect("Could not parse address."); + + process + .write_i32(address, -30, None) + .expect("Could not write onto i32val."); + let value: i32 = { + let response = reqwest::blocking::get(format!("{}/i32val", dummy_process.base_url)) + .expect("Could not request i32val value."); + response + .text() + .expect("Could not decode the charset of i32val value.") + .parse() + } + .expect("Could not parse value."); + + assert_eq!(value, -30); + } + + #[cfg(target_pointer_width = "64")] + #[rstest] + fn test_write_i64(dummy_process: DummyProcess) { + let process = Process::try_new(dummy_process.pid).expect("Could not init Process."); + let address: usize = { + let response = + reqwest::blocking::get(format!("{}/i64val/address", dummy_process.base_url)) + .expect("Could not request i64val address."); + response + .text() + .expect("Could not decode the charset of i64val address.") + .parse() + } + .expect("Could not parse address."); + + process + .write_i64(address, -40, None) + .expect("Could not write onto i64val."); + let value: i64 = { + let response = reqwest::blocking::get(format!("{}/i64val", dummy_process.base_url)) + .expect("Could not request i64val value."); + response + .text() + .expect("Could not decode the charset of i64val value.") + .parse() + } + .expect("Could not parse value."); + + assert_eq!(value, -40); + } +} diff --git a/src/lib.rs b/src/lib.rs index 2a544ed..dbeab79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,9 @@ +// Copyright (c) 2026 Eray Erdin +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + #[cfg(test)] #[macro_use] extern crate rstest; @@ -20,5 +26,6 @@ pub mod prelude { pub use crate::byte_order::ByteOrder; pub use crate::ext::read::{ReadExt, ReadExtError, ReadExtResult}; pub use crate::ext::search::{SearchExt, SearchExtError, SearchExtResult}; + pub use crate::ext::write::{WriteExt, WriteExtError, WriteExtResult}; pub use crate::process::{Process, ProcessError}; } diff --git a/src/process.rs b/src/process.rs index 2979b4a..546c90c 100644 --- a/src/process.rs +++ b/src/process.rs @@ -34,8 +34,12 @@ impl Process { } #[cfg(target_os = "linux")] - pub(crate) fn mem_file(&self) -> io::Result { - fs::File::open(self.mem_path()) + pub(crate) fn mem_file(&self, is_write: bool) -> io::Result { + if is_write { + fs::OpenOptions::new().write(true).open(self.mem_path()) + } else { + fs::OpenOptions::new().read(true).open(self.mem_path()) + } } }