From afbc93ff7949dcee0bb649f963483f13b3776b4d Mon Sep 17 00:00:00 2001 From: patryk4815 Date: Thu, 19 Dec 2024 16:42:31 +0100 Subject: [PATCH] AUXV disable stack exploration (#2641) * add auxv explore + procfs_auxv * add auxv explore + procfs_auxv * catch err --- pwndbg/auxv.py | 196 +++++++++++++++++++--------------------- pwndbg/commands/auxv.py | 17 ++++ pwndbg/lib/elftypes.py | 116 ++++++++++++++++++------ 3 files changed, 196 insertions(+), 133 deletions(-) diff --git a/pwndbg/auxv.py b/pwndbg/auxv.py index 37afdb889..9ba7658fe 100644 --- a/pwndbg/auxv.py +++ b/pwndbg/auxv.py @@ -2,24 +2,39 @@ from __future__ import annotations import os import re -from typing import Dict +import struct from typing import Optional -from typing import Union import pwndbg.aglib.arch +import pwndbg.aglib.file import pwndbg.aglib.memory +import pwndbg.aglib.proc import pwndbg.aglib.qemu import pwndbg.aglib.regs import pwndbg.aglib.stack import pwndbg.aglib.strings import pwndbg.aglib.typeinfo +import pwndbg.color.message as M import pwndbg.lib.cache +import pwndbg.lib.config import pwndbg.lib.memory +from pwndbg.lib.elftypes import AT_CONSTANT_NAMES +from pwndbg.lib.elftypes import AUXV # We use `info.auxv()` when available. if pwndbg.dbg.is_gdblib_available(): import pwndbg.gdblib.info + +auto_explore = pwndbg.config.add_param( + "auto-explore-auxv", + "warn", + "Enable or disable stack exploration for AUXV information; it may be really slow.", + param_class=pwndbg.lib.config.PARAM_ENUM, + enum_sequence=["warn", "yes", "no"], +) + + example_info_auxv_linux = """ 33 AT_SYSINFO_EHDR System-supplied DSO's ELF header 0x7ffff7ffa000 16 AT_HWCAP Machine-dependent CPU capability hints 0xfabfbff @@ -43,84 +58,47 @@ example_info_auxv_linux = """ """ -AT_CONSTANTS = { - 0: "AT_NULL", # /* End of vector */ - 1: "AT_IGNORE", # /* Entry should be ignored */ - 2: "AT_EXECFD", # /* File descriptor of program */ - 3: "AT_PHDR", # /* Program headers for program */ - 4: "AT_PHENT", # /* Size of program header entry */ - 5: "AT_PHNUM", # /* Number of program headers */ - 6: "AT_PAGESZ", # /* System page size */ - 7: "AT_BASE", # /* Base address of interpreter */ - 8: "AT_FLAGS", # /* Flags */ - 9: "AT_ENTRY", # /* Entry point of program */ - 10: "AT_NOTELF", # /* Program is not ELF */ - 11: "AT_UID", # /* Real uid */ - 12: "AT_EUID", # /* Effective uid */ - 13: "AT_GID", # /* Real gid */ - 14: "AT_EGID", # /* Effective gid */ - 15: "AT_PLATFORM", # /* String identifying platform */ - 16: "AT_HWCAP", # /* Machine dependent hints about processor capabilities */ - 17: "AT_CLKTCK", # /* Frequency of times() */ - 18: "AT_FPUCW", - 19: "AT_DCACHEBSIZE", - 20: "AT_ICACHEBSIZE", - 21: "AT_UCACHEBSIZE", - 22: "AT_IGNOREPPC", - 23: "AT_SECURE", - 24: "AT_BASE_PLATFORM", # String identifying real platforms - 25: "AT_RANDOM", # Address of 16 random bytes - 31: "AT_EXECFN", # Filename of executable - 32: "AT_SYSINFO", - 33: "AT_SYSINFO_EHDR", - 34: "AT_L1I_CACHESHAPE", - 35: "AT_L1D_CACHESHAPE", - 36: "AT_L2_CACHESHAPE", - 37: "AT_L3_CACHESHAPE", -} - -AT_CONSTANT_NAMES = {v: k for k, v in AT_CONSTANTS.items()} - - -class AUXV(Dict[str, Union[int, str]]): - AT_PHDR: Optional[int] - AT_BASE: Optional[int] - AT_PLATFORM: Optional[str] - AT_ENTRY: Optional[int] - AT_RANDOM: Optional[int] - AT_EXECFN: Optional[str] - AT_SYSINFO: Optional[int] - AT_SYSINFO_EHDR: Optional[int] - - def set(self, const: int, value: int) -> None: - name = AT_CONSTANTS.get(const, "AT_UNKNOWN%i" % const) - - if name in ["AT_EXECFN", "AT_PLATFORM"]: - try: - value = ( - pwndbg.dbg.selected_inferior() - .create_value(value) - .cast(pwndbg.aglib.typeinfo.pchar) - .string() - ) - except Exception: - value = "couldnt read AUXV!" - - self[name] = value - - def __getattr__(self, attr: str) -> Optional[Union[int, str]]: - if attr in AT_CONSTANT_NAMES: - return self.get(attr) - - raise AttributeError("%r object has no attribute %r" % (self.__class__.__name__, attr)) - - def __str__(self) -> str: - return str({k: v for k, v in self.items() if v is not None}) - - @pwndbg.lib.cache.cache_until("objfile", "start") def get() -> AUXV: - return use_info_auxv() or walk_stack() or AUXV() + if not pwndbg.dbg.selected_inferior().is_linux() or pwndbg.aglib.qemu.is_qemu_kernel(): + return AUXV() + + return use_info_auxv() or procfs_auxv() or explore_stack_auxv() or AUXV() + + +def procfs_auxv() -> AUXV | None: + if pwndbg.aglib.arch.ptrsize == 8: + field_format = "QQ" # for 64bit system + elif pwndbg.aglib.arch.ptrsize == 4: + field_format = "II" # for 32bit system + else: + assert False + field_size = struct.calcsize(field_format) + + try: + data = pwndbg.aglib.file.get(f"/proc/{pwndbg.aglib.proc.tid}/auxv") + except OSError: + return None + + if not data: + return None + + auxv = AUXV() + end_type = AT_CONSTANT_NAMES["AT_NULL"] + for i in range(0, len(data), field_size): + entry = data[i : i + field_size] + + if len(entry) < field_size: + break # Ignore incomplete entry at the end + + a_type, a_val = struct.unpack(field_format, entry) + + # AT_NULL indicates the end of the vector + if a_type == end_type: + break + auxv.set(a_type, a_val) + + return auxv def use_info_auxv() -> Optional[AUXV]: @@ -144,33 +122,22 @@ def use_info_auxv() -> Optional[AUXV]: return auxv -def find_stack_boundary(addr: int) -> int: - # For real binaries, we can just use pwndbg.aglib.memory.find_upper_boundary - # to search forward until we walk off the end of the stack. - # - # Unfortunately, qemu-user emulation likes to paste the stack right - # before binaries in memory. This means that we walk right past the - # stack and to the end of some random ELF. - # - # In order to mitigate this, we search page-by-page until either: - # - # 1) We get a page fault, and stop - # 2) We find an ELF header, and stop - addr = pwndbg.lib.memory.page_align(addr) - try: - while True: - if b"\x7fELF" == pwndbg.aglib.memory.read(addr, 4): - break - addr += pwndbg.lib.memory.PAGE_SIZE - except pwndbg.dbg_mod.Error: - pass - return addr +_warn_explore_once = True -def walk_stack() -> AUXV | None: - if not pwndbg.dbg.selected_inferior().is_linux(): +def explore_stack_auxv() -> AUXV | None: + if auto_explore.value == "warn": + print( + M.warn( + "Warning: All methods to detect AUXV have failed.\n" + "You can explore AUXV using stack exploration, but it may be very slow.\n" + "To explicitly explore, use the command: `auxv_explore`\n" + "Alternatively, enable it by default with: `set auto-explore-auxv yes`\n\n" + "Note: AUXV is probably not necessary for debugging firmware or embedded systems." + ) + ) return None - if pwndbg.aglib.qemu.is_qemu_kernel(): + elif auto_explore.value == "no": return None auxv = walk_stack2(0) @@ -207,7 +174,7 @@ def walk_stack2(offset: int = 0) -> AUXV: # set of known AT_ enums. # 5) Vacuum up between the two. # - end = find_stack_boundary(sp) + end = _find_stack_boundary(sp) p = pwndbg.dbg.selected_inferior().create_value(end).cast(pwndbg.aglib.typeinfo.ulong.pointer()) p -= offset @@ -269,6 +236,29 @@ def walk_stack2(offset: int = 0) -> AUXV: return AUXV() +def _find_stack_boundary(addr: int) -> int: + # For real binaries, we can just use pwndbg.aglib.memory.find_upper_boundary + # to search forward until we walk off the end of the stack. + # + # Unfortunately, qemu-user emulation likes to paste the stack right + # before binaries in memory. This means that we walk right past the + # stack and to the end of some random ELF. + # + # In order to mitigate this, we search page-by-page until either: + # + # 1) We get a page fault, and stop + # 2) We find an ELF header, and stop + addr = pwndbg.lib.memory.page_align(addr) + try: + while True: + if b"\x7fELF" == pwndbg.aglib.memory.read(addr, 4): + break + addr += pwndbg.lib.memory.PAGE_SIZE + except pwndbg.dbg_mod.Error: + pass + return addr + + def _get_execfn() -> str | None: # If the stack is not sane, this won't work if not pwndbg.aglib.memory.peek(pwndbg.aglib.regs.sp): diff --git a/pwndbg/commands/auxv.py b/pwndbg/commands/auxv.py index 9b4d82841..99779c2a7 100644 --- a/pwndbg/commands/auxv.py +++ b/pwndbg/commands/auxv.py @@ -15,3 +15,20 @@ def auxv() -> None: for k, v in pwndbg.auxv.get().items(): if v is not None: print(k.ljust(24), v if not isinstance(v, int) else pwndbg.chain.format(v)) + + +@pwndbg.commands.ArgparsedCommand( + "Explore and print information from the Auxiliary ELF Vector.", category=CommandCategory.LINUX +) +@pwndbg.commands.OnlyWhenRunning +@pwndbg.commands.OnlyWhenUserspace +def auxv_explore() -> None: + old_value = pwndbg.config.auto_explore_auxv.value + pwndbg.config.auto_explore_auxv.value = "yes" + try: + pwndbg.auxv.get.cache.clear() # type: ignore[attr-defined] + pwndbg.auxv.get() + finally: + pwndbg.config.auto_explore_auxv.value = old_value + + auxv() diff --git a/pwndbg/lib/elftypes.py b/pwndbg/lib/elftypes.py index 9c0a9349c..a138fe58d 100644 --- a/pwndbg/lib/elftypes.py +++ b/pwndbg/lib/elftypes.py @@ -31,6 +31,8 @@ from __future__ import annotations import ctypes from typing import Dict +from typing import Optional +from typing import Union import pwndbg.aglib.ctypes @@ -50,42 +52,59 @@ Elf64_Xword = ctypes.c_uint64 Elf64_Sxword = ctypes.c_int64 +# Copied from https://elixir.bootlin.com/glibc/glibc-2.40.9000/source/elf/elf.h#L1193 AT_CONSTANTS: Dict[int, str] = { - 0: "AT_NULL", # /* End of vector */ - 1: "AT_IGNORE", # /* Entry should be ignored */ - 2: "AT_EXECFD", # /* File descriptor of program */ - 3: "AT_PHDR", # /* Program headers for program */ - 4: "AT_PHENT", # /* Size of program header entry */ - 5: "AT_PHNUM", # /* Number of program headers */ - 6: "AT_PAGESZ", # /* System page size */ - 7: "AT_BASE", # /* Base address of interpreter */ - 8: "AT_FLAGS", # /* Flags */ - 9: "AT_ENTRY", # /* Entry point of program */ - 10: "AT_NOTELF", # /* Program is not ELF */ - 11: "AT_UID", # /* Real uid */ - 12: "AT_EUID", # /* Effective uid */ - 13: "AT_GID", # /* Real gid */ - 14: "AT_EGID", # /* Effective gid */ - 15: "AT_PLATFORM", # /* String identifying platform */ - 16: "AT_HWCAP", # /* Machine dependent hints about processor capabilities */ - 17: "AT_CLKTCK", # /* Frequency of times() */ - 18: "AT_FPUCW", - 19: "AT_DCACHEBSIZE", - 20: "AT_ICACHEBSIZE", - 21: "AT_UCACHEBSIZE", - 22: "AT_IGNOREPPC", - 23: "AT_SECURE", + 0: "AT_NULL", # End of vector + 1: "AT_IGNORE", # Entry should be ignored + 2: "AT_EXECFD", # File descriptor of program + 3: "AT_PHDR", # Program headers for program + 4: "AT_PHENT", # Size of program header entry + 5: "AT_PHNUM", # Number of program headers + 6: "AT_PAGESZ", # System page size + 7: "AT_BASE", # Base address of interpreter + 8: "AT_FLAGS", # Flags + 9: "AT_ENTRY", # Entry point of program + 10: "AT_NOTELF", # Program is not ELF + 11: "AT_UID", # Real uid + 12: "AT_EUID", # Effective uid + 13: "AT_GID", # Real gid + 14: "AT_EGID", # Effective gid + 15: "AT_PLATFORM", # String identifying platform + 16: "AT_HWCAP", # Machine-dependent hints about processor capabilities + 17: "AT_CLKTCK", # Frequency of times() + 18: "AT_FPUCW", # Used FPU control word + 19: "AT_DCACHEBSIZE", # Data cache block size + 20: "AT_ICACHEBSIZE", # Instruction cache block size + 21: "AT_UCACHEBSIZE", # Unified cache block size + 22: "AT_IGNOREPPC", # Entry should be ignored + 23: "AT_SECURE", # Boolean, was exec setuid-like? 24: "AT_BASE_PLATFORM", # String identifying real platforms 25: "AT_RANDOM", # Address of 16 random bytes + 26: "AT_HWCAP2", # More machine-dependent hints about processor capabilities + 27: "AT_RSEQ_FEATURE_SIZE", # rseq supported feature size + 28: "AT_RSEQ_ALIGN", # rseq allocation alignment + 29: "AT_HWCAP3", # Extension of AT_HWCAP + 30: "AT_HWCAP4", # Extension of AT_HWCAP 31: "AT_EXECFN", # Filename of executable - 32: "AT_SYSINFO", - 33: "AT_SYSINFO_EHDR", - 34: "AT_L1I_CACHESHAPE", - 35: "AT_L1D_CACHESHAPE", - 36: "AT_L2_CACHESHAPE", - 37: "AT_L3_CACHESHAPE", + 32: "AT_SYSINFO", # Pointer to the global system page used for system calls + 33: "AT_SYSINFO_EHDR", # Header for sysinfo + 34: "AT_L1I_CACHESHAPE", # Shape of L1 instruction cache + 35: "AT_L1D_CACHESHAPE", # Shape of L1 data cache + 36: "AT_L2_CACHESHAPE", # Shape of L2 cache + 37: "AT_L3_CACHESHAPE", # Shape of L3 cache + 40: "AT_L1I_CACHESIZE", # Size of L1 instruction cache + 41: "AT_L1I_CACHEGEOMETRY", # Geometry of L1 instruction cache + 42: "AT_L1D_CACHESIZE", # Size of L1 data cache + 43: "AT_L1D_CACHEGEOMETRY", # Geometry of L1 data cache + 44: "AT_L2_CACHESIZE", # Size of L2 cache + 45: "AT_L2_CACHEGEOMETRY", # Geometry of L2 cache + 46: "AT_L3_CACHESIZE", # Size of L3 cache + 47: "AT_L3_CACHEGEOMETRY", # Geometry of L3 cache + 51: "AT_MINSIGSTKSZ", # Stack needed for signal delivery } +AT_CONSTANT_NAMES = {v: k for k, v in AT_CONSTANTS.items()} + class constants: EI_MAG0 = 0 @@ -318,3 +337,40 @@ class Elf64_Phdr(pwndbg.aglib.ctypes.Structure): ("p_memsz", Elf64_Xword), ("p_align", Elf64_Xword), ] + + +class AUXV(Dict[str, Union[int, str]]): + AT_PHDR: Optional[int] + AT_BASE: Optional[int] + AT_PLATFORM: Optional[str] + AT_BASE_PLATFORM: Optional[str] + AT_ENTRY: Optional[int] + AT_RANDOM: Optional[int] + AT_EXECFN: Optional[str] + AT_SYSINFO: Optional[int] + AT_SYSINFO_EHDR: Optional[int] + + def set(self, const: int, value: int) -> None: + name = AT_CONSTANTS.get(const, "AT_UNKNOWN%i" % const) + + if name in ["AT_EXECFN", "AT_PLATFORM", "AT_BASE_PLATFORM"]: + try: + value = ( + pwndbg.dbg.selected_inferior() + .create_value(value) + .cast(pwndbg.aglib.typeinfo.pchar) + .string() + ) + except Exception: + value = "couldnt read AUXV!" + + self[name] = value + + def __getattr__(self, attr: str) -> Optional[Union[int, str]]: + if attr in AT_CONSTANT_NAMES: + return self.get(attr) + + raise AttributeError("%r object has no attribute %r" % (self.__class__.__name__, attr)) + + def __str__(self) -> str: + return str({k: v for k, v in self.items() if v is not None})