GDB and pwndbg for Exploit Development

GDB and pwndbg for Exploit Development - article cover image RE & Pwn
Time it takes to read this article 7 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

GDB (the GNU Debugger) is the standard Linux debugger for stepping through execution, inspecting registers and memory, and setting breakpoints — but its stock command-line interface is painfully low-level for exploit development: examining a stack frame or heap chunk means manually computing addresses and issuing raw x/ memory-examine commands. pwndbg is a GDB plugin, written by and for exploit developers, that replaces GDB’s default UI with a context-aware one: every stop shows registers, disassembly, and stack contents at a glance, and it adds purpose-built commands for heap analysis, ASLR/PIE-aware address resolution, mitigation checks, and cyclic-pattern offset finding.

The combination is the de facto standard for CTF pwn challenges and real vulnerability research on Linux binaries. Where plain GDB tells you a program crashed at some address, pwndbg tells you *why* — which mitigations are active, what the heap chunk metadata looks like at the point of corruption, whether a leaked pointer looks like a valid libc or heap address, and how far into a cyclic pattern the crash occurred. That turns hours of manual memory arithmetic into a handful of commands, mattering equally for attackers writing exploits and defenders validating a patched binary actually resists the technique it was patched against.

Attack Prerequisites

Using GDB/pwndbg for exploit development assumes local analysis capability against the target binary or a faithful reproduction of it:

  • A runnable copy of the target binary (or a remote target reachable via gdbserver/target remote), ideally matching the exact libc and loader the production target uses — pwntools’ pwninit/patchelf workflow is the common way to pin this.
  • pwndbg installed against a compatible GDBgit clone + its setup.sh, or a distro package; it hooks GDB’s Python API, which needs GDB built with Python scripting support (virtually all modern distro builds are).
  • Symbols or a good guess at the binary’s structure — stripped binaries still work, but function boundaries, the heap allocator in use (glibc ptmalloc2 vs. musl vs. custom), and calling convention affect readability.
  • Explicit authorization / an owned target — debugging production or third-party systems without permission is out of scope; this workflow is for local binaries, labs, and CTF targets.

How It Works

GDB works by using the OS’s ptrace(2) facility to attach to (or launch and control) a process, intercepting signals and reading/writing its memory and register state. Breakpoints are implemented by temporarily overwriting the target instruction byte with an int3 (0xCC on x86) trap opcode; when the CPU hits it, control returns to the debugger, which restores the original byte, reports the stop, and lets the operator inspect state before continuing. Everything pwndbg adds — register panel, disassembly context, heap commands — is built on this same ptrace/ELF-parsing foundation via GDB’s Python scripting API (gdb.Value, gdb.Breakpoint, and friends).

pwndbg’s most exploit-relevant feature is heap introspection: it parses the glibc malloc_state (main_arena) structure and chunk headers directly out of the debuggee’s memory, so heap, bins, and chunk <addr> commands show chunk size, PREV_INUSE/IS_MMAPPED flags, and free-list linkage without the operator computing any of it by hand. That’s what makes UAF and heap-overflow primitives tractable to develop: you can watch a tcache bin’s forward pointer (fd) get corrupted in real time and confirm a poisoning primitive actually redirects malloc() before running it against the real chain.

The other core capability is mitigation-aware analysis. checksec reads the binary’s ELF headers to report Canary, NX, PIE, RELRO, and Fortify status; vmmap reads /proc/<pid>/maps to show the live memory layout (and, with ASLR enabled, each region’s actual randomized base on this run); and address classification (pwndbg tags pointers as stack, heap, or a specific mapped library) lets an operator instantly tell whether a leaked value is a usable pointer or garbage — critical before building on an info-leak.

Practical Example / Configuration

A real pwndbg session against a locally built vulnerable binary (./vuln), from launch through mitigation check, breakpointing, and offset discovery. First, check what protections are compiled in — this decides the whole exploit strategy (no canary means a straight stack smash is viable; PIE means an info leak is needed first):

$ gdb -q ./vuln
pwndbg> checksec
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No
Bash

Set a breakpoint at main and run, so we can inspect the fresh process image before any input is consumed — pwndbg’s context panel (registers, disassembly, stack) prints automatically on every stop:

pwndbg> b main
Breakpoint 1 at 0x1234: file vuln.c, line 12.
pwndbg> run

* REGISTERS *  RAX  0x0   RBX  0x0   RDI  0x1  ...
* DISASM *     0x5555555551a9 <main+8>  mov  edi, 0x0
* STACK *      00:0000│ rsp 0x7fffffffe420 ...

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r--p  /home/user/vuln
0x555555555000 0x555555556000 r-xp  /home/user/vuln
0x7ffff7dc0000 0x7ffff7de3000 r--p  /usr/lib/libc.so.6
0x7ffffffde000 0x7ffffffff000 rw-p  [stack]
Bash

telescope walks a memory region printing each pointer-sized value with automatic dereferencing and type annotation — invaluable for reading a stack frame or a chain of heap pointers at a glance instead of issuing x/8gx and mentally resolving each address:

pwndbg> telescope $rsp 10
00:0000│ rsp  0x7fffffffe420 ◂— 0x0
01:0008│      0x7fffffffe428 ◂— 0x1
02:0010│      0x7fffffffe430 —▸ 0x7fffffffe4c8 ◂— 'AAAABBBBCCCC'
03:0018│      0x7fffffffe438 —▸ 0x555555555189 (main+0x1c) ◂— mov eax, 0
...
pwndbg> heap
Allocated chunk | PREV_INUSE
    Addr: 0x5555555592a0
    Size: 0x21

pwndbg> bins
tcachebins
0x20 [  1]: 0x5555555592c0 —▸ 0x0
Bash

For a stack-overflow, generate a De Bruijn cyclic pattern, feed it to the target, and once it crashes let pwndbg compute the exact offset to instruction-pointer control automatically from the crashing register value — no manual pattern math:

pwndbg> cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaas...

pwndbg> run <<< $(python3 -c "import sys;print('aaaabaaacaaadaaaeaaafaaagaaahaaai...')")
Program received signal SIGSEGV
pwndbg> cyclic -l $rsp
Finding cyclic pattern of 8 bytes: b'haaaiaaa' (offset 8 in rsp)
Found at offset 40
Bash

Walkthrough / Exploitation

Putting the primitives together into an exploit-dev loop: confirm the crash is controllable, locate the exact write offset, then build and validate the payload incrementally rather than guessing the whole chain in one shot.

# 1. Confirm RIP control at the offset found above
pwndbg> run <<< $(python3 -c "print('A'*40 + 'BBBBBBBB')")
pwndbg> print $rip
$1 = (void (*)()) 0x4242424242424242   # confirmed: offset 40 -> RIP

# 2. PIE is enabled, so leak a libc pointer via the target's own output,
#    then compute the base:
pwndbg> vmmap libc
0x7ffff7dc0000  0x7ffff7de3000  r--p  /usr/lib/libc.so.6
pwndbg> p/x leaked_addr - 0x1d0000   # offset of leaked symbol within libc

# 3. Resolve a ROP gadget against the recovered base, then re-verify under
#    the debugger before sending it to the real target:
pwndbg> ropper --search "pop rdi; ret"
pwndbg> b *0x555555555230
pwndbg> c
Bash

Each iteration re-runs under GDB so telescope/heap/vmmap confirm the exploit does what the offline calculation predicted before it fires against a real target with no debugger attached.

Note: cyclic/cyclic -l and pwntools’ cyclic()/cyclic_find() use the same De Bruijn sequence generator, so an offset computed in pwndbg matches one computed in a pwntools exploit script — this is why the two tools are almost always used together, pwndbg for interactive discovery and pwntools for the scripted final exploit.

Opsec: checksec describes the binary on disk, not necessarily the environment it runs in — ASLR can be disabled system-wide (/proc/sys/kernel/randomize_va_space), and container seccomp filters can block syscalls an exploit relies on. Validate assumptions against the actual target environment, not just the local debug session.

Detection and Defense

GDB/pwndbg activity is local, offline analysis and is not itself a network detection surface — the relevant controls are about hardening the binaries and environments this tooling is used to attack or validate:

  • Compile with full mitigations — stack canaries (-fstack-protector-strong), full RELRO (-Wl,-z,relro,-z,now), PIE, and NX remove the easiest primitives pwndbg makes visible.
  • Harden the allocator — glibc’s tcache double-free/safe-linking checks and _FORTIFY_SOURCE wrappers close off many classic heap primitives.
  • Fuzz and sanitize before release — AFL++/libFuzzer plus ASan/UBSan catch the same overflow/UAF classes far earlier and at far greater scale.
  • Restrict ptrace in production — Yama’s ptrace_scope sysctl, seccomp profiles, and CAP_SYS_PTRACE denial stop interactive dynamic analysis against a running production process.
  • Monitor build/CI hosts for anomalous debugger activity — unexpected gdbserver, ptrace attach syscalls, or GDB spawned by unusual parents.

Real-World Impact

GDB has been the standard Linux debugger since 1986 and remains the foundation nearly every Linux exploit-dev and CTF workflow builds on; pwndbg (alongside similar projects GEF and peda) is maintained specifically for offensive security work and is a default component of pwn-focused CTF environments and vulnerability research labs. The same heap and mitigation introspection that speeds an attacker’s exploit-dev loop is equally used by defenders to confirm a patch or compiler mitigation actually closes the primitive it was meant to, rather than assuming it from the source diff.

Conclusion

GDB provides the raw process-control primitives, and pwndbg turns them into an exploit-development cockpit — automatic mitigation checks, heap-chunk visualization, address classification, and cyclic-pattern offset discovery replace pages of manual x/ commands and mental arithmetic. The workflow of checksec, breakpoint, telescope/heap/vmmap, and cyclic pattern-matching is the same whether the session builds an exploit or proves one no longer works against a hardened target.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL