Improve argument parsing and launch handling in LLDB Pwndbg (#3081)

* Improve argument parsing in LLDB Pwndbg

* Use `driver.has_connection()` to determine remote status
pull/3089/head
Matt. 6 months ago committed by GitHub
parent 0f97f0f762
commit 0077cc95b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2,12 +2,30 @@
from __future__ import annotations from __future__ import annotations
import argparse
import os import os
import re import re
import subprocess import subprocess
import sys import sys
from typing import List from typing import List
PARSER = argparse.ArgumentParser(prog="pwndbg-lldb")
PARSER.add_argument("-v", "--verbose", action="store_true", help="Enable debug output")
PARSER.add_argument("target", nargs="?")
parser_attach = PARSER.add_mutually_exclusive_group()
parser_attach.add_argument(
"-n", "--attach-name", help="Tells the debugger to attach to a process with the given name."
)
parser_attach.add_argument(
"-p", "--attach-pid", help="Tells the debugger to attach to a process with the given pid."
)
PARSER.add_argument(
"-w",
"--wait-for",
action="store_true",
help="Tells the debugger to wait for a process with the given pid or name to launch before attaching.",
)
def find_lldb_version() -> List[int]: def find_lldb_version() -> List[int]:
""" """
@ -41,7 +59,8 @@ def find_lldb_python_path() -> str:
if __name__ == "__main__": if __name__ == "__main__":
debug = "PWNDBG_LLDB_DEBUG" in os.environ args = PARSER.parse_args()
debug = args.verbose
# Find the path for the LLDB Python bindings. # Find the path for the LLDB Python bindings.
path = find_lldb_python_path() path = find_lldb_python_path()
@ -96,21 +115,50 @@ if __name__ == "__main__":
print("[-] Launcher: Initializing Pwndbg") print("[-] Launcher: Initializing Pwndbg")
lldbinit.main(debugger, lldb_version[0], lldb_version[1], debug=debug) lldbinit.main(debugger, lldb_version[0], lldb_version[1], debug=debug)
# Run our REPL until the user decides to leave.
if len(sys.argv) > 2:
print(f"Usage: {sys.argv[0]} [filename]", file=sys.stderr)
sys.exit(1)
target = None
if len(sys.argv) == 2:
target = sys.argv[1]
from pwndbg.dbg.lldb.repl import PwndbgController from pwndbg.dbg.lldb.repl import PwndbgController
from pwndbg.dbg.lldb.repl import print_error
from pwndbg.dbg.lldb.repl import print_warn
from pwndbg.dbg.lldb.repl import run as run_repl from pwndbg.dbg.lldb.repl import run as run_repl
if debug: if debug:
print("[-] Launcher: Entering Pwndbg CLI") print("[-] Launcher: Entering Pwndbg CLI")
# Prepare the startup commands.
startup = []
if args.target:
# DEVIATION: The LLDB CLI silently ignores any target information passed
# to it when using either '--attach-name' or '--attach-pid', but Pwndbg
# unconditionally uses it, with a warning.
startup = [f"target create '{args.target}'"]
if args.attach_name is not None:
wait = "--waitfor" if args.wait_for else ""
startup.append(f'process attach --name "{args.attach_name}" {wait}')
elif args.attach_pid is not None:
# DEVIATION: While the LLDB CLI accepts '--wait-for' in combination with
# both '--attach-name' and '--attach-pid', it silently ignores it when
# used with the latter. Pwndbg prints out a warning, instead.
if args.wait_for:
print_warn("'--wait-for' has no effect when used with '--attach-pid'")
startup.append(f'process attach --pid "{args.attach_pid}"')
else:
if args.wait_for:
# Ideally, we would have `ArgumentParser` do this for us, but
# nesting argument groups has been deprecated since Python 3.11, and
# the deprecation message suggests it was never even supported in
# the first place :/
print_error(
"'--wait-for' must be used in combination with either '--attach-name' or '--attach-pid'"
)
PARSER.print_usage()
sys.exit(1)
if (args.attach_pid is not None or args.attach_name is not None) and args.target:
print_warn(
"have both a target and an attach request, your target may be overwritten on attach"
)
def drive(startup: List[str] | None): def drive(startup: List[str] | None):
async def drive(c: PwndbgController): async def drive(c: PwndbgController):
if startup is not None: if startup is not None:
@ -122,7 +170,7 @@ if __name__ == "__main__":
return drive return drive
run_repl(drive([f"target create '{target}'"] if target else None), debug=debug) run_repl(drive(startup), debug=debug)
# Dispose of our debugger and terminate LLDB. # Dispose of our debugger and terminate LLDB.
lldb.SBDebugger.Destroy(debugger) lldb.SBDebugger.Destroy(debugger)

@ -385,8 +385,9 @@ def exec_repl_command(
# Let the user get an LLDB prompt if they so desire. # Let the user get an LLDB prompt if they so desire.
if bits[0] == "lldb": if bits[0] == "lldb":
print_warn("You are now entering LLDB mode. To exit, type 'quit', 'exit' or Ctrl-D.")
print_warn( print_warn(
"You are now entering LLDB mode. In this mode, certain commands may cause Pwndbg to break. Proceed with caution." "In this mode, certain commands may cause Pwndbg to break. Proceed with caution."
) )
dbg.debugger.RunCommandInterpreter( dbg.debugger.RunCommandInterpreter(
True, False, lldb.SBCommandInterpreterRunOptions(), 0, False, False True, False, lldb.SBCommandInterpreterRunOptions(), 0, False, False
@ -493,8 +494,8 @@ def exec_repl_command(
pass pass
if bits[0].startswith("r") and "run".startswith(bits[0]): if bits[0].startswith("r") and "run".startswith(bits[0]):
# `run` is an alias for `process launch` # `run` is an alias for `process launch -X true --`
process_launch(driver, relay, bits[1:], dbg) process_launch(driver, relay, ["-X", "true", "--"] + bits[1:], dbg)
return True return True
if bits[0] == "c" or (bits[0].startswith("con") and "continue".startswith(bits[0])): if bits[0] == "c" or (bits[0].startswith("con") and "continue".startswith(bits[0])):
@ -652,11 +653,48 @@ def exec_repl_command(
return True return True
def parse(args: List[str], parser: argparse.ArgumentParser, unsupported: List[str]) -> Any | None: def _bool_of_string(val: str) -> bool:
"""
Convert a string to a boolean value.
For use with ArgumentParser.
"""
if val.lower() in ("true", "1", "yes"):
return True
elif val.lower() in ("false", "0", "no"):
return False
else:
raise ValueError(f"{val} is not a recognized boolean value")
def parse(
args: List[str],
parser: argparse.ArgumentParser,
unsupported: List[str],
raw_marker: str | None = None,
) -> Any | None:
""" """
Parses a list of string arguments into an object containing the parsed Parses a list of string arguments into an object containing the parsed
data. data.
If `raw_marker` is not `None`, the argument list will be split in
two, with all arguments before the split being fed to the argument parser,
and all arguments after the split being returned as-is. In this case the
return value is a tuple.
""" """
raw = None
if raw_marker is not None:
# Always return something, even if we match nothing.
raw = []
try:
index = args.index(raw_marker)
raw = args[index + 1 :]
args = args[:index]
except ValueError:
# Ugly, but avoids going over the list twice.
pass
try: try:
args = parser.parse_args(args) args = parser.parse_args(args)
except SystemExit: except SystemExit:
@ -673,9 +711,54 @@ def parse(args: List[str], parser: argparse.ArgumentParser, unsupported: List[st
print_error(f"Pwndbg does not support --{unsup} yet") print_error(f"Pwndbg does not support --{unsup} yet")
return None return None
if raw is not None:
# If called with `raw_marked`, return a tuple.
return args, raw
return args return args
class AutoTarget:
"""
During the execution of some commands, the LLDB CLI automatically creates an
empty target and selects it before the command is executed.
"""
def __init__(self, dbg: LLDB):
self.error = lldb.SBError()
self._dbg = dbg
self._created_target = False
count = dbg.debugger.GetNumTargets()
if count == 0:
# Create the target.
self.target = dbg.debugger.CreateTarget(None, None, None, True, self.error)
if not self.error.success:
return
# On success, select it and remember that it has been created.
dbg.debugger.SetSelectedTarget(self.target)
self._created_target = True
elif count == 1:
# Just use the current target.
self.target = dbg.debugger.GetTargetAtIndex(0)
assert self.target, f"SBDebugger::GetNumTargets() is 1, but SBDebugger::GetTargetAtIndex(0) is {self.target}"
else:
raise AssertionError(
f"Pwndbg does not support multiple targets, so SBDebugger::GetNumTargets() must always be 0 or 1, but is {count}"
)
def __bool__(self):
return self.error.success
def close(self):
if self._created_target:
assert self._dbg.debugger.DeleteTarget(
self.target
), "Could not delete the target we've just created. What?"
def run_ipython_shell(): def run_ipython_shell():
@contextmanager @contextmanager
def switch_to_ipython_env(): def switch_to_ipython_env():
@ -707,7 +790,7 @@ def run_ipython_shell():
start_ipi() start_ipi()
target_create_ap = argparse.ArgumentParser(add_help=False) target_create_ap = argparse.ArgumentParser(add_help=False, prog="target create")
target_create_ap.add_argument("-S", "--sysroot") target_create_ap.add_argument("-S", "--sysroot")
target_create_ap.add_argument("-a", "--arch") target_create_ap.add_argument("-a", "--arch")
target_create_ap.add_argument("-b", "--build") target_create_ap.add_argument("-b", "--build")
@ -796,12 +879,12 @@ def target_create(args: List[str], dbg: LLDB) -> None:
return return
process_launch_ap = argparse.ArgumentParser(add_help=False) 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")
process_launch_ap.add_argument("-C", "--script-class") process_launch_ap.add_argument("-C", "--script-class")
process_launch_ap.add_argument("-E", "--environment") process_launch_ap.add_argument("-E", "--environment", action="append")
process_launch_ap.add_argument("-P", "--plugin") process_launch_ap.add_argument("-P", "--plugin")
process_launch_ap.add_argument("-X", "--shell-expand-args") process_launch_ap.add_argument("-X", "--shell-expand-args", type=_bool_of_string)
process_launch_ap.add_argument("-a", "--arch") process_launch_ap.add_argument("-a", "--arch")
process_launch_ap.add_argument("-c", "--shell") process_launch_ap.add_argument("-c", "--shell")
process_launch_ap.add_argument("-e", "--stderr") process_launch_ap.add_argument("-e", "--stderr")
@ -817,9 +900,7 @@ process_launch_ap.add_argument("run-args", nargs="*")
process_launch_unsupported = [ process_launch_unsupported = [
"disable-aslr", "disable-aslr",
"script-class", "script-class",
"environment",
"plugin", "plugin",
"shell-expand-args",
"arch", "arch",
"shell", "shell",
"stderr", "stderr",
@ -837,9 +918,15 @@ def process_launch(driver: ProcessDriver, relay: EventRelay, args: List[str], db
""" """
Launches a process with the given arguments. Launches a process with the given arguments.
""" """
args = parse(args, process_launch_ap, process_launch_unsupported) result = parse(args, process_launch_ap, process_launch_unsupported, raw_marker="--")
if not args: if result is None:
return return
args, raw = result
launch_args = getattr(args, "run-args", []) + raw
if args.shell_expand_args:
# Perform shell expansion.
launch_args = [os.path.expanduser(os.path.expandvars(arg)) for arg in launch_args]
targets = dbg.debugger.GetNumTargets() targets = dbg.debugger.GetNumTargets()
assert targets < 2 assert targets < 2
@ -852,8 +939,9 @@ def process_launch(driver: ProcessDriver, relay: EventRelay, args: List[str], db
return return
target: lldb.SBTarget = dbg.debugger.GetTargetAtIndex(0) target: lldb.SBTarget = dbg.debugger.GetTargetAtIndex(0)
# Make sure the LLDB driver knows that this is a local process.
dbg._current_process_is_gdb_remote = False # Make sure LLDB knows the correct remote or local status of this launch.
dbg._current_process_is_gdb_remote = driver.has_connection()
if target.GetPlatform().GetName() == "qemu-user": if target.GetPlatform().GetName() == "qemu-user":
# Force qemu-user as remote, pwndbg depends on that, eg: for download procfs files # Force qemu-user as remote, pwndbg depends on that, eg: for download procfs files
@ -863,8 +951,9 @@ def process_launch(driver: ProcessDriver, relay: EventRelay, args: List[str], db
result = driver.launch( result = driver.launch(
target, target,
io_driver, io_driver,
[f"{name}={value}" for name, value in os.environ.items()], [f"{name}={value}" for name, value in os.environ.items()]
getattr(args, "run-args", []), + (args.environment if args.environment else []),
launch_args,
os.getcwd(), os.getcwd(),
) )
@ -896,7 +985,7 @@ def process_launch(driver: ProcessDriver, relay: EventRelay, args: List[str], db
dbg._trigger_event(EventType.STOP) dbg._trigger_event(EventType.STOP)
process_attach_ap = argparse.ArgumentParser(add_help=False) process_attach_ap = argparse.ArgumentParser(add_help=False, prog="process attach")
process_attach_ap.add_argument("-C", "--python-class") process_attach_ap.add_argument("-C", "--python-class")
process_attach_ap.add_argument("-P", "--plugin") process_attach_ap.add_argument("-P", "--plugin")
process_attach_ap.add_argument("-c", "--continue", action="store_true") process_attach_ap.add_argument("-c", "--continue", action="store_true")
@ -920,11 +1009,7 @@ def _attach_with_info(
""" """
Attaches to a process based on SBAttachInfo information Attaches to a process based on SBAttachInfo information
""" """
targets = dbg.debugger.GetNumTargets() assert dbg.debugger.GetNumTargets() < 2
assert targets < 2
if targets == 0:
print_error("no target, create one using the 'target create' command")
return
# TODO/FIXME: This should ask: # TODO/FIXME: This should ask:
# 'There is a running process, detach from it and attach?: [Y/n]' # 'There is a running process, detach from it and attach?: [Y/n]'
@ -934,14 +1019,24 @@ def _attach_with_info(
io_driver = get_io_driver() io_driver = get_io_driver()
auto = AutoTarget(dbg)
if not auto:
print_error(f"could not create empty target for attaching: {auto.error.description}")
auto.close()
return
# Make sure LLDB knows the correct remote or local status of this attach.
dbg._current_process_is_gdb_remote = driver.has_connection()
result = driver.attach( result = driver.attach(
dbg.debugger.GetTargetAtIndex(0), auto.target,
io_driver, io_driver,
info, info,
) )
if not result.success: if not result.success:
print_error(f"could not attach to process: {result.description}") print_error(f"could not attach to process: {result.description}")
auto.close()
return return
# Continue execution if the user has requested it. # Continue execution if the user has requested it.
@ -1006,7 +1101,7 @@ def attach(driver: ProcessDriver, relay: EventRelay, args: List[str], dbg: LLDB)
_attach_with_info(driver, relay, dbg, info) _attach_with_info(driver, relay, dbg, info)
process_connect_ap = argparse.ArgumentParser(add_help=False) process_connect_ap = argparse.ArgumentParser(add_help=False, prog="process connect")
process_connect_ap.add_argument("-p", "--plugin") process_connect_ap.add_argument("-p", "--plugin")
process_connect_ap.add_argument("remoteurl") process_connect_ap.add_argument("remoteurl")
@ -1029,53 +1124,21 @@ def process_connect(driver: ProcessDriver, relay: EventRelay, args: List[str], d
print_error("debugger is already connected") print_error("debugger is already connected")
return return
target = dbg.debugger.GetSelectedTarget()
error = lldb.SBError()
created_target = False
if target is None or not target.IsValid():
# Create a new, empty target, the same way the LLDB command line would.
#
# The LLDB command line sets the default triple based on the
# architecture value set in the `target.default-arch` setting. We do the
# same.
try:
result = dbg._execute_lldb_command("settings show target.default-arch")
# The result of this command has the following form:
#
# (lldb) settings show target.default-arch
# target.default-arch (arch) = <value>
#
# Where <value> may be empty, for no value.
arch = result.split("=")[1].strip()
except pwndbg.dbg_mod.Error:
arch = ""
triple = f"{arch}-unknown-unknown" if len(arch) > 0 else None
target = dbg.debugger.CreateTarget(None, triple, None, True, error)
if not error.success or not target.IsValid():
print_error(
f"could not automatically create target for 'process connect': {error.description}"
)
return
dbg.debugger.SetSelectedTarget(target)
created_target = True
# Make sure the LLDB driver knows that this is a remote process. # Make sure the LLDB driver knows that this is a remote process.
dbg._current_process_is_gdb_remote = True dbg._current_process_is_gdb_remote = True
auto = AutoTarget(dbg)
if not auto:
print_error(f"could not create empty target for connection: {auto.error.description}")
auto.close()
return
io_driver = get_io_driver() io_driver = get_io_driver()
error = driver.connect(target, io_driver, args.remoteurl, "gdb-remote") error = driver.connect(auto.target, io_driver, args.remoteurl, "gdb-remote")
if not error.success: if not error.success:
print_error(f"could not connect to remote process: {error.description}") print_error(f"could not connect to remote process: {error.description}")
if created_target: auto.close()
# Delete the target we previously created.
assert dbg.debugger.DeleteTarget(
target
), "Could not delete the target we've just created. What?"
return return
# Tell the debugger that the process was suspended, if there is a process. # Tell the debugger that the process was suspended, if there is a process.
@ -1083,7 +1146,7 @@ def process_connect(driver: ProcessDriver, relay: EventRelay, args: List[str], d
dbg._trigger_event(EventType.STOP) dbg._trigger_event(EventType.STOP)
gdb_remote_ap = argparse.ArgumentParser(add_help=False) gdb_remote_ap = argparse.ArgumentParser(add_help=False, prog="gdb-remote")
gdb_remote_ap.add_argument("remoteurl") gdb_remote_ap.add_argument("remoteurl")
@ -1120,7 +1183,7 @@ def gdb_remote(driver: ProcessDriver, relay: EventRelay, args: List[str], dbg: L
process_connect(driver, relay, ["-p", "gdb-remote", f"connect://{url}:{port}"], dbg) process_connect(driver, relay, ["-p", "gdb-remote", f"connect://{url}:{port}"], dbg)
continue_ap = argparse.ArgumentParser(add_help=False) continue_ap = argparse.ArgumentParser(add_help=False, prog="continue")
continue_ap.add_argument("-i", "--ignore-count") continue_ap.add_argument("-i", "--ignore-count")
continue_unsupported = ["ignore-count"] continue_unsupported = ["ignore-count"]

Loading…
Cancel
Save