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 processdebug()
- Start a new process under a debugger, stopped at the first instructiondebug_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 itssys.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 synchronouspwnlib.gdb.Gdb.continue_and_wait()
or the explicitly asynchronouspwnlib.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.
- class pwnlib.gdb.FinishBreakpoint(*args, **kwargs)[source]
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.
- 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.
- pwnlib.gdb._execve_script(argv, executable, env, ssh) str [source]
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.
- 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
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)[source]
Start GDB in a new terminal and attach to target.
- Parameters
target – The target to attach to.
gdbscript (
str
orfile
) – 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
orremote
.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, thesshpass
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(timeout=10) 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(timeout=1) 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
orlisten
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(timeout=10) 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(timeout=1) 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() '/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 '.../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)[source]
Launch a GDB server with the specified command line, and launches GDB to attach to it.
- Parameters
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
orssh_channel
– A tube connected to the target process. Whenapi=True
,gdb
member of the returned object contains aGdb
instance.
Notes
The debugger is attached automatically, and you can debug everything from the very beginning. This requires that both
gdb
andgdbserver
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
sincelibc.so
has not even been loaded yet.There are several ways to handle this:
- 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
.
- Set a breakpoint on the executable’s entry point (generally,
- 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 internalmalloc
, and eventaully inlibc
’s malloc.
- Use pending breakpoints via
- 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.
- Wait for libraries to be loaded with
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(timeout=30) b'hello\n'
Interact with the process
>>> io.interactive(timeout=1) >>> 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(timeout=10) 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(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(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(timeout=1).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 thessh=
keyword to pass in yourssh
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(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(timeout=10) 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(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()
- 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
Example:
>>> assembly = shellcraft.echo("Hello world!\n") >>> io = gdb.debug_assembly(assembly) >>> io.recvline(timeout=1) 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
Example:
>>> assembly = shellcraft.echo("Hello world!\n") >>> shellcode = asm(assembly) >>> io = gdb.debug_shellcode(shellcode) >>> io.recvline(timeout=1) 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 breaksinfo proc
in GDB, butinfo 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:
Downloading the binaries from the remote server
Scraping GDB for the information
Loading each library into an ELF
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 apwnlib.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'