Skip to content

rbspy/read-process-memory

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

99 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitHub Actions Build status Cirrus CI Build status crates.io

A crate to read memory from another process. Code originally taken from the rbspy project. This crate has now returned home to the rbspy GitHub organization. :)

Example

This example re-executes itself as a child process in order to have a separate process to use for demonstration purposes. If you need to read memory from a process that you are spawning, your usage should look very similar to this:

use std::convert::TryInto;
use std::env;
use std::io::{self, BufReader, BufRead, Read, Result};
use std::process::{Command, Stdio};

use read_process_memory::{
  Pid,
  ProcessHandle,
  CopyAddress,
  copy_address,
};

fn main() -> Result<()> {
    if env::args_os().len() > 1 {
      // We are the child.
      return in_child();
    }
    // Run this executable again so we have a child process to read.
    let mut child = Command::new(env::current_exe()?)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .arg("child")
        .spawn()?;

    // Get a ProcessHandle to work with.
    let handle: ProcessHandle = (&child).try_into().unwrap();

    // The child process will print the address to read from on stdout.
    let mut stdout = BufReader::new(child.stdout.take().unwrap());
    let mut addr_string = String::new();
    stdout.read_line(&mut addr_string)?;
    let address = usize::from_str_radix(addr_string.trim(), 16).unwrap();

    // Try to read 10 bytes from that address
    let bytes = copy_address(address, 10, &handle)?;
    println!("Read: {:?}", bytes);

    // Tell the child to exit by closing its stdin.
    drop(child.stdin.take());
    // And wait for it to exit.
    child.wait()?;
    Ok(())
}

fn in_child() -> Result<()> {
    // Allocate a 10-byte Vec for the parent to read.
    let readable_bytes: Vec<u8> = vec![
        0xc0, 0x72, 0x80, 0x79, 0xeb, 0xf1, 0xbc, 0x87, 0x06, 0x14,
    ];
    // Print the address of the Vec to stdout so the parent can find it.
    println!("{:x}", readable_bytes.as_ptr() as usize);
    // Now wait to exit until the parent closes our stdin, to give
    // it time to read the memory.
    let mut buf = Vec::new();
    // We don't care if this succeeds.
    drop(io::stdin().read_to_end(&mut buf));
    Ok(())
}

How it works

Here's a summary, with some C pseudocode, of how the read-process-memory crate works under the hood on each of the platforms it supports. The three inputs are:

  • PID: the process ID to read from
  • LENGTH: how much memory to read
  • ADDRESS: the address to read from

Linux:

Uses process_vm_readv

void* TARGET = (void*) 0x123412341324;
struct iovec local;
local.iov_base = calloc(LENGTH, sizeof(char));
local.iov_len = LENGTH;
struct iovec remote;
remote[0].iov_base = TARGET;
remote[0].iov_len = LENGTH;
process_vm_readv(PID, local, 2, remote, 1, 0);

Mac OS:

Uses vm_read_overwrite

mach_port_name_t task;
task_for_pid(mach_task_self(), PID, &task)
vm_size_t read_len = LENGTH;
char result[LENGTH];
vm_read_overwrite(task, TARGET, LENGTH, &result, &read_len)

FreeBSD:

Uses ptrace. This one stops the process to read from it.

// attach
int wait_status = 0;
attach_status = ptrace(PT_ATTACH, PID, null, 0);
waitpid(PID, &wait_status, 0);
WIFSTOPPED(wait_status)
char result[LENGTH];
desc = PtraceIoDesc {
  piod_op: PIOD_READ_D,
  piod_offs: TARGET;
  piod_addr: &result;
  piod_len: LENGTH,
};
// read data
ptrace(PT_IO, PID, &desc, 0);
// detach
ptrace(PT_DETACH, PID, null, 0);

Windows:

Uses ReadProcessMemory:

char result[LENGTH];
ReadProcessMemory(PID, ADDRESS, &result, LENGTH, null);