improve kbase (#2097)

Fix kbase for newer kernels and allow automatically rebasing kernel symbol information
pull/2121/head
charif 2 years ago committed by GitHub
parent 3ef485a256
commit 7df87c93c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

32
poetry.lock generated

@ -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"

@ -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"))

@ -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

@ -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__]

@ -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:

@ -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

@ -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

@ -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

@ -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

@ -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"
)

Loading…
Cancel
Save