For years, [MemProcFS](https://github.com/ufrisk/memprocfs) has had one of the nicest workflows in memory forensics: point it at a memory image and it mounts the thing as a filesystem you can `cd` into, `cat`, and `grep` like a live machine. No query language to learn, no plugin invocation to memorize. The analysis surface is a directory tree, and every tool you already use on files works on it. The catch, until now, was that it only understood Windows images.

We spent a hackathon changing that. The result is a working x86-64 Linux memory analyzer built into MemProcFS, designed around a single principle: everything it needs is recovered from the dump itself. No profiles, no symbol packs, no DWARF, no shipped offset database, no per-kernel preparation. You capture a dump, you mount it, you start reading. This post walks through how that works, why it was far less code than you’d expect, and what it unlocks in an actual investigation.

Before the kernel internals, it’s worth being concrete about why that workflow is worth porting at all, and the clearest answer comes from real casework.

 

The workflow we were missing on Linux

This wasn’t an abstract itch. Recently we worked a forensics challenge built around a tidy little intrusion: a malicious “Microsoft Updater” service, a hijacked COM object, and a .NET assembly the malware dropped to disk that turned off device encryption, installed itself as a service, and exfiltrated a browser’s saved-password database to an attacker endpoint.

The investigation started in Volatility, and for the opening moves it was the right tool. `banners` to confirm the image, `hivelist` to enumerate the loaded registry hives, `svcscan` to list services, `printkey` to read the malicious service’s `ImagePath`. These are targeted questions. You know exactly what you want and which plugin produces it, and Volatility answered them cleanly.

The job changed shape when the next step was to do three things more or less at once: pull the actual dropped .NET assembly back out of memory to decompile it, reconstruct a tight timeline around the moment it landed, and then find every registry reference pointing at that file. That’s no longer “run the plugin for X.” It’s exploration and correlation across files, a timeline, and the registry simultaneously – and that’s where the switch to MemProcFS paid off.

The point isn’t that MemProcFS beats Volatility – they’re different shapes of tool. Volatility shines when you know the exact artifact you want and have a plugin that emits it. MemProcFS shines the moment the questions turn open-ended, span multiple artifact types, or require pulling files out to hand to other tooling, because the analysis surface is just a filesystem and every tool you already own (`grep`, `find`, `Select-String`, `Copy-Item`, a decompiler, YARA) works on it directly, no plugin author required.

And that is exactly the capability that didn’t exist for Linux. Catch a Linux dump in an engagement and you were back to profiles and a plugin per question; the filesystem workflow we’d just leaned on simply wasn’t on the table. Closing that gap is what this project is about.

 

Why Linux memory forensics is more annoying than it should be

If you’ve used Volatility on Linux, you know the ritual. Before you can analyze a dump you usually need a profile or symbol table, meaning an ISF/JSON describing the exact kernel the image came from. Same version, same build, same struct layout. Get a dump from a box you no longer control, or from a kernel you’ve never seen, and you can burn an afternoon reconstructing symbols before you read a single process name. In incident response, that afternoon is often the difference between catching something live and catching it gone.

The friction exists for a real reason: the offsets of fields inside kernel structures drift between versions. Where `pid` sits inside `task_struct`, where `pgd` sits inside `mm_struct`, how virtual memory areas are linked, all of it changes across releases. Historically you had to feed that knowledge to the tool from the outside, which meant matching it to the dump, which meant having it in the first place.

On a modern kernel, you don’t have to. The knowledge is already inside the dump. We just have to go get it.

 

The idea we borrowed

Trail of Bits published a tool called [mquire](https://github.com/trailofbits/mquire) that makes this point cleanly. A modern Linux kernel embeds its own type information as BTF (BPF Type Format) and it carries its symbol table in kallsyms. Both live in kernel memory. If you can read the image, you can recover the struct layouts and symbol locations with nothing external.

We didn’t use mquire but we rewrote its approach in C so it could live inside MemProcFS. mquire is Rust; MemProcFS is C. Our goal was to make Linux dumps browsable through MemProcFS’s existing filesystem, plugin system, and language APIs, so that everything the tool can already do for Windows would start working for Linux. So we took the approach (recover everything from in-image BTF, demand nothing from outside) and reimplemented it from the ground up, including our own minimal BTF parser.

What’s actually Windows-specific is only the “personality” layer, the code that turns raw bytes into a process list and memory regions by knowing what an `EPROCESS` looks like. Adding Linux meant writing one new personality layer and a couple of small seams to bolt it in, then reusing everything beneath it. The new code came to roughly 1,700 lines of C plus one small per-process module. The Windows-only views (registry, services, handles, the forensic mode, FindEvil and the rest) are simply gated off when the image is recognized as Linux, so they don’t show up as empty noise.

![Bootstrap pipeline](IMAGE_PLACEHOLDER)

 

Bootstrapping from nothing

Given a flat memory image and zero outside knowledge, how do you get to a process list?

Step 1 – Is this even Linux? We scan physical memory for the `linux_banner` string (`”Linux version “`). Finding it confirms the OS and, as a bonus, hands us the kernel version parsed straight out of the image. That version is useful later as a key for any future fallback path, but the bootstrap itself never depends on it.

Step 2 – Recover the type layouts. We scan for the embedded BTF blob (it begins with the magic bytes `0xeb9f`) and parse it with a small in-memory BTF parser we wrote for the purpose. From it we pull the exact byte offsets, for this specific kernel, of every field we touch: `task_struct.pid`/`tgid`/`comm`/`mm`/`tasks`, `mm_struct.pgd` and `mm_mt`, the maple-tree internals, the dentry and file chains, the page-cache structures, the network device list.

Step 3 – Find the kernel’s page tables without symbols. This is the trickiest step and where we diverged from mquire most. mquire leans on kallsyms here, and kallsyms has format quirks that change across releases. We scan for the `swapper/0` task (the kernel’s idle/init task) by its `comm` name, then validate each candidate against an invariant that’s true only for the real one: the init task’s `parent` and `real_parent` pointers both point back at itself. That self-reference confirms we’ve found a genuine `task_struct` and, because we found it at a known physical location while it also tells us its own virtual address, it hands us a verified (virtual, physical) pair. The kernel page table (`swapper_pg_dir`) is then simply the candidate PML4 with an empty user half that correctly translates that virtual address to that physical address.

**Step 4 – Enumerate processes. With the kernel page-table base in hand, we walk the `task_struct` circular linked list. For each task we read `tgid` (the userspace PID), `comm` (the name), `real_parent` (the PPID), and `mm`. Kernel threads (those with no `mm`) use the kernel page-table base; user processes translate `mm_struct.pgd` to obtain their own. Each becomes an ordinary MemProcFS process object, and the Windows-specific substructure is just left empty and ignored.

From there the existing engine takes over, the OS-agnostic page-table walker already reconstructs them. On top of that we map each Linux VMA, walked from the `mm_mt` maple tree that replaced the old rbtree-and-linked-list in kernel 6.1, onto MemProcFS’s existing region abstraction, so the named, file-backed `.vmem` views light up without a single change to that module. A `vm_area_struct` and a Windows VAD turn out to be similar so that the mapping is almost mechanical.

 

Simple usage

Mount a dump and you can treat it like a read-only live box. Capture, then mount:

“`bash
python3 lime2raw.py mem.lime mem.raw # see note below
./memprocfs -mount /tmp/mpfs -device mem.raw

ls /tmp/mpfs/proc/ # process list
cat /tmp/mpfs/proc/1234/linux/cmdline.txt # “python3 /home/user/x.py –flag”
cat /tmp/mpfs/proc/1234/linux/files.txt # open fds -> resolved paths
cat /tmp/mpfs/proc/1234/memmap.txt # VMAs: ranges, protections, backing file
cat /tmp/mpfs/sys/network/interfaces.txt # eth0: 192.168.x.x/24 …
cat /tmp/mpfs/sys/files/etc/passwd # recovered from the page cache
“`

Everything below is reconstructed from the image alone:

| Capability | How it’s recovered |
|—|—|
| Process list (PID, PPID, name, page-table base) | `task_struct` list walk |
| Per-process memory regions + raw `.vmem` | maple-tree VMAs + reused page-table walker |
| Command lines | `mm->arg_start..arg_end` |
| Open file descriptors → paths | `task->files->fdt->fd[]` → `file` → dentry path |
| File recovery from page cache | `inode->i_mapping` XArray → folios/pages |
| Cached-file tree under `/sys/files` | dentry-cache walk from the filesystem root |
| Network interfaces | `net_device` list in `init_net` |
| Browser tabs / visited URLs | scan a process’s user memory for `http(s)://` |

One acquisition note worth noting: the prebuilt LeechCore caps a LiME image at 256 ranges, and an AVML capture routinely exceeds that. The fix is a tiny transcoder (`lime2raw.py`) that flattens a LiME image into a single-range sparse RAW file.

 

What this unlocks in an investigation

Because the mount is a real filesystem, it is much easier to navigate and find artifacts. Want every process that had a network socket or a suspicious binary path? `grep` across `cmdline.txt` files. Want to know what a process was actually doing? Its `memmap.txt` shows you the mapped regions with protections and backing files, and `memory.vmem` is the raw address space you can carve, `strings`, or feed to YARA, the same way you’d triage a Windows process.

A config file an attacker deleted from disk, a script that ran and was cleaned up, a log rotated away, if it was touched recently enough to still be resident in the page cache, `cat /tmp/mpfs/sys/files/…` pulls it back out of memory. That extends to structured artifacts, such as the system journal. `journalctl -D` at the recovered journal directory under `/sys/files` and browse system logs. The browser-URL scrape works the same way against a live process’s heap, surfacing open tabs and visited URLs that were never written anywhere persistent.

This is a hackathon-grade proof of concept, validated so far against dumps from a single Kali Linux VM (kernel `6.18.12+kali-amd64`, x86-64, KASLR active). It targets modern kernels, BTF must be present (`CONFIG_DEBUG_INFO_BTF`, the default on Ubuntu, Debian, Fedora, Arch and Kali), along with the contemporary struct layouts the parser reads, such as maple-tree VMAs and the folio-based page cache. A stripped or custom kernel without BTF is unsupported today and fails with a clear message rather than guessing.

 

Credits

None of this exists without [Ulf Frisk](https://github.com/ufrisk) and MemProcFS, whose clean OS-agnostic architecture is the only reason a Linux layer could slot in this cheaply. And the whole approach is modeled on mquire by [Trail of Bits](https://github.com/trailofbits/mquire). Our work is a C reimplementation of that approach inside MemProcFS.

George Karagiannidis (gkaragiannidis at twelvesec.com)

Evangelos Katsouras (ekatsouras at twelvesec.com)

Share This

Share this post with your friends!