pwnlib.gdb — Working with GDB

During exploit development, it is frequently useful to debug the target binary under GDB.

Pwntools makes this easy-to-do with a handful of helper routines, designed to make your exploit-debug-update cycles much faster.

Useful Functions

  • attach() - Attach to an existing process
  • debug() - Start a new process under a debugger, stopped at the first instruction
  • debug_shellcode() - Build a binary with the provided shellcode, and start it under a debugger

Debugging Tips

The attach() and debug() functions will likely be your bread and butter for debugging.

Both allow you to provide a script to pass to GDB when it is started, so that it can automatically set your breakpoints.

Attaching to Processes

To attach to an existing process, just use attach(). It is surprisingly versatile, and can attach to a process for simple binaries, or will automatically find the correct process to attach to for a forking server, if given a remote object.

Spawning New Processes

Attaching to processes with attach() is useful, but the state the process is in may vary. If you need to attach to a process very early, and debug it from the very first instruction (or even the start of main), you instead should use debug().

When you use debug(), the return value is a tube object that you interact with exactly like normal.

Using GDB Python API

GDB provides Python API, which is documented at https://sourceware.org/gdb/onlinedocs/gdb/Python-API.html. Pwntools allows you to call it right from the exploit, without having to write a gdbscript. This is useful for inspecting program state, e.g. asserting that leaked values are correct, or that certain packets trigger a particular code path or put the heap in a desired state.

Pass api=True to attach() or debug() in order to enable GDB Python API access. Pwntools will then connect to GDB using RPyC library: https://rpyc.readthedocs.io/en/latest/.

At the moment this is an experimental feature with the following limitations:

  • Only Python 3 is supported.

    Well, technically that’s not quite true. The real limitation is that your GDB’s Python interpreter major version should be the same as that of Pwntools. However, most GDBs use Python 3 nowadays.

    Different minor versions are allowed as long as no incompatible values are sent in either direction. See https://rpyc.readthedocs.io/en/latest/install.html#cross-interpreter-compatibility for more information.

    Use

    $ gdb -batch -ex 'python import sys; print(sys.version)'
    

    in order to check your GDB’s Python version.

  • If your GDB uses a different Python interpreter than Pwntools (for example, because you run Pwntools out of a virtualenv), you should install rpyc package into its sys.path. Use

    $ gdb -batch -ex 'python import rpyc'
    

    in order to check whether this is necessary.

  • Only local processes are supported.

  • It is not possible to tell whether gdb.execute('continue') will be executed synchronously or asynchronously (in gdbscripts it is always synchronous). Therefore it is recommended to use either the explicitly synchronous pwnlib.gdb.Gdb.continue_and_wait() or the explicitly asynchronous pwnlib.gdb.Gdb.continue_nowait() instead.

Tips and Troubleshooting

NOPTRACE magic argument

It’s quite cumbersom to comment and un-comment lines containing attach.

You can cause these lines to be a no-op by running your script with the NOPTRACE argument appended, or with PWNLIB_NOPTRACE=1 in the environment.

$ python exploit.py NOPTRACE
[+] Starting local process '/bin/bash': Done
[!] Skipping debug attach since context.noptrace==True
...

Kernel Yama ptrace_scope

The Linux kernel v3.4 introduced a security mechanism called ptrace_scope, which is intended to prevent processes from debugging eachother unless there is a direct parent-child relationship.

This causes some issues with the normal Pwntools workflow, since the process hierarchy looks like this:

python ---> target
       `--> gdb

Note that python is the parent of target, not gdb.

In order to avoid this being a problem, Pwntools uses the function prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY). This disables Yama for any processes launched by Pwntools via process or via ssh.process().

Older versions of Pwntools did not perform the prctl step, and required that the Yama security feature was disabled systemwide, which requires root access.

Member Documentation

class pwnlib.gdb.Breakpoint(conn, *args, **kwargs)[source]

Mirror of gdb.Breakpoint class.

See https://sourceware.org/gdb/onlinedocs/gdb/Breakpoints-In-Python.html for more information.

Do not create instances of this class directly.

Use pwnlib.gdb.Gdb.Breakpoint instead.

__getattr__(item)[source]

Return attributes of the real breakpoint.

__init__(conn, *args, **kwargs)[source]

Do not create instances of this class directly.

Use pwnlib.gdb.Gdb.Breakpoint instead.

class pwnlib.gdb.Gdb(conn)[source]

Mirror of gdb module.

See https://sourceware.org/gdb/onlinedocs/gdb/Basic-Python.html for more information.

Do not create instances of this class directly.

Use attach() or debug() with api=True instead.

__getattr__(item)[source]

Provide access to the attributes of gdb module.

__init__(conn)[source]

Do not create instances of this class directly.

Use attach() or debug() with api=True instead.

continue_and_wait()[source]

Continue the program and wait until it stops again.

continue_nowait()[source]

Continue the program. Do not wait until it stops again.

interrupt_and_wait()[source]

Interrupt the program and wait until it stops.

quit()[source]

Terminate GDB.

wait()[source]

Wait until the program stops.

pwnlib.gdb._gdbserver_args(pid=None, path=None, args=None, which=None, env=None) → list[source]

Sets up a listening gdbserver, to either connect to the specified PID, or launch the specified binary by its full path.

Parameters:
  • pid (int) – Process ID to attach to
  • path (str) – Process to launch
  • args (list) – List of arguments to provide on the debugger command line
  • which (callaable) – Function to find the path of a binary.
Returns:

A list of arguments to invoke gdbserver.

pwnlib.gdb.attach(*a, **kw)[source]

Start GDB in a new terminal and attach to target.

Parameters:
  • target – The target to attach to.
  • gdbscript (str or file) – GDB script to run after attaching.
  • exe (str) – The path of the target binary.
  • arch (str) – Architechture of the target binary. If exe known GDB will detect the architechture automatically (if it is supported).
  • gdb_args (list) – List of additional arguments to pass to GDB.
  • sysroot (str) – Foreign-architecture sysroot, used for QEMU-emulated binaries and Android targets.
  • api (bool) – Enable access to GDB Python API.
Returns:

PID of the GDB process (or the window which it is running in). When api=True, a (PID, Gdb) tuple.

Notes

The target argument is very robust, and can be any of the following:

int
PID of a process
str
Process name. The youngest process is selected.
tuple
Host, port pair of a listening gdbserver
process
Process to connect to
sock
Connected socket. The executable on the other end of the connection is attached to. Can be any socket type, including listen or remote.
ssh_channel
Remote process spawned via ssh.process(). This will use the GDB installed on the remote machine. If a password is required to connect, the sshpass program must be installed.

Examples

Attach to a process by PID

>>> pid = gdb.attach(1234) # doctest: +SKIP

Attach to the youngest process by name

>>> pid = gdb.attach('bash') # doctest: +SKIP

Attach a debugger to a process tube and automate interaction

>>> io = process('bash')
>>> pid = gdb.attach(io, gdbscript='''
... call puts("Hello from process debugger!")
... detach
... quit
... ''')
>>> io.recvline()
b'Hello from process debugger!\n'
>>> io.sendline('echo Hello from bash && exit')
>>> io.recvall()
b'Hello from bash\n'

Using GDB Python API:

Attach to the remote process from a remote or 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('echo Hello from bash && exit')
>>> io.recvall()
b'Hello from bash\n'

Attach to processes running on a remote machine via an SSH ssh process

>>> shell = ssh('travis', 'example.pwnme', password='demopass')
>>> io = shell.process(['cat'])
>>> pid = gdb.attach(io, gdbscript='''
... call sleep(5)
... call puts("Hello from ssh debugger!")
... detach
... quit
... ''')
>>> io.recvline(timeout=5)  # doctest: +SKIP
b'Hello from ssh debugger!\n'
>>> io.sendline('This will be echoed back')
>>> io.recvline()
b'This will be echoed back\n'
>>> io.close()
pwnlib.gdb.binary() → str[source]
Returns:str – Path to the appropriate gdb binary to use.

Example

>>> gdb.binary() # doctest: +SKIP
'/usr/bin/gdb'
pwnlib.gdb.corefile(process)[source]

Drops a core file for a running local process.

Note

You should use process.corefile() instead of using this method directly.

Parameters:process – Process to dump
Returns:Core – The generated core file

Example

>>> io = process('bash')
>>> core = gdb.corefile(io)
>>> core.exe.name # doctest: +ELLIPSIS
'.../bin/bash'
pwnlib.gdb.debug(*a, **kw)[source]

Launch a GDB server with the specified command line, and launches GDB to attach to it.

Parameters:
  • args (list) – Arguments to the process, similar to process.
  • gdbscript (str) – GDB script to run.
  • exe (str) – Path to the executable on disk
  • env (dict) – Environment to start the binary in
  • ssh (ssh) – Remote ssh session to use to launch the process.
  • sysroot (str) – Foreign-architecture sysroot, used for QEMU-emulated binaries and Android targets.
  • api (bool) – Enable access to GDB Python API.
Returns:

process or ssh_channel – A tube connected to the target process. When api=True, gdb member of the returned object contains a Gdb instance.

Notes

The debugger is attached automatically, and you can debug everything from the very beginning. This requires that both gdb and gdbserver are installed on your machine.

When GDB opens via debug(), it will initially be stopped on the very first instruction of the dynamic linker (ld.so) for dynamically-linked binaries.

Only the target binary and the linker will be loaded in memory, so you cannot set breakpoints on shared library routines like malloc since libc.so has not even been loaded yet.

There are several ways to handle this:

  1. Set a breakpoint on the executable’s entry point (generally, _start)
    • This is only invoked after all of the required shared libraries are loaded.
    • You can generally get the address via the GDB command info file.
  2. Use pending breakpoints via set breakpoint pending on
    • This has the side-effect of setting breakpoints for every function which matches the name. For malloc, this will generally set a breakpoint in the executable’s PLT, in the linker’s internal malloc, and eventaully in libc’s malloc.
  3. Wait for libraries to be loaded with set stop-on-solib-event 1
    • There is no way to stop on any specific library being loaded, and sometimes multiple libraries are loaded and only a single breakpoint is issued.
    • Generally, you just add a few continue commands until things are set up the way you want it to be.

Examples

Create a new process, and stop it at ‘main’

>>> io = gdb.debug('bash', '''
... break main
... continue
... ''')

Send a command to Bash

>>> io.sendline("echo hello")
>>> io.recvline()
b'hello\n'

Interact with the process

>>> io.interactive() # doctest: +SKIP
>>> io.close()

Create a new process, and stop it at ‘_start’

>>> io = gdb.debug('bash', '''
... # Wait until we hit the main executable's entry point
... break _start
... continue
...
... # Now set breakpoint on shared library routines
... break malloc
... break free
... continue
... ''')

Send a command to Bash

>>> io.sendline("echo hello")
>>> io.recvline()
b'hello\n'

Interact with the process

>>> io.interactive() # doctest: +SKIP
>>> io.close()

Using GDB Python API:

Using SSH:

You can use debug() to spawn new processes on remote machines as well, by using the ssh= keyword to pass in your ssh instance.

Connect to the SSH server and start a process on the server

>>> shell = ssh('travis', 'example.pwnme', password='demopass')
>>> io = gdb.debug(['bash'],
...                 ssh = shell,
...                 gdbscript = '''
... break main
... continue
... ''')

Send a command to Bash

>>> io.sendline("echo hello")

Interact with the process >>> io.interactive() # doctest: +SKIP >>> io.close()

pwnlib.gdb.debug_assembly(asm, gdbscript=None, vma=None, api=False) → tube[source]

Creates an ELF file, and launches it under a debugger.

This is identical to debug_shellcode, except that any defined symbols are available in GDB, and it saves you the explicit call to asm().

Parameters:
  • asm (str) – Assembly code to debug
  • gdbscript (str) – Script to run in GDB
  • vma (int) – Base address to load the shellcode at
  • api (bool) – Enable access to GDB Python API
  • **kwargs – Override any pwnlib.context.context values.
Returns:

process

Example:

>>> assembly = shellcraft.echo("Hello world!\n")
>>> io = gdb.debug_assembly(assembly)
>>> io.recvline()
b'Hello world!\n'
pwnlib.gdb.debug_shellcode(data, gdbscript=None, vma=None, api=False) → tube[source]

Creates an ELF file, and launches it under a debugger.

Parameters:
  • data (str) – Assembled shellcode bytes
  • gdbscript (str) – Script to run in GDB
  • vma (int) – Base address to load the shellcode at
  • api (bool) – Enable access to GDB Python API
  • **kwargs – Override any pwnlib.context.context values.
Returns:

process

Example:

>>> assembly = shellcraft.echo("Hello world!\n")
>>> shellcode = asm(assembly)
>>> io = gdb.debug_shellcode(shellcode)
>>> io.recvline()
b'Hello world!\n'
pwnlib.gdb.find_module_addresses(binary, ssh=None, ulimit=False)[source]

Cheat to find modules by using GDB.

We can’t use /proc/$pid/map since some servers forbid it. This breaks info proc in GDB, but info sharedlibrary still works. Additionally, info sharedlibrary works on FreeBSD, which may not have procfs enabled or accessible.

The output looks like this:

info proc mapping
process 13961
warning: unable to open /proc file '/proc/13961/maps'

info sharedlibrary
From        To          Syms Read   Shared Object Library
0xf7fdc820  0xf7ff505f  Yes (*)     /lib/ld-linux.so.2
0xf7fbb650  0xf7fc79f8  Yes         /lib32/libpthread.so.0
0xf7e26f10  0xf7f5b51c  Yes (*)     /lib32/libc.so.6
(*): Shared library is missing debugging information.

Note that the raw addresses provided by info sharedlibrary are actually the address of the .text segment, not the image base address.

This routine automates the entire process of:

  1. Downloading the binaries from the remote server
  2. Scraping GDB for the information
  3. Loading each library into an ELF
  4. Fixing up the base address vs. the .text segment address
Parameters:
  • binary (str) – Path to the binary on the remote server
  • ssh (pwnlib.tubes.tube) – SSH connection through which to load the libraries. If left as None, will use a pwnlib.tubes.process.process.
  • ulimit (bool) – Set to True to run “ulimit -s unlimited” before GDB.
Returns:

A list of pwnlib.elf.ELF objects, with correct base addresses.

Example:

>>> with context.local(log_level=9999):
...     shell =  ssh(host='example.pwnme', user='travis', password='demopass')
...     bash_libs = gdb.find_module_addresses('/bin/bash', shell)
>>> os.path.basename(bash_libs[0].path)
'libc.so.6'
>>> hex(bash_libs[0].symbols['system']) # doctest: +SKIP
'0x7ffff7634660'
pwnlib.gdb.version(program='gdb')[source]

Gets the current GDB version.

Note

Requires that GDB version meets the following format:

GNU gdb (GDB) 7.12

Returns:tuple – A tuple containing the version numbers

Example

>>> (7,0) <= gdb.version() <= (12,0)
True