Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add port, gdb_args, and gdbserver_args to gdb.debug() #2382

Merged
merged 8 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ The table below shows which release corresponds to each branch, and what date th
- [#2391][2391] Fix error message when passing invalid kwargs to `xor`
- [#2376][2376] Return buffered data on first EOF in tube.readline()
- [#2387][2387] Convert apport_corefile() output from bytes-like object to string
- [#2382][2382] added optional port, gdb_args and gdbserver_args parameters to gdb.debug()

[2360]: https://github.com/Gallopsled/pwntools/pull/2360
[2356]: https://github.com/Gallopsled/pwntools/pull/2356
Expand All @@ -93,6 +94,7 @@ The table below shows which release corresponds to each branch, and what date th
[2391]: https://github.com/Gallopsled/pwntools/pull/2391
[2376]: https://github.com/Gallopsled/pwntools/pull/2376
[2387]: https://github.com/Gallopsled/pwntools/pull/2387
[2382]: https://github.com/Gallopsled/pwntools/pull/2382

## 4.13.0 (`beta`)

Expand Down
36 changes: 25 additions & 11 deletions pwnlib/gdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def _execve_script(argv, executable, env, ssh):
return tmp.name


def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None, python_wrapper_script=None):
def _gdbserver_args(pid=None, path=None, port=0, gdbserver_args=None, args=None, which=None, env=None, python_wrapper_script=None):
"""_gdbserver_args(pid=None, path=None, args=None, which=None, env=None) -> list

Sets up a listening gdbserver, to either connect to the specified
Expand All @@ -292,6 +292,8 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None, python
Arguments:
pid(int): Process ID to attach to
path(str): Process to launch
port(int): Port to use for gdbserver, default: random
gdbserver_args(list): List of additional arguments to pass to gdbserver
args(list): List of arguments to provide on the debugger command line
which(callaable): Function to find the path of a binary.
env(dict): Environment variables to pass to the program
Expand All @@ -300,6 +302,11 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None, python
Returns:
A list of arguments to invoke gdbserver.
"""
if gdbserver_args is None:
gdbserver_args = list()
elif not isinstance(gdbserver_args, (list, tuple)):
gdbserver_args = [gdbserver_args]

if [pid, path, args].count(None) != 2:
log.error("Must specify exactly one of pid, path, or args")

Expand All @@ -323,7 +330,7 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None, python

orig_args = args

gdbserver_args = [gdbserver, '--multi']
gdbserver_args = [gdbserver, '--multi'] + gdbserver_args
if context.aslr:
gdbserver_args += ['--no-disable-randomization']
else:
Expand Down Expand Up @@ -351,7 +358,7 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None, python
else:
gdbserver_args += ['--no-startup-with-shell']

gdbserver_args += ['localhost:0']
gdbserver_args += ['localhost:%d' % port]
gdbserver_args += args

return gdbserver_args
Expand Down Expand Up @@ -416,17 +423,20 @@ def _get_runner(ssh=None):
else: return tubes.process.process

@LocalContext
def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=False, **kwargs):
def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, port=0, gdbserver_args=None, sysroot=None, api=False, **kwargs):
r"""
Launch a GDB server with the specified command line,
and launches GDB to attach to it.

Arguments:
args(list): Arguments to the process, similar to :class:`.process`.
gdbscript(str): GDB script to run.
gdb_args(list): List of additional arguments to pass to GDB.
exe(str): Path to the executable on disk
env(dict): Environment to start the binary in
ssh(:class:`.ssh`): Remote ssh session to use to launch the process.
port(int): Gdb port to use, default: random
gdbserver_args(list): List of additional arguments to pass to gdbserver
sysroot(str): Set an alternate system root. The system root is used to
load absolute shared library symbol files. This is useful to instruct
gdb to load a local version of binaries/libraries instead of downloading
Expand Down Expand Up @@ -616,7 +626,9 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
gdbscript = gdbscript or ''

if api and runner is not tubes.process.process:
raise ValueError('GDB Python API is supported only for local processes')
if runner is not ssh.process:
raise ValueError('GDB Python API is supported only for local processes')
log.warn('GDB Python API for ssh processes is not officially tested')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to add the tests instead of this vague warning. I think we can copy the API tests and slap a ssh=shell argument to the .debug call.

See here for instructions on how to test locally. make -C travis/docker ANDROID=no TARGET=gdb.rst should do the job.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, lol. gdb tests are disabled in the docker testing setup apparently. CI will do the work then.

# GDB tests currently don't work inside Docker for unknown reasons.
# Disable these tests until we can get them working.
echo > ~/pwntools/docs/source/gdb.rst

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried running the gdb tests in docker and they all passed, so I guess we can just remove that line from doctest3 and include them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0ee8d36 so i added some test cases (the same as non ssh) and i was wondering how this test case is supposed to work (IMO it doesn't make sense for gdb.attach to work on remote targets and the test case doesn't work for me).

        Attach to the remote process from a :class:`.remote` or :class:`.listen` tube,
        as long as it is running on the same machine.

        >>> server = process(['socat', 'tcp-listen:12345,reuseaddr,fork', 'exec:/bin/bash,nofork'])
        >>> sleep(1) # Wait for socat to start
        >>> io = remote('127.0.0.1', 12345)
        >>> sleep(1) # Wait for process to fork
        >>> pid = gdb.attach(io, gdbscript='''
        ... call puts("Hello from remote debugger!")
        ... detach
        ... quit
        ... ''')
        >>> io.recvline()
        b'Hello from remote debugger!\n'
        >>> io.sendline(b'echo Hello from bash && exit')
        >>> io.recvall()
        b'Hello from bash\n'

this is the error i get

Document: gdb
-------------
**********************************************************************
File "../../pwnlib/gdb.py", line ?, in default
Failed example:
    io.recvline()
Exception raised:
    Traceback (most recent call last):
      File "/usr/lib/python3.10/doctest.py", line 1350, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest default[5]>", line 1, in <module>
        io.recvline()
      File "/home/pwntools/pwntools/pwnlib/tubes/tube.py", line 527, in recvline
        return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
      File "/home/pwntools/pwntools/pwnlib/tubes/tube.py", line 341, in recvuntil
        res = self.recv(timeout=self.timeout)
      File "/home/pwntools/pwntools/pwnlib/tubes/tube.py", line 106, in recv
        return self._recv(numb, timeout) or b''
      File "/home/pwntools/pwntools/pwnlib/tubes/tube.py", line 176, in _recv
        if not self.buffer and not self._fillbuffer(timeout):
      File "/home/pwntools/pwntools/pwnlib/tubes/tube.py", line 155, in _fillbuffer
        data = self.recv_raw(self.buffer.get_fill_size())
      File "/home/pwntools/pwntools/pwnlib/tubes/sock.py", line 39, in recv_raw
        data = self.sock.recv(numb, *a)
      File "/home/pwntools/pwntools/docs/source/conf.py", line 446, in alrm_handler
        raise EndlessLoop()
    EndlessLoop

i also added some timeouts to the other recvline commands for the tests


args, env = misc.normalize_argv_env(args, env, log)
if env:
Expand All @@ -632,17 +644,17 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=

if ssh or context.native or (context.os == 'android'):
if len(args) > 0 and which(packing._decode(args[0])) == packing._decode(exe):
args = _gdbserver_args(args=args, which=which, env=env)
args = _gdbserver_args(gdbserver_args=gdbserver_args, args=args, port=port, which=which, env=env)

else:
# GDBServer is limited in it's ability to manipulate argv[0]
# but can use the ``--wrapper`` option to execute commands and catches
# ``execve`` calls.
# Therefore, we use a wrapper script to execute the target binary
script = _execve_script(args, executable=exe, env=env, ssh=ssh)
args = _gdbserver_args(args=args, which=which, env=env, python_wrapper_script=script)
args = _gdbserver_args(gdbserver_args=gdbserver_args, args=args, port=port, which=which, env=env, python_wrapper_script=script)
else:
qemu_port = random.randint(1024, 65535)
qemu_port = port if port != 0 else random.randint(1024, 65535)
qemu_user = qemu.user_path()
sysroot = sysroot or qemu.ld_prefix(env=env)
if not qemu_user:
Expand Down Expand Up @@ -671,17 +683,19 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
# Set the .executable on the process object.
gdbserver.executable = exe

# Find what port we need to connect to
if ssh or context.native or (context.os == 'android'):
port = _gdbserver_port(gdbserver, ssh)
gdb_port = _gdbserver_port(gdbserver, ssh)
if port != 0 and port != gdb_port:
log.error("gdbserver port (%d) doesn't equals set port (%d)" % (gdb_port, port))
port = gdb_port
else:
port = qemu_port

host = '127.0.0.1'
if not ssh and context.os == 'android':
host = context.adb_host

tmp = attach((host, port), exe=exe, gdbscript=gdbscript, ssh=ssh, sysroot=sysroot, api=api)
tmp = attach((host, port), exe=exe, gdbscript=gdbscript, gdb_args=gdb_args, ssh=ssh, sysroot=sysroot, api=api)
if api:
_, gdb = tmp
gdbserver.gdb = gdb
Expand Down