From a5d59880200fdc66eb6c39f4cd99b0b747156fe2 Mon Sep 17 00:00:00 2001 From: "Matt." <4922458+mbrla0@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:22:04 -0300 Subject: [PATCH] Assorted enhancements and bug fixes to LLDB (#3190) * Add offsets to symbol names in LLDB * Disable context line reservations if colors are disabled * LLDB: More aggresively verify memory writes * LLDB: Add support for disabling ASLR * Add `-a` flag to `plt` command to show all symbols * Start shellcode execution at next aligned instruction address, instead of current PC * Improve execution speed for the `nextproginstr` command * When resolving address expressions in windgb commands, try resolving as symbol firt * LLDB: Relay exceptions from commands * LLDB: Capture stderr in addition to stdout when capturing command output * Move disabling of line reservations to LLDB test host * Update docs --- docs/commands/linux_libc_elf/plt.md | 3 +- pwndbg/aglib/arch.py | 14 +++++++ pwndbg/aglib/next.py | 1 + pwndbg/aglib/shellcode.py | 15 +++++++- pwndbg/commands/__init__.py | 2 +- pwndbg/commands/elf.py | 18 +++++++-- pwndbg/dbg/lldb/__init__.py | 59 +++++++++++++++++++++++++---- pwndbg/dbg/lldb/repl/__init__.py | 37 +++++++++++++----- pwndbg/dbg/lldb/repl/proc.py | 20 +++++++--- tests/host/lldb/launch_guest.py | 1 + 10 files changed, 143 insertions(+), 27 deletions(-) diff --git a/docs/commands/linux_libc_elf/plt.md b/docs/commands/linux_libc_elf/plt.md index 65b75f3e9..48a74ec7e 100644 --- a/docs/commands/linux_libc_elf/plt.md +++ b/docs/commands/linux_libc_elf/plt.md @@ -2,7 +2,7 @@ # plt ```text -usage: plt [-h] +usage: plt [-h] [-a] ``` @@ -12,6 +12,7 @@ Prints any symbols found in Procedure Linkage Table sections if any exist. |Short|Long|Help| | :--- | :--- | :--- | |-h|--help|show this help message and exit| +|-a|--all-symbols|Print all symbols, not just those that end in @plt| diff --git a/pwndbg/aglib/arch.py b/pwndbg/aglib/arch.py index 36a3b5c81..5856be41b 100644 --- a/pwndbg/aglib/arch.py +++ b/pwndbg/aglib/arch.py @@ -79,6 +79,7 @@ class PwndbgArchitecture(ArchDefinition): ### All subclasses must provide values for the following attributes max_instruction_size: int + instruction_alignment: int ### @@ -166,6 +167,7 @@ class PwndbgArchitecture(ArchDefinition): class AMD64Arch(PwndbgArchitecture): max_instruction_size = 16 + instruction_alignment = 1 def __init__(self) -> None: super().__init__("x86-64") @@ -181,6 +183,7 @@ class i386Arch(PwndbgArchitecture): """ max_instruction_size = 16 + instruction_alignment = 1 def __init__(self) -> None: super().__init__("i386") @@ -196,6 +199,7 @@ class i8086Arch(PwndbgArchitecture): """ max_instruction_size = 16 + instruction_alignment = 1 def __init__(self) -> None: super().__init__("i8086") @@ -207,6 +211,7 @@ class i8086Arch(PwndbgArchitecture): class ArmArch(PwndbgArchitecture): max_instruction_size = 4 + instruction_alignment = 4 def __init__(self) -> None: super().__init__("arm") @@ -237,6 +242,7 @@ class ArmCortexArch(PwndbgArchitecture): """ max_instruction_size = 4 + instruction_alignment = 4 def __init__(self) -> None: super().__init__("armcm") @@ -257,6 +263,7 @@ class ArmCortexArch(PwndbgArchitecture): class AArch64Arch(PwndbgArchitecture): max_instruction_size = 4 + instruction_alignment = 4 def __init__(self) -> None: super().__init__("aarch64") @@ -268,6 +275,7 @@ class AArch64Arch(PwndbgArchitecture): class PowerPCArch(PwndbgArchitecture): max_instruction_size = 4 + instruction_alignment = 4 def __init__(self) -> None: super().__init__("powerpc") @@ -279,6 +287,7 @@ class PowerPCArch(PwndbgArchitecture): class SparcArch(PwndbgArchitecture): max_instruction_size = 4 + instruction_alignment = 4 def __init__(self) -> None: super().__init__("sparc") @@ -291,6 +300,7 @@ class SparcArch(PwndbgArchitecture): class RISCV32Arch(PwndbgArchitecture): max_instruction_size = 22 + instruction_alignment = 4 def __init__(self) -> None: super().__init__("rv32") @@ -302,6 +312,7 @@ class RISCV32Arch(PwndbgArchitecture): class RISCV64Arch(PwndbgArchitecture): max_instruction_size = 22 + instruction_alignment = 4 def __init__(self) -> None: super().__init__("rv64") @@ -313,6 +324,7 @@ class RISCV64Arch(PwndbgArchitecture): class MipsArch(PwndbgArchitecture): max_instruction_size = 8 + instruction_alignment = 4 def __init__(self) -> None: super().__init__("mips") @@ -332,6 +344,7 @@ class MipsArch(PwndbgArchitecture): class Loongarch64Arch(PwndbgArchitecture): max_instruction_size = 4 + instruction_alignment = 4 def __init__(self) -> None: super().__init__("loongarch64") @@ -343,6 +356,7 @@ class Loongarch64Arch(PwndbgArchitecture): class S390xArch(PwndbgArchitecture): max_instruction_size = 6 + instruction_alignment = 2 def __init__(self) -> None: super().__init__("s390x") diff --git a/pwndbg/aglib/next.py b/pwndbg/aglib/next.py index 8396cbd85..1c2ad1139 100644 --- a/pwndbg/aglib/next.py +++ b/pwndbg/aglib/next.py @@ -266,6 +266,7 @@ async def break_on_program_code(ec: pwndbg.dbg_mod.ExecutionController) -> bool: if proc.stopped_with_signal: return False + await break_next_ret(ec) await ec.single_step() for start, end in binary_exec_page_ranges: diff --git a/pwndbg/aglib/shellcode.py b/pwndbg/aglib/shellcode.py index 3a3147ae1..a3e7382e8 100644 --- a/pwndbg/aglib/shellcode.py +++ b/pwndbg/aglib/shellcode.py @@ -99,8 +99,21 @@ def _ctx_registers() -> Iterator[int]: registers = {reg: int(uncached_regs.by_name(reg)) for reg in preserve_set} starting_address = registers[register_set.pc] + # Advance by one instruction boundary. + # + # Some debuggers (LLDB) may fail to write to memory if any of the addresses + # being written to overlap the program counter. By aiming at the next valid + # instruction address, we avoid that issue. + shell_starting_address = starting_address + pwndbg.aglib.arch.instruction_alignment + + # Failing this means our value for `instruction_alignment` is wrong. + assert shell_starting_address % pwndbg.aglib.arch.instruction_alignment == 0 + try: - yield starting_address + # Jump to the target address in preparation. + setattr(pwndbg.aglib.regs, register_set.pc, shell_starting_address) + + yield shell_starting_address finally: # Restore the code and the program counter and, if requested, the rest of # the registers. diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py index f629f378a..f8d583290 100644 --- a/pwndbg/commands/__init__.py +++ b/pwndbg/commands/__init__.py @@ -836,7 +836,7 @@ def sloppy_gdb_parse(s: str) -> int | str: assert target, "Reached command expression evaluation with no frame or inferior" try: - val = target.evaluate_expression(s) + val = pwndbg.aglib.symbol.lookup_symbol(s) or target.evaluate_expression(s) if val.type.code == pwndbg.dbg_mod.TypeCode.FUNC: return int(val.address) return int(val) diff --git a/pwndbg/commands/elf.py b/pwndbg/commands/elf.py index 7d4a63a35..5190873db 100644 --- a/pwndbg/commands/elf.py +++ b/pwndbg/commands/elf.py @@ -128,13 +128,25 @@ def gotplt() -> None: # These are derived from this list that GDB recognizes: https://github.com/bminor/binutils-gdb/blob/38d726a24c1a85abdb606e7ab6cefad17872aad7/bfd/elf64-x86-64.c#L5775-L5780 PLT_SECTION_NAMES = (".plt", ".plt.sec", ".plt.got", ".plt.bnd") +parser = argparse.ArgumentParser( + description="Prints any symbols found in Procedure Linkage Table sections if any exist.", +) + +parser.add_argument( + "-a", + "--all-symbols", + help="Print all symbols, not just those that end in @plt", + action="store_true", + default=False, +) + @pwndbg.commands.Command( - "Prints any symbols found in Procedure Linkage Table sections if any exist.", + parser, category=CommandCategory.LINUX, ) @pwndbg.commands.OnlyWithFile -def plt() -> None: +def plt(all_symbols: bool = False) -> None: local_path = pwndbg.aglib.file.get_proc_exe_file() bin_base_addr = 0 @@ -171,7 +183,7 @@ def plt() -> None: sections_found.sort(key=lambda x: x[1]) for section_name, start, end in sections_found: - symbols = get_symbols_in_region(start, end, "@plt") + symbols = get_symbols_in_region(start, end, "" if all_symbols else "@plt") print(message.notice(f"Section {section_name} {start:#x} - {end:#x}:")) diff --git a/pwndbg/dbg/lldb/__init__.py b/pwndbg/dbg/lldb/__init__.py index abb0066cb..00c00450f 100644 --- a/pwndbg/dbg/lldb/__init__.py +++ b/pwndbg/dbg/lldb/__init__.py @@ -92,6 +92,16 @@ class LLDBFrame(pwndbg.dbg_mod.Frame): *, type: pwndbg.dbg_mod.SymbolLookupType = pwndbg.dbg_mod.SymbolLookupType.ANY, ) -> pwndbg.dbg_mod.Value | None: + # `symbol_name_at_address` encodes offsets as part of the name, handle + # that here. + offset = 0 + try: + idx = name.rindex("+") + offset = int(name[idx:], 10) + name = name[:idx] + except ValueError: + pass + # FIXME: how to sanitize symbol name better? if not re.match(r"^[a-zA-Z0-9_.:@*/$]+$", name): raise pwndbg.dbg_mod.Error(f"Symbol {name!r} contains invalid characters") @@ -107,6 +117,9 @@ class LLDBFrame(pwndbg.dbg_mod.Frame): # This issue occurs on certain architectures (e.g., it works fine on x86_64 but fails on aarch64). value = self.proc.lookup_symbol(name, type=type) + if value is not None: + value += offset + return value @override @@ -994,9 +1007,15 @@ class LLDBProcess(pwndbg.dbg_mod.Process): e = lldb.SBError() count = self.process.WriteMemory(address, data, e) - if count < len(data) and not partial: + if (count < len(data) or not e.success) and not partial: raise pwndbg.dbg_mod.Error(f"could not write {len(data)} bytes: {e}") + # In some instances - eg. writing to the PC - writing may still have + # failed when we get here. Make sure we can read it back, to a point. + readback_len = min(len(data), 64) + if self.read_memory(address, readback_len) != data[:readback_len]: + raise pwndbg.dbg_mod.Error(f"could not write {len(data)} bytes: read-back failed") + # We know some memory got changed. self.dbg._trigger_event(pwndbg.dbg_mod.EventType.MEMORY_CHANGED) return count @@ -1229,7 +1248,16 @@ class LLDBProcess(pwndbg.dbg_mod.Process): if not ctx.IsValid() or not ctx.symbol.IsValid(): return None - # TODO: In GDB, we return something like `main+0x10`, but in LLDB, we do not. + sym_addr = ctx.symbol.addr.GetLoadAddress(self.target) + assert ( + sym_addr <= address + ), f"LLDB returned an out-of-range address {sym_addr:#x} for a requested symbol with address {address:#x}" + + if sym_addr != address: + # Print the symbol name along with an offset value if the address we + # were given does not match up with the symbol exactly. + return f"{ctx.symbol.name}+{address - sym_addr}" + return ctx.symbol.name def _resolve_tls_symbol(self, sym: lldb.SBSymbol) -> int | None: @@ -1743,6 +1771,9 @@ class LLDB(pwndbg.dbg_mod.Debugger): # return control to the user. controllers: List[Tuple[LLDBProcess, Coroutine[Any, Any, None]]] + # Relay used for exceptions originating from commands called through LLDB. + _exception_relay: BaseException | None + @override def setup(self, *args, **kwargs): import pwnlib.update @@ -1753,6 +1784,7 @@ class LLDB(pwndbg.dbg_mod.Debugger): self.event_handlers = {} self.controllers = [] self._current_process_is_gdb_remote = False + self._exception_relay = None import pwndbg @@ -1782,6 +1814,15 @@ class LLDB(pwndbg.dbg_mod.Debugger): import pwndbg.dbg.lldb.hooks + def relay_exceptions(self) -> None: + """ + Relay an exception raised during an LLDB command handler. + """ + e = self._exception_relay + self._exception_relay = None + if e is not None: + raise pwndbg.dbg_mod.Error(e) + def _execute_lldb_command(self, command: str) -> str: result = lldb.SBCommandReturnObject() self.debugger.GetCommandInterpreter().HandleCommand( @@ -1811,11 +1852,15 @@ class LLDB(pwndbg.dbg_mod.Debugger): pass def __call__(self, _, command, exe_context, result): - debugger.exec_states.append(exe_context) - handler(debugger, command, True) - assert ( - debugger.exec_states.pop() == exe_context - ), "Execution state mismatch on command handler" + try: + debugger.exec_states.append(exe_context) + handler(debugger, command, True) + assert ( + debugger.exec_states.pop() == exe_context + ), "Execution state mismatch on command handler" + except BaseException as e: + debugger._exception_relay = e + raise # LLDB is very particular with the object paths it will accept. It is at # its happiest when its pulling objects straight off the module that was diff --git a/pwndbg/dbg/lldb/repl/__init__.py b/pwndbg/dbg/lldb/repl/__init__.py index f141e91d1..ef87e69e1 100644 --- a/pwndbg/dbg/lldb/repl/__init__.py +++ b/pwndbg/dbg/lldb/repl/__init__.py @@ -336,7 +336,7 @@ def run( try: if last_exc is not None: - coroutine.throw(last_exc) + action = coroutine.throw(last_exc) else: action = coroutine.send(last_result) except StopIteration: @@ -386,12 +386,20 @@ def run( if not action._prompt_silent: print(f"{PROMPT}{action._command}") - if action._capture: - with TextIOWrapper(BytesIO(), write_through=True) as output: - should_continue = exec_repl_command(action._command, output, dbg, driver, relay) - last_result = output.buffer.getvalue() - else: - should_continue = exec_repl_command(action._command, sys.stdout, dbg, driver, relay) + try: + if action._capture: + with TextIOWrapper(BytesIO(), write_through=True) as output: + should_continue = exec_repl_command( + action._command, output, dbg, driver, relay + ) + last_result = output.buffer.getvalue() + else: + should_continue = exec_repl_command( + action._command, sys.stdout, dbg, driver, relay + ) + except BaseException as e: + last_exc = e + continue if not should_continue: last_exc = asyncio.CancelledError() @@ -409,22 +417,31 @@ def exec_repl_command( Parses and runs the given command, returning whether the event loop should continue. """ stdout = None + stderr = None lldb_out = None + lldb_err = None try: stdout = sys.stdout + stderr = sys.stderr lldb_out = dbg.debugger.GetOutputFile() + lldb_err = dbg.debugger.GetErrorFile() sys.stdout = output_to dbg.debugger.SetOutputFile( lldb.SBFile.Create(output_to, borrow=True, force_io_methods=True) ) + dbg.debugger.SetErrorFile(lldb.SBFile.Create(output_to, borrow=True, force_io_methods=True)) return _exec_repl_command(line, output_to.buffer, dbg, driver, relay) finally: if stdout is not None: sys.stdout = stdout + if stderr is not None: + sys.stderr = stderr if lldb_out is not None: dbg.debugger.SetOutputFile(lldb_out) + if lldb_err is not None: + dbg.debugger.SetErrorFile(lldb_err) def _exec_repl_command( @@ -687,6 +704,7 @@ def _exec_repl_command( # one, or just in a general context. if driver.has_process(): driver.run_lldb_command(line, lldb_out_target) + dbg.relay_exceptions() else: ret = lldb.SBCommandReturnObject() dbg.debugger.GetCommandInterpreter().HandleCommand(line, ret) @@ -700,6 +718,7 @@ def _exec_repl_command( if len(out) > 0: lldb_out_target.write(out.encode(sys.stdout.encoding, errors="backslashreplace")) lldb_out_target.write(b"\n") + dbg.relay_exceptions() # At this point, the last command might've queued up some execution # control procedures for us to chew on. Run them now. @@ -957,7 +976,7 @@ def target_create(args: List[str], dbg: LLDB) -> None: process_launch_ap = argparse.ArgumentParser(add_help=False, prog="process launch") -process_launch_ap.add_argument("-A", "--disable-aslr") +process_launch_ap.add_argument("-A", "--disable-aslr", type=_bool_of_string, default=False) process_launch_ap.add_argument("-C", "--script-class") process_launch_ap.add_argument("-E", "--environment", action="append") process_launch_ap.add_argument("-P", "--plugin") @@ -975,7 +994,6 @@ process_launch_ap.add_argument("-v", "--structured-data-value") process_launch_ap.add_argument("-w", "--working-dir") process_launch_ap.add_argument("run-args", nargs="*") process_launch_unsupported = [ - "disable-aslr", "script-class", "plugin", "arch", @@ -1032,6 +1050,7 @@ def process_launch(driver: ProcessDriver, relay: EventRelay, args: List[str], db + (args.environment if args.environment else []), launch_args, os.getcwd(), + args.disable_aslr, ) match result: diff --git a/pwndbg/dbg/lldb/repl/proc.py b/pwndbg/dbg/lldb/repl/proc.py index b1d55cd58..95f8f42fe 100644 --- a/pwndbg/dbg/lldb/repl/proc.py +++ b/pwndbg/dbg/lldb/repl/proc.py @@ -586,7 +586,11 @@ class ProcessDriver: return LaunchResultSuccess() def _launch_remote( - self, env: List[str], args: List[str], working_dir: str | None + self, + env: List[str], + args: List[str], + working_dir: str | None, + extra_flags: int, ) -> lldb.SBError: """ Launch a process in a remote debugserver. @@ -605,7 +609,7 @@ class ProcessDriver: stdout, stderr, working_dir, - lldb.eLaunchFlagStopAtEntry, + lldb.eLaunchFlagStopAtEntry | extra_flags, True, error, ) @@ -618,6 +622,7 @@ class ProcessDriver: env: List[str], args: List[str], working_dir: str | None, + extra_flags: int, ) -> lldb.SBError: """ Launch a process in the host system. @@ -634,7 +639,7 @@ class ProcessDriver: stdout, stderr, working_dir, - lldb.eLaunchFlagStopAtEntry, + lldb.eLaunchFlagStopAtEntry | extra_flags, True, error, ) @@ -671,6 +676,7 @@ class ProcessDriver: env: List[str], args: List[str], working_dir: str | None, + disable_aslr: bool, ) -> LaunchResult: """ Launches the process and handles startup events. Always stops on first @@ -678,14 +684,18 @@ class ProcessDriver: Fires the created() event. """ + extra_flags = 0 + if disable_aslr: + extra_flags |= lldb.eLaunchFlagDisableASLR + if self.has_connection(): - result = self._enter(self._launch_remote, env, args, working_dir) + result = self._enter(self._launch_remote, env, args, working_dir, extra_flags) if isinstance(result, LaunchResultError): result.disconnected = True return result else: self._prepare_listener_for(target) - return self._enter(self._launch_local, target, io, env, args, working_dir) + return self._enter(self._launch_local, target, io, env, args, working_dir, extra_flags) def attach(self, target: lldb.SBTarget, info: lldb.SBAttachInfo) -> LaunchResult: """ diff --git a/tests/host/lldb/launch_guest.py b/tests/host/lldb/launch_guest.py index 3704800e6..0b1e2ec8f 100644 --- a/tests/host/lldb/launch_guest.py +++ b/tests/host/lldb/launch_guest.py @@ -27,6 +27,7 @@ async def _run(ctrl: Any, outer: Callable[..., Coroutine[Any, Any, None]]) -> No self.pc = pc async def launch(self, binary: Path, args: List[str] = []) -> None: + await self.pc.execute("set context-reserve-lines never") await self.pc.execute(f"target create {binary}") await self.pc.execute( "process launch -s -- " + " ".join(shlex.quote(arg) for arg in args)