Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Memory dumping

Use case

Capture the mapped memory of a process to recover values from RAM.

Explanation

The list of mapped regions is available through /proc/<pid>/maps. The content of these regions can be read via /proc/<pid>/mem. It is a good idea to freeze the process first, then dump the memory. For now, we only dump rw-p regions.

There are other solutions available for dumping memory, e.g. hexdump, fridump or gdb and lldb. The main problem is that attaching to a process with frida, gdb and lldb can be detected.

Usage

$ cargo build --release --target aarch64-linux-android
$ adb push target/aarch64-linux-android/release/dumpmem /data/local/tmp/

Phone:

# ps -ef | grep test                                                                                      
u0_a220       5542   465 0 22:02:57 ?     00:00:02 com.example.test
u0_a220       5984   465 1 22:53:24 ?     00:00:00 com.example.test:worker2
u0_a220       5985   465 1 22:53:24 ?     00:00:00 com.example.test:worker1
root          6093  5955 0 22:55:00 pts/1 00:00:00 grep test
# ./dumpmem -p com.example.test -p com.example.test:worker1                                               
Found PIDs: [5542, 5985]
Stopping PIDs: [5542, 5985]
Dumping rw-p regions...
Done: /data/local/tmp/memdump_5542
Dumping rw-p regions...
Done: /data/local/tmp/memdump_5985
Resuming PIDs: [5542, 5985]
# ls memdump_5542/bin | head -n 5                                                                         
2000000-e000000.bin
26000000-32000000.bin
4a000000-4a002000.bin
57e9108b0000-57e9108b1000.bin
6ffb4000-702a0000.bin
# head -n 5 memdump_5542/dumpmem.log                                                                      
02000000-0e000000 rw-p 00000000 00:00 0                                  [anon:dalvik-main space]
26000000-32000000 rw-p 00000000 00:00 0                                  [anon:dalvik-free list large object space]
4a000000-4a002000 rw-p 00000000 00:00 0 
4a002000-4c002000 r--s 00000000 00:01 1040                               /memfd:jit-zygote-cache (deleted)
4c002000-4e002000 r-xs 02000000 00:01 1040                               [anon_shmem:dalvik-zygote-jit-code-cache]

Code

use std::{
    fs::{self, File},
    io::{self, Read, Seek, SeekFrom, Write},
    path::{Path, PathBuf},
    process::exit,
};

use std::sync::Mutex;

use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;

// https://docs.rs/clap/latest/clap/#example
use clap::Parser;

/// Dump the rw-p memory regions of an Android process.
#[derive(Parser)]
#[command(version, about, arg_required_else_help = true)]
struct Args {
    /// Process names to dump, e.g. -p com.example.test -p com.example.test:worker1.
    /// PIDs are also accepted: -p 1234.
    #[arg(short, long)]
    process: Vec<String>,

    /// Output directory. The output is written to <output>/memdump_<pid>/.
    #[arg(short, long, default_value = "/data/local/tmp")]
    output: PathBuf,
}

static STOPPED_PIDS: Mutex<Vec<i32>> = Mutex::new(Vec::new());

// Resume during cleanup whatever happens.
struct Resumer(Vec<Pid>);
impl Drop for Resumer {
    fn drop(&mut self) {
        let raw: Vec<i32> = self.0.iter().map(|p| p.as_raw()).collect();
        println!("Resuming PIDs: {raw:?}");
        for &p in &self.0 {
            _ = kill(p, Signal::SIGCONT);
        }
        STOPPED_PIDS.lock().unwrap().clear();
    }
}

fn main() {
    let args = Args::parse();
    if let Err(e) = run(&args.process, &args.output) {
        eprintln!("error: {e}");
        exit(1);
    }
}

fn run(names: &[String], out_base: &Path) -> io::Result<()> {
    let mut pids: Vec<i32> = Vec::new();
    for name in names {
        let pid_raw = match name.parse::<i32>() {
            Ok(pid) => pid,
            Err(_) => pidof(name)?.ok_or_else(|| {
                io::Error::new(io::ErrorKind::NotFound, format!("{name} not running?"))
            })?,
        };
        pids.push(pid_raw);
    }

    println!("Found PIDs: {pids:?}");

    // https://crates.io/crates/ctrlc
    ctrlc::set_handler(move || {
        let stopped = STOPPED_PIDS.lock().unwrap();
        println!("Resuming PIDs: {stopped:?}");
        for &p in stopped.iter() {
            _ = kill(Pid::from_raw(p), Signal::SIGCONT);
        }

        // https://man7.org/linux/man-pages/man7/signal.7.html
        // https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
        // 128 + SIGINT = 130
        exit(130);
    })
    .expect("failed to install Ctrl+C handler");

    *STOPPED_PIDS.lock().unwrap() = pids.clone();

    let _resumer = Resumer(pids.iter().map(|&p| Pid::from_raw(p)).collect());
    println!("Stopping PIDs: {pids:?}");
    for &p in &pids {
        kill(Pid::from_raw(p), Signal::SIGSTOP)?;
    }

    for &pid_raw in &pids {
        // output0 = <output>/memdump_<pid>/bin/*.bin
        // output1 = <output>/memdump_<pid>/dumpmem.log
        let out = out_base.join(format!("memdump_{pid_raw}"));
        let bin = out.join("bin");
        let log_path = out.join("dumpmem.log");
        fs::create_dir_all(&bin)?;
        let mut log = File::create(&log_path)?;

        let maps = fs::read_to_string(format!("/proc/{pid_raw}/maps"))?;
        // Good practice: use write_all for bytes we already have.
        log.write_all(maps.as_bytes())?;

        println!("Dumping rw-p regions...");
        let mut mem = File::open(format!("/proc/{pid_raw}/mem"))?;

        // 55fc5b37a000-55fc5b425000 rw-p 00000000 00:00 0    [heap]
        // 7fcd00021000-7fcd04000000 ---p 00000000 00:00 0
        // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map
        for region in maps.lines().filter_map(parse_region) {
            let (start, end) = region;
            writeln!(log, "{start:x}-{end:x}")?;

            let len = (end - start) as usize;
            let mut buf = vec![0; len];

            // Straight to the region and read it.
            mem.seek(SeekFrom::Start(start))?;
            match mem.read_exact(&mut buf) {
                Ok(()) => {
                    let path = bin.join(format!("{start:x}-{end:x}.bin"));
                    fs::write(&path, &buf)?;
                    writeln!(log, "wrote {len} bytes -> {}", path.display())?;
                }
                Err(e) => {
                    writeln!(log, "skip {start:x}-{end:x}: {e}")?;
                }
            }
        }

        writeln!(log, "Done: {}", out.display())?;
        println!("Done: {}", out.display());
    }

    Ok(())
}

fn pidof(name: &str) -> io::Result<Option<i32>> {
    // https://doc.rust-lang.org/std/fs/fn.read_dir.html#examples
    for entry in fs::read_dir("/proc")? {
        let entry = entry?;
        let fname = entry.file_name();
        let Some(pid) = fname.to_str().and_then(|s| s.parse::<i32>().ok()) else {
            continue;
        };

        // argv[0] = package name
        if let Ok(cmdline) = fs::read(format!("/proc/{pid}/cmdline")) {
            let argv0 = cmdline.split(|&b| b == 0).next().unwrap_or(&[]);
            if argv0 == name.as_bytes() {
                return Ok(Some(pid));
            }
        }
    }
    Ok(None)
}

// 55fc5b37a000-55fc5b425000 rw-p 00000000 00:00 0    [heap]
fn parse_region(line: &str) -> Option<(u64, u64)> {
    let mut parts = line.split_whitespace();
    let range = parts.next()?;
    let perms = parts.next()?;
    if perms != "rw-p" {
        return None;
    };
    let (s, e) = range.split_once('-')?;
    let start = u64::from_str_radix(s, 16).ok()?;
    let end = u64::from_str_radix(e, 16).ok()?;
    Some((start, end))
}

View on GitHub.