mirror of https://github.com/pwndbg/pwndbg.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
301 lines
12 KiB
Python
301 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import stat
|
|
from subprocess import CalledProcessError
|
|
from subprocess import check_output
|
|
from typing import Union
|
|
|
|
import gdb
|
|
from tabulate import tabulate
|
|
|
|
import pwndbg.commands
|
|
from pwndbg.color import message
|
|
from pwndbg.commands import CommandCategory
|
|
from pwndbg.ui import get_window_size
|
|
|
|
_NONE = "none"
|
|
_OLDEST = "oldest"
|
|
_NEWEST = "newest"
|
|
_ASK = "ask"
|
|
_OPTIONS = [_NONE, _OLDEST, _NEWEST, _ASK]
|
|
|
|
pwndbg.config.add_param(
|
|
"attachp-resolution-method",
|
|
_ASK,
|
|
f'how to determine the process to attach when multiple candidates exists ("{_OLDEST}", "{_NEWEST}", "{_NONE}" or "{_ASK}"(default))',
|
|
)
|
|
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
description="""Attaches to a given pid, process name, process found with partial argv match or to a device file.
|
|
|
|
This command wraps the original GDB `attach` command to add the ability
|
|
to debug a process with a given name or partial name match. In such cases,
|
|
the process identifier is fetched via the `pidof <name>` command first. If no
|
|
matches are found, then it uses the `ps -eo pid,args` command to search for
|
|
partial name matches.
|
|
|
|
Original GDB attach command help:
|
|
Attach to a process or file outside of GDB.
|
|
This command attaches to another target, of the same type as your last
|
|
"target" command ("info files" will show your target stack).
|
|
The command may take as argument a process id or a device file.
|
|
For a process id, you must have permission to send the process a signal,
|
|
and it must have the same effective uid as the debugger.
|
|
When using "attach" with a process id, the debugger finds the
|
|
program running in the process, looking first in the current working
|
|
directory, or (if not found there) using the source file search path
|
|
(see the "directory" command). You can also use the "file" command
|
|
to specify the program, and to load its symbol table.""",
|
|
)
|
|
|
|
|
|
parser.add_argument("--no-truncate", action="store_true", help="dont truncate command args")
|
|
parser.add_argument("--retry", action="store_true", help="retry until a target is found")
|
|
parser.add_argument("--user", type=str, default=None, help="username or uid to filter by")
|
|
parser.add_argument(
|
|
"-a",
|
|
"--all",
|
|
action="store_true",
|
|
help="get pids for all matches (exact and partial cmdline etc)",
|
|
)
|
|
parser.add_argument(
|
|
"target",
|
|
nargs="?",
|
|
default=None,
|
|
type=str,
|
|
help="pid, process name, part of cmdline to be matched or device file to attach to (uses current loaded file name if not provided)",
|
|
)
|
|
|
|
|
|
def find_pids(target, user, all):
|
|
# Note: we can't use `ps -C <target>` because this does not accept process names with spaces
|
|
# so target='a b' would actually match process names 'a' and 'b' here
|
|
# so instead, we will filter by process name or full cmdline later on
|
|
# if provided, filter by effective username or uid; otherwise, select all processes
|
|
ps_filter = ["-u", user] if user is not None else ["-e"]
|
|
ps_cmd = ["ps", "-o", "pid,args"] + ps_filter
|
|
|
|
try:
|
|
ps_output = check_output(ps_cmd, universal_newlines=True)
|
|
except FileNotFoundError:
|
|
print(message.error("Error: did not find `ps` command"))
|
|
return
|
|
except CalledProcessError:
|
|
print(message.error(f"The `{' '.join(ps_cmd)}` command returned non-zero status"))
|
|
return
|
|
|
|
pids_exact_match_cmd = []
|
|
pids_partial_match_cmd = []
|
|
pids_partial_match_args = []
|
|
|
|
# Skip header line
|
|
for line in ps_output.strip().splitlines()[1:]:
|
|
process_info = line.split(maxsplit=1)
|
|
|
|
if len(process_info) <= 1:
|
|
# No command name?
|
|
continue
|
|
|
|
pid, args = process_info
|
|
cmd = args.split(maxsplit=1)[0]
|
|
|
|
# Cannot attach to gdb itself.
|
|
if int(pid) == os.getpid():
|
|
continue
|
|
|
|
if target == cmd:
|
|
pids_exact_match_cmd.append(pid)
|
|
elif target in cmd:
|
|
pids_partial_match_cmd.append(pid)
|
|
elif target in args:
|
|
pids_partial_match_args.append(pid)
|
|
|
|
if all:
|
|
return pids_exact_match_cmd + pids_partial_match_cmd + pids_partial_match_args
|
|
|
|
return pids_exact_match_cmd or pids_partial_match_cmd or pids_partial_match_args
|
|
|
|
|
|
@pwndbg.commands.ArgparsedCommand(parser, category=CommandCategory.START)
|
|
def attachp(target, no_truncate, retry, all, user=None) -> None:
|
|
# As a default, the user may want to attach to a binary name taken from currently loaded file name
|
|
if target is None:
|
|
bin_path = pwndbg.dbg.selected_inferior().main_module_name()
|
|
if bin_path is None:
|
|
print(
|
|
message.error(
|
|
"No target name/pid/cmdline provided and no binary loaded in the debugger"
|
|
)
|
|
)
|
|
print(message.error("(could not find the process name to attach to)"))
|
|
return
|
|
|
|
target = os.path.basename(bin_path)
|
|
try:
|
|
resolved_target = int(target)
|
|
except ValueError:
|
|
# GDB supposedly supports device files, so let's try it here...:
|
|
# <disconnect3d> hey, does anyone know what does `attach <device-file>` do?
|
|
# <disconnect3d> is this an alias for `target extended /dev/ttyACM0` or similar?
|
|
# <disconnect3d> I mean, `help attach` suggests that the `attach` command supports a device file target...
|
|
# <simark> I had no idea
|
|
# <simark> what you pass to attach is passed directly to target_ops::attach
|
|
# <simark> so it must be very target-specific
|
|
# <disconnect3d> how can it be target specific if it should attach you to a target?
|
|
# <disconnect3d> or do you mean osabi/arch etc?
|
|
# <simark> in "attach foo", foo is interpreted by the target you are connected to
|
|
# <simark> But all targets I can find interpret foo as a PID
|
|
# <simark> So it might be that old targets had some other working mode
|
|
if _is_device(target):
|
|
resolved_target = target
|
|
|
|
else:
|
|
pids = find_pids(target, user, all)
|
|
if not pids and retry:
|
|
user_filter = "" if not user else f" and user={user}"
|
|
print(
|
|
message.warn(
|
|
f"Looking for pids for target={target}{user_filter} in a loop. Hit CTRL+C to cancel"
|
|
)
|
|
)
|
|
while not pids:
|
|
pids = find_pids(target, user, all)
|
|
|
|
if not pids:
|
|
print(message.error(f"Process {target} not found"))
|
|
return
|
|
|
|
if len(pids) > 1:
|
|
method = pwndbg.config.attachp_resolution_method
|
|
|
|
if method not in _OPTIONS:
|
|
print(
|
|
message.warn(
|
|
f'Invalid value for `attachp-resolution-method` config. Fallback to default value("{_ASK}").'
|
|
)
|
|
)
|
|
method = _ASK
|
|
|
|
try:
|
|
ps_output = check_output(
|
|
[
|
|
"ps",
|
|
"--no-headers",
|
|
"-ww",
|
|
"-p",
|
|
",".join(pids),
|
|
"-o",
|
|
"pid,ruser,etime,args",
|
|
"--sort",
|
|
"+lstart",
|
|
]
|
|
).decode()
|
|
except FileNotFoundError:
|
|
print(message.error("Error: did not find `ps` command"))
|
|
print(
|
|
message.warn(f"Use `attach <pid>` instead (found pids: {', '.join(pids)})")
|
|
)
|
|
return
|
|
except CalledProcessError:
|
|
print(message.error("Error: failed to get process details"))
|
|
print(
|
|
message.warn(f"Use `attach <pid>` instead (found pids: {', '.join(pids)})")
|
|
)
|
|
return
|
|
|
|
print(
|
|
message.warn(
|
|
f'Multiple processes found. Current resolution method is "{method}". Run the command `config attachp-resolution-method` to see more informations.'
|
|
)
|
|
)
|
|
|
|
# Here, we can safely use split to capture each field
|
|
# since none of the columns except args can contain spaces
|
|
proc_infos = [row.split(maxsplit=3) for row in ps_output.splitlines()]
|
|
if method == _OLDEST:
|
|
resolved_target = int(proc_infos[0][0])
|
|
elif method == _NEWEST:
|
|
resolved_target = int(proc_infos[-1][0])
|
|
else:
|
|
headers = ["pid", "user", "elapsed", "command"]
|
|
showindex: Union[bool, range] = (
|
|
False if method == _NONE else range(1, len(proc_infos) + 1)
|
|
)
|
|
|
|
# calculate max_col_widths to fit window width
|
|
test_table = tabulate(proc_infos, headers=headers, showindex=showindex)
|
|
table_orig_width = len(test_table.splitlines()[1])
|
|
max_command_width = max(len(command) for _, _, _, command in proc_infos)
|
|
max_col_widths = max(
|
|
max_command_width - (table_orig_width - get_window_size()[1]), 10
|
|
)
|
|
|
|
# truncation
|
|
if not no_truncate:
|
|
for info in proc_infos:
|
|
info[-1] = _truncate_string(info[-1], max_col_widths)
|
|
|
|
msg = tabulate(
|
|
proc_infos,
|
|
headers=headers,
|
|
showindex=showindex,
|
|
maxcolwidths=max_col_widths,
|
|
)
|
|
print(message.notice(msg))
|
|
|
|
if method == _NONE:
|
|
print(message.warn("use `attach <pid>` to attach"))
|
|
return
|
|
elif method == _ASK:
|
|
while True:
|
|
msg = message.notice(f"which process to attach?(1-{len(proc_infos)}) ")
|
|
try:
|
|
inp = input(msg).strip()
|
|
except EOFError:
|
|
return
|
|
try:
|
|
choice = int(inp)
|
|
if not (1 <= choice <= len(proc_infos)):
|
|
continue
|
|
except ValueError:
|
|
continue
|
|
break
|
|
resolved_target = int(proc_infos[choice - 1][0])
|
|
else:
|
|
raise Exception("unreachable")
|
|
else:
|
|
resolved_target = int(pids[0])
|
|
|
|
print(message.on(f"Attaching to {resolved_target}"))
|
|
try:
|
|
gdb.execute(f"attach {resolved_target}")
|
|
except gdb.error as e:
|
|
print(message.error(f"Error: {e}"))
|
|
return
|
|
|
|
|
|
def _is_device(path) -> bool:
|
|
try:
|
|
mode = os.stat(path).st_mode
|
|
except FileNotFoundError:
|
|
return False
|
|
|
|
if stat.S_ISCHR(mode):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _truncate_string(s: str, length: int):
|
|
TRUNCATE_FILLER = " ... "
|
|
if len(s) < length:
|
|
return s
|
|
truncate_point = (length - len(TRUNCATE_FILLER)) // 2
|
|
result = s[:truncate_point]
|
|
result += TRUNCATE_FILLER
|
|
result += s[-(length - len(result)) :]
|
|
return result
|