Memory forensics for exploit investigation

When a crash is ambiguous or post-exploitation activity is suspected, memory analysis of the affected process provides the strongest evidence. This runbook covers both live process inspection and offline analysis from a memory image.

Live process inspection (Linux)

If the process is still running and accessible, inspect its memory maps before it exits or is killed:

# memory map: look for anonymous executable regions
cat /proc/PID/maps

# suspicious patterns:
# rwx regions (writable AND executable: common shellcode staging area)
# r-xp regions not backed by a file (anonymous executable code)
# heap or stack with execute permission

# example: filter for executable anonymous mappings
grep " r-xp " /proc/PID/maps | grep -v "\.so\|\.bin\|vdso\|vsyscall"

Extract a suspicious region:

# note the start and end address from /proc/PID/maps
# e.g.: 7f1234560000-7f1234570000 rwxp 00000000 00:00 0

# dump the region
dd if=/proc/PID/mem bs=1 skip=$((16#7f1234560000)) \
   count=$((16#7f1234570000 - 16#7f1234560000)) \
   of=/tmp/suspicious_region.bin 2>/dev/null

# or with gdb
gdb -p PID
(gdb) dump memory /tmp/region.bin 0x7f1234560000 0x7f1234570000

Capture a full memory image

Linux

# LiME (Linux Memory Extractor): kernel module for full RAM capture
git clone https://github.com/504ensicsLabs/LiME
cd LiME/src && make
sudo insmod lime.ko "path=/tmp/memory.lime format=lime"

# for a single process without kernel module:
# use /proc/PID/mem with a reader script
python3 - <<'EOF'
import re

pid = TARGET_PID
maps_file = f'/proc/{pid}/maps'
mem_file  = f'/proc/{pid}/mem'

with open(maps_file) as maps, open(mem_file, 'rb', 0) as mem:
    with open(f'/tmp/proc_{pid}.bin', 'wb') as out:
        for line in maps:
            m = re.match(r'([0-9a-f]+)-([0-9a-f]+)', line)
            if not m:
                continue
            start, end = int(m.group(1), 16), int(m.group(2), 16)
            try:
                mem.seek(start)
                out.write(mem.read(end - start))
            except OSError:
                pass
EOF

Windows

# WinPmem for full RAM
winpmem_mini_x64_rc2.exe memory.raw

# or use Task Manager / ProcDump for a single process minidump
procdump.exe -ma PID output.dmp

Analyse with Volatility

Volatility analyses memory images offline. It works with LiME captures and Windows raw/crash dumps.

pip install volatility3

# identify the profile / OS version
vol -f memory.lime banners.Banners
vol -f memory.raw windows.info

# list processes
vol -f memory.lime linux.pslist
vol -f memory.raw windows.pslist

# find injected code: malfind identifies executable anonymous regions
vol -f memory.raw windows.malfind
vol -f memory.lime linux.malfind

# check loaded modules (look for unsigned or unusual DLLs)
vol -f memory.raw windows.dlllist --pid PID

# dump a suspicious memory region
vol -f memory.raw windows.memmap --pid PID --dump

Malfind output to investigate:

  • VAD entries marked executable but not backed by a file on disk

  • Memory regions containing PE headers (DLL injected without a corresponding file)

  • Regions containing shellcode signatures (MZ header, NOP sleds, common syscall stubs)

Shellcode identification

Once a suspicious region is extracted:

import re

with open('/tmp/suspicious_region.bin', 'rb') as f:
    data = f.read()

# NOP sled
nop_run = max(len(m.group(0)) for m in re.finditer(b'\x90+', data) or [b''])
print(f'Longest NOP run: {nop_run} bytes')

# common shellcode patterns
patterns = {
    'execve (x86)':       b'\x31\xc0\x50\x68\x2f\x2f\x73\x68',
    'execve (x64)':       b'\x48\x31\xd2\x48\xbb',
    'bind shell marker':  b'\x66\x68',   # push word (port number)
    'meterpreter stub':   b'\xfc\x48\x83',
    'Windows shellcode':  b'\x60\x89\xe5',  # pushad; mov ebp, esp
}

for name, pat in patterns.items():
    pos = data.find(pat)
    if pos != -1:
        print(f'Pattern "{name}" at offset {hex(pos)}')
        print(f'  Context: {data[max(0,pos-8):pos+32].hex()}')

Use scdbg or speakeasy for shellcode emulation:

# scdbg: x86 shellcode emulation
scdbg /f /tmp/suspicious_region.bin

# speakeasy: Windows shellcode and PE emulation
pip install speakeasy-emulator
speakeasy -t /tmp/suspicious_region.bin -r -a x64

ROP chain detection

A ROP chain in memory is a sequence of addresses pointing into executable regions (gadget addresses), each differing by a small fixed offset from a known image base. Detecting this manually:

# given a dump of the stack region:
import struct

with open('/tmp/stack_dump.bin', 'rb') as f:
    stack = f.read()

# known executable regions (from maps or Volatility)
exec_regions = [
    (0x7f1200000000, 0x7f12001b0000, 'libc'),
    (0x400000, 0x401000, 'target'),
]

# scan for pointers into executable regions
potential_gadgets = []
for i in range(0, len(stack) - 8, 8):
    addr = struct.unpack('<Q', stack[i:i+8])[0]
    for start, end, name in exec_regions:
        if start <= addr < end:
            potential_gadgets.append((i, addr, name))

print(f'Potential gadget pointers on stack: {len(potential_gadgets)}')
for offset, addr, region in potential_gadgets[:20]:
    print(f'  stack+{offset:#x}: {addr:#x} ({region})')

A long sequence of pointers into libc with no intervening data is a strong indicator of a ROP chain.

Preserve evidence

Before taking any remediation action:

# hash the binary and memory image
sha256sum ./service_binary > evidence_hashes.txt
sha256sum /tmp/memory.lime >> evidence_hashes.txt

# record process state
ps auxf > process_tree.txt
ss -tulnp > network_state.txt
cat /proc/PID/maps > pid_maps.txt
cat /proc/PID/status > pid_status.txt

# timestamp everything
date -u > collection_time.txt

Chain of custody: document who collected what and when before any changes are made to the system.