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
pull/3206/head
Matt. 4 months ago committed by GitHub
parent 735ebbeba2
commit a5d5988020
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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|
<!-- END OF AUTOGENERATED PART. Do not modify this line or the line below, they mark the end of the auto-generated part of the file. If you want to extend the documentation in a way which cannot easily be done by adding to the command help description, write below the following line. -->
<!-- ------------\>8---- ----\>8---- ----\>8------------ -->

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save