From 040636ef2aa17b5b8b95a5518ac75c30778e881c Mon Sep 17 00:00:00 2001 From: jxuanli <65455765+jxuanli@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:55:21 -0700 Subject: [PATCH] Improving `kconfig` (#3145) * improving kconfig * adding option to specify config file * changed based on suggestions --- docs/commands/index.md | 2 +- docs/commands/kernel/kconfig.md | 5 +- pwndbg/aglib/kernel/__init__.py | 59 +++--------- pwndbg/aglib/kernel/slab.py | 6 -- pwndbg/commands/kchecksec.py | 8 -- pwndbg/commands/kconfig.py | 17 +--- pwndbg/lib/kernel/kconfig.py | 95 ++++++++++++++++++- .../qemu-system/tests/test_gdblib_kernel.py | 5 - 8 files changed, 113 insertions(+), 84 deletions(-) diff --git a/docs/commands/index.md b/docs/commands/index.md index 5c3cb55b4..86208d90d 100644 --- a/docs/commands/index.md +++ b/docs/commands/index.md @@ -74,7 +74,7 @@ - [kbase](kernel/kbase.md) - Finds the kernel virtual base address. - [kchecksec](kernel/kchecksec.md) - Checks for kernel hardening configuration options. - [kcmdline](kernel/kcmdline.md) - Return the kernel commandline (/proc/cmdline). -- [kconfig](kernel/kconfig.md) - Outputs the kernel config (requires CONFIG_IKCONFIG). +- [kconfig](kernel/kconfig.md) - Outputs the kernel config. - [kdmesg](kernel/kdmesg.md) - Displays the kernel ring buffer (dmesg) contents. - [klookup](kernel/klookup.md) - Lookup kernel symbols - [kmod](kernel/kmod.md) - Displays the loaded Linux kernel modules. diff --git a/docs/commands/kernel/kconfig.md b/docs/commands/kernel/kconfig.md index e921e2e2a..5db7f3bd9 100644 --- a/docs/commands/kernel/kconfig.md +++ b/docs/commands/kernel/kconfig.md @@ -2,11 +2,11 @@ # kconfig ```text -usage: kconfig [-h] [config_name] +usage: kconfig [-h] [-l FILE_PATH] [config_name] ``` -Outputs the kernel config (requires CONFIG_IKCONFIG). +Outputs the kernel config. ### Positional arguments |Positional Argument|Help| @@ -18,6 +18,7 @@ Outputs the kernel config (requires CONFIG_IKCONFIG). |Short|Long|Help| | :--- | :--- | :--- | |-h|--help|show this help message and exit| +|-l|--load|load kernel config file| diff --git a/pwndbg/aglib/kernel/__init__.py b/pwndbg/aglib/kernel/__init__.py index a6f848f35..f5f429242 100644 --- a/pwndbg/aglib/kernel/__init__.py +++ b/pwndbg/aglib/kernel/__init__.py @@ -50,26 +50,6 @@ def has_debug_syms() -> bool: ) -# NOTE: This implies requires_debug_syms(), as it is needed for kconfig() to return non-None -def requires_kconfig(default: D = None) -> Callable[[Callable[P, T]], Callable[P, T | D]]: - def decorator(f: Callable[P, T]) -> Callable[P, T | D]: - @functools.wraps(f) - def func(*args: P.args, **kwargs: P.kwargs) -> T | D: - if kconfig(): - return f(*args, **kwargs) - - # If the user doesn't want an exception thrown when CONFIG_IKCONFIG is - # not enabled, they can instead provide a default return value - if default is not None: - return default - - raise Exception(f"Function {f.__name__} requires CONFIG_IKCONFIG enabled in kernel") - - return func - - return decorator - - def requires_debug_syms(default: D = None) -> Callable[[Callable[P, T]], Callable[P, T | D]]: def decorator(f: Callable[P, T]) -> Callable[P, T | D]: @functools.wraps(f) @@ -130,36 +110,28 @@ def get_first_kernel_ro() -> pwndbg.lib.memory.Page | None: return None -def load_kconfig() -> pwndbg.lib.kernel.kconfig.Kconfig | None: +@pwndbg.lib.cache.cache_until("start") +def kconfig() -> pwndbg.lib.kernel.kconfig.Kconfig | None: + global _kconfig + config_start, config_end = None, None if has_debug_syms(): config_start = pwndbg.aglib.symbol.lookup_symbol_addr("kernel_config_data") config_end = pwndbg.aglib.symbol.lookup_symbol_addr("kernel_config_data_end") else: mapping = get_first_kernel_ro() - results = list(pwndbg.search.search(b"IKCFG_ST", mappings=[mapping])) - - if len(results) == 0: - return None - - config_start = results[0] + len("IKCFG_ST") - config_end = list(pwndbg.search.search(b"IKCFG_ED", start=config_start))[0] + result = next(pwndbg.search.search(b"IKCFG_ST", mappings=[mapping]), None) + if result is not None: + config_start = result + len("IKCFG_ST") + config_end = next(pwndbg.search.search(b"IKCFG_ED", start=config_start), None) if config_start is None or config_end is None: - return None + _kconfig = pwndbg.lib.kernel.kconfig.Kconfig(None) + return _kconfig config_size = config_end - config_start compressed_config = pwndbg.aglib.memory.read(config_start, config_size) - return pwndbg.lib.kernel.kconfig.Kconfig(compressed_config) - - -@pwndbg.lib.cache.cache_until("start") -def kconfig() -> pwndbg.lib.kernel.kconfig.Kconfig | None: - global _kconfig - if _kconfig is None: - _kconfig = load_kconfig() - elif len(_kconfig) == 0: - return None + _kconfig = pwndbg.lib.kernel.kconfig.Kconfig(compressed_config) return _kconfig @@ -193,15 +165,6 @@ def krelease() -> Tuple[int, ...]: raise Exception("Linux version tuple not found") -@requires_kconfig() -@pwndbg.lib.cache.cache_until("start") -def is_kaslr_enabled() -> bool: - if "CONFIG_RANDOMIZE_BASE" not in kconfig(): - return False - - return "nokaslr" not in kcmdline() - - def get_idt_entries() -> List[pwndbg.lib.kernel.structs.IDTEntry]: """ Retrieves the IDT entries from memory. diff --git a/pwndbg/aglib/kernel/slab.py b/pwndbg/aglib/kernel/slab.py index cca5dc9f5..98f9b9a58 100644 --- a/pwndbg/aglib/kernel/slab.py +++ b/pwndbg/aglib/kernel/slab.py @@ -157,12 +157,6 @@ class SlabCache: @property def random(self) -> int: - if not kernel.kconfig(): - try: - return int(self._slab_cache["random"]) - except pwndbg.dbg_mod.Error: - return 0 - return ( int(self._slab_cache["random"]) if "SLAB_FREELIST_HARDENED" in kernel.kconfig() else 0 ) diff --git a/pwndbg/commands/kchecksec.py b/pwndbg/commands/kchecksec.py index 2e826de7c..d24f44a17 100644 --- a/pwndbg/commands/kchecksec.py +++ b/pwndbg/commands/kchecksec.py @@ -109,14 +109,6 @@ parser = argparse.ArgumentParser(description="Checks for kernel hardening config def kchecksec() -> None: kconfig = pwndbg.aglib.kernel.kconfig() - if not kconfig: - print( - M.warn( - "No kernel configuration found, make sure the kernel was built with CONFIG_IKCONFIG" - ) - ) - return - options = _hardening_options + _arch_hardening_options.get(pwndbg.aglib.arch.name, []) for opt in options: config_name = opt.name diff --git a/pwndbg/commands/kconfig.py b/pwndbg/commands/kconfig.py index 1e3446775..6c10960ec 100644 --- a/pwndbg/commands/kconfig.py +++ b/pwndbg/commands/kconfig.py @@ -3,29 +3,22 @@ from __future__ import annotations import argparse import pwndbg.aglib.kernel -import pwndbg.color.message as M import pwndbg.commands from pwndbg.commands import CommandCategory -parser = argparse.ArgumentParser( - description="Outputs the kernel config (requires CONFIG_IKCONFIG)." -) +parser = argparse.ArgumentParser(description="Outputs the kernel config.") parser.add_argument("config_name", nargs="?", type=str, help="A config name to search for") +parser.add_argument("-l", "--load", type=str, dest="file_path", help="load kernel config file") @pwndbg.commands.Command(parser, category=CommandCategory.KERNEL) @pwndbg.commands.OnlyWhenQemuKernel @pwndbg.commands.OnlyWhenPagingEnabled -def kconfig(config_name=None) -> None: +def kconfig(config_name=None, file_path=None) -> None: kconfig_ = pwndbg.aglib.kernel.kconfig() - - if not kconfig_: - print( - M.warn( - "No kernel configuration found, make sure the kernel was built with CONFIG_IKCONFIG" - ) - ) + if file_path is not None: + kconfig_.update_with_file(file_path) return if config_name: diff --git a/pwndbg/lib/kernel/kconfig.py b/pwndbg/lib/kernel/kconfig.py index 10f89e300..0ce110f28 100644 --- a/pwndbg/lib/kernel/kconfig.py +++ b/pwndbg/lib/kernel/kconfig.py @@ -5,6 +5,10 @@ from collections import UserDict from typing import Any from typing import Dict +import pwndbg.aglib +import pwndbg.aglib.kernel +import pwndbg.aglib.symbol + def parse_config(config_text: bytes) -> Dict[str, str]: res: Dict[str, str] = {} @@ -27,9 +31,29 @@ def config_to_key(name: str) -> str: class Kconfig(UserDict): # type: ignore[type-arg] - def __init__(self, compressed_config: bytes, *args: Any, **kwargs: Any) -> None: + def __init__(self, compressed_config: bytes | None, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.data = parse_compresed_config(compressed_config) + if compressed_config is not None: + self.data = parse_compresed_config(compressed_config) + return + if self.CONFIG_SLUB_TINY: + self.data["CONFIG_SLUB_TINY"] = "y" + if self.CONFIG_SLUB_CPU_PARTIAL: + self.data["CONFIG_SLUB_CPU_PARTIAL"] = "y" + if self.CONFIG_MEMCG: + self.data["CONFIG_MEMCG"] = "y" + if self.CONFIG_SLAB_FREELIST_RANDOM: + self.data["CONFIG_SLAB_FREELIST_RANDOM"] = "y" + if self.CONFIG_HARDENED_USERCOPY: + self.data["CONFIG_HARDENED_USERCOPY"] = "y" + if self.CONFIG_SLAB_FREELIST_HARDENED: + self.data["CONFIG_SLAB_FREELIST_HARDENED"] = "y" + if self.CONFIG_NUMA: + self.data["CONFIG_NUMA"] = "y" + if self.CONFIG_KASAN_GENERIC: + self.data["CONFIG_KASAN_GENERIC"] = "y" + if self.CONFIG_SMP: + self.data["CONFIG_SMP"] = "y" def get_key(self, name: str) -> str | None: # First attempt to lookup the value assuming the user passed in a name @@ -59,3 +83,70 @@ class Kconfig(UserDict): # type: ignore[type-arg] def __getattr__(self, name: str): return self.get(name) + + @property + def CONFIG_SLUB_TINY(self) -> bool: + if pwndbg.aglib.kernel.krelease() < (6, 2): + return False + return pwndbg.aglib.symbol.lookup_symbol("flushwq") is None + + @property + def CONFIG_SLUB_CPU_PARTIAL(self) -> bool: + if pwndbg.aglib.kernel.krelease() < (6, 8): + if pwndbg.aglib.symbol.lookup_symbol("unfreeze_partials") is not None: + return True + return pwndbg.aglib.symbol.lookup_symbol("__unfreeze_partials") is not None + return pwndbg.aglib.symbol.lookup_symbol("__put_partials") is not None + + @property + def CONFIG_MEMCG(self) -> bool: + return pwndbg.aglib.symbol.lookup_symbol("kpagecgroup_proc_ops") is not None + + @property + def CONFIG_SLAB_FREELIST_RANDOM(self) -> bool: + return pwndbg.aglib.symbol.lookup_symbol("init_cache_random_seq") is not None + + @property + def CONFIG_HARDENED_USERCOPY(self) -> bool: + return pwndbg.aglib.symbol.lookup_symbol("__check_heap_object") is not None + + @property + def CONFIG_SLAB_FREELIST_HARDENED(self) -> bool: + def __helper(name): + addr = pwndbg.aglib.symbol.lookup_symbol_addr(name) + if addr is not None: + for instr in pwndbg.aglib.nearpc.nearpc(addr, 40): + if "get_random" in instr: + return True + return False + + return any( + __helper(name) + for name in ( + "kmem_cache_open", + "do_kmem_cache_create", + "__kmem_cache_create", + ) + ) + + @property + def CONFIG_NUMA(self) -> bool: + return pwndbg.aglib.symbol.lookup_symbol("proc_pid_numa_maps_op") is not None + + @property + def CONFIG_KASAN_GENERIC(self) -> bool: + # TODO: have a kernel build that tests this + if pwndbg.aglib.kernel.krelease() < (5, 11): + return pwndbg.aglib.symbol.lookup_symbol("kasan_cache_create") is not None + return pwndbg.aglib.symbol.lookup_symbol("__kasan_cache_create") is not None + + @property + def CONFIG_SMP(self) -> bool: + return pwndbg.aglib.symbol.lookup_symbol("pcpu_get_vm_areas") is not None + + def update_with_file(self, file_path): + for line in open(file_path, "r").read().splitlines(): + split = line.split("=") + if len(line) == 0 or line[0] == "#" or len(split) != 2: + continue + self.data[split[0]] = split[1] diff --git a/tests/library/qemu-system/tests/test_gdblib_kernel.py b/tests/library/qemu-system/tests/test_gdblib_kernel.py index 22af2e4ba..aff0ccddc 100644 --- a/tests/library/qemu-system/tests/test_gdblib_kernel.py +++ b/tests/library/qemu-system/tests/test_gdblib_kernel.py @@ -42,11 +42,6 @@ def test_gdblib_kernel_krelease(): assert release_str in pwndbg.aglib.kernel.kversion() -@pytest.mark.skipif(not pwndbg.aglib.kernel.has_debug_syms(), reason="test requires debug symbols") -def test_gdblib_kernel_is_kaslr_enabled(): - pwndbg.aglib.kernel.is_kaslr_enabled() - - @pytest.mark.skipif(not pwndbg.aglib.kernel.has_debug_syms(), reason="test requires debug symbols") def test_gdblib_kernel_nproc(): # make sure no exception occurs