From 9c0dd25a132c375bb8b2607082b3ba14ac0c7d51 Mon Sep 17 00:00:00 2001 From: Jason An Date: Mon, 22 Jul 2024 03:53:23 -0700 Subject: [PATCH] Memory exploration bug fixes and minor improvements (#2311) * fix automatic exploration errors and improve exploration * code cleanup * Update pwndbg/commands/vmmap.py --------- Co-authored-by: Disconnect3d --- pwndbg/commands/vmmap.py | 20 +++++++++++ pwndbg/gdblib/elf.py | 8 ++--- pwndbg/gdblib/stack.py | 2 +- pwndbg/gdblib/vmmap.py | 75 +++++++++++++++++++++++++++++++++++----- 4 files changed, 91 insertions(+), 14 deletions(-) diff --git a/pwndbg/commands/vmmap.py b/pwndbg/commands/vmmap.py index 735269639..f373c7219 100644 --- a/pwndbg/commands/vmmap.py +++ b/pwndbg/commands/vmmap.py @@ -366,3 +366,23 @@ def vmmap_load(filename) -> None: for page in pages: pwndbg.gdblib.vmmap.add_custom_page(page) print("%r added" % page) + + +parser = argparse.ArgumentParser(description="Explore a page, trying to guess permissions.") +parser.add_argument( + "address", type=pwndbg.commands.sloppy_gdb_parse, help="Address of the page to explore" +) + + +@pwndbg.commands.ArgparsedCommand(parser, category=CommandCategory.MEMORY) +@pwndbg.commands.OnlyWhenRunning +def vmmap_explore(address: int) -> None: + if not isinstance(address, int): + print("Address is not a valid integer.") + return + page = pwndbg.gdblib.vmmap.explore(address) + if page is None: + print("Exploration failed. Maybe the address isn't readable?") + return + print_vmmap_table_header() + print(page) diff --git a/pwndbg/gdblib/elf.py b/pwndbg/gdblib/elf.py index 7be99be37..123c21583 100644 --- a/pwndbg/gdblib/elf.py +++ b/pwndbg/gdblib/elf.py @@ -307,17 +307,17 @@ def get_ehdr(pointer: int) -> Tuple[int | None, Ehdr | None]: base = None - if pwndbg.gdblib.qemu.is_qemu(): + vmmap = pwndbg.gdblib.vmmap.find(pointer, should_explore=False) + mappings_known = pwndbg.gdblib.vmmap.get_known_maps() is not None + if not vmmap and not mappings_known: # Only check if the beginning of the page contains the ELF magic, - # since we cannot get the memory map in qemu-user. + # since we don't have any information about the memory map. page_start = pwndbg.lib.memory.page_align(pointer) if pwndbg.gdblib.memory.read(page_start, 4, partial=True) == b"\x7fELF": base = page_start else: return None, None else: - vmmap = pwndbg.gdblib.vmmap.find(pointer) - # If there is no vmmap for the requested address, we can't do much # (e.g. it could have been unmapped for whatever reason) if vmmap is None: diff --git a/pwndbg/gdblib/stack.py b/pwndbg/gdblib/stack.py index 8e9650985..b2b0c6459 100644 --- a/pwndbg/gdblib/stack.py +++ b/pwndbg/gdblib/stack.py @@ -58,7 +58,7 @@ def get() -> Dict[int, pwndbg.lib.memory.Page]: @pwndbg.lib.cache.cache_until("stop") -def current(): +def current() -> pwndbg.lib.memory.Page | None: """ Returns the bounds for the stack for the current thread. """ diff --git a/pwndbg/gdblib/vmmap.py b/pwndbg/gdblib/vmmap.py index 0ee2b90af..92b17e8ca 100644 --- a/pwndbg/gdblib/vmmap.py +++ b/pwndbg/gdblib/vmmap.py @@ -66,6 +66,14 @@ Note that the page-tables method will require the QEMU kernel process to be on t enum_sequence=["page-tables", "monitor", "none"], ) +auto_explore = pwndbg.config.add_param( + "auto-explore-pages", + "yes", + "whether to try to infer page permissions when memory maps missing (can cause errors)", + param_class=pwndbg.lib.config.PARAM_ENUM, + enum_sequence=["yes", "warn", "no"], +) + @pwndbg.lib.cache.cache_until("objfile", "start") def is_corefile() -> bool: @@ -87,10 +95,11 @@ inside_no_proc_maps_search = False @pwndbg.lib.cache.cache_until("start", "stop") -def get() -> Tuple[pwndbg.lib.memory.Page, ...]: +def get_known_maps() -> Tuple[pwndbg.lib.memory.Page, ...] | None: """ - Returns a tuple of `Page` objects representing the memory mappings of the - target, sorted by virtual address ascending. + Similar to `vmmap.get()`, except only returns maps in cases where + the mappings are known, like if it's a coredump, or if process + mappings are available. """ # Note: debugging a coredump does still show proc.alive == True if not pwndbg.gdblib.proc.alive: @@ -107,6 +116,19 @@ def get() -> Tuple[pwndbg.lib.memory.Page, ...]: if not proc_maps: proc_maps = proc_tid_maps() + return proc_maps + + +@pwndbg.lib.cache.cache_until("start", "stop") +def get() -> Tuple[pwndbg.lib.memory.Page, ...]: + """ + Returns a tuple of `Page` objects representing the memory mappings of the + target, sorted by virtual address ascending. + """ + proc_maps = get_known_maps() + if proc_maps is not None: + return proc_maps + # The `proc_maps` is usually a tuple of Page objects but it can also be: # None - when /proc/$tid/maps does not exist/is not available # tuple() - when the process has no maps yet which happens only during its very early init @@ -164,8 +186,18 @@ def get() -> Tuple[pwndbg.lib.memory.Page, ...]: return tuple(pages) +_warn_cache: Set[int] = set() + + +@pwndbg.gdblib.events.new_objfile +def clear_warn_cache(): + _warn_cache.clear() + + @pwndbg.lib.cache.cache_until("stop") -def find(address: int | gdb.Value | None) -> pwndbg.lib.memory.Page | None: +def find( + address: int | gdb.Value | None, *, should_explore: bool | None = None +) -> pwndbg.lib.memory.Page | None: if address is None: return None @@ -175,7 +207,22 @@ def find(address: int | gdb.Value | None) -> pwndbg.lib.memory.Page | None: if address in page: return page - return explore(address) + if should_explore is None: + if auto_explore.value == "warn": + page_start = pwndbg.lib.memory.page_align(address) + if page_start not in _warn_cache: + _warn_cache.add(page_start) + print( + M.warn( + f"Warning: Avoided exploring possible address {address:#x}. You can explicitly explore it with `vmmap_explore {page_start:#x}`" + ) + ) + elif auto_explore.value == "yes": + return explore(address) + elif should_explore and not proc_tid_maps(): + return explore(address) + + return None @pwndbg.gdblib.abi.LinuxOnly() @@ -193,8 +240,6 @@ def explore(address_maybe: int) -> pwndbg.lib.memory.Page | None: Also assumes the entire contiguous section has the same permission. """ - if proc_tid_maps(): - return None address_maybe = pwndbg.lib.memory.page_align(address_maybe) @@ -203,8 +248,20 @@ def explore(address_maybe: int) -> pwndbg.lib.memory.Page | None: if not flags: return None - flags |= 2 if pwndbg.gdblib.memory.poke(address_maybe) else 0 - flags |= 1 if not pwndbg.gdblib.stack.is_executable() else 0 + if pwndbg.gdblib.memory.poke(address_maybe): + flags |= 2 + # It's really hard to check for executability, so we just make some guesses: + # If it's in the same page as the stack pointer, try to check the NX bit + # If it's in the same page as the instruction pointer, assume it's executable + # Otherwise, just say it's not executable + if address_maybe == pwndbg.lib.memory.page_align(pwndbg.gdblib.regs.pc): + flags |= 1 + # TODO: could maybe make this check look at the stacks in pwndbg.gdblib.stack.get() but that might have issues + elif ( + address_maybe == pwndbg.lib.memory.page_align(pwndbg.gdblib.regs.sp) + and pwndbg.gdblib.stack.is_executable() + ): + flags |= 1 page = find_boundaries(address_maybe) page.objfile = ""