[ TOOLING ]

Building a Zero-Dependency Linux Process Memory Scanner

Heavyweight forensic frameworks are indispensable during deep investigations, but they are often clunky during initial triage. When you need to inspect memory across dozens of remote Linux nodes quickly, dragging Python runtimes, kernel header dependencies, or multi-gigabyte debugging symbols across the wire introduces friction you don’t need.

For standard userland injection hunting—finding unbacked executable memory, injected shared objects, or wiped binary headers—you can build a self-contained, zero-dependency scanner in standard C or Go using nothing more than /proc and direct kernel syscalls.

Walking the Process Virtual Memory Map

The Linux kernel exposes a process’s memory layout directly through /proc/[pid]/maps. Each line in this pseudo-file represents a contiguous virtual memory region with its start address, end address, permission flags, offset, device, inode, and pathname.

A custom triage scanner starts by parsing this file sequentially. We are specifically looking for two primary red flags:

  1. Unbacked Executable Regions: Memory segments with rwx or r-x permissions where the pathname column is empty. While JIT engines (like Node or JVMs) legitimately allocate anonymous executable pages, standard native binaries should almost always have executable sections mapped directly to a file on disk.
  2. Mismatched Path Inodes: Inodes marked as 0 or flagged with (deleted), which often indicates a binary was unlinked immediately after execution to evade trivial disk scans.

Parsing maps requires minimal overhead. A stream-oriented line buffer reads the addresses directly into 64-bit unsigned integers without loading large files into memory.

Reading Target Memory Safely

Once a suspicious region is flagged, you need to extract its bytes for signature matching or carving. While you could open /proc/[pid]/mem and seek to the target offset, this requires multiple system calls and is prone to synchronization errors if the target process modifies its layout mid-read.

A faster and cleaner alternative is the process_vm_readv syscall. It transfers data directly between the address space of the target process and your scanner process without passing through kernel buffers:

struct iovec local[1];
struct iovec remote[1];

local[0].iov_base = dump_buffer;
local[0].iov_len = region_size;
remote[0].iov_base = (void *)start_address;
remote[0].iov_len = region_size;

ssize_t nread = process_vm_readv(target_pid, local, 1, remote, 1, 0);
if (nread < 0) {
    // Handle unreadable pages or transient allocations
}

This call requires PTRACE_MODE_READ_REALCREDS access, meaning you need root or CAP_SYS_PTRACE, but it avoids attaching a full ptrace debugger state that could freeze the target or trigger basic anti-debugging checks.

Rapid Pattern Matching and Heuristics

With memory chunks read into a local buffer, you can execute fast byte searches. Instead of pulling in an entire regex engine, implement a streaming Boyer-Moore-Horspool algorithm or simple static signature scans for common patterns:

  • ELF Headers in Anonymous Pages: The magic bytes \x7fELF residing inside an unmapped memory region strongly indicate an in-memory reflective load.
  • Shellcode Stubs: Common assembly sequences, such as typical syscall wrappers or stacked register pushes followed by syscall (\x0f\x05).
  • Embedded Configurations: Hardcoded network indicators, encoded strings, or embedded metadata blocks.

When a signature matches, dump the raw memory segment to disk along with metadata: PID, memory protection flags, mapped permissions, and the timestamp.

Operating at Scale

Compiling this logic into a single statically linked binary yields an executable smaller than 2 MB with zero shared library dependencies. You can scp it to a compromised node, stream results over an SSH pipe to stdout, and clean it up in seconds.

Building your own purpose-built tools sharpens your understanding of kernel-userland interfaces. Instead of treating memory collection as a black box, direct kernel interfaces provide predictable, scriptable control over your incident response pipeline.

Photo by Chris Ried on Unsplash.