use crate::byte_order::ByteOrder;
use crate::ext::mapping::MappingExt;
use core::num;
use nix::{sys::ptrace, unistd::Pid};
use std::io;
use std::io::Read;
use std::io::Seek;
use crate::ext::mapping::MappingExtError;
pub trait SearchExt {
fn search_bytes(&self, pattern: &[u8]) -> SearchExtResult<Vec<usize>>;
fn search_u8(&self, value: u8, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
fn search_u16(&self, value: u16, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
fn search_u32(&self, value: u32, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
#[cfg(target_pointer_width = "64")]
fn search_u64(&self, value: u64, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
fn search_i8(&self, value: i8, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
fn search_i16(&self, value: i16, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
fn search_i32(&self, value: i32, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
#[cfg(target_pointer_width = "64")]
fn search_i64(&self, value: i64, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
fn search_f32(&self, value: f32, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
#[cfg(target_pointer_width = "64")]
fn search_f64(&self, value: f64, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>>;
}
#[cfg(target_os = "linux")]
impl SearchExt for crate::prelude::Process {
fn search_bytes(&self, pattern: &[u8]) -> SearchExtResult<Vec<usize>> {
if pattern.is_empty() {
return Ok(Vec::new());
}
ptrace::attach(Pid::from_raw(self.pid))?;
defer! {
let _ = ptrace::detach(Pid::from_raw(self.pid), None);
}
let mappings = self.mappings()?;
let mut file = self.mem_file(false)?;
const BUFFER_SIZE: usize = 8192;
let mut buffer = [0u8; BUFFER_SIZE];
let mut addresses = Vec::new();
let pattern_len = pattern.len();
for mapping in mappings {
if !mapping.permissions.read {
continue;
}
let region_size = mapping.end - mapping.start;
if region_size < pattern_len {
continue;
}
file.seek(io::SeekFrom::Start(mapping.start as u64))?;
let mut total_bytes_read = 0;
if pattern_len == 1 {
while total_bytes_read < region_size {
let remaining = region_size - total_bytes_read;
let to_read = std::cmp::min(BUFFER_SIZE, remaining);
match file.read(&mut buffer[..to_read]) {
Ok(0) => break,
Ok(bytes_read) => {
for (offset, &byte) in buffer[..bytes_read].iter().enumerate() {
if byte == pattern[0] {
addresses.push(mapping.start + total_bytes_read + offset);
}
}
total_bytes_read += bytes_read;
}
Err(_) => break,
}
}
continue;
}
let mut window = vec![0u8; pattern_len];
let mut window_filled = 0;
while total_bytes_read < region_size {
let remaining = region_size - total_bytes_read;
let to_read = std::cmp::min(BUFFER_SIZE, remaining);
match file.read(&mut buffer[..to_read]) {
Ok(0) => break,
Ok(bytes_read) => {
let data = if window_filled > 0 {
let mut combined = Vec::with_capacity(window_filled + bytes_read);
combined.extend_from_slice(&window[..window_filled]);
combined.extend_from_slice(&buffer[..bytes_read]);
combined
} else {
buffer[..bytes_read].to_vec()
};
for i in 0..=data.len().saturating_sub(pattern_len) {
if &data[i..i + pattern_len] == pattern {
let offset = if window_filled > 0 {
total_bytes_read - window_filled + i
} else {
total_bytes_read + i
};
addresses.push(mapping.start + offset);
}
}
if bytes_read >= pattern_len - 1 {
window_filled = pattern_len - 1;
window[..window_filled]
.copy_from_slice(&buffer[bytes_read - window_filled..bytes_read]);
} else if bytes_read > 0 {
window_filled = bytes_read;
window[..window_filled].copy_from_slice(&buffer[..bytes_read]);
} else {
window_filled = 0;
}
total_bytes_read += bytes_read;
}
Err(_) => break,
}
}
}
ptrace::detach(Pid::from_raw(self.pid), None)?;
Ok(addresses)
}
fn search_u8(&self, value: u8, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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_u32(&self, value: u32, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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(),
}),
)
}
#[cfg(target_pointer_width = "64")]
fn search_u64(&self, value: u64, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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_i8(&self, value: i8, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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_i16(&self, value: i16, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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_i32(&self, value: i32, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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(),
}),
)
}
#[cfg(target_pointer_width = "64")]
fn search_i64(&self, value: i64, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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_f32(&self, value: f32, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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(),
}),
)
}
#[cfg(target_pointer_width = "64")]
fn search_f64(&self, value: f64, byte_order: Option<ByteOrder>) -> SearchExtResult<Vec<usize>> {
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(),
}),
)
}
}
#[derive(Debug, Error)]
pub enum SearchExtError {
#[error("An IO error occured. {0:?} {0}")]
IO(#[from] io::Error),
#[error("Could not convert number. {0} {0:?}")]
TryFromIntError(#[from] num::TryFromIntError),
#[error("A *nix error occured. {0:?} {0}")]
Nix(#[from] nix::Error),
#[error("An error occured while checking process mapping. {0} {0:?}")]
MappingError(#[from] MappingExtError),
}
pub type SearchExtResult<T> = Result<T, SearchExtError>;
#[cfg(target_os = "linux")]
#[cfg(test)]
mod linux_tests {
use std::{
env,
io::{self, 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::<u16>()
.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::<i32>()
.expect("Could not parse PID of dummy process")
};
DummyProcess {
child,
pid,
base_url,
}
}
#[rstest]
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, None)
.expect("Could not search u8 value.");
let target_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 usize.");
assert_contains!(addresses, &target_address);
}
#[rstest]
fn test_search_u16(dummy_process: DummyProcess) {
let process =
Process::try_new(dummy_process.pid).expect("Could not get dummy process with id.");
let addresses = process
.search_u16(3, None)
.expect("Could not search u16 value.");
let target_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 usize.");
assert_contains!(addresses, &target_address);
}
#[rstest]
fn test_search_u32(dummy_process: DummyProcess) {
let process =
Process::try_new(dummy_process.pid).expect("Could not get dummy process with id.");
let addresses = process
.search_u32(4, None)
.expect("Could not search u32 value.");
let target_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 usize.");
assert_contains!(addresses, &target_address);
}
#[cfg(target_pointer_width = "64")]
#[rstest]
fn test_search_u64(dummy_process: DummyProcess) {
let process =
Process::try_new(dummy_process.pid).expect("Could not get dummy process with id.");
let addresses = process
.search_u64(5, None)
.expect("Could not search u64 value.");
let target_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 usize.");
assert_contains!(addresses, &target_address);
}
#[rstest]
fn test_search_i8(dummy_process: DummyProcess) {
let process =
Process::try_new(dummy_process.pid).expect("Could not get dummy process with id.");
let addresses = process
.search_i8(-1, None)
.expect("Could not search i8 value.");
let target_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 usize.");
assert_contains!(addresses, &target_address);
}
#[rstest]
fn test_search_i16(dummy_process: DummyProcess) {
let process =
Process::try_new(dummy_process.pid).expect("Could not get dummy process with id.");
let addresses = process
.search_i16(-2, None)
.expect("Could not search i16 value.");
let target_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 usize.");
assert_contains!(addresses, &target_address);
}
#[rstest]
fn test_search_i32(dummy_process: DummyProcess) {
let process =
Process::try_new(dummy_process.pid).expect("Could not get dummy process with id.");
let addresses = process
.search_i32(-3, None)
.expect("Could not search i32 value.");
let target_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 usize.");
assert_contains!(addresses, &target_address);
}
#[cfg(target_pointer_width = "64")]
#[rstest]
fn test_search_i64(dummy_process: DummyProcess) {
let process =
Process::try_new(dummy_process.pid).expect("Could not get dummy process with id.");
let addresses = process
.search_i64(-4, None)
.expect("Could not search i64 value.");
let target_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 usize.");
assert_contains!(addresses, &target_address);
}
#[rstest]
fn test_search_f32(dummy_process: DummyProcess) {
let process =
Process::try_new(dummy_process.pid).expect("Could not get dummy process with id.");
let addresses = process
.search_f32(1.5, None)
.expect("Could not search f32 value.");
let target_address: usize = {
let response =
reqwest::blocking::get(format!("{}/f32val/address", dummy_process.base_url))
.expect("Could not request f32val address.");
response
.text()
.expect("Could not decode the charset of f32val address.")
.parse()
}
.expect("Could not parse address usize.");
assert_contains!(addresses, &target_address);
}
#[cfg(target_pointer_width = "64")]
#[rstest]
fn test_search_f64(dummy_process: DummyProcess) {
let process =
Process::try_new(dummy_process.pid).expect("Could not get dummy process with id.");
let addresses = process
.search_f64(2.5, None)
.expect("Could not search f64 value.");
let target_address: usize = {
let response =
reqwest::blocking::get(format!("{}/f64val/address", dummy_process.base_url))
.expect("Could not request f64val address.");
response
.text()
.expect("Could not decode the charset of f64val address.")
.parse()
}
.expect("Could not parse address usize.");
assert_contains!(addresses, &target_address);
}
}