diff --git a/docs/commands/index.md b/docs/commands/index.md index 86208d90d..e27aee6e0 100644 --- a/docs/commands/index.md +++ b/docs/commands/index.md @@ -90,9 +90,10 @@ - [kversion](kernel/kversion.md) - Outputs the kernel version (/proc/version). - [msr](kernel/msr.md) - Read or write to Model Specific Register (MSR) - [p2v](kernel/p2v.md) - Translate physical address to its corresponding virtual address. +- [pageinfo](kernel/pageinfo.md) - Convert a pointer to a `struct page` to its corresponding virtual address. - [pagewalk](kernel/pagewalk.md) - Performs pagewalk. - [slab](kernel/slab.md) - Prints information about the linux kernel's slab allocator SLUB. -- [v2p](kernel/v2p.md) - Translate virtual address to its corresponding physical address. +- [v2p](kernel/v2p.md) - Translate virtual address to its corresponding physmap address. ## Linux/libc/ELF diff --git a/docs/commands/kernel/pageinfo.md b/docs/commands/kernel/pageinfo.md new file mode 100644 index 000000000..b54c69924 --- /dev/null +++ b/docs/commands/kernel/pageinfo.md @@ -0,0 +1,23 @@ + +# pageinfo + +```text +usage: pageinfo [-h] page + +``` + +Convert a pointer to a `struct page` to its corresponding virtual address. +### Positional arguments + +|Positional Argument|Help| +| :--- | :--- | +|page|| + +### Optional arguments + +|Short|Long|Help| +| :--- | :--- | :--- | +|-h|--help|show this help message and exit| + + + diff --git a/docs/commands/kernel/v2p.md b/docs/commands/kernel/v2p.md index f2f087e01..20dfbeb5a 100644 --- a/docs/commands/kernel/v2p.md +++ b/docs/commands/kernel/v2p.md @@ -6,7 +6,7 @@ usage: v2p [-h] vaddr ``` -Translate virtual address to its corresponding physical address. +Translate virtual address to its corresponding physmap address. ### Positional arguments |Positional Argument|Help| diff --git a/pwndbg/aglib/kernel/__init__.py b/pwndbg/aglib/kernel/__init__.py index f5f429242..2d3913155 100644 --- a/pwndbg/aglib/kernel/__init__.py +++ b/pwndbg/aglib/kernel/__init__.py @@ -564,6 +564,14 @@ def kbase() -> int | None: raise NotImplementedError() +def pagewalk(addr, entry=None): + pi = arch_paginginfo() + if pi: + return pi.pagewalk(addr, entry) + else: + raise NotImplementedError() + + def paging_enabled() -> bool: arch_name = pwndbg.aglib.arch.name if arch_name == "i386": diff --git a/pwndbg/aglib/kernel/paging.py b/pwndbg/aglib/kernel/paging.py index 2d9945b39..109aa450d 100644 --- a/pwndbg/aglib/kernel/paging.py +++ b/pwndbg/aglib/kernel/paging.py @@ -1,6 +1,7 @@ from __future__ import annotations import math +from typing import Dict from typing import List from typing import Tuple @@ -9,8 +10,8 @@ import pwndbg.aglib.vmmap_custom import pwndbg.color.message as M import pwndbg.lib.cache import pwndbg.lib.memory +from pwndbg.lib.regs import BitFlags -ENTRYMASK = ~((1 << 12) - 1) & ((1 << 51) - 1) # don't return None but rather an invalid value for address markers # this way arithmetic ops do not panic if physmap is not found INVALID_ADDR = 1 << 64 @@ -46,6 +47,9 @@ class ArchPagingInfo: vmemmap: int kbase: int addr_marker_sz: int + va_bits: int + pagetable_cache: Dict[pwndbg.dbg_mod.Value, Dict[int, int]] = {} + pagetableptr_cache: Dict[int, pwndbg.dbg_mod.Value] = {} @property @pwndbg.lib.cache.cache_until("start") @@ -93,8 +97,86 @@ class ArchPagingInfo: return None + def pagewalk( + self, target, entry + ) -> Tuple[Tuple[str, ...], List[Tuple[int | None, int | None]]]: + raise NotImplementedError() + + def pagewalk_helper( + self, target, entry, kernel_phys_base=0 + ) -> List[Tuple[int | None, int | None]]: + base = self.physmap + if entry > base: + # user inputted a physmap address as pointer to pgd + entry -= base + level = self.paging_level + result: List[Tuple[int | None, int | None]] = [(None, None)] * (level + 1) + page_shift = self.page_shift + ENTRYMASK = ~((1 << page_shift) - 1) & ((1 << self.va_bits) - 1) + for i in range(level, 0, -1): + vaddr = (entry & ENTRYMASK) + base - kernel_phys_base + if self.should_stop_pagewalk(entry): + break + shift = (i - 1) * (page_shift - 3) + page_shift + offset = target & ((1 << shift) - 1) + idx = (target & (0x1FF << shift)) >> shift + entry = 0 + try: + # with this optimization, roughly x2 as fast on average + # especially useful when parsing a large number of pages, e.g. set kernel-vmmap monitor + if vaddr not in self.pagetableptr_cache: + self.pagetableptr_cache[vaddr] = pwndbg.aglib.memory.get_typed_pointer( + "unsigned long", vaddr + ) + table = self.pagetableptr_cache[vaddr] + if table not in self.pagetable_cache: + self.pagetable_cache[table] = {} + table_cache = self.pagetable_cache[table] + if idx not in table_cache: + table_cache[idx] = int(table[idx]) + entry = table_cache[idx] + # Prior to optimization: + # table = pwndbg.aglib.memory.get_typed_pointer("unsigned long", vaddr) + # entry = int(table[idx]) + except Exception as e: + print(M.warn(f"Exception while page walking: {e}")) + entry = 0 + if entry == 0: + return result + result[i] = (entry, vaddr) + result[0] = (entry, (entry & ENTRYMASK) + base + offset - kernel_phys_base) + return result + + def pageentry_flags(self, level) -> BitFlags: + raise NotImplementedError() + + def should_stop_pagewalk(self, is_last): + raise NotImplementedError() + class x86_64PagingInfo(ArchPagingInfo): + def __init__(self): + self.va_bits = 48 if self.paging_level == 4 else 51 + # https://blog.zolutal.io/understanding-paging/ + self.pagetable_level_names = ( + ( + "Page", + "PT", + "PMD", + "PUD", + "PGD", + ) + if self.paging_level == 4 + else ( + "Page", + "PT", + "PMD", + "P4D", + "PUD", + "PGD", + ) + ) + @property @pwndbg.lib.cache.cache_until("stop") def physmap(self): @@ -222,6 +304,19 @@ class x86_64PagingInfo(ArchPagingInfo): if pwndbg.aglib.regs[pwndbg.aglib.regs.stack] in page: page.objfile = "kernel [stack]" + def pagewalk( + self, target, entry + ) -> Tuple[Tuple[str, ...], List[Tuple[int | None, int | None]]]: + if entry is None: + entry = pwndbg.aglib.regs["cr3"] + return self.pagetable_level_names, self.pagewalk_helper(target, entry) + + def pageentry_flags(self, is_last) -> BitFlags: + return BitFlags([("NX", 63), ("PS", 7), ("A", 5), ("U", 2), ("W", 1), ("P", 0)]) + + def should_stop_pagewalk(self, entry): + return entry & (1 << 7) > 0 + class Aarch64PagingInfo(ArchPagingInfo): def __init__(self): @@ -236,6 +331,29 @@ class Aarch64PagingInfo(ArchPagingInfo): self.vmalloc = module_start_wo_kaslr + 0x80000000 shift = self.page_shift - self.STRUCT_PAGE_SHIFT self.VMEMMAP_SIZE = (module_start_wo_kaslr - ((-1 << self.va_bits) + 2**64)) >> shift + # correct for linux + if self.paging_level == 4: + self.pagetable_level_names = ( + "Page", + "L3", + "L2", + "L1", + "L0", + ) + elif self.paging_level == 3: + self.pagetable_level_names = ( + "Page", + "L3", + "L2", + "L1", + ) + + elif self.paging_level == 2: + self.pagetable_level_names = ( + "Page", + "L3", + "L2", + ) @property @pwndbg.lib.cache.cache_until("stop") @@ -336,13 +454,33 @@ class Aarch64PagingInfo(ArchPagingInfo): @property @pwndbg.lib.cache.cache_until("stop") def page_shift(self) -> int: - # TODO: this might be arm version dependent - if self.tcr_el1["TG1"] == 1: + if self.tcr_el1["TG1"] == 0b01: return 14 - elif self.tcr_el1["TG1"] == 0: + elif self.tcr_el1["TG1"] == 0b10: return 12 - else: + elif self.tcr_el1["TG1"] == 0b11: return 16 + raise NotImplementedError() + + @property + @pwndbg.lib.cache.cache_until("stop") + def page_shift_user(self) -> int: + if self.tcr_el1["TG0"] == 0b00: + return 12 + elif self.tcr_el1["TG0"] == 0b01: + return 16 + elif self.tcr_el1["TG0"] == 0b10: + return 14 + raise NotImplementedError() + + @property + @pwndbg.lib.cache.cache_until("stop") + def paging_level(self): + # https://www.kernel.org/doc/html/v5.3/arm64/memory.html + if self.page_shift == 16: + return 2 + # in some cases, not all addressing bits are used + return (self.va_bits - self.page_shift + (self.page_shift - 4)) // (self.page_shift - 3) @pwndbg.lib.cache.cache_until("stop") def markers(self) -> Tuple[Tuple[str, int], ...]: @@ -413,35 +551,41 @@ class Aarch64PagingInfo(ArchPagingInfo): if pwndbg.aglib.regs[pwndbg.aglib.regs.stack] in page: page.objfile = "kernel [stack]" - -@pwndbg.aglib.proc.OnlyWithArch(["x86-64"]) -def pagewalk(target, entry=None) -> List[Tuple[int | None, int | None]]: - level = pwndbg.aglib.kernel.arch_paginginfo().paging_level - base = pwndbg.aglib.kernel.arch_paginginfo().physmap - if entry is None: - entry = pwndbg.aglib.regs["cr3"] - else: - entry = int(pwndbg.dbg.selected_frame().evaluate_expression(entry)) - if entry > base: - # user inputted a physmap address as pointer to pgd - entry -= base - result: List[Tuple[int | None, int | None]] = [(None, None)] * (level + 1) - for i in range(level, 0, -1): - vaddr = (entry & ENTRYMASK) + base - if entry & (1 << 7) > 0: - break - shift = (i - 1) * 9 + 12 - offset = target & ((1 << shift) - 1) - idx = (target & (0x1FF << shift)) >> shift - entry = 0 + @property + @pwndbg.lib.cache.cache_until("start") + def kernel_phys_start(self): + found_system = False try: - table = pwndbg.aglib.memory.get_typed_pointer("unsigned long", vaddr) - entry = int(table[idx]) - except Exception as e: - print(M.warn(f"Exception while page walking: {e}")) - entry = 0 - if entry == 0: - return result - result[i] = (entry, vaddr) - result[0] = (entry, (entry & ENTRYMASK) + base + offset) - return result + for line in pwndbg.dbg.selected_inferior().send_monitor("info mtree -f").splitlines(): + line = line.strip() + if "Root memory region: system" in line: + found_system = True + if found_system: + split = line.split("-") + if "ram" in line and len(split) > 1: + return int(split[0], 16) + except Exception: + pass + return 0x40000000 # default + + def pagewalk( + self, target, entry + ) -> Tuple[Tuple[str, ...], List[Tuple[int | None, int | None]]]: + if entry is None: + if pwndbg.aglib.memory.is_kernel(target): + entry = pwndbg.aglib.regs.TTBR1_EL1 + else: + entry = pwndbg.aglib.regs.TTBR0_EL1 + self.entry = entry + return self.pagetable_level_names, self.pagewalk_helper( + target, entry, self.kernel_phys_start + ) + + def pageentry_flags(self, is_last) -> BitFlags: + if is_last: + return BitFlags([("UNX", 54), ("PNX", 53), ("AP", (6, 7))]) + return BitFlags([("UNX", 60), ("PNX", 59), ("AP", (6, 7))]) + + def should_stop_pagewalk(self, entry): + # self.entry is set because the call chain + return (((entry & 1) == 0) or ((entry & 3) == 1)) and entry != self.entry diff --git a/pwndbg/aglib/kernel/vmmap.py b/pwndbg/aglib/kernel/vmmap.py index 8e0fa4cc1..5b38566a8 100644 --- a/pwndbg/aglib/kernel/vmmap.py +++ b/pwndbg/aglib/kernel/vmmap.py @@ -388,7 +388,7 @@ def kernel_vmmap(process_pages=True) -> Tuple[pwndbg.lib.memory.Page, ...]: for page in pages: if page.objfile == kv.pi.ESPSTACK: continue - pgwalk_res = pwndbg.aglib.kernel.paging.pagewalk(page.start) + _, pgwalk_res = pwndbg.aglib.kernel.pagewalk(page.start) entry, _ = pgwalk_res[0] if entry and entry >> 63 == 0: page.flags |= 1 diff --git a/pwndbg/color/context.py b/pwndbg/color/context.py index cdd16cc68..e4dc9c2ea 100644 --- a/pwndbg/color/context.py +++ b/pwndbg/color/context.py @@ -103,7 +103,7 @@ def format_flags(value: int | None, flags: BitFlags, last: int | None = None): size = 1 else: assert len(bit) == 2 - size = bit[1] + size = bit[1] - bit[0] + 1 bit = bit[0] mask = (1 << size) - 1 @@ -112,7 +112,10 @@ def format_flags(value: int | None, flags: BitFlags, last: int | None = None): # If the bitfield is larger than a single bit, we can't communicate the value # with just the case of the name, so append the actual value if size > 1: - name = f"{name}:{flag_val}" + if size == 2: + name = f"{name}:{flag_val:0{size}b}" + else: + name = f"{name}:{flag_val:#x}" if flag_val == 0: name = flag_unset(name.lower()) diff --git a/pwndbg/commands/paging.py b/pwndbg/commands/paging.py index f62a0b948..2b1d2b4e2 100644 --- a/pwndbg/commands/paging.py +++ b/pwndbg/commands/paging.py @@ -8,17 +8,25 @@ import pwndbg.aglib.regs import pwndbg.color as C import pwndbg.color.message as M from pwndbg.commands import CommandCategory -from pwndbg.lib.regs import BitFlags parser = argparse.ArgumentParser(description="Performs pagewalk.") parser.add_argument("vaddr", type=str, help="virtual address to walk") parser.add_argument("--pgd", dest="entry", type=str, default=None, help="") - -pageflags = BitFlags([("NX", 63), ("PS", 7), ("A", 5), ("W", 1), ("P", 0)]) +PAGETYPES = ( + "buddy", + "offline", + "table", + "guard", + "hugetlb", + "slab", + "zsmalloc", + "unaccepted", +) -def print_pagetable_entry(name: str, paddr: int | None, vaddr: int): +def print_pagetable_entry(name: str, paddr: int | None, vaddr: int, level: int, is_last: bool): + pageflags = pwndbg.aglib.kernel.arch_paginginfo().pageentry_flags(is_last) flags = "" arrow_right = pwndbg.chain.c.arrow(f"{pwndbg.chain.config_arrow_right}") if paddr is not None: @@ -26,45 +34,56 @@ def print_pagetable_entry(name: str, paddr: int | None, vaddr: int): print(f"{C.blue(name)} @ {C.yellow(hex(vaddr))} {flags}") -def pg_indices(vaddr, nr_level): - result = [vaddr & (0x1000 - 1)] - vaddr >>= 12 - for _ in range(nr_level): - result.append(vaddr & (0x1FF)) - vaddr >>= 9 - return result +def page_type(page): + names = PAGETYPES + page_type_val = pwndbg.aglib.memory.s32(page + 0x30) + if page_type_val == -1: + return "initialized" + if page_type_val >= 0: + return f"mapcount: {page_type_val}" + page_type_val = pwndbg.aglib.memory.u32(page + 0x30) + if pwndbg.aglib.kernel.krelease() >= (6, 12): + idx = (page_type_val >> 24) - 0xF0 + if idx < len(names): + return names[idx] + if pwndbg.aglib.kernel.krelease() >= (6, 11): + names = names[:-1][::-1] + for i in range(len(names)): + if page_type_val & (1 << (i + 24)) == 0: + return names[i] + if pwndbg.aglib.kernel.krelease() >= (5, 0): + names = names[:5] + for i in range(len(names)): + if page_type_val & (1 << (7 + i)) == 0: + return names[i] + return "unknown" + + +def page_info(page): + try: + refcount = pwndbg.aglib.memory.u32(page + 0x34) + print( + f"{C.green('page')} @ {C.yellow(hex(page))} [{page_type(page)}, refcount: {refcount}]" + ) + except (ValueError, TypeError): + print(M.warn("invalid page address")) @pwndbg.commands.Command(parser, category=CommandCategory.KERNEL) @pwndbg.commands.OnlyWhenQemuKernel @pwndbg.commands.OnlyWhenPagingEnabled -@pwndbg.aglib.proc.OnlyWithArch(["x86-64"]) +@pwndbg.aglib.proc.OnlyWithArch(["x86-64", "aarch64"]) def pagewalk(vaddr, entry=None): + if entry is not None: + entry = int(pwndbg.dbg.selected_frame().evaluate_expression(entry)) vaddr = int(pwndbg.dbg.selected_frame().evaluate_expression(vaddr)) - # https://blog.zolutal.io/understanding-paging/ - level = pwndbg.aglib.kernel.arch_paginginfo().paging_level - names = ( - "Page", - "PT", - "PMD", - "PUD", - "PGD", - ) - if level == 5: - names = ( - "Page", - "PT", - "PMD", - "P4D", - "PUD", - "PGD", - ) - entries = pwndbg.aglib.kernel.paging.pagewalk(vaddr, entry) - for i in range(level, 0, -1): + names, entries = pwndbg.aglib.kernel.pagewalk(vaddr, entry) + for i in range(len(names) - 1, 0, -1): entry, vaddr = entries[i] + next, _ = entries[i - 1] if entry is None: break - print_pagetable_entry(names[i], entry, vaddr) + print_pagetable_entry(names[i], entry, vaddr, i, next is None or i == 1) _, vaddr = entries[0] if vaddr is None: print(M.warn("address is not mapped")) @@ -74,7 +93,7 @@ def pagewalk(vaddr, entry=None): def paging_print_helper(name, addr): - print(f"{C.green(name)}: {C.yellow(hex(pwndbg.aglib.kernel.phys_to_virt(int(addr))))}") + print(f"{C.green(name)}: {C.yellow(hex(addr))}") p2v_parser = argparse.ArgumentParser( @@ -87,14 +106,17 @@ p2v_parser.add_argument("paddr", type=str, help="") @pwndbg.commands.OnlyWhenQemuKernel @pwndbg.commands.OnlyWithKernelDebugSyms @pwndbg.commands.OnlyWhenPagingEnabled +@pwndbg.aglib.proc.OnlyWithArch(["x86-64", "aarch64"]) def p2v(paddr): - paddr = pwndbg.dbg.selected_frame().evaluate_expression(paddr) - vaddr = pwndbg.aglib.kernel.phys_to_virt(int(paddr)) + paddr = int(pwndbg.dbg.selected_frame().evaluate_expression(paddr)) + vaddr = pwndbg.aglib.kernel.phys_to_virt(paddr) paging_print_helper("Virtual address", vaddr) + page = pwndbg.aglib.kernel.virt_to_page(vaddr) + page_info(page) v2p_parser = argparse.ArgumentParser( - description="Translate virtual address to its corresponding physical address." + description="Translate virtual address to its corresponding physmap address." ) v2p_parser.add_argument("vaddr", type=str, help="") @@ -103,7 +125,31 @@ v2p_parser.add_argument("vaddr", type=str, help="") @pwndbg.commands.OnlyWhenQemuKernel @pwndbg.commands.OnlyWithKernelDebugSyms @pwndbg.commands.OnlyWhenPagingEnabled +@pwndbg.aglib.proc.OnlyWithArch(["x86-64", "aarch64"]) def v2p(vaddr): - vaddr = pwndbg.dbg.selected_frame().evaluate_expression(vaddr) - paddr = pwndbg.aglib.kernel.virt_to_phys(int(vaddr)) - paging_print_helper("Physical address", paddr) + vaddr = int(pwndbg.dbg.selected_frame().evaluate_expression(vaddr)) + entry, paddr = pwndbg.aglib.kernel.pagewalk(vaddr)[1][0] # more accurate + if not entry: + print(M.warn("virtual to page failed")) + paging_print_helper("Physmap address", paddr) + # paddr is the physmap address which is a virtual address + page = pwndbg.aglib.kernel.virt_to_page(paddr) + page_info(page) + + +page_parser = argparse.ArgumentParser( + description="Convert a pointer to a `struct page` to its corresponding virtual address." +) +page_parser.add_argument("page", type=str, help="") + + +@pwndbg.commands.Command(page_parser, category=CommandCategory.KERNEL) +@pwndbg.commands.OnlyWhenQemuKernel +@pwndbg.commands.OnlyWithKernelDebugSyms +@pwndbg.commands.OnlyWhenPagingEnabled +@pwndbg.aglib.proc.OnlyWithArch(["x86-64", "aarch64"]) +def pageinfo(page): + page = int(pwndbg.dbg.selected_frame().evaluate_expression(page)) + vaddr = pwndbg.aglib.kernel.page_to_virt(page) + paging_print_helper("Virtual address", vaddr) + page_info(page) diff --git a/pwndbg/lib/regs.py b/pwndbg/lib/regs.py index 51506d6d5..d83247479 100644 --- a/pwndbg/lib/regs.py +++ b/pwndbg/lib/regs.py @@ -46,7 +46,7 @@ class BitFlags: if isinstance(r, int): return (self.value >> r) & 1 s, e = r - return ((~((1 << s) - 1) & ((1 << e) - 1)) & self.value) >> s + return ((~((1 << s) - 1) & ((1 << (e + 1)) - 1)) & self.value) >> s def __setitem__(self, key, value): self.flags[key] = value @@ -485,7 +485,7 @@ aarch64_cpsr_flags = BitFlags( ("A", 8), ("I", 7), ("F", 6), - ("EL", (2, 2)), + ("EL", 2), ("SP", 0), ] ) @@ -535,6 +535,7 @@ aarch64_tcr_flags = BitFlags( [ ("TG1", (30, 31)), ("T1SZ", (16, 21)), + ("TG0", (14, 15)), ("T0SZ", (0, 5)), ] ) @@ -608,6 +609,8 @@ aarch64 = RegisterSet( "spsr_el2": aarch64_cpsr_flags, "spsr_el3": aarch64_cpsr_flags, "tcr_el1": aarch64_tcr_flags, + "ttbr0_el1": BitFlags(), + "ttbr1_el1": BitFlags(), }, # X29 is the frame pointer register (FP) but setting it # as frame here messes up the register order to the point diff --git a/tests/library/qemu-system/tests/test_commands_kernel.py b/tests/library/qemu-system/tests/test_commands_kernel.py index e3e5028db..5888092e7 100644 --- a/tests/library/qemu-system/tests/test_commands_kernel.py +++ b/tests/library/qemu-system/tests/test_commands_kernel.py @@ -127,6 +127,11 @@ def test_x64_extra_registers_under_kernel_mode(): assert flag in res or flag.upper() in res +def get_slab_freelist_elements(out): + out = pwndbg.color.strip(out) + return re.findall(r"- \[0x[0-9a-fA-F\-]{2}\] (0x[0-9a-fA-F]+)", out) + + def get_slab_object_address(): """helper function to get the address of some kmalloc slab object and the associated slab cache name""" @@ -134,9 +139,7 @@ def get_slab_object_address(): for cache in caches: cache_name = cache.name info = gdb.execute(f"slab info -v {cache_name}", to_string=True) - ansi_escape = re.compile(r"\x1b\[[0-9;]*m") - info = ansi_escape.sub("", info) - matches = re.findall(r"- \[0x[0-9a-fA-F\-]{2}\] (0x[0-9a-fA-F]+)", info) + matches = get_slab_freelist_elements(info) if len(matches) > 0: return (matches[0], cache_name) raise ValueError("Could not find any slab objects") @@ -192,6 +195,11 @@ def test_command_kernel_vmmap(): ) +def get_buddy_freelist_elements(out): + out = pwndbg.color.strip(out) + return re.findall(r"\[0x[0-9a-fA-F\-]{2}\] (0x[0-9a-fA-F]{16})", out) + + @pytest.mark.skipif(not pwndbg.aglib.kernel.has_debug_syms(), reason="test requires debug symbols") def test_command_buddydump(): res = gdb.execute("buddydump", to_string=True) @@ -201,16 +209,13 @@ def test_command_buddydump(): # this indicates the buddy allocator contains at least one entry assert "Order" in res and "Zone" in res and ("per_cpu_pageset" in res or "free_area" in res) - ansi_escape = re.compile(r"\x1b\[[0-9;]*m") - res = ansi_escape.sub("", res) # find the starting addresses of all entries within the freelists - matches = re.findall(r"\[0x[0-9a-fA-F\-]{2}\] (0x[0-9a-fA-F]{16})", res) + matches = get_buddy_freelist_elements(res) for i in range(0, len(matches), 20): # check every 20 elements so tests do not take too long match = int(matches[i], 16) res = gdb.execute(f"bud -f {hex(match + random.randint(0, 0x1000 - 1))}", to_string=True) - res = ansi_escape.sub("", res) - _matches = re.findall(r"\[0x[0-9a-fA-F\-]{2}\] (0x[0-9a-fA-F]{16})", res) + _matches = get_buddy_freelist_elements(res) # asserting `bud -f` behaviour -- should be able to find the corresponding entry to an address # even if the address is not aligned assert len(_matches) == 1 and int(_matches[0], 16) == match @@ -236,28 +241,81 @@ def test_command_buddydump(): assert "free_area" not in filter_res -@pytest.mark.skipif( - pwndbg.aglib.arch.name not in ["i386", "x86-64"], - reason="pagewalk is only fully implemented for x86 (partially relies on cr3)", -) +def check_0x100_bytes(address, physmap_addr): + # compare the first 0x100 bytes of the page (e.g. first kernel image page) with its physmap conterpart + expected = pwndbg.aglib.memory.read(address, 0x100) + actual = pwndbg.aglib.memory.read(physmap_addr, 0x100) + assert all(expected[i] == actual[i] for i in range(0x100)) + + def test_command_pagewalk(): address = pwndbg.aglib.kernel.kbase() if address is None: - # no kbase? fine pages = pwndbg.aglib.vmmap.get() address = pages[0].start res = gdb.execute(f"pagewalk {hex(address)}", to_string=True) - assert "PMD" in res # Page Size is only set for PMDe or PTe + assert any( + name in res + for name in ( + "PMD", # Page Size is only set for PMDe or PTe + "L1", + "L3", + ) + ) res = res.splitlines()[-1] match = re.findall(r"0x[0-9a-fA-F]{16}", res)[0] physmap_addr = int(match, 16) - # compare the first 0x100 bytes of the page (e.g. first kernel image page) with its physmap conterpart - expected = pwndbg.aglib.memory.read(address, 0x100) - actual = pwndbg.aglib.memory.read(physmap_addr, 0x100) - assert all(expected[i] == actual[i] for i in range(0x100)) + check_0x100_bytes(address, physmap_addr) # make sure that when using cr3 for pgd, it still works - res2 = gdb.execute(f"pagewalk {hex(address)} --pgd $cr3", to_string=True).splitlines()[-1] + pgd_ptr = "$cr3" + if pwndbg.aglib.arch.name == "aarch64": + if pwndbg.aglib.memory.is_kernel(address): + pgd_ptr = pwndbg.aglib.regs.TTBR1_EL1 + else: + pgd_ptr = pwndbg.aglib.regs.TTBR0_EL1 + res2 = gdb.execute(f"pagewalk {hex(address)} --pgd {pgd_ptr}", to_string=True).splitlines()[-1] assert res == res2 # test non nonexistent address res = gdb.execute("pagewalk 0", to_string=True) assert res.splitlines()[-1] == "address is not mapped" + + +@pytest.mark.skipif(not pwndbg.aglib.kernel.has_debug_syms(), reason="test requires debug symbols") +def test_command_paging(): + def test_command_paging_helper(pagetype, addr): + out = gdb.execute(f"v2p {addr}", to_string=True) + out = pwndbg.color.strip(out) + # pagetype should be correct + assert pagetype in out + page = int(out.splitlines()[1].split()[2], 16) + physmap_addr = int(out.splitlines()[0].split()[-1], 16) + # the first 0x100 bytes of the resolved address should match the original + check_0x100_bytes(addr, physmap_addr) + phys_addr = pwndbg.aglib.kernel.virt_to_phys(physmap_addr) + out = gdb.execute(f"p2v {phys_addr}", to_string=True) + out = pwndbg.color.strip(out) + # the virtual address should be the physmap address + assert physmap_addr == int(out.splitlines()[0].split()[-1], 16) + out = gdb.execute(f"pageinfo {page}", to_string=True) + out = pwndbg.color.strip(out) + # the virtual address should be the physmap address + assert physmap_addr == int(out.splitlines()[0].split()[-1], 16) + + # kbase, slab, buddy, vmemmap + kbase = pwndbg.aglib.kernel.kbase() + test_command_paging_helper("initialized", kbase) + vmemmap = pwndbg.aglib.kernel.arch_paginginfo().vmemmap + test_command_paging_helper("initialized", vmemmap) + res = gdb.execute("buddydump", to_string=True) + matches = get_buddy_freelist_elements(res) + if len(matches) > 0 and "free_area" in res: # only pages in free_area is marked "buddy" + buddy = int(matches[-1], 16) + test_command_paging_helper("buddy", buddy) + if pwndbg.aglib.kernel.krelease() >= (6, 11): + # the slab marker is only added after v6.11 + res = gdb.execute("slab info -v -p kmalloc-32", to_string=True) + matches = get_slab_freelist_elements(res) + if len(matches) > 0: + slab = int(matches[-1].split()[-1], 16) + test_command_paging_helper("slab", slab) + res = gdb.execute(f"pagewalk {kbase}") diff --git a/tests/library/qemu-user/tests/test_aarch64.py b/tests/library/qemu-user/tests/test_aarch64.py index 03c3c54d2..ca9d95d4b 100644 --- a/tests/library/qemu-user/tests/test_aarch64.py +++ b/tests/library/qemu-user/tests/test_aarch64.py @@ -218,7 +218,7 @@ def test_aarch64_conditional_jump_output(qemu_assembly_run): " ↓\n" " 0x1010140 ✔ tbnz w2, #3, D \n" " ↓\n" - " 0x1010148 cmp x2, x3 0xa - 0x0 CPSR => 0x20000000 [ n z C v q pan il d a i f el:0 sp ]\n" + " 0x1010148 cmp x2, x3 0xa - 0x0 CPSR => 0x20000000 [ n z C v q pan il d a i f el sp ]\n" " 0x101014c ✘ b.eq E \n" " \n" " 0x1010150 nop \n" @@ -510,7 +510,7 @@ def test_aarch64_write_cpsr_when_zero(qemu_assembly_run): "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n" "─────────────────────[ DISASM / aarch64 / set emulate on ]──────────────────────\n" " 0x1010120 <_start> mov x19, #8 X19 => 8\n" - " 0x1010124 <_start+4> cmn x19, #8 8 + 8 CPSR => 0x0 [ n z c v q pan il d a i f el:0 sp ]\n" + " 0x1010124 <_start+4> cmn x19, #8 8 + 8 CPSR => 0x0 [ n z c v q pan il d a i f el sp ]\n" " ► 0x1010128 <_start+8> ✔ b.ne exit \n" " ↓\n" " 0x1010140 mov x0, #0 X0 => 0\n" @@ -735,7 +735,7 @@ def test_aarch64_reference(qemu_start_binary): gdb.execute("auxv", to_string=True) assert ( gdb.execute("cpsr", to_string=True, from_tty=False).strip() - == "cpsr 0x0 [ n z c v q pan il d a i f el:0 sp ]" + == "cpsr 0x0 [ n z c v q pan il d a i f el sp ]" ) gdb.execute("context", to_string=True) gdb.execute("hexdump", to_string=True)