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))
}