diff --git a/pwndbg/color/memory.py b/pwndbg/color/memory.py index 7bc532adbbc..1efff595d6b 100644 --- a/pwndbg/color/memory.py +++ b/pwndbg/color/memory.py @@ -21,6 +21,7 @@ ColorParamSpec("data", "purple", "color for all other writable memory"), ColorParamSpec("rodata", "normal", "color for all read only memory"), ColorParamSpec("rwx", "underline", "color added to all RWX memory"), + ColorParamSpec("guard", "cyan", "color added to all guard pages (no perms)"), ], ) @@ -76,6 +77,8 @@ def get(address: int | gdb.Value, text: str | None = None, prefix: str | None = color = c.code elif page.rw: color = c.data + elif page.is_guard: + color = c.guard else: color = c.rodata diff --git a/pwndbg/commands/vmmap.py b/pwndbg/commands/vmmap.py index ccecd12977b..43b9a20b8e7 100644 --- a/pwndbg/commands/vmmap.py +++ b/pwndbg/commands/vmmap.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse +from typing import Tuple import gdb from elftools.elf.constants import SH_FLAGS @@ -14,8 +15,12 @@ import pwndbg.commands import pwndbg.gdblib.elf import pwndbg.gdblib.vmmap +from pwndbg.color import cyan +from pwndbg.color import green +from pwndbg.color import red from pwndbg.commands import CommandCategory from pwndbg.gdblib import gdb_version +from pwndbg.lib.memory import Page integer_types = (int, gdb.Value) @@ -44,6 +49,102 @@ def print_vmmap_table_header() -> None: ) +def print_vmmap_gaps_table_header() -> None: + """ + Prints the table header for the vmmap --gaps command. + """ + header = ( + f"{'Start':>{2 + 2 * pwndbg.gdblib.arch.ptrsize}} " + f"{'End':>{2 + 2 * pwndbg.gdblib.arch.ptrsize}} " + f"{'Perm':>4} " + f"{'Size':>8} " + f"{'Note':>9} " + f"{'Accumulated Size':>{2 + 2 * pwndbg.gdblib.arch.ptrsize}}" + ) + print(header) + + +def calculate_total_memory(pages: Tuple[Page, ...]) -> None: + total = 0 + for page in pages: + total += page.memsz + if total > 1024 * 1024: + print(f"Total memory mapped: {total:#x} ({total//1024//1024} MB)") + else: + print(f"Total memory mapped: {total:#x} ({total//1024} KB)") + + +def gap_text(page: Page) -> str: + # Strip out offset and objfile from stringified page + display_text = " ".join(str(page).split(" ")[:-2]) + return display_text.rstrip() + + +def print_map(page: Page) -> None: + print(green(gap_text(page))) + + +def print_adjacent_map(map_start: Page, map_end: Page) -> None: + print( + green( + f"{gap_text(map_end)} {'ADJACENT':>9} {hex(map_end.end - map_start.start):>{2 + 2 * pwndbg.gdblib.arch.ptrsize}}" + ) + ) + + +def print_guard(page: Page) -> None: + print(cyan(f"{gap_text(page)} {'GUARD':>9} ")) + + +def print_gap(current: Page, last_map: Page): + print( + red( + " - " * int(51 / 3) + + f" {'GAP':>9} {hex(current.start - last_map.end):>{2 + 2 * pwndbg.gdblib.arch.ptrsize}}" + ) + ) + + +def print_vmmap_gaps(pages: Tuple[Page, ...]) -> None: + """ + Indicates the size of adjacent memory regions and unmapped gaps between them in process memory + """ + print(f"LEGEND: {green('MAPPED')} | {cyan('GUARD')} | {red('GAP')}") + print_vmmap_gaps_table_header() + + last_map = None # The last mapped region we looked at + last_start = None # The last starting region of a series of mapped regions + + for page in pages: + if last_map: + # If there was a gap print it, and also print the last adjacent map set length + if last_map.end != page.start: + if last_start and last_start != last_map: + print_adjacent_map(last_start, last_map) + print_gap(page, last_map) + + # If this is a guard page, print the last map and the guard page + elif page.is_guard: + if last_start and last_start != last_map: + print_adjacent_map(last_start, last_map) + print_guard(page) + last_start = None + last_map = page + continue + + # If we are tracking an adjacent set, don't print the current one yet + elif last_start: + if last_start != last_map: + print_map(last_map) + last_map = page + continue + + print_map(page) + last_start = page + last_map = page + calculate_total_memory(pages) + + parser = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter, description="""Print virtual memory map pages. @@ -78,6 +179,11 @@ def print_vmmap_table_header() -> None: parser.add_argument( "-B", "--lines-before", type=int, help="Number of pages to display before result", default=1 ) +parser.add_argument( + "--gaps", + action="store_true", + help="Display unmapped memory gap information in the memory map.", +) @pwndbg.commands.ArgparsedCommand( @@ -85,7 +191,7 @@ def print_vmmap_table_header() -> None: ) @pwndbg.commands.OnlyWhenRunning def vmmap( - gdbval_or_str=None, writable=False, executable=False, lines_after=1, lines_before=1 + gdbval_or_str=None, writable=False, executable=False, lines_after=1, lines_before=1, gaps=False ) -> None: lookaround_lines_limit = 64 @@ -96,7 +202,7 @@ def vmmap( # All displayed pages, including lines after and lines before total_pages = pwndbg.gdblib.vmmap.get() - # Filtered memory pages, indicated by an backtrace arrow in results + # Filtered memory pages, indicated by a backtrace arrow in results filtered_pages = [] # Only filter when -A and -B arguments are valid @@ -133,6 +239,10 @@ def vmmap( print("There are no mappings for specified address or module.") return + if gaps: + print_vmmap_gaps(total_pages) + return + print(M.legend()) print_vmmap_table_header() diff --git a/pwndbg/lib/memory.py b/pwndbg/lib/memory.py index 01faf35911f..cd49a3ae205 100644 --- a/pwndbg/lib/memory.py +++ b/pwndbg/lib/memory.py @@ -118,6 +118,10 @@ def rw(self) -> bool: def rwx(self) -> bool: return self.read and self.write and self.execute + @property + def is_guard(self) -> bool: + return not (self.read or self.write or self.execute) + @property def permstr(self) -> str: flags = self.flags diff --git a/tests/gdb-tests/tests/binaries/mmap_gaps.c b/tests/gdb-tests/tests/binaries/mmap_gaps.c new file mode 100644 index 00000000000..3a19429abc9 --- /dev/null +++ b/tests/gdb-tests/tests/binaries/mmap_gaps.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include +#include + +void break_here(void) {} + +#define ADDR (void *)0xcafe0000 +#define PGSZ 0x1000 + +void *xmmap(void *addr, size_t length, int prot, int flags, int fd, + off_t offset) { + void *p = mmap(addr, length, prot, flags, fd, offset); + if (MAP_FAILED == p) { + printf("Failed to map fixed address at %p\n", (void *)addr); + perror("mmap"); + exit(EXIT_FAILURE); + } + return p; +} + +int main(void) { + // We want to allocate multiple adjacent regions, too confirm that vmmap + // --gaps detects them properly. So iensure we have adjacent allocation, + // unmapped holes, as well as some guard page with no permissions. + + uint64_t address = (uint64_t)ADDR; + void *p; + + // 2 adjacent pages + p = xmmap((void *)address, PGSZ, PROT_READ, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + address += PGSZ; + p = xmmap((void *)address, PGSZ, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + address += PGSZ; + + // GUARD page + p = xmmap((void *)address, PGSZ, PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + mprotect(p, 0x1000, PROT_NONE); + address += PGSZ; + + p = xmmap((void *)address, PGSZ, PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + address += PGSZ; + + break_here(); +} diff --git a/tests/gdb-tests/tests/test_command_vmmap.py b/tests/gdb-tests/tests/test_command_vmmap.py index 541c78365f2..d8ea6731900 100644 --- a/tests/gdb-tests/tests/test_command_vmmap.py +++ b/tests/gdb-tests/tests/test_command_vmmap.py @@ -8,6 +8,7 @@ import pwndbg import tests +GAPS_MAP_BINARY = tests.binaries.get("mmap_gaps.out") CRASH_SIMPLE_BINARY = tests.binaries.get("crash_simple.out.hardcoded") BINARY_ISSUE_1565 = tests.binaries.get("issue_1565.out") @@ -182,3 +183,27 @@ def test_vmmap_issue_1565(start_binary): gdb.execute("run") gdb.execute("next") gdb.execute("context") + + +def test_vmmap_gaps_option(start_binary): + start_binary(GAPS_MAP_BINARY) + + gdb.execute("break break_here") + gdb.execute("continue") + + # Test vmmap with gap option + vmmaps = gdb.execute("vmmap --gaps", to_string=True).splitlines() + seen_gap = False + seen_adjacent = False + seen_guard = False + # Skip the first line since the legend has gard and + for line in vmmaps[1:]: + if "GAP" in line: + seen_gap = True + if "ADJACENT" in line: + seen_adjacent = True + if "GUARD" in line: + seen_guard = True + assert seen_gap + assert seen_adjacent + assert seen_guard