From 4ee3ce2e4f9564684730539a11f1ffbb9ae93710 Mon Sep 17 00:00:00 2001 From: jxuanli <65455765+jxuanli@users.noreply.github.com> Date: Tue, 1 Jul 2025 04:00:22 -0700 Subject: [PATCH] Adding custom address markers (#3123) * added new bitflags to aarch64 * added custom address markers * optimization * supporting buddydump on aarch64 * supporting access to paging level * cleaned up marker handling * cleaned up marker handling further * fixed doc * supporting access to ptr_siz * improved aarch64 vmmap * fixed stuff based on comments * cleaned up * refactored * cleaned up * ldt remap * improved paging commands * improved physmap handling for aarch64 * improved physmap handling for aarch64 when ptrace scope is enabled * improved physmap handling for aarch64 when ptrace scope is enabled * linting * improved caching * cleaned up * cleaned up kernel-vmmap=monitor handling * cleaned up kernel-vmmap=monitor handling * cleaned up vmemmap size calc for aarch64 --- docs/configuration/config.md | 11 - pwndbg/aglib/kernel/__init__.py | 222 +++------ pwndbg/aglib/kernel/paging.py | 451 +++++++++++++++--- pwndbg/aglib/kernel/vmmap.py | 148 ++---- pwndbg/aglib/memory.py | 4 + pwndbg/commands/kbase.py | 2 +- pwndbg/commands/paging.py | 17 +- pwndbg/lib/regs.py | 19 +- .../qemu-system/tests/test_commands_kernel.py | 9 - 9 files changed, 529 insertions(+), 354 deletions(-) diff --git a/docs/configuration/config.md b/docs/configuration/config.md index 8f11a3043..27ae9c7ba 100644 --- a/docs/configuration/config.md +++ b/docs/configuration/config.md @@ -581,17 +581,6 @@ The soft line width for go-dump pretty printing. ---------- -## **guess-physmap** - - -Should guess physmap base address when debug symbols are not present. - - - -**Default:** off - ----------- - ## **hexdump-bytes** diff --git a/pwndbg/aglib/kernel/__init__.py b/pwndbg/aglib/kernel/__init__.py index adf199793..a6f848f35 100644 --- a/pwndbg/aglib/kernel/__init__.py +++ b/pwndbg/aglib/kernel/__init__.py @@ -26,6 +26,8 @@ import pwndbg.lib.kernel.kconfig import pwndbg.lib.kernel.structs import pwndbg.lib.memory import pwndbg.search +from pwndbg.aglib.kernel.paging import ArchPagingInfo +from pwndbg.lib.regs import BitFlags _kconfig: pwndbg.lib.kernel.kconfig.Kconfig | None = None @@ -116,7 +118,7 @@ def get_first_kernel_ro() -> pwndbg.lib.memory.Page | None: if base is None: return None - for mapping in pwndbg.aglib.vmmap.get(): + for mapping in pwndbg.aglib.kernel.paging.get_memory_map_raw(): if mapping.vaddr < base: continue @@ -200,11 +202,6 @@ def is_kaslr_enabled() -> bool: return "nokaslr" not in kcmdline() -@pwndbg.lib.cache.cache_until("start") -def kbase() -> int | None: - return pwndbg.aglib.kernel.paging.kbase() - - def get_idt_entries() -> List[pwndbg.lib.kernel.structs.IDTEntry]: """ Retrieves the IDT entries from memory. @@ -234,10 +231,6 @@ class ArchOps(ABC): # use through kernel configuration, enabling support for additional models # in the page_to_pfn() and pfn_to_page() methods in the future. - @abstractmethod - def page_size(self) -> int: - raise NotImplementedError() - @abstractmethod def per_cpu(self, addr: pwndbg.dbg_mod.Value, cpu=None) -> pwndbg.dbg_mod.Value: raise NotImplementedError() @@ -266,10 +259,40 @@ class ArchOps(ABC): def page_to_pfn(self, page: int) -> int: raise NotImplementedError() + @property + @pwndbg.lib.cache.cache_until("start") + def STRUCT_PAGE_SIZE(self): + return arch_paginginfo().STRUCT_PAGE_SIZE + + @property + @pwndbg.lib.cache.cache_until("start") + def STRUCT_PAGE_SHIFT(self): + return arch_paginginfo().STRUCT_PAGE_SHIFT + @property def page_offset(self) -> int: + return arch_paginginfo().physmap + + @property + def page_shift(self) -> int: + return arch_paginginfo().page_shift + + @property + def vmemmap(self) -> int: + return arch_paginginfo().vmemmap + + @property + def kbase(self) -> int: + return arch_paginginfo().kbase + + @property + def ptr_size(self) -> int: raise NotImplementedError() + @property + def page_size(self) -> int: + return 1 << self.page_shift + def virt_to_pfn(self, virt: int) -> int: return phys_to_pfn(virt_to_phys(virt)) @@ -293,9 +316,6 @@ class ArchOps(ABC): class x86Ops(ArchOps): - def page_size(self) -> int: - return 1 << self.page_shift - def phys_to_virt(self, phys: int) -> int: return (phys + self.page_offset) % (1 << self.ptr_size) @@ -310,41 +330,16 @@ class x86Ops(ArchOps): def ptr_size(self) -> int: raise NotImplementedError() - @property - @abstractmethod - def page_shift(self) -> int: - raise NotImplementedError() - - @property - @abstractmethod - def page_offset(self) -> int: - raise NotImplementedError() - @staticmethod def paging_enabled() -> bool: return int(pwndbg.aglib.regs.cr0) & BIT(31) != 0 class i386Ops(x86Ops): - @requires_kconfig() - def __init__(self) -> None: - # https://elixir.bootlin.com/linux/v6.2/source/arch/x86/include/asm/page_32_types.h#L18 - self._PAGE_OFFSET = int(kconfig()["CONFIG_PAGE_OFFSET"], 16) - self.START_KERNEL_map = self._PAGE_OFFSET - @property def ptr_size(self) -> int: return 32 - @property - def page_offset(self) -> int: - return self._PAGE_OFFSET - - @property - def page_shift(self) -> int: - # https://elixir.bootlin.com/linux/v6.2/source/arch/x86/include/asm/page_types.h#L10 - return 12 - def virt_to_phys(self, virt: int) -> int: return (virt - self.page_offset) % (1 << 32) @@ -360,51 +355,12 @@ class i386Ops(x86Ops): class x86_64Ops(x86Ops): def __init__(self) -> None: - self.STRUCT_PAGE_SIZE = pwndbg.aglib.typeinfo.load("struct page").sizeof - self.STRUCT_PAGE_SHIFT = int(math.log2(self.STRUCT_PAGE_SIZE)) self.phys_base = 0x1000000 - try: - self.START_KERNEL_map = pwndbg.aglib.kernel.kbase() - except Exception: - print("WARNING: an error ocurred when retrieving kbase") - self.START_KERNEL_map = None - - if self.START_KERNEL_map is None: - # put this here in case kbase also returns None - self.START_KERNEL_map = 0xFFFFFFFF80000000 - - if pwndbg.aglib.kernel.has_debug_syms(): - # if there are debug symbols - self._PAGE_OFFSET = pwndbg.aglib.kernel.paging.physmap_base() - self.VMEMMAP_START = pwndbg.aglib.symbol.lookup_symbol_value("vmemmap_base") - if self._PAGE_OFFSET is not None and self.VMEMMAP_START is not None: - return - - if self.uses_5lvl_paging(): - # https://elixir.bootlin.com/linux/v6.2/source/arch/x86/include/asm/page_64_types.h#L41 - self._PAGE_OFFSET = 0xFF11000000000000 - # https://elixir.bootlin.com/linux/v6.2/source/arch/x86/include/asm/pgtable_64_types.h#L131 - self.VMEMMAP_START = 0xFFD4000000000000 - else: - # https://elixir.bootlin.com/linux/v6.2/source/arch/x86/include/asm/page_64_types.h#L42 - self._PAGE_OFFSET = 0xFFFF888000000000 - # https://elixir.bootlin.com/linux/v6.2/source/arch/x86/include/asm/pgtable_64_types.h#L130 - self.VMEMMAP_START = 0xFFFFEA0000000000 - @property def ptr_size(self) -> int: return 64 - @property - def page_offset(self) -> int: - return self._PAGE_OFFSET - - @property - def page_shift(self) -> int: - # https://elixir.bootlin.com/linux/v6.2/source/arch/x86/include/asm/page_64_types.h#L50 - return 12 - @requires_debug_syms() def per_cpu(self, addr: pwndbg.dbg_mod.Value, cpu: int | None = None) -> pwndbg.dbg_mod.Value: if cpu is None: @@ -418,78 +374,25 @@ class x86_64Ops(x86Ops): return pwndbg.dbg.selected_inferior().create_value(per_cpu_addr, addr.type) def virt_to_phys(self, virt: int) -> int: - if virt < self.START_KERNEL_map: + if virt < self.kbase: return (virt - self.page_offset) % (1 << 64) - return ((virt - self.START_KERNEL_map) + self.phys_base) % (1 << 64) + return ((virt - self.kbase) + self.phys_base) % (1 << 64) def pfn_to_page(self, pfn: int) -> int: # assumption: SPARSEMEM_VMEMMAP memory model used # FLATMEM or SPARSEMEM not (yet) implemented - return (pfn << self.STRUCT_PAGE_SHIFT) + self.VMEMMAP_START + return (pfn << self.STRUCT_PAGE_SHIFT) + self.vmemmap def page_to_pfn(self, page: int) -> int: # assumption: SPARSEMEM_VMEMMAP memory model used # FLATMEM or SPARSEMEM not (yet) implemented - return (page - self.VMEMMAP_START) >> self.STRUCT_PAGE_SHIFT - - @staticmethod - @requires_debug_syms() - def cpu_feature_capability(feature: int) -> bool: - boot_cpu_data = pwndbg.aglib.symbol.lookup_symbol("boot_cpu_data") - assert boot_cpu_data is not None, "Symbol boot_cpu_data not exists" - boot_cpu_data = boot_cpu_data.dereference() - - capabilities = boot_cpu_data["x86_capability"] - return (int(capabilities[feature // 32]) >> (feature % 32)) & 1 == 1 - - @staticmethod - @requires_debug_syms() - def uses_5lvl_paging() -> bool: - # https://elixir.bootlin.com/linux/v6.2/source/arch/x86/include/asm/cpufeatures.h#L381 - X86_FEATURE_LA57 = 16 * 32 + 16 - # Separate to avoid using kconfig if possible - if not x86_64Ops.cpu_feature_capability(X86_FEATURE_LA57) or "no5lvl" in kcmdline(): - return False - return x86_64Ops._kconfig_5lvl_paging() - - @staticmethod - @requires_kconfig() - def _kconfig_5lvl_paging() -> bool: - return kconfig().get("CONFIG_X86_5LEVEL") == "y" + return (page - self.vmemmap) >> self.STRUCT_PAGE_SHIFT class Aarch64Ops(ArchOps): - @requires_kconfig(default={}) - def __init__(self) -> None: - page_type = pwndbg.aglib.typeinfo.load("struct page") - assert page_type is not None, "Type 'struct page' not exists" - - self.STRUCT_PAGE_SIZE = page_type.sizeof - self.STRUCT_PAGE_SHIFT = int(math.log2(self.STRUCT_PAGE_SIZE)) - - self.VA_BITS = int(kconfig()["ARM64_VA_BITS"]) - self.PAGE_SHIFT = int(kconfig()["CONFIG_ARM64_PAGE_SHIFT"]) - - addr = pwndbg.aglib.symbol.lookup_symbol_addr("memstart_addr") - assert addr is not None, "Symbol memstart_addr not exists" - - self.PHYS_OFFSET = pwndbg.aglib.memory.u(addr) - self.PAGE_OFFSET = (-1 << self.VA_BITS) + 2**64 - - VA_BITS_MIN = 48 if self.VA_BITS > 48 else self.VA_BITS - PAGE_END = (-1 << (VA_BITS_MIN - 1)) + 2**64 - VMEMMAP_SIZE = (PAGE_END - self.PAGE_OFFSET) >> (self.PAGE_SHIFT - self.STRUCT_PAGE_SHIFT) - - if pwndbg.aglib.kernel.krelease() >= (5, 11): - # Linux 5.11 changed the calculation for VMEMMAP_START - # https://elixir.bootlin.com/linux/v5.11/source/arch/arm64/include/asm/memory.h#L53 - self.VMEMMAP_SHIFT = self.PAGE_SHIFT - self.STRUCT_PAGE_SHIFT - self.VMEMMAP_START = -(1 << (self.VA_BITS - self.VMEMMAP_SHIFT)) % (1 << 64) - else: - self.VMEMMAP_START = (-VMEMMAP_SIZE - 2 * 1024 * 1024) + 2**64 - - def page_size(self) -> int: - return 1 << self.PAGE_SHIFT + @property + def ptr_size(self): + return 64 @requires_debug_syms() def per_cpu(self, addr: pwndbg.dbg_mod.Value, cpu: int | None = None) -> pwndbg.dbg_mod.Value: @@ -504,32 +407,46 @@ class Aarch64Ops(ArchOps): return pwndbg.dbg.selected_inferior().create_value(per_cpu_addr, addr.type) def virt_to_phys(self, virt: int) -> int: - return virt - self.PAGE_OFFSET + return virt - self.page_offset def phys_to_virt(self, phys: int) -> int: - return phys + self.PAGE_OFFSET + return phys + self.page_offset def phys_to_pfn(self, phys: int) -> int: - return phys >> self.PAGE_SHIFT + return phys >> self.page_shift def pfn_to_phys(self, pfn: int) -> int: - return pfn << self.PAGE_SHIFT + return pfn << self.page_shift def pfn_to_page(self, pfn: int) -> int: # assumption: SPARSEMEM_VMEMMAP memory model used # FLATMEM or SPARSEMEM not (yet) implemented - return (pfn << self.STRUCT_PAGE_SHIFT) + self.VMEMMAP_START + return (pfn << self.STRUCT_PAGE_SHIFT) + self.vmemmap def page_to_pfn(self, page: int) -> int: # assumption: SPARSEMEM_VMEMMAP memory model used # FLATMEM or SPARSEMEM not (yet) implemented - return (page - self.VMEMMAP_START) >> self.STRUCT_PAGE_SHIFT + return (page - self.vmemmap) >> self.STRUCT_PAGE_SHIFT @staticmethod def paging_enabled() -> bool: return int(pwndbg.aglib.regs.SCTLR) & BIT(0) != 0 +_arch_paginginfo: ArchPagingInfo = None + + +@pwndbg.lib.cache.cache_until("start") +def arch_paginginfo() -> ArchPagingInfo: + global _arch_paginginfo + if _arch_paginginfo is None: + if pwndbg.aglib.arch.name == "aarch64": + _arch_paginginfo = pwndbg.aglib.kernel.paging.Aarch64PagingInfo() + elif pwndbg.aglib.arch.name == "x86-64": + _arch_paginginfo = pwndbg.aglib.kernel.paging.x86_64PagingInfo() + return _arch_paginginfo + + _arch_ops: ArchOps = None @@ -547,10 +464,18 @@ def arch_ops() -> ArchOps: return _arch_ops +def ptr_size() -> int: + ops = arch_ops() + if ops: + return ops.ptr_size + else: + raise NotImplementedError() + + def page_size() -> int: ops = arch_ops() if ops: - return ops.page_size() + return ops.page_size else: raise NotImplementedError() @@ -667,6 +592,15 @@ def virt_to_pfn(virt: int) -> int: raise NotImplementedError() +@pwndbg.lib.cache.cache_until("stop") +def kbase() -> int | None: + ops = arch_ops() + if ops: + return ops.kbase + 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 af586800c..2d9945b39 100644 --- a/pwndbg/aglib/kernel/paging.py +++ b/pwndbg/aglib/kernel/paging.py @@ -1,5 +1,6 @@ from __future__ import annotations +import math from typing import List from typing import Tuple @@ -8,97 +9,415 @@ import pwndbg.aglib.vmmap_custom import pwndbg.color.message as M import pwndbg.lib.cache import pwndbg.lib.memory -from pwndbg import config 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 -@pwndbg.lib.cache.cache_until("start", "stop") +@pwndbg.lib.cache.cache_until("stop") def get_memory_map_raw() -> Tuple[pwndbg.lib.memory.Page, ...]: return pwndbg.aglib.kernel.vmmap.kernel_vmmap(False) -def find_kbase(pages) -> int | None: - arch_name = pwndbg.aglib.arch.name +def guess_physmap(): + # this is mostly true + # https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt + for page in get_memory_map_raw(): + if page.start and pwndbg.aglib.memory.is_kernel(page.start): + return page.start + return INVALID_ADDR - address = 0 - if arch_name == "x86-64": - address = pwndbg.aglib.kernel.get_idt_entries()[0].offset - elif arch_name == "aarch64": - address = pwndbg.aglib.regs.vbar - else: +class ArchPagingInfo: + USERLAND = "userland" + KERNELLAND = "kernel [.text]" + KERNELRO = "kernel [.rodata]" + KERNELBSS = "kernel [.bss]" + KERNELDRIVER = "kernel [.driver .bpf]" + ESPSTACK = "espfix" + PHYSMAP = "physmap" + VMALLOC = "vmalloc" + VMEMMAP = "vmemmap" + + physmap: int + vmalloc: int + vmemmap: int + kbase: int + addr_marker_sz: int + + @property + @pwndbg.lib.cache.cache_until("start") + def STRUCT_PAGE_SIZE(self): + a = pwndbg.aglib.typeinfo.load("struct page") + if a is None: + # this has been the case for all v5 and v6 releases + return 0x40 + return a.sizeof + + @property + @pwndbg.lib.cache.cache_until("start") + def STRUCT_PAGE_SHIFT(self): + return int(math.log2(self.STRUCT_PAGE_SIZE)) + + @property + def page_shift(self) -> int: + raise NotImplementedError() + + @property + def paging_level(self) -> int: + raise NotImplementedError() + + def adjust(self, name: str) -> str: + raise NotImplementedError() + + def markers(self) -> Tuple[Tuple[str, int], ...]: + raise NotImplementedError() + + def handle_kernel_pages(self, pages): + # this is arch dependent + raise NotImplementedError() + + def kbase_helper(self, address): + for mapping in get_memory_map_raw(): + # should be page aligned -- either from pt-dump or info mem + + # only search in kernel mappings: + # https://www.kernel.org/doc/html/v5.3/arm64/memory.html + if not pwndbg.aglib.memory.is_kernel(mapping.vaddr): + continue + + if address in mapping: + return mapping.vaddr + return None - mappings = pages - for mapping in mappings: - # should be page aligned -- either from pt-dump or info mem - # only search in kernel mappings: - # https://www.kernel.org/doc/html/v5.3/arm64/memory.html - if mapping.vaddr & (0xFFFF << 48) == 0: - continue +class x86_64PagingInfo(ArchPagingInfo): + @property + @pwndbg.lib.cache.cache_until("stop") + def physmap(self): + pob = pwndbg.aglib.symbol.lookup_symbol_addr("page_offset_base") + result = None + if pob is not None: + if pwndbg.aglib.memory.peek(pob): + result = pwndbg.aglib.memory.u64(pob) + if result is None: + return guess_physmap() + return result - if not mapping.execute: - continue + @property + @pwndbg.lib.cache.cache_until("stop") + def kbase(self): + return self.kbase_helper(pwndbg.aglib.kernel.get_idt_entries()[0].offset) - if address in mapping: - return mapping.vaddr + @property + def page_shift(self) -> int: + return 12 - return None + @property + @pwndbg.lib.cache.cache_until("stop") + def vmalloc(self): + addr = pwndbg.aglib.symbol.lookup_symbol_addr("vmalloc_base") + if addr: + return pwndbg.aglib.memory.u64(addr) + return None + @property + @pwndbg.lib.cache.cache_until("stop") + def vmemmap(self): + addr = pwndbg.aglib.symbol.lookup_symbol_addr("vmemmap_base") + if addr: + return pwndbg.aglib.memory.u64(addr) + # resort to default + if self.paging_level == 5: + return 0xFFD4000000000000 + return 0xFFFFEA0000000000 -@pwndbg.aglib.proc.OnlyWithArch(["x86-64"]) -def uses_5lvl_paging() -> bool: - if pwndbg.aglib.kernel.has_debug_syms(): - ops: pwndbg.aglib.kernel.x86_64Ops = pwndbg.aglib.kernel.arch_ops() - return ops.uses_5lvl_paging() - pages = get_memory_map_raw() - for page in pages: - if page.start & (1 << 63) > 0: - return page.start < (0xFFF << (4 * 13)) - return False - - -guess_physmap = config.add_param( - "guess-physmap", - False, - "Should guess physmap base address when debug symbols are not present", -) - - -def physmap_base() -> int: - if pwndbg.aglib.kernel.has_debug_syms() and pwndbg.aglib.arch.name == "x86-64": - result = pwndbg.aglib.symbol.lookup_symbol_addr("page_offset_base") - if pwndbg.aglib.memory.peek(result): - result = pwndbg.aglib.memory.u64(result) - else: + @property + def paging_level(self) -> int: + if pwndbg.aglib.kernel.has_debug_syms(): + # https://elixir.bootlin.com/linux/v6.2/source/arch/x86/include/asm/cpufeatures.h#L381 + X86_FEATURE_LA57 = 16 * 32 + 16 + feature = X86_FEATURE_LA57 + # Separate to avoid using kconfig if possible + boot_cpu_data = pwndbg.aglib.symbol.lookup_symbol("boot_cpu_data") + assert boot_cpu_data is not None, "Symbol boot_cpu_data not exists" + boot_cpu_data = boot_cpu_data.dereference() + + capabilities = boot_cpu_data["x86_capability"] + cpu_feature_capability = (int(capabilities[feature // 32]) >> (feature % 32)) & 1 == 1 + if not cpu_feature_capability or "no5lvl" in pwndbg.aglib.kernel.kcmdline(): + return 4 + return 5 + # CONFIG_X86_5LEVEL is only a hint -- whether 5lvl paging is used depends on the hardware + # see also: https://www.kernel.org/doc/html/next/x86/x86_64/mm.html + pages = get_memory_map_raw() + for page in pages: + if pwndbg.aglib.memory.is_kernel(page.start): + if page.start < (0xFFF << (4 * 13)): + return 5 + return 4 + + @pwndbg.lib.cache.cache_until("stop") + def markers(self) -> Tuple[Tuple[str, int], ...]: + return ( + (self.USERLAND, 0), + (None, 0x8000000000000000), + ("ldt remap", 0xFFFF880000000000 if self.paging_level == 4 else 0xFF10000000000000), + (self.PHYSMAP, self.physmap), + (self.VMALLOC, self.vmalloc), + (self.VMEMMAP, self.vmemmap), + # TODO: find better ways to handle the following constants + # I cound not find kernel symbols that reference their values + # the actual region base may differ but the region always falls within the below range + # even if KASLR is enabled + ("cpu entry", 0xFFFFFE0000000000), + (self.ESPSTACK, 0xFFFFFF0000000000), + ("EFI", 0xFFFFFFEF00000000), + (self.KERNELLAND, self.kbase), + ("fixmap", 0xFFFFFFFFFF000000), + ("legacy abi", 0xFFFFFFFFFF600000), + (None, 0xFFFFFFFFFFFFFFFF), + ) + + def adjust(self, name): + name = name.lower() + if "low kernel" in name: + return self.PHYSMAP + if "high kernel" in name: + return self.KERNELLAND + if self.VMALLOC in name: + return self.VMALLOC + if self.VMEMMAP in name: + return self.VMEMMAP + if " area" in name: + return name[:-5] + return name + + def handle_kernel_pages(self, pages): + kernel_idx = None + for i, page in enumerate(pages): + if kernel_idx is None and self.kbase in page: + kernel_idx = i + kbase = self.kbase + if kernel_idx is None: + return + has_loadable_driver = False + for i in range(kernel_idx, len(pages)): + page = pages[i] + if page.objfile != self.KERNELLAND: + break + if not page.execute: + if page.write: + page.objfile = self.KERNELBSS + else: + page.objfile = self.KERNELRO + if has_loadable_driver: + page.objfile = self.KERNELDRIVER + if page.execute and page.start != kbase: + page.objfile = self.KERNELDRIVER + has_loadable_driver = True + if pwndbg.aglib.regs[pwndbg.aglib.regs.stack] in page: + page.objfile = "kernel [stack]" + + +class Aarch64PagingInfo(ArchPagingInfo): + def __init__(self): + self.tcr_el1 = pwndbg.lib.regs.aarch64_tcr_flags + self.tcr_el1.value = pwndbg.aglib.regs.TCR_EL1 + # TODO: this is probably not entirely correct + # https://elixir.bootlin.com/linux/v6.16-rc2/source/arch/arm64/include/asm/memory.h#L56 + self.va_bits = 64 - self.tcr_el1["T1SZ"] # this is prob only `vabits_actual` + self.va_bits_min = 48 if self.va_bits > 48 else self.va_bits + # https://elixir.bootlin.com/linux/v6.13.12/source/arch/arm64/include/asm/memory.h#L47 + module_start_wo_kaslr = (-1 << (self.va_bits_min - 1)) + 2**64 + 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 + + @property + @pwndbg.lib.cache.cache_until("stop") + def physmap(self): + # addr = pwndbg.aglib.symbol.lookup_symbol_addr("memstart_addr") + # if addr is None: + # return guess_physmap() + # return pwndbg.aglib.memory.u(addr) + return guess_physmap() + + @property + @pwndbg.lib.cache.cache_until("stop") + def kbase(self): + return self.kbase_helper(pwndbg.aglib.regs.vbar) + + @property + @pwndbg.lib.cache.cache_until("stop") + def kversion(self): + try: + return pwndbg.aglib.kernel.krelease() + except Exception: return None - if result is not None: - return result - if guess_physmap or pwndbg.aglib.arch.name == "aarch64": - # this is mostly true - # https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt + + @property + @pwndbg.lib.cache.cache_until("stop") + def physmap_end(self): + res = None + for page in get_memory_map_raw(): + if page.end >= self.vmalloc: + break + res = page.end + if res is None: + return INVALID_ADDR + return res + + @property + @pwndbg.lib.cache.cache_until("stop") + def module_start(self): + # this is only used for marking the end of module_start + self.module_end = -1 + res = None for page in get_memory_map_raw(): - if page.start & (1 << 63) > 0: - return page.start - print(M.warn("physmap base cannot be determined, resort to default")) - if uses_5lvl_paging(): - return 0xFF11000000000000 - return 0xFFFF888000000000 + if page.start >= self.kbase: + break + if page.execute: + res = page.start + if res is None: + return INVALID_ADDR + prev = None + for page in get_memory_map_raw(): + if page.start >= res: + if prev is not None and page.start > prev + 0x1000: + break + prev = self.module_end = page.end + return res + + @property + @pwndbg.lib.cache.cache_until("stop") + def vmemmap(self): + if self.kversion is None: + return None + if self.kversion >= (6, 9): + # https://elixir.bootlin.com/linux/v6.16-rc2/source/arch/arm64/include/asm/memory.h#L33 + return (-0x40000000 % INVALID_ADDR) - self.VMEMMAP_SIZE + if self.kversion >= (5, 11): + # Linux 5.11 changed the calculation for VMEMMAP_START + # https://elixir.bootlin.com/linux/v5.11/source/arch/arm64/include/asm/memory.h#L53 + VMEMMAP_SHIFT = self.page_shift - self.STRUCT_PAGE_SHIFT + return -(1 << (self.va_bits - VMEMMAP_SHIFT)) % INVALID_ADDR + return (-self.VMEMMAP_SIZE - 2 * 1024 * 1024) + 2**64 + + @property + @pwndbg.lib.cache.cache_until("stop") + def pci(self): + if self.kversion is None: + return None + self.pci_end = INVALID_ADDR + if self.kversion >= (6, 9): + pci = self.vmemmap + self.VMEMMAP_SIZE + 0x00800000 + self.pci_end = pci + 0x01000000 + return pci + if self.kversion >= (5, 11): + self.pci_end = self.vmemmap - 0x00800000 + return self.pci_end - 0x01000000 + self.pci_end = self.vmemmap - 0x00200000 + return self.pci_end - 0x01000000 + @property + @pwndbg.lib.cache.cache_until("stop") + def ksize(self): + start = pwndbg.aglib.symbol.lookup_symbol_addr("_text") + end = pwndbg.aglib.symbol.lookup_symbol_addr("_end") + if start is not None and end is not None: + return end - start + # fallback + return 100 << 21 # 100M + + @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: + return 14 + elif self.tcr_el1["TG1"] == 0: + return 12 + else: + return 16 + + @pwndbg.lib.cache.cache_until("stop") + def markers(self) -> Tuple[Tuple[str, int], ...]: + address_markers = pwndbg.aglib.symbol.lookup_symbol_addr("address_markers") + if address_markers is not None: + sections = [(self.USERLAND, 0)] + value = 0 + name = None + for i in range(20): + value = pwndbg.aglib.memory.u64(address_markers + i * 0x10) + name_ptr = pwndbg.aglib.memory.u64(address_markers + i * 0x10 + 8) + name = None + if name_ptr > 0: + name = pwndbg.aglib.memory.string(name_ptr).decode() + name = self.adjust(name) + if value > 0: + sections.append((name, value)) + if value == 0xFFFFFFFFFFFFFFFF: + break + return tuple(sections) + if self.kversion is None: + return () + return ( + (self.USERLAND, 0), + (None, 0x8000000000000000), + (self.PHYSMAP, self.physmap), + (None, self.physmap_end), + (self.VMALLOC, self.vmalloc), + (self.VMEMMAP, self.vmemmap), + (None, self.vmemmap + self.VMEMMAP_SIZE), + ("pci", self.pci), + (None, self.pci_end), + # TODO: prob not entirely correct but the computation is too complicated + ("fixmap", self.pci_end), + (None, 0xFFFFFFFFFFFFFFFF), + ) + + def adjust(self, name): + name = name.lower() + if "end" in name: + return None + if "linear" in name: + return self.PHYSMAP + if "modules" in name: + return self.KERNELDRIVER + if self.VMEMMAP in name: + return self.VMEMMAP + if self.VMALLOC in name: + return self.VMALLOC + return " ".join(name.strip().split()[:-1]) -@pwndbg.lib.cache.cache_until("start") -def kbase(): - return find_kbase(get_memory_map_raw()) + def handle_kernel_pages(self, pages): + for i in range(len(pages)): + page = pages[i] + if page.start < self.module_start or page.start > self.kbase + self.ksize: + continue + if self.module_start <= page.start < self.module_end: + page.objfile = self.KERNELDRIVER + continue + if page.start < self.kbase: + continue + page.objfile = self.KERNELLAND + if not page.execute: + if page.write: + page.objfile = self.KERNELBSS + else: + page.objfile = self.KERNELRO + 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 = 4 - if uses_5lvl_paging(): - level = 5 - base = physmap_base() + 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: @@ -124,5 +443,5 @@ def pagewalk(target, entry=None) -> List[Tuple[int | None, int | None]]: if entry == 0: return result result[i] = (entry, vaddr) - result[0] = (None, (entry & ENTRYMASK) + base + offset) + result[0] = (entry, (entry & ENTRYMASK) + base + offset) return result diff --git a/pwndbg/aglib/kernel/vmmap.py b/pwndbg/aglib/kernel/vmmap.py index 1cacded24..8e0fa4cc1 100644 --- a/pwndbg/aglib/kernel/vmmap.py +++ b/pwndbg/aglib/kernel/vmmap.py @@ -28,73 +28,13 @@ import pwndbg.lib.memory class KernelVmmap: - USERLAND = "userland" - KERNELLAND = "kernel [.text]" - KERNELRO = "kernel [.rodata]" - KERNELBSS = "kernel [.bss]" - KERNELDRIVER = "kernel [.driver .bpf]" - ESPSTACK = "%esp fixup" - def __init__(self, pages: Tuple[pwndbg.lib.memory.Page, ...]): self.pages = pages self.sections = None + self.pi = pwndbg.aglib.kernel.arch_paginginfo() if not pwndbg.aglib.kernel.has_debug_syms(): return - self.kbase = kbase = pwndbg.aglib.kernel.paging.find_kbase(pages) - if pwndbg.aglib.arch.name == "x86-64": - # https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt - # works for v5.x and v6.x - physmap = pwndbg.aglib.kernel.paging.physmap_base() - if physmap is None: # what?? - return - vmalloc = pwndbg.aglib.symbol.lookup_symbol_value("vmalloc_base") - vmemmap = pwndbg.aglib.symbol.lookup_symbol_value("vmemmap_base") - self.sections = ( - (self.USERLAND, 0), - (None, 0x8000000000000000), - ("physmap", physmap), - ("vmalloc", vmalloc), - ("vmemmap", vmemmap), - # TODO: find better ways to handle the following constants - # I cound not find kernel symbols that reference their values - # the actual region base may differ but the region always falls within the below range - # even if KASLR is enabled - ("cpu entry", 0xFFFFFE0000000000), - ("rand cpu entry", 0xFFFFFE0000001000), - (self.ESPSTACK, 0xFFFFFF0000000000), - ("EFI", 0xFFFFFFEF00000000), - (self.KERNELLAND, kbase), - ("fixmap", 0xFFFFFFFFFF000000), - ("legacy abi", 0xFFFFFFFFFF600000), - (None, 0xFFFFFFFFFFFFFFFF), - ) - if pwndbg.aglib.arch.name == "aarch64": - # https://www.kernel.org/doc/html/v5.3/arm64/memory.html - # https://elixir.bootlin.com/linux/v6.15/source/arch/arm64/mm/ptdump.c#L351 - # TODO: I don't think those are necessarily accurate when KASLR is enabled - # but I'm not familiar with ARM enough quite yet to find better ways - address_markers = pwndbg.aglib.symbol.lookup_symbol_addr("address_markers") - if address_markers is None: - return - sections = [(self.USERLAND, 0)] - value = 0 - name = None - for i in range(20): - value = pwndbg.aglib.memory.u64(address_markers + i * 0x10) - if value > 0: - name_ptr = pwndbg.aglib.memory.u64(address_markers + i * 0x10 + 8) - name = None - if name_ptr > 0: - name = pwndbg.aglib.memory.string(name_ptr).decode() - name = name.split(" ")[0].lower() - if "end" in name: - name = None - if name == "linear": - name = "physmap" - sections.append((name, value)) - if value == 0xFFFFFFFFFFFFFFFF: - break - self.sections = tuple(sections) + self.sections = self.pi.markers() def get_name(self, addr: int) -> str: if addr is None or self.sections is None: @@ -109,27 +49,21 @@ class KernelVmmap: return None def adjust(self): + if self.pages is None or len(self.pages) == 0: + return for i, page in enumerate(self.pages): name = self.get_name(page.start) if name is not None: page.objfile = name - user_idx, kernel_idx = None, None - for i, page in enumerate(self.pages): - if user_idx is None and page.objfile == self.USERLAND: - user_idx = i - if kernel_idx is None and page.objfile == self.KERNELLAND: - kernel_idx = i - self.handle_user_pages(user_idx) - self.handle_kernel_pages(kernel_idx) + self.handle_user_pages() + self.pi.handle_kernel_pages(self.pages) self.handle_offsets() - def handle_user_pages(self, user_idx): - if user_idx is None: - return - base_offset = self.pages[user_idx].start - for i in range(user_idx, len(self.pages)): + def handle_user_pages(self): + base_offset = self.pages[0].start + for i in range(len(self.pages)): page = self.pages[i] - if page.objfile != self.USERLAND: + if page.objfile != self.pi.USERLAND: break diff = page.start - base_offset if diff > 0x100000: @@ -144,32 +78,11 @@ class KernelVmmap: # page.objfile += f"_{hex(i)[2:]}" base_offset = page.start - def handle_kernel_pages(self, kernel_idx): - if kernel_idx is None: - return - has_loadable_driver = False - for i in range(kernel_idx, len(self.pages)): - page = self.pages[i] - if page.objfile != self.KERNELLAND: - break - if not page.execute: - if page.write: - page.objfile = self.KERNELBSS - else: - page.objfile = self.KERNELRO - if has_loadable_driver: - page.objfile = self.KERNELDRIVER - if page.execute and page.start != self.kbase: - page.objfile = self.KERNELDRIVER - has_loadable_driver = True - if pwndbg.aglib.regs[pwndbg.aglib.regs.stack] in page: - page.objfile = "kernel [stack]" - def handle_offsets(self): prev_objfile, base = "", 0 for page in self.pages: # the check on KERNELRO is to make getting offsets for symbols such as `init_creds` more convinient - if page.objfile != self.KERNELRO and prev_objfile != page.objfile: + if page.objfile != self.pi.KERNELRO and prev_objfile != page.objfile: prev_objfile = page.objfile base = page.start page.offset = page.start - base @@ -391,13 +304,17 @@ def kernel_vmmap_via_monitor_info_mem() -> Tuple[pwndbg.lib.memory.Page, ...]: global monitor_info_mem_not_warned pages: List[pwndbg.lib.memory.Page] = [] for line in lines: - dash_idx = line.index("-") - space_idx = line.index(" ") - rspace_idx = line.rindex(" ") - - start = int(line[:dash_idx], 16) - end = int(line[dash_idx + 1 : space_idx], 16) - size = int(line[space_idx + 1 : rspace_idx], 16) + try: + dash_idx = line.index("-") + space_idx = line.index(" ") + rspace_idx = line.rindex(" ") + + start = int(line[:dash_idx], 16) + end = int(line[dash_idx + 1 : space_idx], 16) + size = int(line[space_idx + 1 : rspace_idx], 16) + except Exception: + # invalid format + continue if end - start != size and monitor_info_mem_not_warned: print( M.warn( @@ -418,9 +335,8 @@ def kernel_vmmap_via_monitor_info_mem() -> Tuple[pwndbg.lib.memory.Page, ...]: flags |= 4 if "w" in perm: flags |= 2 - if len(perm) == 4: # if the qemu version displays if the page is executable - if "x" in perm: - flags |= 1 + if "x" in perm: + flags |= 1 pages.append(pwndbg.lib.memory.Page(start, size, flags, 0, "")) return tuple(pages) @@ -462,17 +378,19 @@ def kernel_vmmap(process_pages=True) -> Tuple[pwndbg.lib.memory.Page, ...]: pages = kernel_vmmap_via_page_tables() elif kernel_vmmap_mode == "monitor": pages = kernel_vmmap_via_monitor_info_mem() - if process_pages and pwndbg.aglib.arch.name == "x86-64": - # TODO: check version here when QEMU displays the x bit for x64 - for page in pages: - pgwalk_res = pwndbg.aglib.kernel.paging.pagewalk(page.start) - entry, vaddr = pgwalk_res[0] - if entry and entry >> 63 == 0: - page.flags |= 1 if pages is None: return () if process_pages: kv = KernelVmmap(pages) kv.adjust() + if kernel_vmmap_mode == "monitor" and pwndbg.aglib.arch.name == "x86-64": + # TODO: check version here when QEMU displays the x bit for x64 + for page in pages: + if page.objfile == kv.pi.ESPSTACK: + continue + pgwalk_res = pwndbg.aglib.kernel.paging.pagewalk(page.start) + entry, _ = pgwalk_res[0] + if entry and entry >> 63 == 0: + page.flags |= 1 return tuple(pages) diff --git a/pwndbg/aglib/memory.py b/pwndbg/aglib/memory.py index 67ae01192..e6a5763d5 100644 --- a/pwndbg/aglib/memory.py +++ b/pwndbg/aglib/memory.py @@ -441,3 +441,7 @@ def is_pagefault_supported() -> bool: # TODO: use a better detection method return pwndbg.dbg.selected_inferior().is_linux() + + +def is_kernel(addr: int): + return addr >> 63 == 1 diff --git a/pwndbg/commands/kbase.py b/pwndbg/commands/kbase.py index eda8584b5..24fb0c09d 100644 --- a/pwndbg/commands/kbase.py +++ b/pwndbg/commands/kbase.py @@ -26,7 +26,7 @@ def kbase(rebase=False) -> None: print(M.error("kbase does not work when kernel-vmmap is set to none")) return - base = pwndbg.aglib.kernel.kbase() + base = pwndbg.aglib.kernel.arch_paginginfo().kbase if base is None: print(M.error("Unable to locate the kernel base")) diff --git a/pwndbg/commands/paging.py b/pwndbg/commands/paging.py index 6ad4b40e8..f62a0b948 100644 --- a/pwndbg/commands/paging.py +++ b/pwndbg/commands/paging.py @@ -42,7 +42,7 @@ def pg_indices(vaddr, nr_level): def pagewalk(vaddr, entry=None): vaddr = int(pwndbg.dbg.selected_frame().evaluate_expression(vaddr)) # https://blog.zolutal.io/understanding-paging/ - level = 4 + level = pwndbg.aglib.kernel.arch_paginginfo().paging_level names = ( "Page", "PT", @@ -50,8 +50,7 @@ def pagewalk(vaddr, entry=None): "PUD", "PGD", ) - if pwndbg.aglib.kernel.paging.uses_5lvl_paging(): - level = 5 + if level == 5: names = ( "Page", "PT", @@ -70,10 +69,14 @@ def pagewalk(vaddr, entry=None): if vaddr is None: print(M.warn("address is not mapped")) return - phys = vaddr - pwndbg.aglib.kernel.paging.physmap_base() + phys = vaddr - pwndbg.aglib.kernel.arch_paginginfo().physmap print(f"pagewalk result: {C.green(hex(vaddr))} [phys: {C.yellow(hex(phys))}]") +def paging_print_helper(name, addr): + print(f"{C.green(name)}: {C.yellow(hex(pwndbg.aglib.kernel.phys_to_virt(int(addr))))}") + + p2v_parser = argparse.ArgumentParser( description="Translate physical address to its corresponding virtual address." ) @@ -86,7 +89,8 @@ p2v_parser.add_argument("paddr", type=str, help="") @pwndbg.commands.OnlyWhenPagingEnabled def p2v(paddr): paddr = pwndbg.dbg.selected_frame().evaluate_expression(paddr) - return pwndbg.aglib.kernel.phys_to_virt(int(paddr)) + vaddr = pwndbg.aglib.kernel.phys_to_virt(int(paddr)) + paging_print_helper("Virtual address", vaddr) v2p_parser = argparse.ArgumentParser( @@ -101,4 +105,5 @@ v2p_parser.add_argument("vaddr", type=str, help="") @pwndbg.commands.OnlyWhenPagingEnabled def v2p(vaddr): vaddr = pwndbg.dbg.selected_frame().evaluate_expression(vaddr) - return pwndbg.aglib.kernel.virt_to_phys(int(vaddr)) + paddr = pwndbg.aglib.kernel.virt_to_phys(int(vaddr)) + paging_print_helper("Physical address", paddr) diff --git a/pwndbg/lib/regs.py b/pwndbg/lib/regs.py index 81a0b06ce..51506d6d5 100644 --- a/pwndbg/lib/regs.py +++ b/pwndbg/lib/regs.py @@ -27,12 +27,14 @@ class BitFlags: # - aarch64_sctlr_flags is used for "sctlr", "sctlr_el2", "sctlr_el3" regname: str flags: OrderedDict[str, Union[int, Tuple[int, int]]] + value: int - def __init__(self, flags: List[Tuple[str, Union[int, Tuple[int, int]]]] = []): + def __init__(self, flags: List[Tuple[str, Union[int, Tuple[int, int]]]] = [], value=None): self.regname = "" self.flags = {} for name, bits in flags: self.flags[name] = bits + self.value = value def __getattr__(self, name): if name in {"regname"}: @@ -40,7 +42,11 @@ class BitFlags: return getattr(self.flags, name) def __getitem__(self, key): - return self.flags[key] + r = self.flags[key] + if isinstance(r, int): + return (self.value >> r) & 1 + s, e = r + return ((~((1 << s) - 1) & ((1 << e) - 1)) & self.value) >> s def __setitem__(self, key, value): self.flags[key] = value @@ -525,6 +531,14 @@ aarch64_sctlr_flags = BitFlags( ] ) +aarch64_tcr_flags = BitFlags( + [ + ("TG1", (30, 31)), + ("T1SZ", (16, 21)), + ("T0SZ", (0, 5)), + ] +) + aarch64_scr_flags = BitFlags( [ ("HCE", 8), @@ -593,6 +607,7 @@ aarch64 = RegisterSet( "spsr_el1": aarch64_cpsr_flags, "spsr_el2": aarch64_cpsr_flags, "spsr_el3": aarch64_cpsr_flags, + "tcr_el1": aarch64_tcr_flags, }, # 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 176362803..e3e5028db 100644 --- a/tests/library/qemu-system/tests/test_commands_kernel.py +++ b/tests/library/qemu-system/tests/test_commands_kernel.py @@ -193,10 +193,6 @@ def test_command_kernel_vmmap(): @pytest.mark.skipif(not pwndbg.aglib.kernel.has_debug_syms(), reason="test requires debug symbols") -@pytest.mark.skipif( - pwndbg.aglib.arch.name not in ["i386", "x86-64"], - reason="function page_offset is only implemented for x86", -) def test_command_buddydump(): res = gdb.execute("buddydump", to_string=True) NOFREEPAGE = "No free pages with specified filters found.\n" @@ -250,11 +246,6 @@ def test_command_pagewalk(): # no kbase? fine pages = pwndbg.aglib.vmmap.get() address = pages[0].start - if not pwndbg.aglib.kernel.has_debug_syms(): - # even if no debug symbols, still gracefully handle it - res = gdb.execute(f"pagewalk {hex(address)}") - # now let's guess the phymap base and should work as intended - res = gdb.execute("set guess-physmap on") res = gdb.execute(f"pagewalk {hex(address)}", to_string=True) assert "PMD" in res # Page Size is only set for PMDe or PTe res = res.splitlines()[-1]