diff --git a/docs/commands/index.md b/docs/commands/index.md index 24147e191..dd32a27ed 100644 --- a/docs/commands/index.md +++ b/docs/commands/index.md @@ -256,6 +256,9 @@ ## musl - [mallocng-explain](musl/mallocng-explain.md) - Gives a quick explanation of musl's mallocng allocator. +- [mallocng-group](musl/mallocng-group.md) - Print out information about a mallocng group at the given address. +- [mallocng-meta](musl/mallocng-meta.md) - Print out information about a mallocng group given the address of its meta. +- [mallocng-slot-user](musl/mallocng-slot-user.md) - Dump information about a mallocng slot, given its user address. diff --git a/docs/commands/musl/mallocng-group.md b/docs/commands/musl/mallocng-group.md new file mode 100644 index 000000000..17524226d --- /dev/null +++ b/docs/commands/musl/mallocng-group.md @@ -0,0 +1,25 @@ + +# mallocng-group + +```text +usage: mallocng-group [-h] address + +``` + +Print out information about a mallocng group at the given address. + +**Alias:** ng-group +### Positional arguments + +|Positional Argument|Help| +| :--- | :--- | +|address|The address of the group object.| + +### Optional arguments + +|Short|Long|Help| +| :--- | :--- | :--- | +|-h|--help|show this help message and exit| + + + diff --git a/docs/commands/musl/mallocng-meta.md b/docs/commands/musl/mallocng-meta.md new file mode 100644 index 000000000..ce4ec59e2 --- /dev/null +++ b/docs/commands/musl/mallocng-meta.md @@ -0,0 +1,25 @@ + +# mallocng-meta + +```text +usage: mallocng-meta [-h] address + +``` + +Print out information about a mallocng group given the address of its meta. + +**Alias:** ng-meta +### Positional arguments + +|Positional Argument|Help| +| :--- | :--- | +|address|The address of the meta object.| + +### Optional arguments + +|Short|Long|Help| +| :--- | :--- | :--- | +|-h|--help|show this help message and exit| + + + diff --git a/docs/commands/musl/mallocng-slot-user.md b/docs/commands/musl/mallocng-slot-user.md new file mode 100644 index 000000000..09f3cc484 --- /dev/null +++ b/docs/commands/musl/mallocng-slot-user.md @@ -0,0 +1,26 @@ + +# mallocng-slot-user + +```text +usage: mallocng-slot-user [-h] [-a] address + +``` + +Dump information about a mallocng slot, given its user address. + +**Alias:** ng-slotu +### Positional arguments + +|Positional Argument|Help| +| :--- | :--- | +|address|The start of user memory. Referred to as `p` in the source.| + +### Optional arguments + +|Short|Long|Help| +| :--- | :--- | :--- | +|-h|--help|show this help message and exit| +|-a|--all|Print out all information. Including meta and group data.| + + + diff --git a/pwndbg/aglib/heap/heap.py b/pwndbg/aglib/heap/heap.py index cb7577c89..cf7467f5c 100644 --- a/pwndbg/aglib/heap/heap.py +++ b/pwndbg/aglib/heap/heap.py @@ -1,29 +1,9 @@ from __future__ import annotations -from typing import Any - class MemoryAllocator: """Heap abstraction layer.""" - # This function isn't actually implemented anywhere. It originally returned - # `gdb.Breakpoint`, but, in order to facilitate the port to aglib, that - # type association was removed. It should be put back as soon as the - # Debugger-agnostic API gains the ability to set breakpoints. - # - # TODO: Change `Any` to the Debugger-agnostic breakpoint type when it gets created - - def summarize(self, address: int, **kwargs: Any) -> str: - """Returns a textual summary of the specified address. - - Arguments: - address: Address of the heap block to summarize. - - Returns: - A string. - """ - raise NotImplementedError() - def containing(self, address: int) -> int: """Returns the address of the allocation which contains 'address'. diff --git a/pwndbg/aglib/heap/mallocng.py b/pwndbg/aglib/heap/mallocng.py new file mode 100644 index 000000000..adb385857 --- /dev/null +++ b/pwndbg/aglib/heap/mallocng.py @@ -0,0 +1,537 @@ +""" +Implements handling of musl's allocator mallocng. +https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng +""" + +from __future__ import annotations + +from typing import List + +import pwndbg +import pwndbg.aglib.arch +import pwndbg.aglib.memory as memory +import pwndbg.aglib.typeinfo + +# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L14 +# Slot granularity. +UNIT = 16 +# Size of in-band metadata. +IB = 4 + +# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/malloc.c#L12 +# Describes the possible sizes a slot can be. These are `/ UNIT`. +# fmt: off +size_classes: List[int] = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 18, 20, + 25, 31, 36, 42, 50, 63, 72, 84, 102, 127, 146, + 170, 204, 255, 292, 340, 409, 511, 584, 682, 818, + 1023, 1169, 1364, 1637, 2047, 2340, 2730, 3276, + 4095, 4680, 5460, 6552, 8191, +] +# fmt: on + + +# Shorthand +def int_size(): + return pwndbg.aglib.typeinfo.sint.sizeof + + +class Group: + """ + A group is an array of slots. + + https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L17 + struct group { + struct meta *meta; + unsigned char active_idx:5; + char pad[UNIT - sizeof(struct meta *) - 1]; + unsigned char storage[]; + }; + """ + + def __init__(self, addr: int) -> None: + self.addr = addr + + self._meta = None + self._active_idx = None + + def preload(self) -> None: + """ + Read all the necessary process memory to populate the group's + fields. + + Do this if you know you will be using most of the + fields of the group. It will be faster, since we can do one + reads instead of two small ones. You may also catch + inaccessible memory exceptions here and not worry about it later. + + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + data = memory.read(self.addr, pwndbg.aglib.arch.ptrsize + 1) + self._meta = Meta(pwndbg.aglib.arch.unpack(data[: pwndbg.aglib.arch.ptrsize])) + self._active_idx = data[-1] & 0b11111 + + @property + def meta(self) -> Meta: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._meta is None: + self._meta = Meta(memory.read_pointer_width(self.addr)) + + return self._meta + + @property + def active_idx(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._active_idx is None: + self._active_idx = memory.u8(self.addr + pwndbg.aglib.arch.ptrsize) & 0b11111 + + return self._active_idx + + @property + def storage(self) -> int: + return self.addr + UNIT + + @property + def group_size(self) -> int: + """ + The size of this group, in bytes. + + Raises: + pwndbg.dbg_mod.Error: When reading meta fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/malloc.c#L234 + return self.meta.stride * self.meta.cnt + UNIT + + +class Slot: + """ + The "unit of allocation" (analogous to glibc's "chunk"). + There is no struct in the source code that describes it. + """ + + def __init__(self, p: int) -> None: + # The start of user memory. It may + # not be the actual start of the slot. + self.p: int = p + self._offset: int = None + self._idx: int = None + # Not exactly sure what this is. + self._check4: int = None + + self._group: Group = None + self._meta: Meta = None + self._reserved: int = None + + def preload(self) -> None: + """ + Read all the necessary process memory to populate the slot's + fields. + + Do this if you know you will be using most of the + fields of the slot. It will be faster, since we can do a few + big reads instead of many small ones. You may also catch + inaccessible memory exceptions here and not worry about it later. + + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + # Read all the in-band data. + inband_data = memory.read(self.p - 8, 8) + + self._check4 = inband_data[4] + if self._check4: + self._offset = int.from_bytes(inband_data[0:4], pwndbg.aglib.arch.endian, signed=False) + else: + self._offset = int.from_bytes(inband_data[6:8], pwndbg.aglib.arch.endian, signed=False) + idxv = inband_data[5] + if idxv != 255: + self._idx = idxv & 31 + else: + self._idx = 0 + + # Read the group's meta pointer. + _ = self.meta + # Need this loaded for lots of fields, + # but we will let it be since we want to be able to + # say stuff about this slot even with a corrupt meta. + # _ = self.meta.stride + + self._reserved = inband_data[5] >> 5 + if self._reserved == 5: + # self.end doesn't need a read. + self._reserved = memory.u32(self.end - 4) + + # All the other fields are calculated without + # memory reads. + + @property + def check4(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L134 + if self._check4 is None: + self._check4 = memory.u8(self.p - 4) + + return self._check4 + + @property + def offset(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L132 + if self._offset is None: + if self.check4: + # assert(!offset); + self._offest = memory.u32(self.p - 8) + # assert(offset > 0xffff); + else: + self._offset = memory.u16(self.p - 2) + + return self._offset + + @property + def idx(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L133 + if self._idx is None: + v = memory.u8(self.p - 3) + if v != 255: + self._idx = v & 31 + else: + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/donate.c#L29 + self._idx = 0 + + return self._idx + + @property + def group(self) -> Group: + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L139 + if self._group is None: + self._group = Group(self.p - UNIT * self.offset - UNIT) + + return self._group + + @property + def meta(self) -> Meta: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L140 + if self._meta is None: + self._meta = Meta(memory.read_pointer_width(self.group.addr)) + + return self._meta + + @property + def start(self) -> int: + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/free.c#L108 + return self.group.storage + self.meta.stride * self.idx + + @property + def end(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading meta fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/free.c#L109 + return self.start + self.meta.stride - IB + + @property + def reserved(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L161 + # Lots of asserts here.. + if self._reserved is None: + self._reserved = memory.u8(self.p - 3) >> 5 + if self._reserved == 5: + self._reserved = memory.u32(self.end - 4) + + return self._reserved + + @property + def nominal_size(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading meta fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L159 + return self.end - self.reserved - self.p + + @property + def user_size(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading meta fails. + """ + return self.nominal_size + + @property + def slack(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading meta fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L199 + return (self.meta.stride - self.nominal_size - IB) // UNIT + + @property + def internal_offset(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading meta fails. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L204 + # Not sure why musl saves it, it doesn't seem to use it. + # We can calculate it more easily than musl does: + return (self.p - self.start) // UNIT + + +class Meta: + """ + The metadata of a group. + + https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L24 + struct meta { + struct meta *prev, *next; + struct group *mem; + volatile int avail_mask, freed_mask; + uintptr_t last_idx:5; + uintptr_t freeable:1; + uintptr_t sizeclass:6; + uintptr_t maplen:8*sizeof(uintptr_t)-12; + }; + """ + + def __init__(self, addr: int) -> None: + self.addr: int = addr + + self._prev: int = None + self._next: int = None + self._mem: int = None + self._avail_mask: int = None + self._freed_mask: int = None + self._last_idx: int = None + self._freeable: int = None + self._sizeclass: int = None + self._maplen: int = None + + self._stride: int = None + + def preload(self) -> None: + """ + Read all the necessary process memory to populate the meta's + fields. + + Do this if you know you will be using most of the + fields of the meta. It will be faster, since we can do a one + big read instead of many small ones. You may also catch + inaccessible memory exceptions here and not worry about it later. + + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + ptrsize = pwndbg.aglib.arch.ptrsize + endian = pwndbg.aglib.arch.endian + + # Read the whole struct. + data = memory.read(self.addr, ptrsize * 3 + 2 * int_size() + 8 * ptrsize) + + cur_offset = 0 + self._prev = pwndbg.aglib.arch.unpack(data[cur_offset:ptrsize]) + cur_offset += ptrsize + self._next = pwndbg.aglib.arch.unpack(data[cur_offset : (cur_offset + ptrsize)]) + cur_offset += ptrsize + self._mem = pwndbg.aglib.arch.unpack(data[cur_offset : (cur_offset + ptrsize)]) + cur_offset += ptrsize + self._avail_mask = int.from_bytes( + data[cur_offset : (cur_offset + int_size())], endian, signed=False + ) + cur_offset += int_size() + self._freed_mask = int.from_bytes( + data[cur_offset : (cur_offset + int_size())], endian, signed=False + ) + cur_offset += int_size() + # I think this is how I should read a bitfield. + # http://mjfrazer.org/mjfrazer/bitfields/ + flags = int.from_bytes(data[cur_offset : (cur_offset + ptrsize)], endian, signed=False) + self._last_idx = flags & 0b11111 + self._freeable = (flags >> 5) & 1 + self._sizeclass = (flags >> 6) & 0b111111 + self._maplen = flags >> 12 + + # All the other fields are calculated without + # memory reads. + + @property + def prev(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._prev is None: + self._prev = memory.read_pointer_width(self.addr) + + return self._prev + + @property + def next(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._next is None: + self._next = memory.read_pointer_width(self.addr + pwndbg.aglib.arch.ptrsize) + + return self._next + + @property + def mem(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._mem is None: + self._mem = memory.read_pointer_width(self.addr + pwndbg.aglib.arch.ptrsize * 2) + + return self._mem + + @property + def avail_mask(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._avail_mask is None: + # While the type is technically a signed int, it makes more + # sense to interpret it as unsigned semantically. + self._avail_mask = memory.uint(self.addr + pwndbg.aglib.arch.ptrsize * 3) + + return self._avail_mask + + @property + def freed_mask(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._freed_mask is None: + offset = pwndbg.aglib.arch.ptrsize * 3 + int_size() + # Technically signed. + self._freed_mask = memory.uint(self.addr + offset) + + return self._freed_mask + + @property + def last_idx(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._last_idx is None: + offset = pwndbg.aglib.arch.ptrsize * 3 + int_size() * 2 + # reading pointer width so it works regardless of endianness + self._last_idx = memory.read_pointer_width(self.addr + offset) & 0b11111 + + return self._last_idx + + @property + def freeable(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._freeable is None: + offset = pwndbg.aglib.arch.ptrsize * 3 + int_size() * 2 + self._freeable = (memory.read_pointer_width(self.addr + offset) >> 5) & 1 + + return self._freeable + + @property + def sizeclass(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._sizeclass is None: + offset = pwndbg.aglib.arch.ptrsize * 3 + int_size() * 2 + self._sizeclass = (memory.read_pointer_width(self.addr + offset) >> 6) & 0b111111 + + return self._sizeclass + + @property + def maplen(self) -> int: + """ + Raises: + pwndbg.dbg_mod.Error: When reading memory fails. + """ + if self._maplen is None: + offset = pwndbg.aglib.arch.ptrsize * 3 + int_size() * 2 + self._maplen = memory.read_pointer_width(self.addr + offset) >> 12 + + return self._maplen + + @property + def stride(self): + """ + Returns -1 if sizeclass >= len(size_classes). + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L175 + if self._stride is None: + if not self.last_idx and self.maplen: + self._stride = self.maplen * 4096 - UNIT + else: + if self.sizeclass < len(size_classes): + self._stride = UNIT * size_classes[self.sizeclass] + else: + # The meta is corrupted. + self._stride = -1 + + return self._stride + + @property + def cnt(self): + """ + Number of slots in the group. + """ + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/free.c#L60 + return self.last_idx + 1 + + @property + def slot_size(self): + """ + The size of a slot in this group, in bytes. + + Returns -1 if sizeclass >= len(size_classes). + """ + if self.sizeclass < len(size_classes): + return size_classes[self.sizeclass] * UNIT + else: + # The meta is corrupted. + return -1 + + +class MetaArea: + def __init__(self, addr: int) -> None: + self.addr = addr + + +class Mallocng: + pass diff --git a/pwndbg/aglib/memory.py b/pwndbg/aglib/memory.py index 67b128da5..67ae01192 100644 --- a/pwndbg/aglib/memory.py +++ b/pwndbg/aglib/memory.py @@ -269,6 +269,14 @@ def s64(addr: int) -> int: return readtype(pwndbg.aglib.typeinfo.int64, addr) +def sint(addr: int) -> int: + """ + Read one `signed int` from the specified + address. + """ + return readtype(pwndbg.aglib.typeinfo.sint, addr) + + def cast_pointer( type: pwndbg.dbg_mod.Type, addr: int | pwndbg.dbg_mod.Value ) -> pwndbg.dbg_mod.Value: diff --git a/pwndbg/aglib/typeinfo.py b/pwndbg/aglib/typeinfo.py index ebc2e7df1..988377128 100644 --- a/pwndbg/aglib/typeinfo.py +++ b/pwndbg/aglib/typeinfo.py @@ -18,6 +18,7 @@ long: pwndbg.dbg_mod.Type uchar: pwndbg.dbg_mod.Type ushort: pwndbg.dbg_mod.Type uint: pwndbg.dbg_mod.Type +sint: pwndbg.dbg_mod.Type void: pwndbg.dbg_mod.Type uint8: pwndbg.dbg_mod.Type @@ -61,6 +62,9 @@ def update() -> None: module.uchar = lookup_types("unsigned char", "ubyte", "u8", "uint8") module.ushort = lookup_types("unsigned short", "ushort", "u16", "uint16", "uint16_t") module.uint = lookup_types("unsigned int", "uint", "u32", "uint32") + # Putting 'signed int' at the front is slow for kernel. Not sure how it's possible + # that type is missing. See PR #3115. + module.sint = lookup_types("int", "signed int", "signed") module.void = lookup_types("void", "()") module.uint8 = module.uchar diff --git a/pwndbg/commands/mallocng.py b/pwndbg/commands/mallocng.py index 6a2c2f686..221b722ff 100644 --- a/pwndbg/commands/mallocng.py +++ b/pwndbg/commands/mallocng.py @@ -4,10 +4,17 @@ Commands that help with debugging musl's allocator, mallocng. from __future__ import annotations +import argparse + import pwndbg -import pwndbg.aglib.heap +import pwndbg.aglib.heap.mallocng as mallocng +import pwndbg.aglib.memory as memory +import pwndbg.aglib.typeinfo as typeinfo import pwndbg.color as C +import pwndbg.color.message as message from pwndbg.commands import CommandCategory +from pwndbg.lib.pretty_print import Property +from pwndbg.lib.pretty_print import PropertyPrinter @pwndbg.commands.Command( @@ -144,3 +151,322 @@ def mallocng_explain() -> None: # TODO: explain what a slot looks like. print(txt) + + +def dump_group(group: mallocng.Group) -> str: + try: + # May fail on corrupt meta. + group_size = group.group_size + except pwndbg.dbg_mod.Error as e: + print(message.error(f"Error while reading meta: {e}")) + print(C.bold("Cannot determine group size.")) + group_size = -1 + + group_range = "@ " + C.memory.get(group.addr) + if group_size != -1: + group_range += " - " + C.memory.get(group.addr + group_size) + + pp = PropertyPrinter() + pp.start_section("group", group_range) + pp.set_padding(2) + pp.add( + [ + Property(name="meta", value=group.meta.addr, is_addr=True), + Property(name="active_idx", value=group.active_idx), + Property(name="storage", value=group.storage, is_addr=True, extra="start of slots"), + ] + ) + + if group_size != -1: + pp.write("---\n") + pp.set_padding(3) + pp.add( + [ + Property(name="group size", value=group_size), + ] + ) + + pp.end_section() + return pp.dump() + + +def dump_meta(meta: mallocng.Meta) -> str: + int_size = str(typeinfo.sint.sizeof * 8) + avail_binary = "0b" + format(meta.avail_mask, f"0{int_size}b") + freed_binary = "0b" + format(meta.freed_mask, f"0{int_size}b") + + pp = PropertyPrinter() + pp.start_section("meta", "@ " + C.memory.get(meta.addr)) + pp.set_padding(2) + pp.add( + [ + Property(name="prev", value=meta.prev, is_addr=True), + Property(name="next", value=meta.next, is_addr=True), + Property(name="mem", value=meta.mem, is_addr=True, extra="the group"), + Property(name="avail_mask", value=meta.avail_mask, extra=avail_binary), + Property(name="freed_mask", value=meta.freed_mask, extra=freed_binary), + Property(name="last_idx", value=meta.last_idx, extra="index of last slot"), + Property(name="freeable", value=str(bool(meta.freeable))), + Property(name="sizeclass", value=meta.sizeclass), + Property(name="maplen", value=meta.maplen), + ] + ) + pp.write("---\n") + pp.set_padding(3) + pp.add( + [ + Property(name="cnt", value=meta.cnt, extra="the number of slots"), + Property(name="slot size", value=meta.slot_size, extra='aka "stride"'), + ] + ) + pp.end_section() + + output = pp.dump() + + if not meta.freeable: + # When mapped object files contain unused memory, they are donated + # to the heap. See https://elixir.bootlin.com/musl/v1.2.5/source/ldso/dynlink.c#L600 + # and https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/donate.c#L36 . + # Only in this case is `meta.freeable = 0;` + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/donate.c#L25 + output += C.bold("\nGroup donated by ld as unused part of ") + + try: + mapping = pwndbg.aglib.vmmap.find(mallocng.Group(meta.mem).addr) + except pwndbg.dbg_mod.Error as e: + print(message.error(f"Could not fetch parent group: {e}")) + mapping = None + + if mapping is None: + output += C.red("") + else: + output += C.bold(f'"{mapping.objfile}"') + + output += C.bold(".\n") + + elif not meta.last_idx and meta.maplen: + # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L177 + output += C.bold("\nGroup allocated with mmap().\n") + else: + output += C.bold("\nGroup nested in slot of another group") + try: + parent_group = mallocng.Slot(mallocng.Group(meta.mem).addr).group.addr + output += " (" + C.memory.get(parent_group) + ")" + except pwndbg.dbg_mod.Error as e: + print(message.error(f"Could not fetch parent group: {e}")) + output += C.bold(".\n") + + return output + + +parser = argparse.ArgumentParser( + description=""" +Dump information about a mallocng slot, given its user address. + """, +) +parser.add_argument( + "address", + type=int, + help="The start of user memory. Referred to as `p` in the source.", +) +parser.add_argument( + "-a", + "--all", + action="store_true", + help="Print out all information. Including meta and group data.", +) + + +@pwndbg.commands.Command( + parser, + category=CommandCategory.MUSL, + aliases=["ng-slotu"], +) +@pwndbg.commands.OnlyWhenRunning +def mallocng_slot_user(address: int, all: bool) -> None: + if not memory.is_readable_address(address): + print(message.error(f"Address {address:#x} not readable.")) + return + + slot = mallocng.Slot(address) + + try: + slot.preload() + except pwndbg.dbg_mod.Error as e: + print(message.error(f"Error while reading slot: {e}")) + return + + read_success: bool = True + + try: + slot.group.preload() + except pwndbg.dbg_mod.Error as e: + print(message.error(f"Error while reading group: {e}")) + read_success = False + + try: + slot.meta.preload() + except pwndbg.dbg_mod.Error as e: + print(message.error(f"Error while reading meta: {e}")) + read_success = False + + if not read_success: + print(message.info("Only showing partial information.")) + all = False + + pp = PropertyPrinter() + + if not all: + pp.start_section("slab") + pp.set_padding(7) + if read_success: + pp.add( + [ + Property(name="group", value=slot.group.addr, is_addr=True), + Property(name="meta", value=slot.meta.addr, is_addr=True), + ] + ) + else: + pp.add( + [ + Property(name="group", value=slot.group.addr, is_addr=True), + ] + ) + pp.end_section() + + if read_success: + pp.start_section("general") + pp.set_padding(2) + pp.add( + [ + Property(name="start", value=slot.start, is_addr=True), + Property(name="user start", value=slot.p, is_addr=True, extra="aka `p`"), + Property(name="end", value=slot.end, is_addr=True, extra="start + stride - 4"), + Property( + name="stride", value=slot.meta.stride, extra="distance between adjacent slots" + ), + Property(name="user size", value=slot.user_size, extra='aka "nominal size", `n`'), + Property(name="slack", value=slot.slack, extra="slot's unused memory / 0x10"), + ] + ) + pp.end_section() + + pp.start_section("in-band") + pp.set_padding(4) + + reserved_extra = ["end - p - n", ""] + if slot.reserved >= 5: + reserved_extra[1] = "located near slot end" + if slot.reserved == 6: + reserved_extra.append("this slot is a nested group") + else: + reserved_extra[1] = "located in slot header" + + inband_group = [ + Property(name="offset", value=slot.offset, extra="distance to first slot / 0x10"), + Property(name="index", value=slot.idx, extra="index of slot in its group"), + Property(name="reserved", value=slot.reserved, extra=reserved_extra), + ] + + if read_success: + # While it is technically saved in-band, there is no way + # for us to locate it without metadata. + inband_group.append( + Property( + name="rnd-off", + value=slot.internal_offset, + extra="prevents double free, (p - start) / 0x10", + ), + ) + + pp.add(inband_group) + pp.end_section() + + pp.print() + + if all: + print(dump_group(slot.group), end="") + print(dump_meta(slot.meta), end="") + + +parser = argparse.ArgumentParser( + description=""" +Print out information about a mallocng group given the address of its meta. + """, +) +parser.add_argument( + "address", + type=int, + help="The address of the meta object.", +) + + +@pwndbg.commands.Command( + parser, + category=CommandCategory.MUSL, + aliases=["ng-meta"], +) +@pwndbg.commands.OnlyWhenRunning +def mallocng_meta(address: int) -> None: + if not memory.is_readable_address(address): + print(message.error(f"Address {address:#x} not readable.")) + return + + meta = mallocng.Meta(address) + + try: + meta.preload() + except pwndbg.dbg_mod.Error as e: + print(message.error(str(e))) + return + + try: + group = mallocng.Group(meta.mem) + group.preload() + print(dump_group(group), end="") + except pwndbg.dbg_mod.Error as e: + print(message.error(f"Failed loading group: {e}")) + + print(dump_meta(meta), end="") + + +parser = argparse.ArgumentParser( + description=""" +Print out information about a mallocng group at the given address. + """, +) +parser.add_argument( + "address", + type=int, + help="The address of the group object.", +) + + +@pwndbg.commands.Command( + parser, + category=CommandCategory.MUSL, + aliases=["ng-group"], +) +@pwndbg.commands.OnlyWhenRunning +def mallocng_group(address: int) -> None: + if not memory.is_readable_address(address): + print(message.error(f"Address {address:#x} not readable.")) + return + + group = mallocng.Group(address) + + try: + group.preload() + except pwndbg.dbg_mod.Error as e: + print(message.error(str(e))) + return + + print(dump_group(group), end="") + + try: + meta = group.meta + meta.preload() + print(dump_meta(meta), end="") + except pwndbg.dbg_mod.Error as e: + print(message.error(f"Failed loading meta: {e}")) + return diff --git a/pwndbg/dbg/__init__.py b/pwndbg/dbg/__init__.py index 13e08da0b..14e26ce07 100644 --- a/pwndbg/dbg/__init__.py +++ b/pwndbg/dbg/__init__.py @@ -922,7 +922,7 @@ class Value: def dereference(self) -> Value: """ - If this is a poitner value, dereferences the pointer and returns a new + If this is a pointer value, dereferences the pointer and returns a new instance of Value, containing the value pointed to by this pointer. """ raise NotImplementedError() diff --git a/pwndbg/lib/pretty_print.py b/pwndbg/lib/pretty_print.py index f5df84ce3..edf8fd046 100644 --- a/pwndbg/lib/pretty_print.py +++ b/pwndbg/lib/pretty_print.py @@ -20,7 +20,7 @@ class Property: name: str value: Any - extra: str = "" + extra: str | List[str] = "" is_addr: bool = False use_hex: bool = True @@ -61,24 +61,54 @@ class PropertyPrinter: """ Add a group of properties that should be aligned. """ - max_name_len = max(len(self.name_color_func(prop.name)) for prop in prop_group) + # Transform prop values to string representation + for prop in prop_group: + if isinstance(prop.value, int): + if prop.use_hex: + prop.value = hex(prop.value) + else: + prop.value = str(prop.value) + + # Get max lengths to calculate proper ljust + # + 1 to account for the ":" + max_name_len = max(len(prop.name) for prop in prop_group) + 1 + # max_value_len = max(len(prop.value) for prop in prop_group) + # Use constant so it works between different groups + max_value_len = 16 + + indentation_str = self.indent_level * self.indent_size * " " + padding_str = self.padding * " " + name_pad_str = max_name_len * " " + val_pad_str = max_value_len * " " + extra_list_pad_str = indentation_str + name_pad_str + padding_str + val_pad_str for prop in prop_group: - self.text += self.indent_level * self.indent_size * " " - colored_name = self.name_color_func(prop.name) + ":" - self.text += colored_name.ljust(max_name_len + 1, " ") - self.text += self.padding * " " + self.text += ( + indentation_str + + color.ljust_colored(self.name_color_func(prop.name) + ":", max_name_len) + + padding_str + ) if prop.is_addr: - self.text += color.memory.get(prop.value) + base = 16 if prop.use_hex else 10 + colored_val = color.memory.get(int(prop.value, base)) else: - if isinstance(prop.value, int) and prop.use_hex: - val = hex(prop.value) - else: - val = prop.value - self.text += self.value_color_func(val) + colored_val = self.value_color_func(prop.value) - self.text += " " + prop.extra + self.text += color.ljust_colored(colored_val, max_value_len) + + if isinstance(prop.extra, str): + self.text += " " + prop.extra + else: + # list of strings, we want each one under the other + assert isinstance(prop.extra, list) + assert len(prop.extra) > 1 + + self.text += " " + prop.extra[0] + for i in range(1, len(prop.extra)): + self.text += "\n" + self.text += extra_list_pad_str + self.text += " " + prop.extra[i] self.text += "\n" @@ -92,7 +122,7 @@ class PropertyPrinter: """ Print the built up string. """ - print(self.text) + print(self.text, end="") def clear(self) -> None: """ @@ -119,14 +149,22 @@ class PropertyPrinter: """ self.text += string - def start_section(self, title: str) -> None: + def start_section(self, title: str, preamble: str = "") -> None: """ Start a named section of properties that will have increased indentation. Don't forget to call end_section()! """ - self.text += self.section_color_func(title) + "\n" + self.text += " " * self.indent_level * self.indent_size + self.text += self.section_color_func(title) + + if preamble: + self.text += "\n" + self.text += " " * (self.indent_level + 1) * self.indent_size + self.text += preamble + + self.text += "\n" self.indent() def end_section(self) -> None: