diff --git a/docs/commands/memory/vmmap.md b/docs/commands/memory/vmmap.md index 8d0c08f91..25016684a 100644 --- a/docs/commands/memory/vmmap.md +++ b/docs/commands/memory/vmmap.md @@ -2,8 +2,8 @@ # vmmap ```text -usage: vmmap [-h] [-w] [-x] [-A LINES_AFTER] [-B LINES_BEFORE] [-C CONTEXT] - [--gaps] +usage: vmmap [-h] [-w] [-x] [-s] [-A LINES_AFTER] [-B LINES_BEFORE] + [-C CONTEXT] [--gaps] [gdbval_or_str] ``` @@ -39,6 +39,7 @@ Memory pages can also be added manually with the use of vmmap-add, vmmap-clear a |-h|--help|show this help message and exit| |-w|--writable|Display writable maps only| |-x|--executable|Display executable maps only| +|-s|--expand-shared-cache|Expand all entries in the DYLD Shared Cache (Darwin only)| |-A|--lines-after|Number of pages to display after result (default: 1)| |-B|--lines-before|Number of pages to display before result (default: 1)| |-C|--context|Number of pages to display around the result| diff --git a/pwndbg/aglib/macho.py b/pwndbg/aglib/macho.py index 8abfa5621..aff4f27ad 100644 --- a/pwndbg/aglib/macho.py +++ b/pwndbg/aglib/macho.py @@ -422,10 +422,14 @@ class DyldSharedCache: the size of the struct to denote its version. """ + slide: int + "The slide value of the DyLD Shared Cache, in bytes." + def __init__(self, addr: int): self.addr = addr # Preload a few a few values, to speed things up later. + self.slide = self._slide() images_offset = 0x18 if self._header_size() <= 0x1C4 else 0x1C0 self._images_base = self.addr + pwndbg.aglib.memory.u32(self.addr + images_offset) self.image_count = pwndbg.aglib.memory.u32(self.addr + images_offset + 4) @@ -514,8 +518,7 @@ class DyldSharedCache: return end - start - @property - def slide(self) -> int: + def _slide(self) -> int: "The slide value of the DyLD Shared Cache, in bytes." mapping_ptr = self.base + self._header_size() mapping_base = pwndbg.aglib.memory.u64(mapping_ptr) @@ -552,7 +555,7 @@ class DyldSharedCache: def image_base(self, index: int): assert self.image_count > index - return pwndbg.aglib.memory.u64(self._images_base + index * 0x20) + return pwndbg.aglib.memory.u64(self._images_base + index * 0x20) + self.slide def image_name(self, index: int): assert self.image_count > index @@ -577,7 +580,7 @@ class DyldSharedCache: pwndbg.aglib.memory.string( self.addr + struct.unpack(" MemoryMap: + if not ( + pwndbg.aglib.arch.platform == Platform.DARWIN + and pwndbg.aglib.macho.shared_cache() is not None + ): + return pages + + # Darwin platforms use something called the Shared Cache for system + # libraries. Debuggers may report mapping ranges that belong to the + # shared cache in many ways, but we would like to tag those with a + # little more information. + final_pages = [] + + shared_cache = pwndbg.aglib.macho.shared_cache() + shared_cache_start = shared_cache.base + shared_cache_end = shared_cache_start + shared_cache.size + + images = list(shared_cache.images_sorted) + images_base = [image[1] for image in images] + + for page in pages.ranges(): + if page.end < shared_cache_start or page.start >= shared_cache_end: + # No overlap with the shared cache. + final_pages.append(page) + continue + + # We do not support partial overlaps between other mappings and the + # shared cache. + # + # While conceptually there's nothing stopping these from happening, + # if we ever encounter such a situation, it likely means that we + # either got something wrong, or that Darwin/LLDB has changed in + # such a way that we are likely not able to gracefully handle. + # + assert page.start >= shared_cache_start and page.end <= shared_cache_end + + one_past_index = bisect.bisect_right(images_base, page.start) + curr_base = page.start + + while True: + if one_past_index > len(images): + break + + if one_past_index == 0: + # Indicates that this mapping is not part of any image, but + # still part of the shared cache itself. Use a special name + # for it. + objfile = "[SharedCacheHeader]" + elif images_base[one_past_index - 1] >= page.end: + break + else: + # Name this mapping after the image it belongs to. + objfile = images[one_past_index - 1][0].decode("ascii") + curr_base = max(images_base[one_past_index - 1], page.start) + + if one_past_index == len(images): + end = page.end + else: + end = min(page.end, images_base[one_past_index]) + + final_pages.append( + Page( + curr_base, + end - curr_base, + page.flags, + curr_base - shared_cache_start, + objfile, + in_darwin_shared_cache=True, + ) + ) + + one_past_index += 1 + + return type(pages)(final_pages) + + @pwndbg.lib.cache.cache_until("start", "stop") def get_memory_map() -> MemoryMap: - return pwndbg.dbg.selected_inferior().vmmap() + return _refine_memory_map(pwndbg.dbg.selected_inferior().vmmap()) @pwndbg.lib.cache.cache_until("start", "stop") diff --git a/pwndbg/commands/rop.py b/pwndbg/commands/rop.py index eae1aad1a..1d8769c55 100644 --- a/pwndbg/commands/rop.py +++ b/pwndbg/commands/rop.py @@ -167,7 +167,7 @@ def iterate_over_pages(mem_limit: int) -> Iterator[Tuple[str, pwndbg.lib.memory. return proc = pwndbg.dbg.selected_inferior() - for page in proc.vmmap().ranges(): + for page in pwndbg.aglib.vmmap.get_memory_map().ranges(): if not page.execute: continue diff --git a/pwndbg/commands/vmmap.py b/pwndbg/commands/vmmap.py index eab35aca8..210bafcfd 100644 --- a/pwndbg/commands/vmmap.py +++ b/pwndbg/commands/vmmap.py @@ -178,6 +178,12 @@ parser.add_argument( ) parser.add_argument("-w", "--writable", action="store_true", help="Display writable maps only") parser.add_argument("-x", "--executable", action="store_true", help="Display executable maps only") +parser.add_argument( + "-s", + "--expand-shared-cache", + action="store_true", + help="Expand all entries in the DYLD Shared Cache (Darwin only)", +) parser.add_argument( "-A", "--lines-after", type=int, help="Number of pages to display after result", default=1 ) @@ -206,6 +212,7 @@ def vmmap( lines_before=1, context=None, gaps=False, + expand_shared_cache=False, ) -> None: lookaround_lines_limit = 64 @@ -217,7 +224,7 @@ def vmmap( lines_before = min(lookaround_lines_limit, lines_before) # All displayed pages, including lines after and lines before - vmmap = pwndbg.dbg.selected_inferior().vmmap() + vmmap = pwndbg.aglib.vmmap.get_memory_map() total_pages = vmmap.ranges() # Filtered memory pages, indicated by a backtrace arrow in results @@ -225,6 +232,9 @@ def vmmap( # Only filter when -A and -B arguments are valid if gdbval_or_str and lines_after >= 0 and lines_before >= 0: + # Always expand shared cache on detailed output. + expand_shared_cache = True + # Find matching page in memory filtered_pages = list(filter(pages_filter(gdbval_or_str), total_pages)) pages_to_display = [] @@ -264,10 +274,40 @@ def vmmap( print(M.legend()) print_vmmap_table_header() + shared_cache_first = None + shared_cache_last = None + shared_cache_collapsed = 0 + + def flush_shared_cache_info(): + nonlocal shared_cache_first + nonlocal shared_cache_last + if shared_cache_last is not None: + print( + pwndbg.lib.memory.format_address( + shared_cache_first.start, + shared_cache_last.end - shared_cache_first.start, + "---p", + shared_cache_first.offset, + "[DYLD Shared Cache]", + ) + ) + + shared_cache_first = None + shared_cache_last = None + for page in total_pages: if (executable and not page.execute) or (writable and not page.write): continue + # Omit ranges from the shared cache if requested. + if page is not None and page.in_darwin_shared_cache and not expand_shared_cache: + if shared_cache_first is None: + shared_cache_first = page + shared_cache_last = page + shared_cache_collapsed += 1 + continue + flush_shared_cache_info() + backtrace_prefix = None display_text = str(page) @@ -281,6 +321,13 @@ def vmmap( print(M.get(page.vaddr, text=display_text, prefix=backtrace_prefix)) + flush_shared_cache_info() + if shared_cache_collapsed > 0: + print( + f"[Omitted {shared_cache_collapsed} {'entry' if shared_cache_collapsed == 1 else 'entries'} from the DYLD Shared Cache in total, use '-s' to expand]" + ) + shared_cache_collapsed = 0 + if vmmap.is_qemu(): print( "\n[QEMU <8.1 target detected - vmmap result might not be accurate; see `help vmmap`]" diff --git a/pwndbg/dbg/__init__.py b/pwndbg/dbg/__init__.py index de5eba222..6bc18c417 100644 --- a/pwndbg/dbg/__init__.py +++ b/pwndbg/dbg/__init__.py @@ -395,7 +395,13 @@ class Process: def vmmap(self) -> MemoryMap: """ - Returns the virtual memory map of this process. + Returns the virtual memory map of this process, as seen by the debugger. + + Generally, one should prefer `pwndbg.aglib.vmmap.get()` over this + function, as this passes the raw information from the debugger more or + less straight through, without applying more general Pwndbg enhancements + to the memory map. This is the lower-level functionality on top of which + the function in `aglib` is implemented. """ raise NotImplementedError() diff --git a/pwndbg/dbg/lldb/__init__.py b/pwndbg/dbg/lldb/__init__.py index 95f420b1f..4ecbeb321 100644 --- a/pwndbg/dbg/lldb/__init__.py +++ b/pwndbg/dbg/lldb/__init__.py @@ -887,6 +887,54 @@ class LLDBProcess(pwndbg.dbg_mod.Process): return pages + def _process_vmmap_pages( + self, pages: List[pwndbg.lib.memory.Page] + ) -> List[pwndbg.lib.memory.Page]: + # Do a final, coalescing pass, for identical ranges that are sequential + # and contiguous to each other in the virtual address space, and join + # them into a single range. + # + # LLDB - particularly in macOS - may yield multiple ranges that describe + # contiguous sequential regions of virtual memory, but are otherwise + # identical. This seems to happen because LLDB internally distinguishes + # between different Mach-O sections. That information, however, is not + # made reliably available to us. + final_pages: List[pwndbg.lib.memory.Page] = [] + start = None + end = None + for page in pages: + if start is None: + start = page + continue + + target = end if end is not None else start + otherwise_equal = ( + target.flags == page.flags + and target.objfile == page.objfile + and target.in_darwin_shared_cache == page.in_darwin_shared_cache + ) + + if target.end == page.start and otherwise_equal: + end = page + else: + final_pages.append( + pwndbg.lib.memory.Page( + start.start, + target.end - start.start, + start.flags, + start.offset, + start.objfile, + start.in_darwin_shared_cache, + ) + ) + start = page + end = None + + if start is not None: + final_pages.append(start) + + return final_pages + @override def vmmap(self) -> pwndbg.dbg_mod.MemoryMap: from pwndbg.aglib.commpage import get_commpage_mappings @@ -896,7 +944,7 @@ class LLDBProcess(pwndbg.dbg_mod.Process): pages.extend(get_commpage_mappings()) pages.sort() - return LLDBMemoryMap(pages) + return LLDBMemoryMap(self._process_vmmap_pages(pages)) from pwndbg.aglib.kernel.vmmap import kernel_vmmap from pwndbg.aglib.vmmap_custom import get_custom_pages @@ -905,7 +953,8 @@ class LLDBProcess(pwndbg.dbg_mod.Process): pages.extend(kernel_vmmap()) pages.extend(get_custom_pages()) pages.sort() - return LLDBMemoryMap(pages) + + return LLDBMemoryMap(self._process_vmmap_pages(pages)) def find_largest_range_len( self, min_search: int, max_search: int, test: Callable[[int], bool] diff --git a/pwndbg/lib/memory.py b/pwndbg/lib/memory.py index ddcde3852..cdf794701 100644 --- a/pwndbg/lib/memory.py +++ b/pwndbg/lib/memory.py @@ -29,6 +29,13 @@ def round_up(address: int, align: int) -> int: return (address + (align - 1)) & (~(align - 1)) +def format_address(vaddr: int, memsz: int, permstr: str, offset: int, objfile: str | None = None) -> str: + "Format the given address as a string." + + width = 2 + 2 * pwndbg.aglib.arch.ptrsize + return f"{vaddr:#{width}x} {vaddr + memsz:#{width}x} {permstr} {memsz:8x} {offset:7x} {objfile or ''}" + + align_down = round_down align_up = round_up @@ -67,12 +74,21 @@ class Page: - A path to a file, such as `/usr/lib/libc.so.6` """ - def __init__(self, start: int, size: int, flags: int, offset: int, objfile: str = "") -> None: + in_darwin_shared_cache: bool + """ + Whether this mapping is part of the Darwin Shared Cache. + + This is an interesting property to know, as these entries may not be useful + to us at all times, and having an easy way to filter them out is helpful.. + """ + + def __init__(self, start: int, size: int, flags: int, offset: int, objfile: str = "", in_darwin_shared_cache: bool = False) -> None: self.vaddr = start self.memsz = size self.flags = flags self.offset = offset self.objfile = objfile + self.in_darwin_shared_cache = in_darwin_shared_cache # if self.rwx: # self.flags = self.flags ^ 1 @@ -147,8 +163,8 @@ class Page: objfile = self.objfile if len(rel) > len(self.objfile) else rel else: objfile = self.objfile - width = 2 + 2 * pwndbg.aglib.arch.ptrsize - return f"{self.vaddr:#{width}x} {self.vaddr + self.memsz:#{width}x} {self.permstr} {self.memsz:8x} {self.offset:7x} {objfile or ''}" + + return format_address(self.vaddr, self.memsz, self.permstr, self.offset, objfile=objfile) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.__str__()!r})" diff --git a/tests/library/qemu_user/tests/test_aarch64.py b/tests/library/qemu_user/tests/test_aarch64.py index d5f2238a7..65a6195b8 100644 --- a/tests/library/qemu_user/tests/test_aarch64.py +++ b/tests/library/qemu_user/tests/test_aarch64.py @@ -819,7 +819,7 @@ def test_memory_read_error_handling(qemu_assembly_run): # Find the first memory page where there is a gap after it stack_end_addr = -1 page_prev = None - for page in pwndbg.dbg.selected_inferior().vmmap().ranges(): + for page in pwndbg.aglib.vmmap.get_memory_map().ranges(): if page_prev is not None and page_prev.end != page.start: stack_end_addr = page_prev.end break