# `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)

  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)

    Return attributes of the real breakpoint.

  __init__(conn, *args, **kwargs)

    Do not create instances of this class directly.

    Use `pwnlib.gdb.Gdb.Breakpoint` instead.

  __setattr__(name, value)

    Set attributes of the real breakpoint.

  __weakref__

    list of weak references to the object

class pwnlib.gdb.FinishBreakpoint(*args, **kwargs)

  Mirror of `gdb.FinishBreakpoint` class.

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

  Do not create instances of this class directly.

  Use `pwnlib.gdb.Gdb.FinishBreakpoint` instead.

  __getattr__(item)

    Return attributes of the real breakpoint.

  __init__(*args, **kwargs)

    Do not create instances of this class directly.

    Use `pwnlib.gdb.Gdb.FinishBreakpoint` instead.

class pwnlib.gdb.Gdb(conn)

  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)

    Provide access to the attributes of gdb module.

  __init__(conn)

    Do not create instances of this class directly.

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

  continue_and_wait()

    Continue the program and wait until it stops again.

  continue_nowait()

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

  interrupt_and_wait()

    Interrupt the program and wait until it stops.

  quit()

    Terminate GDB.

  wait()

    Wait until the program stops.

  __weakref__

    list of weak references to the object

pwnlib.gdb._execve_script(argv, executable, env, ssh) → str

  Returns the filename of a python script that calls execve the specified program with the specified arguments. This script is suitable to call with gdbservers `--wrapper` option, so we have more control over the environment of the debugged process.

  Parameters:

    - argv (list) – List of arguments to pass to the program

    - executable (bytes) – Path to the program to run

    - env (dict) – Environment variables to pass to the program

    - ssh (ssh) – SSH connection to use if we are debugging a remote process

  Returns:

    The filename of the created script.

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

  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

    - 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

    - python_wrapper_script (str) – Path to a python script to use with `--wrapper`

  Returns:

    A list of arguments to invoke gdbserver.

pwnlib.gdb.attach(target, gdbscript='', exe=None, gdb_args=None, ssh=None, sysroot=None, api=False)

  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) – 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 them from the gdbserver, which is faster

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

  Attach to the youngest process by name

      >>> pid = gdb.attach('bash')

  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(b'echo Hello from bash && exit')
      >>> io.recvall()
      b'Hello from bash\n'

  Using GDB Python API:

       >>> io = process('bash')

       Attach a debugger

       >>> pid, io_gdb = gdb.attach(io, api=True)

       Force the program to write something it normally wouldn't

       >>> io_gdb.execute('call puts("Hello from process debugger!")')

       Resume the program

       >>> io_gdb.continue_nowait()

       Observe the forced line

       >>> io.recvline()
       b'Hello from process debugger!\n'

       Interact with the program in a regular way

       >>> io.sendline(b'echo Hello from bash && exit')

       Observe the results

       >>> io.recvall()
       b'Hello from bash\n'

  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(b'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)
      b'Hello from ssh debugger!\n'
      >>> io.sendline(b'This will be echoed back')
      >>> io.recvline()
      b'This will be echoed back\n'
      >>> io.close()

pwnlib.gdb.binary() → str
  Returns:

    str – Path to the appropriate `gdb` binary to use.

  Example

      >>> gdb.binary()
      '/usr/bin/gdb'

pwnlib.gdb.corefile(process)

  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
      '.../bin/bash'

pwnlib.gdb.debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, port=0, gdbserver_args=None, sysroot=None, api=False, **kwargs)

  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.

    - 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 (`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 them from the gdbserver, which is faster

    - 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(b"echo hello")
      >>> io.recvline()
      b'hello\n'

  Interact with the process

      >>> io.interactive()
      >>> 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(b"echo hello")
      >>> io.recvline()
      b'hello\n'

  Interact with the process

      >>> io.interactive()
      >>> io.close()

  Start a new process with modified argv[0]

      >>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], gdbscript='continue', exe="/bin/sh")
      >>> io.sendline(b"echo $0")
      >>> io.recvline()
      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]
      >>> 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]
      b'.../local-libc.so'
      >>> io.close()
      >>> os.remove("./local-libc.so") # cleanup

  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(['whoami'],
  >     ...                 ssh = shell,
  >     ...                 gdbscript = '''
  >     ... break main
  >     ... continue
  >     ... ''')

  > Send a command to Bash

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

  > Interact with the process

  >     >>> io.interactive()
  >     >>> io.close()

  > Using a modified args[0] on a remote process

  >     >>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], gdbscript='continue', exe="/bin/sh", ssh=shell)
  >     >>> io.sendline(b"echo $0")
  >     >>> io.recvline()
  >     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()
  >     b'$ \n'
  >     >>> io.close()

  Using GDB Python API:

       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()
       b'foo\n'
       >>> io.close()

       >>> ssh_io.gdb.continue_nowait()
       >>> ssh_io.recvline()
       b'foo\n'
       >>> ssh_io.close()
       >>> shell.close()

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

  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

  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)

  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'])
      '0x7ffff7634660'

pwnlib.gdb.version(program='gdb')

  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() <= (19,0)
      True