Debt Collector Series: §3 UMM — Universal Memory Mapper

§3 UMM — Universal Memory Mapper

Layer §2 of the Debt Collector Stack

Author: Shrikant Bhosale (@debtcollector21)


3.1 What It Is

UMM (Universal Memory Mapper) is a dual-mode memory analysis tool:
1. Runtime mode: Reads /proc/<pid>/maps to analyze a running process’s memory layout
2. Binary mode: Parses ELF, Mach-O, and PE binaries to analyze their static memory layout

It detects 8 memory debt patterns — structural memory layout characteristics that correlate with exploitable vulnerabilities.

The core insight: memory geometry is not random. Every exploitable vulnerability leaves a geometric trace in the memory layout — an RWX page here, an adjacent allocation there, a kernel-page leak elsewhere. UMM finds these traces.


3.2 Architecture

┌──────────────────────────────────────────────────────────────┐
│                    UMM Entry Point                           │
├──────────────────────────────────────────────────────────────┤
│  Runtime Mode:                       Binary Mode:            │
│  /proc/<pid>/maps                    ELF/Mach-O/PE parser    │
│  /proc/<pid>/smaps (detailed)        Section headers         │
│  /proc/<pid>/status                  Segment layout          │
│  /proc/<pid>/maps (stack/heap)       Symbol tables           │
└──────────┬───────────────────────────┬──────────────────────-┘
           ↓                           ↓
┌──────────────────────────────────────────────────────────────┐
│                   8 Debt Pattern Detectors                    │
│  RWX_Region │ AdjacentAlloc │ Fragmentation │ KernelPage     │
│  RO_Data    │ HeapSparsity  │ StackGap      │ VDMismatch     │
└──────────────────────────┬──────────────────────────────────-┘
                          ↓
┌──────────────────────────────────────────────────────────────┐
│                    Output Generators                          │
│  Color HTML Map │ JSON Report │ ASAN-compatible Trace        │
│  Watch Mode     │ [Vendor]-specific │ Terminal Summary             │
└──────────────────────────────────────────────────────────────┘

3.3 The 8 Debt Patterns

3.3.1 RWX Region (κ_severity: 0.95)

What it detects: Memory regions with Read-Write-Execute permissions.

Why it matters: RWX memory is the holy grail for shellcode injection. An attacker who can write to an RWX region can execute arbitrary code without needing ROP or other bypass techniques.

Detection logic:

def detect_rwx(segments):
    for seg in segments:
        if seg.perms == "rwx" and seg.size > PAGE_SIZE:
            yield DebtFinding(
                type="rwx_region",
                severity="critical",
                address=seg.start,
                size=seg.size,
                description=f"RWX region {seg.name} ({seg.size} bytes)"
            )

Empirical context: Most modern binaries have zero RWX regions. Any finding is significant.

3.3.2 Adjacent Allocation (κ_severity: 0.90)

What it detects: Two allocations of different types placed adjacent in memory (e.g., a data buffer next to a function pointer table).

Why it matters: Adjacent allocations enable type-confusion attacks. A buffer overflow in a data allocation can corrupt the adjacent metadata allocation.

Detection logic:

def detect_adjacent_allocation(segments):
    for a, b in pairwise(segments):
        if a.end == b.start:
            if a.type != b.type and a.size < 4096:
                yield DebtFinding(
                    type="adjacent_allocation",
                    severity="high",
                    address=a.start,
                    size=b.end - a.start,
                    description=f"Adjacent {a.type}/{b.type} at offset {hex(b.start - a.start)}"
                )

3.3.3 Fragmentation (κ_severity: 0.70)

What it detects: Excessive number of small memory regions (>100 regions).

Why it matters: Fragmentation increases the probability of adjacent allocations and makes ASLR less effective. A fragmented process has more opportunities for allocation confusion.

Detection logic:

def detect_fragmentation(segments):
    small_regions = [s for s in segments if s.size < 65536]
    if len(small_regions) > 100:
        return DebtFinding(
            type="fragmentation",
            severity="medium",
            count=len(small_regions),
            description=f"{len(small_regions)} small regions — high fragmentation"
        )

3.3.4 Kernel Page Leak (κ_severity: 0.90)

What it detects: Memory regions mapped from kernel space (vsyscall, vvar, vsyscall_trampoline).

Why it matters: Kernel pages in user space are a historical source of information leaks. While modern kernels restrict them, their presence indicates an older or hardened kernel — or an exploitation path through kernel-assisted mechanisms.

Detection logic:

def detect_kernel_pages(segments):
    kernel_regions = [s for s in segments if s.path in KERNEL_PATHS]
    for kr in kernel_regions:
        yield DebtFinding(
            type="kernel_page_leak",
            severity="high",
            address=kr.start,
            size=kr.size,
            path=kr.path,
            description=f"Kernel page mapped: {kr.path}"
        )

3.3.5 Read-Only Data in Writeable Region (κ_severity: 0.65)

What it detects: RO data (constants, vtables) placed in writeable memory.

Why it matters: If RO data is in writeable memory, a write primitive can corrupt const data, function pointers, or vtables — bypassing CFI and other exploit mitigations.

Detection logic:

def detect_ro_in_rw(segments):
    for seg in segments:
        if "ro" in seg.perms and "w" in seg.perms:
            yield DebtFinding(
                type="ro_data_in_rw",
                severity="medium",
                address=seg.start,
                size=seg.size,
                description=f"RO flag in RW region {seg.name}"
            )

3.3.6 Heap Sparsity (κ_severity: 0.55)

What it detects: Heap regions with unusual gaps or sparse layout.

Why it matters: Sparse heaps indicate either a memory leak (allocations not freed) or a fragmentation issue. Both correlate with use-after-free vulnerabilities.

Detection logic:

def detect_heap_sparsity(heap_segments):
    gaps = []
    for a, b in pairwise(heap_segments):
        gap = b.start - a.end
        if gap > 65536:
            gaps.append(gap)
    if gaps:
        return DebtFinding(
            type="heap_sparsity",
            severity="low",
            gap_count=len(gaps),
            max_gap=max(gaps),
            description=f"Heap sparsity: {len(gaps)} gaps >64KB"
        )

3.3.7 Stack Gap (κ_severity: 0.50)

What it detects: Unusual gap between stack base and current stack pointer.

Why it matters: Large stack gaps can hide buffer overflows that would otherwise crash into unmapped memory.

Detection logic:

def detect_stack_gap(stack_seg, current_sp):
    gap = current_sp - stack_seg.start
    if gap > 1048576:  # 1MB
        return DebtFinding(
            type="stack_gap",
            severity="low",
            gap_size=gap,
            description=f"Stack gap {gap} bytes — may hide overflows"
        )

3.3.8 VDSO Version Mismatch (κ_severity: 0.45)

What it detects: VDSO version differs from kernel version.

Why it matters: VDSO mismatches indicate either a container/VM mismatch or a system that has been patched without reboot — both signals of misconfiguration that can be exploited.

Detection logic:

def detect_vdso_mismatch(vdso_version, kernel_version):
    if vdso_version != kernel_version:
        return DebtFinding(
            type="vdso_mismatch",
            severity="info",
            vdso=vdso_version,
            kernel=kernel_version,
            description=f"VDSO {vdso_version} != kernel {kernel_version}"
        )

3.4 Output Modes

3.4.1 Color HTML Map

A 2D visualization of the process’s memory space, colored by:
Red: RWX regions (critical)
Orange: Adjacent allocations (high)
Yellow: Fragmentation (medium)
Blue: Kernel pages
Green: Normal heap
Gray: Libraries

The HTML is self-contained (no external dependencies), rendered as a scrollable, zoomable SVG embedded in the page.

3.4.2 JSON Report

Machine-readable output for pipeline consumption:

{
    "pid": 12345,
    "timestamp": "2026-07-17T10:30:00Z",
    "os": "linux",
    "kernel": "6.8.0",
    "total_regions": 184,
    "total_size_mb": 2847,
    "findings": [
        {
            "type": "rwx_region",
            "severity": "critical",
            "address": "0x7f1234000000",
            "size": 4096,
            "path": "[vdso]",
            "description": "RWX region [vdso] (4096 bytes)"
        }
    ],
    "debt_score": 0.72
}

3.4.3 ASAN-compatible Trace

Outputs findings in a format parsable by the fuzzer integration pipeline:

=================================================================
==12345==ERROR: AddressSanitizer: DEBT-PATTERN on 0x7f1234000000
    Memory type: rwx
    Region: [vdso]
    Size: 4096
    Severity: critical
    Score: 0.95
=================================================================

3.4.4 [Vendor]-specific Detection

When the target process is [Vendor]/d8, additional debt patterns are checked:
– Wasm code region permissions (RWX for JIT?)
– Heap page layout (sparse pages, large gaps)
– Code page adjacency (wasm code next to JS builtins)


3.5 Watch Mode

UMM can monitor a process over time, generating debt scores at intervals:

umm watch --pid 12345 --interval 5 --threshold 0.7

Output (every 5 seconds):

[10:30:00] PID 12345: 184 regions, 7 findings, debt 0.72
[10:30:05] PID 12345: 186 regions, 7 findings, debt 0.71
[10:30:10] PID 12345: 190 regions, 8 findings, debt 0.74 ← NEW FINDING

Useful for detecting:
– Memory leaks (growing region count)
– Heap spray preparation (uniform allocation pattern)
– JIT page changes (RWX creation/destruction)


3.6 Debt Score

The overall debt score is computed as:

def compute_debt_score(findings, total_regions):
    """
    Weighted sum of finding severities, normalized by region count.
    """
    weights = {
        "critical": 1.0,
        "high": 0.7,
        "medium": 0.5,
        "low": 0.3,
        "info": 0.1
    }
    raw = sum(weights[f.severity] for f in findings)
    normalized = raw / (1 + math.log(total_regions))
    return min(1.0, normalized)

A score of 0.0 means no debt patterns detected. 1.0 is maximum (theoretical — would require all 8 patterns at critical severity). Typical production binaries score 0.0-0.2. A score above 0.5 indicates significant memory layout concerns.


3.7 Validation

Binary Regions Findings Debt Score Notes
d8 (debug) [Vendor] 184 7 0.72 VDSO RWX, fragmentation, heap sparsity
node v22 156 3 0.31 Kernel pages, no RWX
[Vendor] 42 210 5 0.45 Fragmentation, adjacent allocations
python3.12 98 1 0.12 Clean

3.8 CLI Usage

# Runtime analysis (by PID)
umm 12345

# Runtime analysis (by command name)
umm $(pgrep -f d8)

# Binary analysis
umm ./binary.elf
umm ./binary.macho
umm ./binary.exe

# Detailed output
umm --json 12345
umm --html 12345 -o memory_map.html
umm --asan 12345

# Watch mode
umm watch --pid 12345 --interval 5

# [Vendor]-specific analysis
umm --[Vendor] 12345

3.9 Limitations

  1. Runtime mode requires process access. Cannot analyze processes you don’t have permission to read /proc/<pid>/maps for.

  2. Binary mode is static. It analyzes the on-disk layout, which may differ from the runtime layout after ASLR, lazy binding, and JIT compilation.

  3. Patterns are heuristic. An RWX region is not automatically exploitable — it’s only a signal. Context (from VMF scan) is required to determine actual exploitability.

  4. Linux-only for runtime. /proc/<pid>/maps is Linux-specific. macOS and Windows runtime analysis would require platform-specific implementations.

Leave a Comment