Skip to content

Commit

Permalink
Add port, gdb_args, and gdbserver_args to gdb.debug() (#2382)
Browse files Browse the repository at this point in the history
* added two arguments to gdb.debug:
+ port: specifies the port that should be used by the gdbserver (this is useful for alpine)
+ gdb_args: allows forwarding arguments to the gdb binary spawned by gdb.attach() (just a passthrough)

aslo downgraded "GDB Python API is supported only for local processes" to a warning, because it seems to work on some systems.

* added changelog entry and fixed python2.7 compatibility

* also added optional gdbserver_args parameter to gdb.debug()

* implemented requested fixes for #2382:
+ gdb.debug port is now also used for qemu
+ GDB Python API is now tested for tubes.process.process a warning for ssh.process and an error for everything else
+ updated docs to use mention that gdbserver ports are randomized by default
+ now using gdbserver_port to check if the correct port was set
+ fixed CHANGELOG.md structure

* implemented requested fixes for #2382:
+ gdb.debug port is now also used for qemu
+ GDB Python API is now tested for tubes.process.process a warning for ssh.process and an error for everything else
+ updated docs to use mention that gdbserver ports are randomized by default
+ now using gdbserver_port to check if the correct port was set
+ fixed CHANGELOG.md structure
+ added timeouts for recvline() gdb tests
+ also run gdb api tests for ssh runner

---------

Co-authored-by: gfelber <@users.noreply.github.com>
Co-authored-by: peace-maker <[email protected]>
  • Loading branch information
gfelber and peace-maker committed Jul 23, 2024
1 parent 18c8bfb commit 00663aa
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 55 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The table below shows which release corresponds to each branch, and what date th
- [#2415][2415] Add shellcraft template for IPv6 socket
- [#2405][2405] Add "none" ssh authentication method
- [#2427][2427] Document behaviour of remote()'s sni argument as 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 @@ -103,6 +104,7 @@ The table below shows which release corresponds to each branch, and what date th
[2415]: https://github.com/Gallopsled/pwntools/pull/2415
[2405]: https://github.com/Gallopsled/pwntools/pull/2405
[2427]: https://github.com/Gallopsled/pwntools/pull/2405
[2382]: https://github.com/Gallopsled/pwntools/pull/2382

## 4.13.0 (`beta`)

Expand Down
139 changes: 84 additions & 55 deletions pwnlib/gdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def debug_assembly(asm, gdbscript=None, vma=None, api=False):
>>> assembly = shellcraft.echo("Hello world!\n")
>>> io = gdb.debug_assembly(assembly)
>>> io.recvline()
>>> io.recvline(timeout=1)
b'Hello world!\n'
"""
tmp_elf = make_elf_from_assembly(asm, vma=vma, extract=False)
Expand Down Expand Up @@ -230,7 +230,7 @@ def debug_shellcode(data, gdbscript=None, vma=None, api=False):
>>> assembly = shellcraft.echo("Hello world!\n")
>>> shellcode = asm(assembly)
>>> io = gdb.debug_shellcode(shellcode)
>>> io.recvline()
>>> io.recvline(timeout=1)
b'Hello world!\n'
"""
if isinstance(data, six.text_type):
Expand Down 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 @@ -480,12 +490,12 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
Send a command to Bash
>>> io.sendline(b"echo hello")
>>> io.recvline()
>>> io.recvline(timeout=30)
b'hello\n'
Interact with the process
>>> io.interactive() # doctest: +SKIP
>>> io.interactive(timeout=1) # doctest: +SKIP
>>> io.close()
Create a new process, and stop it at '_start'
Expand All @@ -504,7 +514,7 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
Send a command to Bash
>>> io.sendline(b"echo hello")
>>> io.recvline()
>>> io.recvline(timeout=10)
b'hello\n'
Interact with the process
Expand All @@ -516,53 +526,24 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
>>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], gdbscript='continue', exe="/bin/sh")
>>> io.sendline(b"echo $0")
>>> io.recvline()
>>> io.recvline(timeout=10)
b'\xde\xad\xbe\xef\n'
>>> io.close()
Demonstrate that LD_PRELOAD is respected
>>> io = process(["grep", "libc.so.6", "/proc/self/maps"])
>>> real_libc_path = io.recvline().split()[-1]
>>> real_libc_path = io.recvline(timeout=1).split()[-1]
>>> io.close()
>>> import shutil
>>> local_path = shutil.copy(real_libc_path, "./local-libc.so") # make a copy of libc to demonstrate that it is loaded
>>> io = gdb.debug(["grep", "local-libc.so", "/proc/self/maps"], gdbscript="continue", env={"LD_PRELOAD": "./local-libc.so"})
>>> io.recvline().split()[-1] # doctest: +ELLIPSIS
>>> io.recvline(timeout=1).split()[-1] # doctest: +ELLIPSIS
b'.../local-libc.so'
>>> io.close()
>>> os.remove("./local-libc.so") # cleanup
Using GDB Python API:
.. doctest::
:skipif: is_python2
Debug a new process
>>> io = gdb.debug(['echo', 'foo'], api=True)
Stop at 'write'
>>> bp = io.gdb.Breakpoint('write', temporary=True)
>>> io.gdb.continue_and_wait()
Dump 'count'
>>> count = io.gdb.parse_and_eval('$rdx')
>>> long = io.gdb.lookup_type('long')
>>> int(count.cast(long))
4
Resume the program
>>> io.gdb.continue_nowait()
>>> io.recvline()
b'foo\n'
>>> io.close()
Using SSH:
You can use :func:`debug` to spawn new processes on remote machines as well,
Expand Down Expand Up @@ -591,17 +572,63 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
>>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], gdbscript='continue', exe="/bin/sh", ssh=shell)
>>> io.sendline(b"echo $0")
>>> io.recvline()
>>> io.recvline(timeout=10)
b'$ \xde\xad\xbe\xef\n'
>>> io.close()
Using an empty args[0] on a remote process
>>> io = gdb.debug(args=[], gdbscript='continue', exe="/bin/sh", ssh=shell)
>>> io.sendline(b"echo $0")
>>> io.recvline()
>>> io.recvline(timeout=10)
b'$ \n'
>>> io.close()
Using GDB Python API:
.. doctest::
:skipif: is_python2
Debug a new process
>>> io = gdb.debug(['echo', 'foo'], api=True)
or using ssh
>>> shell = ssh('travis', 'example.pwnme', password='demopass')
>>> ssh_io = gdb.debug(['/bin/echo', 'foo'], ssh=shell, api=True)
Stop at 'write'
>>> bp = io.gdb.Breakpoint('write', temporary=True)
>>> io.gdb.continue_and_wait()
>>> ssh_bp = ssh_io.gdb.Breakpoint('write', temporary=True)
>>> ssh_io.gdb.continue_and_wait()
Dump 'count'
>>> count = io.gdb.parse_and_eval('$rdx')
>>> long = io.gdb.lookup_type('long')
>>> int(count.cast(long))
4
>>> count = ssh_io.gdb.parse_and_eval('$rdx')
>>> long = ssh_io.gdb.lookup_type('long')
>>> int(count.cast(long))
4
Resume the program
>>> io.gdb.continue_nowait()
>>> io.recvline(timeout=1)
b'foo\n'
>>> io.close()
>>> ssh_io.gdb.continue_nowait()
>>> ssh_io.recvline(timeout=1)
b'foo\n'
>>> ssh_io.close()
>>> shell.close()
"""
if isinstance(args, six.integer_types + (tubes.process.process, tubes.ssh.ssh_channel)):
log.error("Use gdb.attach() to debug a running process")
Expand All @@ -615,8 +642,8 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
which = _get_which(ssh)
gdbscript = gdbscript or ''

if api and runner is not tubes.process.process:
raise ValueError('GDB Python API is supported only for local processes')
if api and runner is not tubes.process.process and not ssh:
raise ValueError('GDB Python API is supported only for local and ssh processes')

args, env = misc.normalize_argv_env(args, env, log)
if env:
Expand All @@ -632,17 +659,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 +698,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 Expand Up @@ -948,7 +977,7 @@ def attach(target, gdbscript = '', exe = None, gdb_args = None, ssh = None, sysr
... detach
... quit
... ''')
>>> io.recvline()
>>> io.recvline(timeout=10)
b'Hello from process debugger!\n'
>>> io.sendline(b'echo Hello from bash && exit')
>>> io.recvall()
Expand All @@ -975,7 +1004,7 @@ def attach(target, gdbscript = '', exe = None, gdb_args = None, ssh = None, sysr
Observe the forced line
>>> io.recvline()
>>> io.recvline(timeout=1)
b'Hello from process debugger!\n'
Interact with the program in a regular way
Expand All @@ -999,7 +1028,7 @@ def attach(target, gdbscript = '', exe = None, gdb_args = None, ssh = None, sysr
... detach
... quit
... ''')
>>> io.recvline()
>>> io.recvline(timeout=10)
b'Hello from remote debugger!\n'
>>> io.sendline(b'echo Hello from bash && exit')
>>> io.recvall()
Expand All @@ -1018,7 +1047,7 @@ def attach(target, gdbscript = '', exe = None, gdb_args = None, ssh = None, sysr
>>> io.recvline(timeout=5) # doctest: +SKIP
b'Hello from ssh debugger!\n'
>>> io.sendline(b'This will be echoed back')
>>> io.recvline()
>>> io.recvline(timeout=1)
b'This will be echoed back\n'
>>> io.close()
"""
Expand Down

0 comments on commit 00663aa

Please sign in to comment.