charal.sh

Memory Scanning with Rust

Table of Contents

In this article, we’ll find out where a program stores its heap in memory and how to access it from another process. We’ll take the example of a simple password manager storing a password as a string on the heap and attempt to access it from another process.

You can find the code for this article on GitHub.

How does a program handle memory ?

The program starts by requesting some memory to the operating system using a call like malloc. The OS allocates some memory and provides the program with a pointer to it.

The address of the pointer given by the OS is a virtual memory address. The computer’s memory management unit (MMU) handles the translations between a program’s virtual addresses and the physical memory addresses.

The programs are prohibited from handling memory directly; instead they use their own virtual address space. The program is allowed to modify its memory. The OS enforces access permissions over the memory.

How to access another process’ memory

In order to access another process’ memory, we can’t simply create a pointer to the data we’re interesting in and start reading. The OS wouldn’t allow that.

What we can do, however, is use the libc. With admin privileges, we can copy some bytes from another process’ memory using the method process_vm_readv.

Knowing this, let’s try to put it in practice. We’ll build a simple password manager that keeps a string on the heap and use another program to go read the password from memory.

In practice

Password manager

Let’s start with our password manager. It will prompt the user for a password, store it on the heap in a Rust string and then wait for the user to press enter to close the program. Nothing is stored on the disk, we only keep the password in memory while the password manager is running.

password-manager/src/main.rs
[Rust]
use std::io::Write;

fn main() {
	// The string we'll try to access
    let mut pass: String = String::new();

	// Prompt the user for the password
    print!("Enter a password: ");
    let _ = std::io::stdout().flush();
    std::io::stdin()
        .read_line(&mut pass)
        .expect("Failed to read password");

	// Print the virtual memory address of the String
    println!("String.as_ptr {:p}", pass.as_ptr());

	// Wait for the user to quit the program
    print!("Press enter to quit.");
    let _ = std::io::stdout().flush();
    let _ = std::io::stdin().read_line(&mut String::new());
}

We’ll run it, enter a password and leave it waiting while we try to access its memory.

Memory scanner

First, note that we’ll need to start our memory scanner with admin privileges in order to access the other process’ memory.

We’ll use the system call process_vm_readv from the libc in order to copy bytes from the password manager to our memory scanner.

See the documentation with man 2 process_vm_readv

Here’s the definition of the function. It takes the process id of our target, structs called iovec (buffers), the number of structs used (liovcnt and riovcnt) and some flags.

ssize_t process_vm_readv(pid_t pid,                  // The target PID
                      const struct iovec *local_iov, // Local iovec
                      unsigned long liovcnt,
                      const struct iovec *remote_iov,// Remote iovec
                      unsigned long riovcnt,
                      unsigned long flags);

Great, now how do we call this function from a Rust program ?

In order to use process_vm_readv, we’ll use the crate called libc. You can add it to the Cargo.toml using cargo add libc.

We import the crate in our main.rs with

use libc::{c_void, iovec, pid_t, process_vm_readv};

We start by preparing the parameters.

let data: [u8; 16] = [0; 16]; // We'll store the bytes we copy here

// Local buffer
let local = iovec {
    iov_base: data.as_ptr() as *mut c_void,
    iov_len: 16, // Number of bytes in our buffer
};

// The remote buffer we want to read from
let remote = iovec {
    iov_base: 0x12345 as *mut c_void,  // Pointer to the the virtual memory of
    iov_len: 16,							// the other processs
};

// We retrieve the pointers to our two iovec structs
let p_local = &local as *const iovec;
let p_remote = &remote as *const iovec;

We can set the remote.iov_base to the address of our string in the password manager as displayed in its output.

The call to the function is simple.

let res = unsafe { process_vm_readv(pid, p_local, 1, p_remote, 1, 0) };

You can find the PID of the password manager with the command pidof password-manager.

In the man page, we find that the return value is -1 when an error has occurred.

if res == -1 {
	println!("ERROR: Could not read data");
} else {
    for b in &data {
        print!("{:02x} ", b);      // Print each byte in our data array
    }
    println!();
}

We now have read bytes from the other process’ memory.

Currently, our program is hard to work with. We can only read 16 bytes and we are required to input the PID and the address to the string by hand. Let’s fix all these points.

Finding the PID

The command pidof can be used to retrieve the PID of our target process from our program.

use std::process::Command;

/// Return the pid of the target process using `pidof`
fn get_pid(process_name: &str) -> pid_t {
	// Call the `pidof` command
    let v = Command::new("sh")
        .arg("-c")
        .arg(format!("pidof {}", process_name))
        .output()
        .expect("failed to get pid")
        .stdout;

 	// If the process doesn't exist, `pidof` returns nothing
    assert!(v.len() > 0, "PID not found");

    // Convert the output of `pidof` to a String
    let s = match String::from_utf8(v[0..(v.len() - 1)].to_vec()) {
        Ok(s) => s,
        Err(_) => panic!("Can't convert from utf8"),
    };

	// Convert to i32
    let i: i32 = match s.parse() {
        Ok(i) => i,
        Err(_) => panic!("Could not parse string"),
    };

	// Returns the pid as type pid_t
    pid_t::from(i)
}

Finding the virtual memory addresses

The memory map used by a process can is stored in the file /proc/$pid/maps. It shows the virtual memory addresses used by the program. We’ll use it to get the range of addresses used to store the heap.

See the documentation with man 5 proc

The line of interest to us is this one:

5633733f2000-563373413000 rw-p 00000000 00:00 0                          [heap]

We need the first part: 5633733f2000-563373413000. It represents the range of addresses used by the heap, in hexadecimal.

We’ll use it to loop over the range and read all of the content of the heap.

We can automate the process of retrieving the heap’s address range:

use std::fs;

fn get_heap_addresses(pid: pid_t) -> (usize, usize) {
	// Read the maps file
    let data = fs::read_to_string(format!("/proc/{}/maps", pid))
        .expect(&format!("Can't read /proc/{}/maps", pid));

	// String manipulation to get the range as a 2-valued tuple
    for line in data.split('\n') {
        if line.contains("[heap]") {
            let heap = line.split(' ').next().unwrap();
            let mut heap_v = heap.split('-');

			// Conversion &str -> i64 -> usize
            let start_addr = i64::from_str_radix(heap_v.next().unwrap(), 16).unwrap() as usize;
            let end_addr = i64::from_str_radix(heap_v.next().unwrap(), 16).unwrap() as usize;

            return (start_addr, end_addr);
        }
    }

    panic!("No heap found in /proc/{}/maps", pid)
}

Putting it all together

The entire code is accessible in this GitHub repository. I added some improvements: a for-loop for iterating over the range of addresses, cli parameters using clap and improved formatting for the output.

Let’s begin with the password manager. Here’s what it looks like after a user has input a password. Password Manager It is now waiting to be closed with the password stored in the heap. We’ll now to try to access the password from the memory scanner.

memscan needs administrative privileges:

sudo memscan/target/release/memscan password-manager -za

The z flag ensures that lines consisting of nothing but zeroes are not displayed and a attempts to display the bytes as ASCII.

memscan.png The password is visible at the address 0x557dd9581bb0. It appears we have our password stored in two locations with the second one at address 0x557dd9581ba0.

Why is that ?

It’s actually the buffer used by stdin. We can experiment with it by modifying the password-manager.

password-manager/src/main.rs
[Rust]
use std::io::Write;

fn main() {
    // The string we'll try to access
    let mut pass: String = String::new();

    // Prompt the user for the password
    print!("Enter a password: ");
    let _ = std::io::stdout().flush();
    std::io::stdin()
        .read_line(&mut pass)
        .expect("Failed to read password");

    // Print the virtual memory address of the String
    println!("String.as_ptr {:p}", pass.as_ptr());

    // Add a useless input to fill the stdin buffer with something new
    print!("Write anything: ");
    let _ = std::io::stdout().flush();
    let _ = std::io::stdin().read_line(&mut String::new());

    // Wait for the user to quit the program
    print!("Press enter to quit.");
    let _ = std::io::stdout().flush();
    let _ = std::io::stdin().read_line(&mut String::new());
}

We added a new prompt to overwrite the stdin buffer.

Here’s what our password-manager looks like now. Password Manager with Buffer overwrite

And what memscan shows: memscan with buffer overwritten We can see we have (partially) overwritten the stdin buffer.

Conclusion

Programs use virtual memory addressing. The memory management unit (MMU) translates between virtual addresses and physical memory addresses. With admin privileges, we can use the function process_vm_readv in order to copy bytes from another running process.

Going further

We could use the system call process_vm_writev to modify memory within another process. This could be used to alter variables in a running program, such as a video game.

Resources