Add attachp command and tests for it

This commit is contained in:
Disconnect3d 2021-09-20 17:42:52 +02:00
parent 1e28920440
commit 6fd42dd5ab
8 changed files with 240 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

78
tests/test_attachp.py Normal file
View File

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

View File

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

52
tests/utils.py Normal file
View File

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