mirror of https://github.com/pwndbg/pwndbg
Add attachp command and tests for it
This commit is contained in:
parent
1e28920440
commit
6fd42dd5ab
|
@ -28,6 +28,7 @@ jobs:
|
|||
echo 'Installed py:'
|
||||
python -V
|
||||
|
||||
# We use `sudo` for `attachp` command tests
|
||||
- name: Run tests
|
||||
run: |
|
||||
PWNDBG_TRAVIS_TEST_RUN=1 ./tests.sh
|
||||
PWNDBG_GITHUB_ACTIONS_TEST_RUN=1 sudo ./tests.sh
|
||||
|
|
|
@ -12,6 +12,7 @@ import pwndbg.color
|
|||
import pwndbg.commands
|
||||
import pwndbg.commands.argv
|
||||
import pwndbg.commands.aslr
|
||||
import pwndbg.commands.attachp
|
||||
import pwndbg.commands.auxv
|
||||
import pwndbg.commands.canary
|
||||
import pwndbg.commands.checksec
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
import argparse
|
||||
import os
|
||||
import stat
|
||||
from subprocess import CalledProcessError
|
||||
from subprocess import check_output
|
||||
|
||||
import gdb
|
||||
|
||||
import pwndbg.color.message as message
|
||||
import pwndbg.commands
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="""Attaches to a given pid, process name or device file.
|
||||
|
||||
This command wraps the original GDB `attach` command to add the ability
|
||||
to debug a process with given name. In such case the process identifier is
|
||||
fetched via the `pidof <name>` command.
|
||||
|
||||
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("target", type=str, help="pid, process name or device file to attach to")
|
||||
@pwndbg.commands.ArgparsedCommand(parser)
|
||||
def attachp(target):
|
||||
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:
|
||||
try:
|
||||
pids = check_output(['pidof', target]).decode().rstrip('\n').split(' ')
|
||||
except FileNotFoundError:
|
||||
print(message.error("Error: did not find `pidof` command"))
|
||||
return
|
||||
except CalledProcessError:
|
||||
pids = []
|
||||
|
||||
if not pids:
|
||||
print(message.error("Process %s not found" % target))
|
||||
return
|
||||
|
||||
if len(pids) > 1:
|
||||
print(message.warn("Found pids: %s (use `attach <pid>`)" % ', '.join(pids)))
|
||||
return
|
||||
|
||||
resolved_target = int(pids[0])
|
||||
|
||||
print(message.on("Attaching to %s" % resolved_target))
|
||||
try:
|
||||
gdb.execute('attach %s' % resolved_target)
|
||||
except gdb.error as e:
|
||||
print(message.error('Error: %s' % e))
|
||||
|
||||
|
||||
def _is_device(path):
|
||||
try:
|
||||
mode = os.stat(path).st_mode
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
if stat.S_ISCHR(mode):
|
||||
return True
|
||||
|
||||
return False
|
|
@ -22,7 +22,12 @@ class CollectTestFunctionNames:
|
|||
|
||||
|
||||
collector = CollectTestFunctionNames()
|
||||
pytest.main(['--collect-only', TESTS_PATH], plugins=[collector])
|
||||
rv = pytest.main(['--collect-only', TESTS_PATH], plugins=[collector])
|
||||
|
||||
if rv == pytest.ExitCode.INTERRUPTED:
|
||||
print("Failed to collect all tests, perhaps there is a syntax error in one of test files?")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
print('Listing collected tests:')
|
||||
for nodeid in collector.collected:
|
||||
|
|
9
tests.sh
9
tests.sh
|
@ -6,7 +6,14 @@ cd ../../
|
|||
|
||||
# NOTE: We run tests under GDB sessions and because of some cleanup/tests dependencies problems
|
||||
# we decided to run each test in a separate GDB session
|
||||
TESTS_LIST=$(gdb --silent --nx --nh --command gdbinit.py --command pytests_collect.py --eval-command quit | grep -o "tests/.*::.*")
|
||||
TESTS_COLLECT_OUTPUT=$(gdb --silent --nx --nh --command gdbinit.py --command pytests_collect.py --eval-command quit)
|
||||
|
||||
if [ $? -eq 1 ]; then
|
||||
echo -E "$TESTS_COLLECT_OUTPUT";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
TESTS_LIST=$(echo -E "$TESTS_COLLECT_OUTPUT" | grep -o "tests/.*::.*")
|
||||
|
||||
tests_passed_or_skipped=0
|
||||
tests_failed=0
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import gdb
|
||||
import pytest
|
||||
|
||||
import tests
|
||||
|
||||
from .utils import run_gdb_with_script
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def launched_bash_binary():
|
||||
path = '/tmp/pwndbg_test_bash'
|
||||
subprocess.check_output(['cp', '/bin/bash', path])
|
||||
|
||||
process = subprocess.Popen([path], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
|
||||
yield process.pid, path
|
||||
|
||||
process.kill()
|
||||
|
||||
os.remove(path) # Cleanup
|
||||
|
||||
|
||||
def test_attachp_command_attaches_to_procname(launched_bash_binary):
|
||||
pid, binary_path = launched_bash_binary
|
||||
|
||||
binary_name = binary_path.split('/')[-1]
|
||||
result = run_gdb_with_script(pyafter='attachp %s' % binary_name)
|
||||
|
||||
matches = re.search(r'Attaching to ([0-9]+)', result).groups()
|
||||
assert matches == (str(pid),)
|
||||
|
||||
assert re.search(r'Detaching from program: %s, process %s' % (binary_path, pid), result)
|
||||
|
||||
def test_attachp_command_attaches_to_pid(launched_bash_binary):
|
||||
pid, binary_path = launched_bash_binary
|
||||
|
||||
result = run_gdb_with_script(pyafter='attachp %s' % pid)
|
||||
|
||||
matches = re.search(r'Attaching to ([0-9]+)', result).groups()
|
||||
assert matches == (str(pid),)
|
||||
|
||||
assert re.search(r'Detaching from program: %s, process %s' % (binary_path, pid), result)
|
||||
|
||||
def test_attachp_command_attaches_to_procname_too_many_pids(launched_bash_binary):
|
||||
pid, binary_path = launched_bash_binary
|
||||
|
||||
process = subprocess.Popen([binary_path], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
|
||||
binary_name = binary_path.split('/')[-1]
|
||||
result = run_gdb_with_script(pyafter='attachp %s' % binary_name)
|
||||
|
||||
process.kill()
|
||||
|
||||
matches = re.search(r'Found pids: ([0-9]+), ([0-9]+) \(use `attach <pid>`\)', result).groups()
|
||||
matches = list(map(int, matches))
|
||||
matches.sort()
|
||||
|
||||
expected_pids = [pid, process.pid]
|
||||
expected_pids.sort()
|
||||
|
||||
assert matches == expected_pids
|
||||
|
||||
def test_attachp_command_nonexistent_procname():
|
||||
result = run_gdb_with_script(pyafter='attachp some-nonexistent-process-name') # No chance there is a process name like this
|
||||
assert 'Process some-nonexistent-process-name not found' in result
|
||||
|
||||
def test_attachp_command_no_pids():
|
||||
result = run_gdb_with_script(pyafter='attachp 99999999') # No chance there is a PID like this
|
||||
assert 'Error: ptrace: No such process.' in result
|
||||
|
|
@ -1,60 +1,16 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
import tests
|
||||
|
||||
|
||||
def run_gdb_with_script(binary='', core='', pybefore=None, pyafter=None):
|
||||
"""
|
||||
Runs GDB with given commands launched before and after loading of gdbinit.py
|
||||
Returns GDB output.
|
||||
"""
|
||||
pybefore = ([pybefore] if isinstance(pybefore, str) else pybefore) or []
|
||||
pyafter = ([pyafter] if isinstance(pyafter, str) else pyafter) or []
|
||||
|
||||
command = ['gdb', '--silent', '--nx', '--nh']
|
||||
|
||||
for cmd in pybefore:
|
||||
command += ['--eval-command', cmd]
|
||||
|
||||
command += ['--command', 'gdbinit.py']
|
||||
|
||||
if binary:
|
||||
command += [binary]
|
||||
|
||||
if core:
|
||||
command += ['--core', core]
|
||||
|
||||
for cmd in pyafter:
|
||||
command += ['--eval-command', cmd]
|
||||
|
||||
command += ['--eval-command', 'quit']
|
||||
|
||||
print("Launching command: %s" % command)
|
||||
output = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
# Python 3 returns bytes-like object so lets have it consistent
|
||||
output = codecs.decode(output, 'utf8')
|
||||
|
||||
# The pwndbg banner shows number of loaded commands, it might differ between
|
||||
# testing environments, so lets change it to ###
|
||||
output = re.sub(r'loaded [0-9]+ commands', r'loaded ### commands', output)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def compile_binary(binary_source, binary_out):
|
||||
assert os.path.isfile(binary_source)
|
||||
|
||||
subprocess.check_call(['gcc', binary_source, '-o', binary_out])
|
||||
|
||||
from .utils import compile_binary
|
||||
from .utils import launched_locally
|
||||
from .utils import run_gdb_with_script
|
||||
|
||||
HELLO = [
|
||||
'pwndbg: loaded ### commands. Type pwndbg [filter] for a list.',
|
||||
|
@ -65,8 +21,6 @@ BINARY_SOURCE = tests.binaries.div_zero_binary.get('binary.c')
|
|||
BINARY = tests.binaries.div_zero_binary.get('binary')
|
||||
CORE = tests.binaries.div_zero_binary.get('core')
|
||||
|
||||
launched_locally = not (os.environ.get('PWNDBG_TRAVIS_TEST_RUN'))
|
||||
|
||||
|
||||
def test_loads_pure_gdb_without_crashing():
|
||||
output = run_gdb_with_script().splitlines()
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import codecs
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
launched_locally = not (os.environ.get('PWNDBG_GITHUB_ACTIONS_TEST_RUN'))
|
||||
|
||||
|
||||
def run_gdb_with_script(binary='', core='', pybefore=None, pyafter=None):
|
||||
"""
|
||||
Runs GDB with given commands launched before and after loading of gdbinit.py
|
||||
Returns GDB output.
|
||||
"""
|
||||
pybefore = ([pybefore] if isinstance(pybefore, str) else pybefore) or []
|
||||
pyafter = ([pyafter] if isinstance(pyafter, str) else pyafter) or []
|
||||
|
||||
command = ['gdb', '--silent', '--nx', '--nh']
|
||||
|
||||
for cmd in pybefore:
|
||||
command += ['--eval-command', cmd]
|
||||
|
||||
command += ['--command', 'gdbinit.py']
|
||||
|
||||
if binary:
|
||||
command += [binary]
|
||||
|
||||
if core:
|
||||
command += ['--core', core]
|
||||
|
||||
for cmd in pyafter:
|
||||
command += ['--eval-command', cmd]
|
||||
|
||||
command += ['--eval-command', 'quit']
|
||||
|
||||
print("Launching command: %s" % command)
|
||||
output = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
# Python 3 returns bytes-like object so lets have it consistent
|
||||
output = codecs.decode(output, 'utf8')
|
||||
|
||||
# The pwndbg banner shows number of loaded commands, it might differ between
|
||||
# testing environments, so lets change it to ###
|
||||
output = re.sub(r'loaded [0-9]+ commands', r'loaded ### commands', output)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def compile_binary(binary_source, binary_out):
|
||||
assert os.path.isfile(binary_source)
|
||||
|
||||
subprocess.check_call(['gcc', binary_source, '-o', binary_out])
|
||||
|
Loading…
Reference in New Issue