1. Introduction
Reading process memory is useful for troubleshooting performance issues, analyzing security vulnerabilities, and debugging software. However, the traditional approach of attaching a debugger to a running process and halting its execution can disrupt the system’s normal operation.
In this article, we’ll explore how to read living process memory without interrupting it.
2. /proc Filesystem
The /proc filesystem is a virtual filesystem in Linux that exposes information about the system’s state and running processes in a hierarchical directory structure. Unlike traditional filesystems, the /proc filesystem doesn’t contain actual files on the disk but rather dynamically generated files and directories that show the system’s current state.
Each process on the system has a corresponding directory under /proc, with its process ID (PID) as the directory name. Inside the process directory, various files and directories provide information about the process, such as the process’s command line arguments, environment variables, open files, and memory usage.
2.1. Memory Maps
The memory map of a process shows how the process’s virtual address space is organized. The process uses the virtual address space to access memory, and it isn’t necessarily the same as the physical address space used by the system’s memory hardware.
In the memory maps, we can find a list of memory regions allocated to the process, along with information about the attributes of each region, such as its starting address, ending address, and access permissions.
2.2. Memory Map Structure
The memory map of a process can be found in the /proc/PID/maps file, where PID is the process ID of the process. Each line in the maps file represents a single memory region, and the format of each line is as follows:
start_address-end_address permissions offset device inode filename
Here’s what each field in the line means:
- start_address and end_address: The starting and ending addresses of the memory region. These addresses are in hexadecimal format and represent the virtual addresses of the first and last byte of the region.
- permissions: The access permissions of the memory region. The permissions are characterized by a string of characters that indicate whether the region is readable (r), writable (w), executable (x), or shared (s).
- offset: The offset of the memory region in the file or device that it’s associated with. This field is only relevant for memory-mapped files.
- device and inode: The device number and inode number of the file or device associated with the memory region. This field is only relevant for memory-mapped files.
- filename: The name of the file or device associated with the memory region. This field is only present for memory-mapped files, and it’s usually blank for other types of memory regions.
2.3. Direct Memory Access
The /proc/PID/mem file in Linux provides direct access to the process’s memory with the given PID. We can use it to read or write arbitrary data in the process’s virtual address space, bypassing the normal system calls for memory access. To read the memory of a running process, we’ll read data from the memory map to navigate the mem file.
3. Reading Memory of a Process
Let’s use knowledge about the structure of memory maps to write a script that will print memory content for each record in a memory map. We’ll get one parameter for our script – the process ID.
3.1. Checking Parameters
Let’s start with checking if that argument was provided:
if [ -z "$1" ]; then
echo "Usage: $0 <pid>"
exit 1
fi
We should also check if the process for the given PID exists:
if [ ! -d "/proc/$1" ]; then
echo "PID $1 does not exist"
exit 1
fi
3.2. Reading Memory Range
Then, we’ll read lines from the memory map and extract memory ranges and permissions from them using awk:
while read -r line; do
mem_range="$(echo "$line" | awk '{print $1}')"
perms="$(echo "$line" | awk '{print $2}')"
# reading will happen here
done < "/proc/$1/maps"
After that, if the memory range is readable, we can print its content using the dd command. To do that, we’ll extract the start and end addresses of the range and transform them from hex to decimal base:
if [[ "$perms" == *"r"* ]]; then
start_addr="$(echo "$mem_range" | cut -d'-' -f1)"
end_addr="$(echo "$mem_range" | cut -d'-' -f2)"
echo "Reading memory range $mem_range..."
dd if="/proc/$1/mem" of="/dev/stdout" bs=1 skip="$(( 16#$start_addr ))" count="$((16#$end_addr - 16#$start_addr))" 2>/dev/null
fi
3.3. Full Script
Finally, the whole script looks like that:
#!/bin/bash
if [ -z "$1" ]; then
echo "Usage: $0 <pid>"
exit 1
fi
if [ ! -d "/proc/$1" ]; then
echo "PID $1 does not exist"
exit 1
fi
while read -r line; do
mem_range="$(echo "$line" | awk '{print $1}')"
perms="$(echo "$line" | awk '{print $2}')"
if [[ "$perms" == *"r"* ]]; then
start_addr="$(echo "$mem_range" | cut -d'-' -f1)"
end_addr="$(echo "$mem_range" | cut -d'-' -f2)"
echo "Reading memory range $mem_range..."
dd if="/proc/$1/mem" of="/dev/stdout" bs=1 skip="$start_addr" count="$((16#$end_addr - 16#$start_addr))" 2>/dev/null
fi
done < "/proc/$1/maps"
4. Summary
Reading process memory without interrupting it’s a useful technique for troubleshooting, analyzing, and debugging Linux systems. In this article, we learned how to use the /proc filesystem to get an insight into the memory of a process.
Finally, we wrote a script that can fetch the contents of the memory with the help of memory maps.