use std::{
error::Error,
fmt, fs,
io::{self, BufRead, BufReader, Read, Seek, Write},
mem,
num::ParseIntError,
ptr,
};
use libc::{PTRACE_ATTACH, PTRACE_DETACH, c_void, pid_t, ptrace};
use memchr::memchr;
use regex::Regex;
#[macro_export]
macro_rules! hex {
($hex:expr) => {
u64::from_str_radix($hex, 16).expect(concat!("Invalid hex: ", $hex))
};
}
#[derive(Debug)]
pub enum MemoryError {
ProcessNotFound(String),
BaseAddressNotFound(String),
ModuleNotFound(String),
InsufficientPermissions,
IoError(io::Error),
ParseError(String),
PatternParseError(String),
InvalidAddress,
PtraceError(String),
}
impl fmt::Display for MemoryError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MemoryError::ProcessNotFound(name) => write!(f, "Process '{name}' not found"),
MemoryError::BaseAddressNotFound(name) => {
write!(f, "Base address for '{name}' not found")
}
MemoryError::ModuleNotFound(name) => write!(f, "Module '{name}' not found"),
MemoryError::InsufficientPermissions => {
write!(f, "Insufficient permissions to access memory")
}
MemoryError::IoError(e) => write!(f, "IO error: {e}"),
MemoryError::ParseError(e) => write!(f, "Failed to parse data: {e}"),
MemoryError::PatternParseError(e) => write!(f, "Couldn't parse pattern: {e}"),
MemoryError::InvalidAddress => write!(f, "Invalid memory address"),
MemoryError::PtraceError(e) => write!(f, "Ptrace error: {e}"),
}
}
}
impl Error for MemoryError {}
impl From<io::Error> for MemoryError {
fn from(e: io::Error) -> Self {
MemoryError::IoError(e)
}
}
impl From<ParseIntError> for MemoryError {
fn from(e: ParseIntError) -> Self {
MemoryError::ParseError(e.to_string())
}
}
pub struct GameMemUtils {
pid: pid_t,
base_address: u64,
attached: bool,
mem_file: fs::File,
debug_enabled: bool, }
impl GameMemUtils {
pub fn new(process_name: &str, debug_enabled: bool) -> Result<Self, MemoryError> {
let pid = Self::find_process_id(process_name)?;
let base_address = Self::find_base_address(pid, process_name)?;
let mut utils = GameMemUtils {
pid: pid_t::try_from(pid).map_err(|e| MemoryError::ParseError(e.to_string()))?,
base_address,
attached: false,
mem_file: fs::OpenOptions::new()
.read(true)
.write(true)
.open(format!("/proc/{pid}/mem"))?,
debug_enabled, };
utils.attach()?;
Ok(utils)
}
pub fn from_pid(pid: u64, debug_enabled: bool) -> Result<Self, MemoryError> {
let mut utils = GameMemUtils {
pid: pid_t::try_from(pid).map_err(|e| MemoryError::ParseError(e.to_string()))?,
base_address: 0, attached: false,
mem_file: fs::OpenOptions::new()
.read(true)
.write(true)
.open(format!("/proc/{pid}/mem"))?,
debug_enabled, };
utils.attach()?;
Ok(utils)
}
fn attach(&mut self) -> Result<(), MemoryError> {
errno::set_errno(errno::Errno(0)); let res = unsafe {
ptrace(
PTRACE_ATTACH,
self.pid,
ptr::null_mut::<c_void>(),
ptr::null_mut::<c_void>(),
)
};
if res != 0 {
let current_errno = errno::errno();
if self.debug_enabled {
eprintln!(
"DEBUG: ptrace(PTRACE_ATTACH) failed for PID {}. errno: {} ({})",
self.pid, current_errno.0, current_errno
);
}
return Err(MemoryError::PtraceError(format!(
"Failed to attach to PID {}: {}",
self.pid, current_errno
)));
}
let mut status = 0;
errno::set_errno(errno::Errno(0)); let wait_res = unsafe { libc::waitpid(self.pid, &mut status, 0) };
if wait_res == -1 {
let current_errno = errno::errno();
if self.debug_enabled {
eprintln!(
"DEBUG: waitpid failed for PID {} after attach. errno: {} ({})",
self.pid, current_errno.0, current_errno
);
}
return Err(MemoryError::PtraceError(format!(
"Failed to wait for PID {} after attach: {}",
self.pid, current_errno
)));
}
self.attached = true;
Ok(())
}
pub fn detach(&mut self) -> Result<(), MemoryError> {
if !self.attached {
return Ok(());
}
errno::set_errno(errno::Errno(0)); let res = unsafe {
ptrace(
PTRACE_DETACH,
self.pid,
ptr::null_mut::<c_void>(),
ptr::null_mut::<c_void>(),
)
};
if res != 0 {
let current_errno = errno::errno();
if self.debug_enabled {
eprintln!(
"DEBUG: ptrace(PTRACE_DETACH) failed for PID {}. errno: {} ({})",
self.pid, current_errno.0, current_errno
);
}
return Err(MemoryError::PtraceError(format!(
"Failed to detach from PID {}: {}",
self.pid, current_errno
)));
}
self.attached = false;
Ok(())
}
pub fn pid(&self) -> u64 {
self.pid as u64
}
pub fn base_address(&self) -> u64 {
self.base_address
}
pub fn set_base_address(&mut self, address: u64) {
self.base_address = address;
}
pub fn pattern_scan_all_process_memory(
&mut self,
pattern: &str,
) -> Result<Option<u64>, MemoryError> {
let all_modules = self.find_loaded_modules()?;
let target_regions: Vec<(u64, u64, String)> = all_modules
.iter()
.filter(|(_, _, _, perms)| perms.contains('r') && perms.contains('x'))
.map(|(name, base, end, _perms)| (*base, *end, name.clone())) .collect();
if target_regions.is_empty() {
if self.debug_enabled {
eprintln!("DEBUG: No readable+executable regions found to scan in the process.");
}
return Ok(None);
}
let (pattern_bytes, mask) = Self::parse_pattern(pattern)?;
for (base, end, name) in target_regions {
let size = (end - base) as usize;
if size == 0 {
if self.debug_enabled {
eprintln!("DEBUG: Skipping empty region {base:#x}-{end:#x} (name: {name})",);
}
continue;
}
if self.debug_enabled {
eprintln!(
"DEBUG: Scanning all-mem region {base:#x}-{end:#x} (size: {size} bytes, name: {name})",
);
}
let buffer_result = self.read_bytes(base, size);
match buffer_result {
Ok(buffer) => {
if let Some(offset) = Self::find_pattern(&buffer, &pattern_bytes, &mask) {
let found_address = base + offset as u64;
if self.debug_enabled {
eprintln!(
"DEBUG: Pattern found at: {found_address:#x} in region {base:#x}-{end:#x} (name: {name})",
);
}
return Ok(Some(found_address)); }
}
Err(e) => {
if self.debug_enabled {
eprintln!(
"DEBUG: Failed to read all-mem region {base:#x}-{end:#x} (name: {name}): {e}",
);
}
}
}
}
Ok(None)
}
pub fn find_loaded_modules(&self) -> Result<Vec<(String, u64, u64, String)>, MemoryError> {
let maps_path = format!("/proc/{}/maps", self.pid);
let file = fs::File::open(maps_path)?;
let reader = BufReader::new(file);
let mut modules = Vec::new();
for line in reader.lines() {
let line = line?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 5 {
continue;
}
let range = parts[0];
let permissions = parts[1];
let pathname_start_idx = if parts.len() >= 6 && parts[5].starts_with('/') {
5
} else {
parts.len()
};
let pathname = parts[pathname_start_idx..].join(" ");
let mut addr_parts = range.split('-');
let (Some(start_str), Some(end_str)) = (addr_parts.next(), addr_parts.next()) else {
continue;
};
let start = u64::from_str_radix(start_str, 16)?;
let end = u64::from_str_radix(end_str, 16)?;
modules.push((pathname, start, end, permissions.to_string())); }
Ok(modules)
}
pub fn filter_modules_by_regex(
&self,
modules: &[(String, u64, u64, String)], pattern: &str,
) -> Result<Vec<(String, u64, u64, String)>, regex::Error> {
let re = Regex::new(pattern)?;
Ok(modules
.iter()
.filter(|(path, _, _, _)| re.is_match(path))
.cloned()
.collect())
}
pub fn find_module_base(&self, module_name: &str) -> Result<u64, MemoryError> {
Self::find_base_address(self.pid as u64, module_name)
}
pub fn pattern_scan_module(
&mut self,
module_name: &str,
pattern: &str,
) -> Result<Option<u64>, MemoryError> {
let modules = self.find_loaded_modules()?;
let target_module_regions: Vec<(u64, u64)> = modules
.iter()
.filter(|(name, _, _, _)| name.contains(module_name))
.map(|(_, base, end, _)| (*base, *end))
.collect();
if target_module_regions.is_empty() {
return Err(MemoryError::ModuleNotFound(module_name.to_string()));
}
let (pattern_bytes, mask) = Self::parse_pattern(pattern)?;
for (base, end) in target_module_regions {
let size = (end - base) as usize;
if size == 0 {
if self.debug_enabled {
eprintln!("DEBUG: Skipping empty region {base:#x}-{end:#x} for {module_name}",);
}
continue;
}
if self.debug_enabled {
eprintln!(
"DEBUG: Scanning region {base:#x}-{end:#x} (size: {size} bytes) for {module_name}",
);
}
let buffer_result = self.read_bytes(base, size);
match buffer_result {
Ok(buffer) => {
if let Some(offset) = Self::find_pattern(&buffer, &pattern_bytes, &mask) {
return Ok(Some(base + offset as u64));
}
}
Err(e) => {
if self.debug_enabled {
eprintln!(
"DEBUG: Failed to read module region {base:#x}-{end:#x} for {module_name}: {e}",
);
}
}
}
}
Ok(None)
}
fn parse_pattern(pattern: &str) -> Result<(Vec<u8>, Vec<bool>), MemoryError> {
let mut bytes = Vec::new();
let mut mask = Vec::new();
for token in pattern.split_whitespace() {
match token {
"?" | "??" => {
bytes.push(0);
mask.push(false);
}
_ => {
let byte = u8::from_str_radix(token, 16).map_err(|_| {
MemoryError::PatternParseError(format!("Invalid byte token '{token}'"))
})?;
bytes.push(byte);
mask.push(true);
}
}
}
if bytes.is_empty() {
return Err(MemoryError::PatternParseError(
"Pattern cannot be empty".to_string(),
));
}
Ok((bytes, mask))
}
fn find_pattern(buffer: &[u8], pattern: &[u8], mask: &[bool]) -> Option<usize> {
if pattern.is_empty() {
return None; }
let first_masked_byte_idx = mask.iter().position(|&m| m)?; let first_significant_byte = pattern[first_masked_byte_idx];
let mut current_search_start_offset = 0;
while let Some(mut pos) = memchr(
first_significant_byte,
&buffer[current_search_start_offset..],
) {
pos += current_search_start_offset;
let potential_pattern_start = pos.checked_sub(first_masked_byte_idx)?;
if potential_pattern_start + pattern.len() > buffer.len() {
break; }
let mut full_match = true;
for i in 0..pattern.len() {
if mask[i] && buffer[potential_pattern_start + i] != pattern[i] {
full_match = false;
break;
}
}
if full_match {
return Some(potential_pattern_start); }
current_search_start_offset = pos + 1;
}
None }
pub fn read_at<T>(&self, address: u64) -> Result<T, MemoryError>
where
T: Copy,
{
let size = mem::size_of::<T>();
if size == 0 {
return Err(MemoryError::InvalidAddress);
}
let mut buffer = vec![0u8; size];
self.read_bytes_into(address, &mut buffer)?;
Ok(unsafe { ptr::read_unaligned(buffer.as_ptr() as *const T) })
}
pub fn read<T>(&self, offset: u64) -> Result<T, MemoryError>
where
T: Copy,
{
self.read_at(self.base_address + offset)
}
pub fn read_hex<T>(&self, hex_offset: &str) -> Result<T, MemoryError>
where
T: Copy,
{
let offset = u64::from_str_radix(hex_offset, 16)?;
self.read(offset)
}
pub fn write_at<T>(&self, address: u64, value: T) -> Result<(), MemoryError>
where
T: Copy,
{
let size = mem::size_of::<T>();
let buffer = unsafe { std::slice::from_raw_parts((&value) as *const T as *const u8, size) };
self.write_bytes(address, buffer)
}
pub fn write<T>(&self, offset: u64, value: T) -> Result<(), MemoryError>
where
T: Copy,
{
self.write_at(self.base_address + offset, value)
}
pub fn write_hex<T>(&self, hex_offset: &str, value: T) -> Result<(), MemoryError>
where
T: Copy,
{
let offset = u64::from_str_radix(hex_offset, 16)?;
self.write(offset, value)
}
pub fn read_u32_le(&self, address: u64) -> Result<u32, MemoryError> {
let mut bytes = [0u8; 4];
self.read_bytes_into(address, &mut bytes)?;
Ok(u32::from_le_bytes(bytes))
}
pub fn read_u64_le(&self, address: u64) -> Result<u64, MemoryError> {
let mut bytes = [0u8; 8];
self.read_bytes_into(address, &mut bytes)?;
Ok(u64::from_le_bytes(bytes))
}
pub fn write_u32_le(&self, address: u64, value: u32) -> Result<(), MemoryError> {
self.write_bytes(address, &value.to_le_bytes())
}
pub fn write_u64_le(&self, address: u64, value: u64) -> Result<(), MemoryError> {
self.write_bytes(address, &value.to_le_bytes())
}
pub fn read_bytes(&self, address: u64, size: usize) -> Result<Vec<u8>, MemoryError> {
let mut buffer = vec![0u8; size];
self.read_bytes_into(address, &mut buffer)?;
Ok(buffer)
}
fn read_bytes_into(&self, address: u64, buffer: &mut [u8]) -> Result<(), MemoryError> {
let mut mem_file = &self.mem_file;
mem_file.seek(io::SeekFrom::Start(address))?;
mem_file.read_exact(buffer)?;
Ok(())
}
pub fn write_bytes(&self, address: u64, data: &[u8]) -> Result<(), MemoryError> {
let mut mem_file = &self.mem_file;
mem_file.seek(io::SeekFrom::Start(address))?;
mem_file.write_all(data)?;
Ok(())
}
pub fn read_string(&self, address: u64, max_length: usize) -> Result<String, MemoryError> {
let bytes = self.read_bytes(address, max_length)?;
let null_pos = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8(bytes[..null_pos].to_vec())
.map_err(|e| MemoryError::ParseError(e.to_string()))
}
fn find_process_id(name: &str) -> Result<u64, MemoryError> {
fs::read_dir("/proc")?
.filter_map(Result::ok)
.filter_map(|entry| entry.file_name().into_string().ok())
.filter_map(|dir_name| dir_name.parse::<u64>().ok())
.find(|&pid| {
if let Ok(comm) = fs::read_to_string(format!("/proc/{pid}/comm")) {
return comm.trim() == name;
}
false
})
.ok_or_else(|| MemoryError::ProcessNotFound(name.to_string()))
}
fn find_base_address(pid: u64, name: &str) -> Result<u64, MemoryError> {
let maps_path = format!("/proc/{pid}/maps");
let maps_file = fs::File::open(maps_path)?;
for line_result in BufReader::new(maps_file).lines() {
let line = line_result?;
if !line.contains(name) {
continue;
}
if let Some(range) = line.split_whitespace().next() {
if let Some(addr_str) = range.split('-').next() {
if let Ok(addr) = u64::from_str_radix(addr_str, 16) {
return Ok(addr);
}
}
}
}
Err(MemoryError::BaseAddressNotFound(name.to_string()))
}
}
impl Drop for GameMemUtils {
fn drop(&mut self) {
if self.attached {
let _ = self.detach(); }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_macro() {
assert_eq!(hex!("FF"), 255);
assert_eq!(hex!("100"), 256);
assert_eq!(hex!("8320b84"), 137497476);
}
#[test]
fn test_memory_error_display() {
let err = MemoryError::ProcessNotFound("test".to_string());
assert_eq!(format!("{err}"), "Process 'test' not found");
}
#[test]
fn test_parse_pattern() {
let (bytes, mask) = GameMemUtils::parse_pattern("AB ?? CD 12 ? EF").unwrap();
assert_eq!(bytes, vec![0xAB, 0, 0xCD, 0x12, 0, 0xEF]);
assert_eq!(mask, vec![true, false, true, true, false, true]);
}
#[test]
fn test_find_pattern() {
let buffer = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77];
let (pattern, mask) = GameMemUtils::parse_pattern("33 ?? 55").unwrap();
assert_eq!(
GameMemUtils::find_pattern(&buffer, &pattern, &mask),
Some(2)
);
let (pattern, mask) = GameMemUtils::parse_pattern("11 22 33").unwrap();
assert_eq!(
GameMemUtils::find_pattern(&buffer, &pattern, &mask),
Some(0)
);
let (pattern, mask) = GameMemUtils::parse_pattern("99").unwrap();
assert_eq!(GameMemUtils::find_pattern(&buffer, &pattern, &mask), None);
let (pattern, mask) = GameMemUtils::parse_pattern("?? 22 33").unwrap();
assert_eq!(
GameMemUtils::find_pattern(&buffer, &pattern, &mask),
Some(0)
);
let (pattern, mask) = GameMemUtils::parse_pattern("55 66 ??").unwrap();
assert_eq!(
GameMemUtils::find_pattern(&buffer, &pattern, &mask),
Some(4)
);
let (pattern, mask) = GameMemUtils::parse_pattern("?? ?? ??").unwrap();
assert_eq!(GameMemUtils::find_pattern(&buffer, &pattern, &mask), None);
}
}