From 7df87c93c667339bb46aef4718444da9469ea40a Mon Sep 17 00:00:00 2001 From: charif <72693057+chrf01@users.noreply.github.com> Date: Fri, 19 Apr 2024 22:11:39 +0200 Subject: [PATCH] improve kbase (#2097) Fix kbase for newer kernels and allow automatically rebasing kernel symbol information --- poetry.lock | 32 ++++----- pwndbg/commands/kbase.py | 50 ++++++-------- pwndbg/gdblib/kernel/__init__.py | 55 +++++++++++++++ pwndbg/gdblib/proc.py | 35 ++++++++++ pwndbg/gdblib/regs.py | 33 +++++++++ pwndbg/gdblib/vmmap.py | 6 +- pwndbg/lib/kernel/structs.py | 67 +++++++++++++++++++ pyproject.toml | 4 +- .../tests/system/test_commands_kernel.py | 4 -- .../tests/system/test_gdblib_kernel.py | 10 +++ 10 files changed, 241 insertions(+), 55 deletions(-) create mode 100644 pwndbg/lib/kernel/structs.py diff --git a/poetry.lock b/poetry.lock index 478c0f972..82b0d1864 100644 --- a/poetry.lock +++ b/poetry.lock @@ -820,13 +820,13 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -1024,8 +1024,8 @@ develop = false [package.source] type = "git" url = "https://github.com/martinradev/gdb-pt-dump" -reference = "89ea252f6efc5d75eacca16fc17ff8966a389690" -resolved_reference = "89ea252f6efc5d75eacca16fc17ff8966a389690" +reference = "50227bda0b6332e94027f811a15879588de6d5cb" +resolved_reference = "50227bda0b6332e94027f811a15879588de6d5cb" [[package]] name = "ptyprocess" @@ -1445,28 +1445,28 @@ files = [ [[package]] name = "traitlets" -version = "5.14.1" +version = "5.14.2" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, - {file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, + {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, + {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "types-docutils" -version = "0.20.0.20240304" +version = "0.20.0.20240331" description = "Typing stubs for docutils" optional = false python-versions = ">=3.8" files = [ - {file = "types-docutils-0.20.0.20240304.tar.gz", hash = "sha256:c35ae35ca835a5aeead758df411cd46cfb7e7f19f2b223c413dae7e069d5b0be"}, - {file = "types_docutils-0.20.0.20240304-py3-none-any.whl", hash = "sha256:ef02f9d05f2b61500638b1358cdf3fbf975cc5dedaa825a2eb5ea71b7318a760"}, + {file = "types-docutils-0.20.0.20240331.tar.gz", hash = "sha256:ac99cdf34040c982081f54237d6017f8f5dafe0bebb818a598bf97a65f5b1715"}, + {file = "types_docutils-0.20.0.20240331-py3-none-any.whl", hash = "sha256:b9042e1cf064b4a82c87a71ed3c5f0f96e81fb6d402ca4daa6ced65a91397679"}, ] [[package]] @@ -1522,13 +1522,13 @@ urllib3 = ">=2" [[package]] name = "types-setuptools" -version = "69.1.0.20240302" +version = "69.2.0.20240317" description = "Typing stubs for setuptools" optional = false python-versions = ">=3.8" files = [ - {file = "types-setuptools-69.1.0.20240302.tar.gz", hash = "sha256:ed5462cf8470831d1bdbf300e1eeea876040643bfc40b785109a5857fa7d3c3f"}, - {file = "types_setuptools-69.1.0.20240302-py3-none-any.whl", hash = "sha256:99c1053920a6fa542b734c9ad61849c3993062f80963a4034771626528e192a0"}, + {file = "types-setuptools-69.2.0.20240317.tar.gz", hash = "sha256:b607c4c48842ef3ee49dc0c7fe9c1bad75700b071e1018bb4d7e3ac492d47048"}, + {file = "types_setuptools-69.2.0.20240317-py3-none-any.whl", hash = "sha256:cf91ff7c87ab7bf0625c3f0d4d90427c9da68561f3b0feab77977aaf0bbf7531"}, ] [[package]] @@ -1613,4 +1613,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "889ed31a63588e5896e592b71ec89bb28f46e8e6fbbad8d027e375eea1dc1b53" +content-hash = "309e934df3d9b50e3f37c1df586cf5420897949141928d7a0f8ee16ce38e90a2" diff --git a/pwndbg/commands/kbase.py b/pwndbg/commands/kbase.py index e893c1a78..611d36c22 100644 --- a/pwndbg/commands/kbase.py +++ b/pwndbg/commands/kbase.py @@ -6,48 +6,38 @@ import gdb import pwndbg.color.message as M import pwndbg.commands -import pwndbg.gdblib.memory -import pwndbg.gdblib.vmmap +import pwndbg.gdblib.kernel from pwndbg.commands import CommandCategory from pwndbg.gdblib.config import config parser = argparse.ArgumentParser(description="Finds the kernel virtual base address.") +parser.add_argument("-r", "--rebase", action="store_true", help="rebase loaded symbol file") + @pwndbg.commands.ArgparsedCommand(parser, category=CommandCategory.KERNEL) @pwndbg.commands.OnlyWhenQemuKernel @pwndbg.commands.OnlyWhenPagingEnabled -def kbase() -> None: +def kbase(rebase=False) -> None: if config.kernel_vmmap == "none": print(M.error("kbase does not work when kernel-vmmap is set to none")) return - arch_name = pwndbg.gdblib.arch.name - if arch_name == "x86-64": - # First opcode, seems to be consistent - magic = 0x48 - elif arch_name == "aarch64": - # First byte of "MZ" header - magic = 0x4D - else: - print(M.error(f"kbase does not support the {arch_name} architecture")) + base = pwndbg.gdblib.kernel.kbase() + + if base is None: + print(M.error("Unable to locate the kernel base")) + return + + print(M.success(f"Found virtual text base address: {hex(base)}")) + + if not rebase: return - mappings = pwndbg.gdblib.vmmap.get() - for mapping in mappings: - # TODO: Check alignment - # TODO: Check if the supervisor bit is set for aarch64 - if not mapping.execute: - continue - try: - b = pwndbg.gdblib.memory.byte(mapping.vaddr) - except gdb.MemoryError: - print( - M.error( - f"Could not read memory at {mapping.vaddr:#x}. Kernel vmmap may be incorrect." - ) - ) - continue - if b == magic: - print(M.success(f"Found virtual base address: {mapping.vaddr:#x}")) - break + symbol_file = gdb.current_progspace().filename + + if symbol_file: + gdb.execute("symbol-file") + gdb.execute(f"add-symbol-file {symbol_file} {hex(base)}") + else: + print(M.error("No symbol file is currently loaded")) diff --git a/pwndbg/gdblib/kernel/__init__.py b/pwndbg/gdblib/kernel/__init__.py index cb5d71090..fac6e2158 100644 --- a/pwndbg/gdblib/kernel/__init__.py +++ b/pwndbg/gdblib/kernel/__init__.py @@ -8,6 +8,7 @@ from abc import abstractmethod from typing import Any from typing import Callable from typing import Dict +from typing import List from typing import Optional from typing import Tuple @@ -15,9 +16,11 @@ import gdb import pwndbg.color.message as M import pwndbg.gdblib.memory +import pwndbg.gdblib.regs import pwndbg.gdblib.symbol import pwndbg.lib.cache import pwndbg.lib.kernel.kconfig +import pwndbg.lib.kernel.structs _kconfig: pwndbg.lib.kernel.kconfig.Kconfig = None @@ -135,6 +138,58 @@ def is_kaslr_enabled() -> bool: return "nokaslr" not in kcmdline() +@pwndbg.lib.cache.cache_until("start") +def kbase() -> int | None: + arch_name = pwndbg.gdblib.arch.name + + address = 0 + + if arch_name == "x86-64": + address = get_idt_entries()[0].offset + elif arch_name == "aarch64": + address = pwndbg.gdblib.regs.vbar + else: + return None + + mappings = pwndbg.gdblib.vmmap.get() + for mapping in mappings: + # TODO: Check alignment + + # only search in kernel mappings: + # https://www.kernel.org/doc/html/v5.3/arm64/memory.html + if mapping.vaddr & (0xFFFF << 48) == 0: + continue + + if not mapping.execute: + continue + + if address in mapping: + return mapping.vaddr + + return None + + +def get_idt_entries() -> List[pwndbg.lib.kernel.structs.IDTEntry]: + """ + Retrieves the IDT entries from memory. + """ + base = pwndbg.gdblib.regs.idt + limit = pwndbg.gdblib.regs.idt_limit + + size = pwndbg.gdblib.arch.ptrsize * 2 + num_entries = (limit + 1) // size + + entries = [] + + # TODO: read the entire IDT in one call? + for i in range(num_entries): + entry_addr = base + i * size + entry = pwndbg.lib.kernel.structs.IDTEntry(pwndbg.gdblib.memory.read(entry_addr, size)) + entries.append(entry) + + return entries + + class ArchOps(ABC): # More information on the physical memory model of the Linux kernel and # especially the mapping between pages and page frame numbers (pfn) can diff --git a/pwndbg/gdblib/proc.py b/pwndbg/gdblib/proc.py index bb0167b50..8a1499121 100644 --- a/pwndbg/gdblib/proc.py +++ b/pwndbg/gdblib/proc.py @@ -11,6 +11,8 @@ import sys from types import ModuleType from typing import Any from typing import Callable +from typing import List +from typing import Optional from typing import TypeVar import gdb @@ -35,6 +37,8 @@ binary_vmmap: tuple[pwndbg.lib.memory.Page, ...] # dump_relocations_by_section_name: tuple[Relocation, ...] | None # get_section_address_by_name: Callable[[str], int] OnlyWhenRunning: Callable[[Callable[..., T]], Callable[..., T]] +OnlyWhenQemuKernel: Callable[[Callable[..., T]], Callable[..., T]] +OnlyWithArch: Callable[[List[str]], Callable[[Callable[..., T]], Callable[..., Optional[T]]]] class module(ModuleType): @@ -157,6 +161,37 @@ class module(ModuleType): return wrapper + def OnlyWhenQemuKernel(self, func: Callable[..., T]) -> Callable[..., T]: + @functools.wraps(func) + def wrapper(*a: Any, **kw: Any) -> T: + if pwndbg.gdblib.qemu.is_qemu_kernel(): + return func(*a, **kw) + return None + + return wrapper + + def OnlyWithArch( + self, arch_names: List[str] + ) -> Callable[[Callable[..., T]], Callable[..., Optional[T]]]: + """Decorates function to work only with the specified archictectures.""" + for arch in arch_names: + if arch not in pwndbg.gdblib.arch_mod.ARCHS: + raise ValueError( + f"OnlyWithArch used with unsupported arch={arch}. Must be one of {', '.join(arch_names)}" + ) + + def decorator(function: Callable[..., T]) -> Callable[..., Optional[T]]: + @functools.wraps(function) + def _OnlyWithArch(*a: Any, **kw: Any) -> Optional[T]: + if pwndbg.gdblib.arch.name in arch_names: + return function(*a, **kw) + else: + return None + + return _OnlyWithArch + + return decorator + # To prevent garbage collection tether = sys.modules[__name__] diff --git a/pwndbg/gdblib/regs.py b/pwndbg/gdblib/regs.py index 2112e8ce7..2b48fdab5 100644 --- a/pwndbg/gdblib/regs.py +++ b/pwndbg/gdblib/regs.py @@ -20,6 +20,7 @@ import gdb import pwndbg.gdblib.arch import pwndbg.gdblib.events import pwndbg.gdblib.proc +import pwndbg.gdblib.qemu import pwndbg.gdblib.remote import pwndbg.lib.cache from pwndbg.lib.regs import BitFlags @@ -36,6 +37,24 @@ def gdb_get_register(name: str) -> gdb.Value: return frame.read_register(name.upper()) +@pwndbg.gdblib.proc.OnlyWhenQemuKernel +@pwndbg.gdblib.proc.OnlyWhenRunning +def get_qemu_register(name: str) -> int: + out = gdb.execute("monitor info registers", to_string=True) + match = re.search(rf'{name.split("_")[0]}=\s+([\da-fA-F]+)\s+([\da-fA-F]+)', out) + + if match: + base = int(match.group(1), 16) + limit = int(match.group(2), 16) + + if name.endswith("LIMIT"): + return limit + else: + return base + + return None + + # We need to manually make some ptrace calls to get fs/gs bases on Intel PTRACE_ARCH_PRCTL = 30 ARCH_GET_FS = 0x1003 @@ -174,6 +193,20 @@ class module(ModuleType): delta.append(reg) return delta + @property + @pwndbg.gdblib.proc.OnlyWhenQemuKernel + @pwndbg.gdblib.proc.OnlyWithArch(["i386", "x86-64"]) + @pwndbg.lib.cache.cache_until("stop") + def idt(self) -> int: + return get_qemu_register("IDT") + + @property + @pwndbg.gdblib.proc.OnlyWhenQemuKernel + @pwndbg.gdblib.proc.OnlyWithArch(["i386", "x86-64"]) + @pwndbg.lib.cache.cache_until("stop") + def idt_limit(self) -> int: + return get_qemu_register("IDT_LIMIT") + @property @pwndbg.lib.cache.cache_until("stop") def fsbase(self) -> int: diff --git a/pwndbg/gdblib/vmmap.py b/pwndbg/gdblib/vmmap.py index cc1295da0..cca813f54 100644 --- a/pwndbg/gdblib/vmmap.py +++ b/pwndbg/gdblib/vmmap.py @@ -494,11 +494,11 @@ def proc_tid_maps() -> Tuple[pwndbg.lib.memory.Page, ...] | None: @pwndbg.lib.cache.cache_until("stop") def kernel_vmmap_via_page_tables() -> Tuple[pwndbg.lib.memory.Page, ...]: - import pt + import pt_gdb as pt retpages: List[pwndbg.lib.memory.Page] = [] - p = pt.PageTableDump() + p = pt.PageTableDumpGdbFrontend() try: p.lazy_init() except Exception: @@ -515,7 +515,7 @@ def kernel_vmmap_via_page_tables() -> Tuple[pwndbg.lib.memory.Page, ...]: if not pwndbg.gdblib.kernel.paging_enabled(): return tuple(retpages) - pages = p.backend.parse_tables(p.cache, p.parser.parse_args("")) + pages = p.pt.arch_backend.parse_tables(p.pt.cache, p.pt.parser.parse_args("")) for page in pages: start = page.va diff --git a/pwndbg/lib/kernel/structs.py b/pwndbg/lib/kernel/structs.py new file mode 100644 index 000000000..a41bb213a --- /dev/null +++ b/pwndbg/lib/kernel/structs.py @@ -0,0 +1,67 @@ +from __future__ import annotations + + +class IDTEntry: + """ + Represents an entry in the Interrupt Descriptor Table (IDT) + + The IDTEntry class stores information about an IDT entry, including its index, + offset, segment selector, descriptor privilege level (DPL), gate type, and + interrupt stack table (IST) index. + + https://wiki.osdev.org/Interrupt_Descriptor_Table + """ + + def __init__(self, entry): + self.offset = None + self.segment = None + self.dpl = None + self.type = None + self.ist = None + self.present = None + + if len(entry) == 8: + self._parse_entry32(entry) + elif len(entry) == 16: + self._parse_entry64(entry) + + def _parse_entry32(self, entry): + """ + Parse a 32-bit IDT entry. + + Gate Descriptor (32-bit) + 63 48 47 45 44 40 32 + +------------------------------------+--+---+--+---------+----------------+ + | |P |DPL|0 |Gate Type| Reserved | + | Offset 31..16 | | | | | | + | | | | | | | + +------------------+------------------+------------------+----------------+ + 31 16 0 + +-------------------------------------+------------------+----------------+ + | | | + | Segment Selector | Offset 15..0 | + | | | + +------------------+------------------+------------------+----------------+ + """ + entry = int.from_bytes(entry, byteorder="little") + + self.offset = entry & 0xFFFF + self.offset |= ((entry >> 48) & 0xFFFF) << 16 + + self.segment = (entry >> 16) & 0xFFFF + self.type = (entry >> 40) & 0xF + self.dpl = (entry >> 45) & 0x3 + self.present = (entry >> 47) & 0x1 + + def _parse_entry64(self, entry): + """Parse a 64-bit IDT entry.""" + entry = int.from_bytes(entry, byteorder="little") + + self.offset = entry & 0xFFFF + self.offset |= ((entry >> 48) & 0xFFFF) << 16 + self.offset |= ((entry >> 64) & 0xFFFFFFFF) << 32 + + self.segment = (entry >> 16) & 0xFFFF + self.ist = (entry >> 32) & 0x7 + self.type = (entry >> 40) & 0xF + self.dpl = (entry >> 45) & 0x3 diff --git a/pyproject.toml b/pyproject.toml index cc2e18056..2b6c780eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,7 @@ module = [ disable_error_code = ["name-defined", "attr-defined"] [[tool.mypy.overrides]] -module = ["capstone.*", "unicorn.*", "pwnlib.*", "elftools.*", "ipdb.*", "r2pipe", "rzpipe", "rich.*", "pt"] +module = ["capstone.*", "unicorn.*", "pwnlib.*", "elftools.*", "ipdb.*", "r2pipe", "rzpipe", "rich.*", "pt_gdb"] ignore_missing_imports = true [tool.isort] @@ -311,7 +311,7 @@ tabulate = "0.9.0" typing-extensions = "4.6.1" unicorn = "2.0.1.post1" requests = "2.31.0" -pt = {git = "https://github.com/martinradev/gdb-pt-dump", rev = "89ea252f6efc5d75eacca16fc17ff8966a389690"} +pt = {git = "https://github.com/martinradev/gdb-pt-dump", rev = "50227bda0b6332e94027f811a15879588de6d5cb"} [tool.poetry.group.dev] optional = true diff --git a/tests/qemu-tests/tests/system/test_commands_kernel.py b/tests/qemu-tests/tests/system/test_commands_kernel.py index 5ebe049f7..2b666083a 100644 --- a/tests/qemu-tests/tests/system/test_commands_kernel.py +++ b/tests/qemu-tests/tests/system/test_commands_kernel.py @@ -5,10 +5,6 @@ import gdb import pwndbg -def test_command_kbase(): - pass # TODO - - def test_command_kchecksec(): res = gdb.execute("kchecksec", to_string=True) assert res != "" # for F841 warning diff --git a/tests/qemu-tests/tests/system/test_gdblib_kernel.py b/tests/qemu-tests/tests/system/test_gdblib_kernel.py index 1fed0e64a..9da066a93 100644 --- a/tests/qemu-tests/tests/system/test_gdblib_kernel.py +++ b/tests/qemu-tests/tests/system/test_gdblib_kernel.py @@ -48,3 +48,13 @@ def test_gdblib_kernel_is_kaslr_enabled(): def test_gdblib_kernel_nproc(): # make sure no exception occurs pwndbg.gdblib.kernel.nproc() + + +@pytest.mark.skipif(not pwndbg.gdblib.kernel.has_debug_syms(), reason="test requires debug symbols") +def test_gdblib_kernel_kbase(): + # newer arm/arm64 kernels reserve (_stext, _end] and other kernels reserve [_text, _end) + # https://elixir.bootlin.com/linux/v6.8.4/source/arch/arm64/mm/init.c#L306 + base = pwndbg.gdblib.kernel.kbase() + assert base == pwndbg.gdblib.symbol.address("_text") or base == pwndbg.gdblib.symbol.address( + "_stext" + )