Legend:
[x86]= 32-bit |[x64]= 64-bit |[GEF]= GDB Enhanced Features |[PWNDBG]= pwndbg extension |[VANILLA]= stock GDB, no plugin |[LIN]= Linux
1. What is GDB and Why It Matters
What it means: GDB (GNU Debugger) is the standard command-line debugger for Linux binaries (ELF). It lets you pause a running program, inspect its registers and memory, step through instructions one at a time, and modify its state on the fly. For exploit development and CTF pwn challenges, GDB is where you actually watch a buffer overflow happen — confirm the offset, see EIP/RIP get overwritten, inspect the stack layout, and verify your payload lands correctly before firing it at a real target.
Stock ("vanilla") GDB is functional but low-level and unfriendly — no color, no
register/stack view, cryptic x syntax. In practice almost nobody uses vanilla
GDB for pwn work; everyone layers an extension on top:
┌───────────────────────────────────────────────────────────┐
│ GDB Ecosystem │
│ │
│ Vanilla GDB → works, but bare-bones │
│ + │
│ GEF / pwndbg / PEDA → adds: │
│ - Colorized register/stack/disas context on each stop│
│ - checksec, vmmap, heap-chunk, got, plt views │
│ - cyclic pattern generation built-in │
│ - ROP gadget search │
│ - Better `x`/examine syntax │
└───────────────────────────────────────────────────────────┘
Why Learn GDB?
✔ Confirm buffer overflow offsets and EIP/RIP control (OSCP BOF room)
✔ Step through shellcode instruction-by-instruction to catch bugs
✔ Inspect stack/heap layout for ROP chain construction
✔ Reverse engineer CTF binaries (crackmes, pwn challenges)
✔ Debug crashes and understand why a program segfaults
✔ Attach to processes launched by pwntools mid-exploit (gdb.attach)
✔ Read AT&T disassembly (GDB's default) and switch to Intel when needed
This note assumes Linux x86/x64 ELF binaries — the primary target for OSCP, CPTS, and CTF pwn challenges. GEF and pwndbg command sets are both covered since you'll encounter both in the wild.
2. Installation & Extensions (GEF / pwndbg / PEDA)
What it means: GDB ships with every Linux distro but is unusable for serious exploit dev without an extension. The three major ones — GEF, pwndbg, PEDA — all do roughly the same job (context display, pattern generation, better memory inspection) but have different command names and philosophies. Pick one and learn it deeply; you'll recognize the others' commands easily once you know one.
# GDB itself
sudo apt install gdb
# ── GEF (GDB Enhanced Features) — recommended, actively maintained ──
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
# Installs and auto-configures ~/.gdbinit
# ── pwndbg — most popular for CTF, deep pwntools integration ──
git clone https://github.com/pwndbg/pwndbg
cd pwndbg && ./setup.sh
# ── PEDA (Python Exploit Development Assistance) — older, still used ──
git clone https://github.com/longld/peda.git ~/.peda
echo "source ~/.peda/peda.py" >> ~/.gdbinit
# Verify GDB version
gdb --version
# Only ONE extension should be active at a time — they conflict if
# sourced together in .gdbinit. Comment out the ones you're not using.
2.1 Which One Should You Use?
| Extension | Best for | Command style | Notes |
|---|---|---|---|
| GEF | General exploit dev, modern, actively developed | gef> ... |
Best default choice in 2024+, Python 3 |
| pwndbg | CTF pwn, tightest pwntools integration | pwndbg> ... |
Excellent heap analysis (bins, tcache) |
| PEDA | Older tutorials/courses reference it | peda> ... |
Less maintained, still fine to know |
For OSCP/CPTS/CTF work: GEF or pwndbg are both excellent. This note gives the vanilla command first (works everywhere), then the GEF/pwndbg shortcut.
3. Starting GDB — Launch Options
What it means: How you invoke GDB determines what you're debugging (a fresh binary, a running process, a core dump) and with what arguments/input. Getting comfortable with launch flags saves huge amounts of time versus starting GDB plain and manually loading everything.
# Basic launch
gdb ./binary
# Quiet mode (skip the GPL banner — cleaner output)
gdb -q ./binary
gdb --quiet ./binary
# Pass arguments to the target program
gdb --args ./binary arg1 arg2
(gdb) run arg1 arg2 # equivalent, set at runtime
# Attach to an already-running process (by PID)
gdb -p 1234
sudo gdb -p $(pidof binary_name)
# Load a core dump for post-mortem analysis
gdb ./binary core
# Load a binary and immediately execute a GDB script
gdb -x commands.gdb ./binary
# Batch mode — run commands and exit (scripting/automation)
gdb -batch -ex "run" -ex "bt" ./binary
# Debug a binary with stdin redirected from a file
gdb ./binary
(gdb) run < input.txt
# Debug a setuid/setgid binary (needed for local privesc CTFs)
sudo gdb ./binary
3.1 First Things to Run Every Session
(gdb) file ./binary # load a binary (if not passed at launch)
(gdb) set disassembly-flavor intel # switch AT&T → Intel syntax (see §9)
(gdb) starti # start and break at the very first instruction
(gdb) start # start and break at main()
4. Breakpoints
What it means: A breakpoint pauses execution at a specific point so you can inspect state. This is the single most-used GDB feature — almost every debugging session is "set a breakpoint, run, inspect, step."
# Break at a function name
(gdb) break main
(gdb) break vulnerable_function
(gdb) b main # 'b' is shorthand for 'break'
# Break at a specific address
(gdb) break *0x08048560
(gdb) break *0x555555555169
# Break at a specific address relative to a symbol (useful with PIE)
(gdb) break *main+42
(gdb) break *vuln+0x1a
# Break at a line number in source (if compiled with -g / debug symbols)
(gdb) break file.c:25
(gdb) break 25 # if already in that file's context
# Conditional breakpoint — only stops if condition is true
(gdb) break vuln if $eax == 0
(gdb) break vuln if i == 5
# Temporary breakpoint — deleted automatically after it's hit once
(gdb) tbreak main
# List all breakpoints
(gdb) info breakpoints
(gdb) info b
# Delete breakpoints
(gdb) delete 1 # delete breakpoint number 1
(gdb) delete # delete ALL breakpoints
(gdb) clear main # delete breakpoint(s) at 'main'
# Enable / disable without deleting
(gdb) disable 1
(gdb) enable 1
# Run commands automatically every time a breakpoint hits
(gdb) commands 1
> print $eax
> continue
> end
4.1 Breaking on PIE Binaries (No Fixed Base Address)
# With PIE (Position Independent Executable) enabled, the binary loads
# at a randomized base each run — you CANNOT break on a raw address
# from a previous run. Use symbol names or offsets from the base instead.
(gdb) break main # symbol names still work fine
(gdb) break *main+0x20 # offset from a symbol also works
# To break at a raw offset into the binary (as shown by objdump),
# add the runtime base first:
(gdb) info proc mappings # find the loaded base address
(gdb) break *(0x555555554000 + 0x1189) # base + offset from objdump
5. Execution Control
What it means: Once stopped at a breakpoint, these commands move execution forward — one line, one instruction, into a function, or out of one. Knowing the difference between "step" and "next" (does it dive into function calls or skip over them?) is essential for not getting lost in library code.
# Start execution
(gdb) run # run freely until breakpoint/crash/exit
(gdb) run < input.txt # with stdin redirected from a file
(gdb) run $(python3 -c "print('A'*200)") # with generated argv input
(gdb) r # shorthand
# Continue after a breakpoint
(gdb) continue
(gdb) c
(gdb) continue 5 # ignore breakpoint next 5 times, then stop
# Step by SOURCE LINE (requires debug symbols, -g)
(gdb) next # step over — does NOT enter function calls
(gdb) n
(gdb) step # step into — DOES enter function calls
(gdb) s
# Step by single MACHINE INSTRUCTION (works without debug symbols — key for BOF)
(gdb) nexti # step over one instruction (skip past 'call')
(gdb) ni
(gdb) stepi # step into one instruction (enter 'call')
(gdb) si
# Run until the current function returns
(gdb) finish
# Prints the return value in $eax/$rax on x86/x64 (cdecl / SysV convention)
# Run until a specific line/address is reached (without setting a breakpoint)
(gdb) until 30
(gdb) until *0x08048560
# Kill the currently running program (without exiting GDB)
(gdb) kill
# Restart from scratch
(gdb) kill
(gdb) run
Rule of thumb for exploit dev: use
nexti/stepi(instruction-level), notnext/step(source-line level) — stripped binaries and shellcode have no debug symbols, so source-line stepping won't work anyway.
6. Examining Memory — the x command
What it means: x (examine) is GDB's core memory-inspection command. Its
syntax x/NFU ADDRESS looks cryptic at first but is extremely dense and fast
once memorized: Number of units, Format, Unit size.
# Syntax: x/NFU address
# N = count (how many units to display)
# F = format: x=hex, d=decimal, u=unsigned, o=octal, t=binary,
# s=string, i=instruction, c=char, a=address, f=float
# U = unit size: b=byte, h=halfword(2B), w=word(4B), g=giant(8B)
# Examples
(gdb) x/20x $esp # 20 hex words (4B each) from ESP [x86]
(gdb) x/20x $rsp # 20 hex giants (8B each) from RSP [x64] ← note: still 'x' fmt, size defaults per-arch
(gdb) x/20xg $rsp # explicitly 20 hex QWORDS from RSP [x64]
(gdb) x/20xw $esp # explicitly 20 hex DWORDS from ESP [x86]
(gdb) x/s 0x804a000 # interpret memory as a C string
(gdb) x/s $rdi # string pointed to by RDI (e.g. syscall arg)
(gdb) x/i $eip # disassemble ONE instruction at EIP
(gdb) x/i $rip # disassemble ONE instruction at RIP
(gdb) x/20i main # disassemble 20 instructions starting at main
(gdb) x/5i $pc-10 # 5 instructions around current PC
(gdb) x/4dw &myvar # 4 decimal dwords at address of myvar
(gdb) x/8cb buf # 8 bytes as characters — good for spotting ASCII
(gdb) x/1gx $rbp # 1 hex giant (8B) at RBP — read saved RBP value
(gdb) x/1gx $rbp+8 # the QWORD just above saved RBP == saved return addr (x64)
# GDB remembers the last format/size — repeating 'x' or hitting Enter
# continues from where the last one left off
(gdb) x/20x $esp
(gdb) x # shows next 20 words automatically
6.1 Format & Unit Quick Reference
| Format char | Meaning | Unit char | Size | |
|---|---|---|---|---|
x |
hex | b |
byte (1) | |
d |
signed decimal | h |
halfword (2) | |
u |
unsigned decimal | w |
word (4) | |
o |
octal | g |
giant/quadword (8) | |
t |
binary | |||
s |
null-terminated string | |||
i |
machine instruction | |||
c |
char | |||
a |
address (resolves to nearest symbol) | |||
f |
float |
6.2 print vs x
# 'print' (p) evaluates a C-like expression and shows a typed value
(gdb) print $eax
(gdb) print/x $eax # print in hex
(gdb) print buf # if 'buf' is a known variable
(gdb) print *(int*)$esp # dereference ESP as an int pointer
# 'x' examines raw memory at an address — no type needed, just format+size
(gdb) x/4xb $esp
# Rule of thumb: use 'print' for values/expressions, 'x' for raw memory dumps
7. Registers
What it means: Registers hold the CPU's current working values — the instruction pointer (where execution is), the stack pointer, and general-purpose data. In exploit dev, watching EIP/RIP get overwritten by your payload is the literal moment you confirm control of execution flow.
# Show all general-purpose registers + flags
(gdb) info registers
(gdb) info r
(gdb) i r
# Show a single register
(gdb) info registers eax
(gdb) info registers rip
# print a register (can apply a format)
(gdb) print $eax
(gdb) print $rip
(gdb) print/x $eax # hex
(gdb) print/d $eax # decimal
(gdb) print/t $eax # binary
# Register names — prefix with $ always
$eax $ebx $ecx $edx $esi $edi $esp $ebp $eip $eflags # x86
$rax $rbx $rcx $rdx $rsi $rdi $rsp $rbp $rip # x64
$r8 $r9 $r10 $r11 $r12 $r13 $r14 $r15 # x64 extra
$pc # alias for $eip/$rip (arch-agnostic)
$sp # alias for $esp/$rsp
# Extended/vector registers (relevant for some CTF crypto/SIMD challenges)
(gdb) info all-registers # includes FPU/SSE/AVX registers
(gdb) print $xmm0
7.1 EFLAGS / RFLAGS Bits
(gdb) info registers eflags
# Example output: [ CF PF AF ZF SF IF ]
# CF = Carry Flag SF = Sign Flag
# PF = Parity Flag TF = Trap Flag
# AF = Auxiliary Flag IF = Interrupt Flag
# ZF = Zero Flag DF = Direction Flag
# OF = Overflow Flag
8. Examining the Stack
What it means: The stack holds local variables, saved registers, and return addresses — it's the primary target in a stack-based buffer overflow. Reading the stack correctly (and knowing which QWORD/DWORD is the saved return address) is the core mechanical skill of BOF exploitation.
# Dump the top of the stack
(gdb) x/20x $esp # 20 words from ESP [x86]
(gdb) x/20gx $rsp # 20 giants from RSP [x64]
# View stack as a "frame" — saved RBP + saved return address are always
# right after the local variables buffer, in this order (low → high addr):
# [ locals / buffer ] [ saved RBP ] [ saved return address ] [ args ]
(gdb) x/2gx $rbp # QWORD at RBP = saved RBP; RBP+8 = return addr
# Vanilla GDB has no dedicated "stack view" — GEF/pwndbg print it automatically
# on every stop (see §14/§15). In vanilla GDB, x/Nx $esp is your stack view.
# Print current stack frame info
(gdb) info frame
(gdb) frame # show current frame
(gdb) frame 1 # switch to frame 1 (caller)
# Show local variables of current frame (needs debug symbols)
(gdb) info locals
# Show function arguments of current frame (needs debug symbols)
(gdb) info args
8.1 Reading Off a Stack Frame Manually (Vanilla GDB, x64 example)
# Classic frame layout after 'push rbp; mov rbp, rsp; sub rsp, N':
#
# High addr ┌─────────────────────────┐
# │ Caller's frame / args │
# ├─────────────────────────┤
# │ Saved return address │ ← RBP + 8
# ├─────────────────────────┤
# │ Saved RBP │ ← RBP (this is [rbp])
# ├─────────────────────────┤
# │ Local vars / buffer │ ← RSP .. RBP-1
# Low addr └─────────────────────────┘
(gdb) x/1gx $rbp # saved RBP
(gdb) x/1gx $rbp+8 # saved return address — THIS is what BOF overwrites
(gdb) x/20gx $rsp # dump everything from RSP up through the frame
9. Disassembly
What it means: Disassembly turns raw machine code back into readable
assembly. GDB's default output is AT&T syntax (mov %eax, %ebx) — most CTF
and OSCP material uses Intel syntax (mov ebx, eax), so switching flavor is
usually the first thing you do in a new session.
# Switch to Intel syntax (do this immediately — or put it in .gdbinit, see §17)
(gdb) set disassembly-flavor intel
(gdb) set disassembly-flavor att # switch back if needed
# Disassemble a function
(gdb) disassemble main
(gdb) disas main # shorthand
(gdb) disas vulnerable_function
# Disassemble with raw opcode bytes shown (useful for finding bad chars/gadgets)
(gdb) disas /r main
# Disassemble with source interleaved (needs debug symbols)
(gdb) disas /s main
# Disassemble a specific address range
(gdb) disas 0x08048490,0x080484c0
(gdb) disas 0x08048490,+50 # 50 bytes from start address
# Disassemble around current PC (context view)
(gdb) x/10i $pc
(gdb) x/10i $pc-20 # instructions before current PC too
# Show the CURRENT instruction only (what's about to execute)
(gdb) x/i $eip
(gdb) x/i $rip
9.1 TUI Mode — Split-Screen Disassembly View
# GDB's built-in Text UI — shows source/asm + registers in split panes
# without needing GEF/pwndbg. Handy on a bare install.
(gdb) layout asm # assembly view
(gdb) layout regs # + registers pane
(gdb) layout src # source code view (needs debug symbols)
(gdb) layout split # source + asm together
# Navigate within TUI
Ctrl+X, A # toggle TUI on/off
Ctrl+X, 2 # cycle layouts
Ctrl+L # refresh screen (if garbled)
# TUI can be flaky over SSH — GEF/pwndbg's context-on-stop is usually
# preferred by pwn practitioners instead of TUI mode.
10. Backtrace & Call Stack
What it means: A backtrace shows the chain of function calls that got you to
the current point — "who called whom." Essential for understanding how you
reached a crash, especially in larger binaries or after a SIGSEGV.
(gdb) backtrace
(gdb) bt # shorthand
(gdb) bt full # + local variables at each frame
(gdb) bt 5 # only the innermost 5 frames
(gdb) bt -5 # only the outermost 5 frames
# Move between frames to inspect their local state
(gdb) frame 2 # jump to frame #2
(gdb) up # move one frame OUT (toward caller)
(gdb) down # move one frame IN (toward callee)
# Show where you are after moving frames
(gdb) info frame
(gdb) frame # print current frame summary
11. Watchpoints
What it means: A watchpoint pauses execution when a value CHANGES (or is read/written), rather than at a fixed location like a breakpoint. Useful for finding exactly which instruction corrupts a variable — e.g. tracking down where a canary gets overwritten, or catching a use-after-free write.
# Break when a variable's VALUE changes (write watchpoint)
(gdb) watch myvar
(gdb) watch *0x804a010 # watch a raw memory address
(gdb) watch $eax # watch a register (less common, but works)
# Break when a variable is READ (not just written)
(gdb) rwatch myvar
# Break on either read OR write
(gdb) awatch myvar
# List active watchpoints
(gdb) info watchpoints
# Delete a watchpoint (same 'delete N' as breakpoints — shared numbering)
(gdb) delete 2
12. Searching Memory & Patterns
What it means: In BOF work you constantly need to (a) generate a unique
cyclic pattern to find your exact offset to EIP/RIP, and (b) search memory for
strings like /bin/sh or a leaked address. Vanilla GDB can search memory
natively; pattern generation is normally left to Metasploit's pattern_create/
pattern_offset, pwntools' cyclic(), or GEF/pwndbg's built-in pattern commands.
# ── Vanilla GDB: search memory for a byte string ──
(gdb) find 0x08048000, 0x08050000, "/bin/sh"
(gdb) find &buf, &buf+200, 0x41414141 # search for a 4-byte value
# ── Generate cyclic pattern (outside GDB, then feed into GDB's run) ──
msf-pattern_create -l 3000
python3 -c "from pwn import *; print(cyclic(3000).decode())"
(gdb) run $(python3 -c "from pwn import *; print(cyclic(3000).decode())")
# ... crashes ...
(gdb) print $eip # note the value, e.g. 0x6161616c
# find offset OUTSIDE gdb:
# msf-pattern_offset -l 3000 -q 0x6161616c
# python3 -c "from pwn import *; print(cyclic_find(0x6161616c))"
# ── GEF has this built-in (no external tool needed) — see §14 ──
gef> pattern create 200
gef> pattern search $eip
# ── pwndbg has this built-in too — see §15 ──
pwndbg> cyclic 200
pwndbg> cyclic -l 0x61616164
13. Modifying State (Registers, Memory, Flow)
What it means: GDB isn't read-only — you can rewrite registers, patch memory,
and force execution to jump to an arbitrary address. This is invaluable for
testing "if I control EIP, can I actually reach my shellcode?" without needing a
full working exploit yet, and for patching out annoying checks (like alarm()
calls or anti-debug tricks) while you work.
# Set a register
(gdb) set $eip = 0x08048490
(gdb) set $rip = 0x555555555169
(gdb) set $eax = 0
(gdb) p $eip = 0x08048490 # 'print' as an assignment also works
# Write to memory
(gdb) set {int}0x804a010 = 0x41414141
(gdb) set {char}0x804a010 = 0x41
(gdb) set {long}$rsp = 0xdeadbeef
# Patch a byte in memory (e.g., NOP-out a call to alarm())
(gdb) set {unsigned char}0x0804852a = 0x90 # single NOP byte
# Force a jump — redirect execution to an arbitrary address right now
(gdb) jump *0x08048490
(gdb) jump *main
# Return immediately from the current function with a chosen value
(gdb) return 0
(gdb) return
# Call a function directly from within GDB (useful to test 'win()' functions)
(gdb) call win()
(gdb) print win()
(gdb) call (int)system("/bin/sh")
14. GEF — Command Reference
What it means: GEF replaces GDB's bare prompt with a rich, colorized context
screen every time execution stops — registers, stack, disassembly, and code all
visible at once, plus dozens of extra commands (checksec, vmmap, heap,
pattern generation, ROP search) that vanilla GDB doesn't have.
# ── Session control ──
gef> start # run and pause at main (like GDB's 'start')
gef> run # run freely
gef> run <<< "AAAA" # run with piped stdin
gef> run $(python3 -c "print('A'*200)") # run with generated argv
# ── Context display ──
gef> context # manually re-print the context screen
gef> ctx # shorthand
# Context auto-prints after every breakpoint/step/stepi by default
# ── Breakpoints & execution (same as vanilla, GEF just prettifies output) ──
gef> break main
gef> break *0x08048560
gef> continue
gef> nexti / ni
gef> stepi / si
gef> finish
# ── Examine memory (GEF adds smart type-following) ──
gef> x/20x $esp
gef> x/20i $pc
gef> hexdump $rsp --size 64 # nicer hexdump than raw x/
# ── Registers ──
gef> info registers
gef> print $eip
gef> registers # GEF's own colorized register dump
# ── checksec — binary protections at a glance (huge time-saver) ──
gef> checksec
# Shows: Canary / NX / PIE / RELRO / Fortify — all in one command
# ── vmmap — memory map of the running process ──
gef> vmmap
gef> vmmap libc # filter to libc mappings only
# ── heap analysis ──
gef> heap chunks
gef> heap chunk 0x55550000 # inspect a specific chunk
gef> heap bins # tcache/fastbin/unsorted bin state
# ── Pattern generation (replaces msf-pattern_create) ──
gef> pattern create 200
gef> pattern search $esp
gef> pattern search $eip
# ── Search memory for a string/bytes ──
gef> grep "/bin/sh"
gef> search-pattern "/bin/sh"
gef> search-pattern 0x41414141
# ── GOT / PLT ──
gef> got # show GOT entries and resolution state
# ── ROP gadget search (basic — ROPgadget/pwntools still better for complex chains) ──
gef> rop --grep "pop rdi"
# ── Process info ──
gef> pid
gef> process-search bash
14.1 GEF Config Tips
# Change context sections shown (fewer panes = less scroll)
gef> gef config context.layout "regs stack code"
# Save config permanently
gef> gef save
15. pwndbg — Command Reference
What it means: pwndbg covers similar ground to GEF but leans harder into CTF
pwn workflows — its cyclic, heap, and ROP tooling integrate tightly with
pwntools conventions, which is why many CTF players prefer it.
# ── Session control (same base commands as vanilla GDB) ──
pwndbg> run
pwndbg> start
pwndbg> break main
pwndbg> continue
pwndbg> nexti
pwndbg> stepi
# ── Context display ──
pwndbg> context # manually reprint (auto-shown on stop)
pwndbg> context regs
pwndbg> context stack
pwndbg> context disasm
# ── checksec ──
pwndbg> checksec
# ── vmmap ──
pwndbg> vmmap
pwndbg> vmmap libc
# ── Cyclic pattern (built-in, matches pwntools cyclic() exactly) ──
pwndbg> cyclic 200
pwndbg> cyclic -l 0x61616164 # find offset from a leaked/crashed value
# ── Heap analysis (pwndbg's strongest area) ──
pwndbg> heap
pwndbg> bins # all bins (tcache/fast/unsorted/small/large)
pwndbg> tcache
pwndbg> fastbins
pwndbg> malloc_chunk 0x55550000 # inspect one chunk in detail
# ── ROP gadget search ──
pwndbg> rop --grep "pop rdi"
pwndbg> ropgadget # alias, if configured
# ── Search memory ──
pwndbg> search "/bin/sh"
pwndbg> search -t string "flag"
# ── GOT / PLT / symbols ──
pwndbg> got
pwndbg> plt
# ── Telescope — smart stack/memory dump, follows pointers recursively ──
pwndbg> telescope $rsp
pwndbg> telescope $rsp 30 # 30 entries
pwndbg> telescope $rbp-0x40
# ── Stepping until a specific condition (useful in loops) ──
pwndbg> nextcall # run until the next 'call' instruction
pwndbg> nextjmp # run until the next jump
# ── Print canary value directly ──
pwndbg> canary
15.1 telescope — Why It's pwndbg's Signature Command
# 'telescope' dumps a memory region and, for each QWORD/DWORD, shows what
# it POINTS TO recursively (string? another pointer? code?). This single
# command replaces manually chaining x/gx + x/s over and over.
pwndbg> telescope $rsp 20
# 00:0000│ rsp 0x7ffee... —▸ 0x7ffee... ◂— 'AAAABBBBCCCC'
# 01:0008│ 0x7ffee... —▸ 0x555555555169 (main+42) ◂— endbr64
# ...
16. checksec, vmmap & Binary Protections
What it means: Before touching a binary, you need to know what mitigations are active — this dictates your entire exploitation strategy (shellcode vs ROP, whether you need an info leak, whether SEH/canary defeats are needed).
# Inside GDB with GEF or pwndbg loaded
gef> checksec
pwndbg> checksec
# Outside GDB — standalone tools, same info
checksec --file=./binary # from pwntools' checksec CLI
pwn checksec ./binary
# What each protection means:
| Protection | What it means | Exploitation impact |
|---|---|---|
| Canary | Random value before saved return address, checked before ret |
Must leak canary or find a non-overflow write primitive |
| NX / DEP | Stack/heap not executable | Can't jump to shellcode on stack — need ROP/ret2libc |
| PIE | Binary itself loads at a randomized base | Need an info leak to compute real addresses |
| RELRO (Full) | GOT is read-only after linking | Can't do GOT overwrite — target other primitives |
| RELRO (Partial) | GOT is writable | GOT overwrite is viable |
| ASLR (OS-level, not per-binary) | Libraries/stack load at random base each run | Need a leak to compute libc/stack addresses |
# Check/toggle ASLR system-wide (for LOCAL testing only — never affects remote target)
cat /proc/sys/kernel/randomize_va_space
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space # disable (0=off,1=on,2=on+heap)
# Memory map of a running/stopped process
gef> vmmap
pwndbg> vmmap
(gdb) info proc mappings # vanilla GDB equivalent — no extension needed
17. .gdbinit — Configuration File
What it means: .gdbinit is GDB's startup script — every command in it runs
automatically each time GDB launches. This is where you set Intel syntax, load
GEF/pwndbg, and define custom aliases so you don't retype setup commands every
session.
# Location: ~/.gdbinit (global) or ./gdbinit / .gdbinit in a project dir (local)
# ── Typical minimal .gdbinit for pwn work ──
cat ~/.gdbinit
# ~/.gdbinit example
set disassembly-flavor intel
# Load ONE extension only (uncomment the one you use)
source ~/.gef.py # if using GEF
# source ~/pwndbg/gdbinit.py # if using pwndbg
# Useful defaults
set pagination off # don't pause output with "---Type <return>---"
set confirm off # don't ask y/n before quitting with running inferior
# Custom command example — break at main and show initial context automatically
define pwn-start
start
context
end
# Reload .gdbinit changes without restarting GDB
(gdb) source ~/.gdbinit
Local .gdbinit safety note: by default, GDB refuses to auto-load a
.gdbinitfrom the current directory (security feature — prevents a malicious binary's folder from running arbitrary GDB Python on you). If you want a project-local one, add to~/.gdbinit:add-auto-load-safe-path /path/to/project
18. GDB Scripting — Python API
What it means: GDB embeds a full Python interpreter, letting you script complex, repeated inspection tasks — automating a "run until crash, dump registers, extract offset" loop instead of doing it by hand every time. GEF and pwndbg are themselves built almost entirely on this API.
# Enter GDB's Python interpreter interactively
(gdb) python
>print("hello from gdb python")
>end
# One-liner Python from the GDB prompt
(gdb) python print("hello")
# Run a Python script file
(gdb) source myscript.py
# ── Example: dump EIP every time a breakpoint hits ──
# myscript.py
import gdb
class PrintEIP(gdb.Command):
"""Custom command: print current EIP/RIP."""
def __init__(self):
super().__init__("print-pc", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
pc = gdb.parse_and_eval("$pc")
print(f"[+] Current PC: {pc}")
PrintEIP()
(gdb) source myscript.py
(gdb) print-pc
# ── Common gdb.* API patterns ──
gdb.execute("break main") # run a GDB command from Python
gdb.execute("run", to_string=True) # capture output as a string
val = gdb.parse_and_eval("$eax") # read a register/expression
gdb.Breakpoint("vuln_function") # set a breakpoint programmatically
# Read process memory directly
inferior = gdb.selected_inferior()
mem = inferior.read_memory(0x08048000, 16) # read 16 bytes
# Write process memory directly
inferior.write_memory(0x08048000, b"\x90"*4)
19. GDB with pwntools (gdb.attach)
What it means: pwntools can launch GDB, attach it to a process it just started, plant breakpoints, and drop you into an interactive GDB session — all from your exploit script. This is the standard CTF workflow: write the exploit, attach GDB before sending the trigger, step through it live.
from pwn import *
context.terminal = ['tmux', 'splitw', '-h'] # how GDB window opens — REQUIRED setting
# alternatives:
# context.terminal = ['gnome-terminal', '-e']
# context.terminal = ['tmux', 'splitw', '-v'] # horizontal split instead
elf = ELF('./binary')
context.binary = elf
# Start the process
p = process('./binary')
# Attach GDB to the already-running process, with breakpoints preset
gdb.attach(p, '''
break *main+42
break vuln_function
continue
''')
# ── Or attach + break with symbol offsets ──
gdb.attach(p, f'break *{elf.sym["vuln"]}\ncontinue')
# ── Debug from the very start (before any code runs) ──
gdb.debug('./binary', '''
break main
run
''')
# gdb.debug() = process() + gdb.attach() in one call, paused at entry
# ── Attaching to a REMOTE target is NOT possible the same way — ──
# ── you can only gdb.attach() to a LOCAL process pwntools started ──
p.interactive() # after your GDB window is done, resume driving the tube here
19.1 Common gdb.attach() Patterns
# Break right before a suspected vulnerable call, inspect stack, then continue
gdb.attach(p, 'break *0x555555555169\ncontinue')
# Break, don't auto-continue — inspect manually then type 'c' yourself
gdb.attach(p, 'break vuln')
# Attach with GEF/pwndbg context auto-shown (works automatically if it's
# in your ~/.gdbinit — pwntools just launches gdb normally)
20. Remote Debugging (gdbserver)
What it means: gdbserver runs on the TARGET machine (e.g. a CTF box, a VM,
an embedded device) and exposes a debugging port; your local GDB connects to it
over the network. This lets you fully debug a binary running on a different
machine/architecture than your attacking box — common for embedded/ARM CTF
challenges or when you have shell access to a box but want to debug locally.
# ── On the TARGET machine ──
gdbserver 0.0.0.0:1234 ./binary # start binary under gdbserver, listen on 1234
gdbserver 0.0.0.0:1234 --attach 1337 # attach to already-running PID 1337
# ── On your LOCAL attacking machine ──
gdb ./binary # load a local COPY of the same binary
(gdb) target remote 192.168.1.50:1234 # connect to the gdbserver
# From here, all normal GDB commands work exactly as if it were local:
(gdb) break main
(gdb) continue
(gdb) info registers
# Detach without killing the remote process
(gdb) detach
# pwntools equivalent for spawning gdbserver + connecting
from pwn import *
# ssh session to target, then run gdbserver over it, then connect local gdb —
# typically scripted per-environment; the manual two-terminal method above
# is far more common in CTF/OSCP practice than automating this.
21. Core Dumps
What it means: A core dump is a snapshot of a crashed process's full memory state saved to disk. Instead of reproducing a crash live, you can load the core dump straight into GDB and inspect exactly what the registers/stack/memory looked like at the moment of the crash — useful when a crash is hard to reproduce or happened during automated fuzzing.
# Enable core dumps (often disabled by default — ulimit is 0)
ulimit -c unlimited
# Check current core dump size limit
ulimit -c
# Where core dumps get written (varies by distro)
cat /proc/sys/kernel/core_pattern
# Common: 'core' in cwd, or piped to apport/systemd-coredump
# Run the binary until it crashes and produces a core file
./binary < crashing_input
ls core* # find the dump (e.g. core, core.1234)
# Load the binary + core dump together into GDB
gdb ./binary core
gdb ./binary core.1234
# Once loaded, inspect exactly like a live crash
(gdb) bt # see the call chain at crash time
(gdb) info registers # register state at crash
(gdb) x/20x $rsp # stack at crash time
# systemd-coredump systems — retrieve a dump by PID
coredumpctl list
coredumpctl gdb 1234 # launches gdb on that core directly
22. Common BOF/CTF Workflow in GDB
What it means: This is the practical, step-by-step sequence you'll run in GDB for almost every stack-based buffer overflow challenge — whether it's a CTF pwn binary or an OSCP-style BOF. Pairs directly with the methodology in the Buffer Overflow reference note.
# STEP 1 — Load and check protections
gdb ./binary
gef> checksec # or: pwndbg> checksec
# STEP 2 — Find the crash / confirm the vuln
gef> run $(python3 -c "print('A'*500)")
# or with pwntools cyclic pattern:
gef> run $(python3 -c "from pwn import *; print(cyclic(500).decode())")
# STEP 3 — Read the crash state
gef> info registers # note EIP/RIP value — is it your pattern?
gef> print $eip # e.g. 0x6161616c
# STEP 4 — Find exact offset
gef> pattern search $eip # GEF tells you the offset directly
# or externally: cyclic_find(0x6161616c)
# STEP 5 — Confirm exact control
gef> run $(python3 -c "print('A'*OFFSET + 'BBBB')")
gef> info registers # EIP should now read exactly 0x42424242
# STEP 6 — Inspect the stack for a landing spot (shellcode injection case)
gef> x/40x $esp # find where your 'C's / shellcode start
gef> print $esp # this address, if stable, may be your return target
# STEP 7 — If ASLR/PIE is involved, find the runtime base
gef> vmmap
gef> info proc mappings
# STEP 8 — Set a breakpoint right before 'ret' to verify stack just before jump
gef> break *vuln+123 # address of the 'ret' or 'leave;ret'
gef> continue
gef> x/gx $rsp # what's about to be popped into RIP
# STEP 9 — Step through your shellcode/ROP chain instruction by instruction
gef> stepi
gef> x/i $pc # confirm each instruction executes as expected
# STEP 10 — Once it works reliably in GDB, remove the debugger and fire at the
# real target (GDB changes stack layout slightly vs. running standalone —
# always do a final un-debugged local test before going remote)
23. Troubleshooting & Gotchas
What it means: A handful of GDB quirks trip up almost everyone early on — knowing them ahead of time saves real debugging time later.
# ── "No symbol table is loaded" ──
# The binary is stripped, or you forgot to load it.
(gdb) file ./binary
# ── Offsets found in GDB don't match the offset outside GDB ──
# GDB slightly changes the stack (extra env vars, different argv[0] length).
# ALWAYS verify your final offset with the binary run standalone (no GDB)
# before trusting it for a remote exploit. This is especially true for
# Windows Immunity Debugger too, not just GDB.
# ── Breakpoint address doesn't exist anymore after rerun (PIE) ──
# PIE binaries re-randomize their base every 'run'. Use symbol-based
# breakpoints (break main) or offsets from a symbol, not raw addresses
# from objdump, unless ASLR is disabled locally (see §16).
# ── 'run' seems to hang / no output ──
# The target may be waiting on stdin — check if it needs input, or if
# stdout is buffered (common with C's printf without a newline/flush).
# ── GEF/pwndbg commands "not found" ──
# Only one extension should be sourced in .gdbinit — check for conflicting
# 'source' lines if commands from both seem half-broken.
# ── Can't set a breakpoint on a stripped, no-PIE binary at a raw address ──
(gdb) break *0x08048490 # raw addresses always work regardless of symbols
# ── ptrace / permission denied when attaching to a PID ──
cat /proc/sys/kernel/yama/ptrace_scope
# 0 = anyone can ptrace any process owned by same user (needed for gdb -p)
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
# ── Disassembly shows AT&T syntax and it's confusing ──
(gdb) set disassembly-flavor intel # do this every session, or put in .gdbinit
# ── "Program received signal SIGSEGV" but you don't know why ──
gef> context # re-print full context immediately after crash
gef> bt # backtrace to see the call chain
24. Cheat Sheet
Absolute Essentials (memorize these first)
gdb -q ./binary # launch quiet
(gdb) set disassembly-flavor intel # Intel syntax
(gdb) start # run, break at main
(gdb) break FUNC | *ADDR # breakpoint
(gdb) run [args] / run < file # start execution
(gdb) continue / c # resume
(gdb) nexti / ni stepi / si # step 1 instruction (over / into)
(gdb) info registers / i r # all registers
(gdb) x/20x $rsp # dump stack (x64)
(gdb) x/i $rip # current instruction
(gdb) disas main # disassemble a function
(gdb) bt # backtrace
(gdb) set $rip = 0x... # overwrite a register
GEF Quick Reference
gef> checksec
gef> vmmap
gef> pattern create 200
gef> pattern search $eip
gef> heap chunks / heap bins
gef> got
gef> search-pattern "/bin/sh"
gef> rop --grep "pop rdi"
pwndbg Quick Reference
pwndbg> checksec
pwndbg> vmmap
pwndbg> cyclic 200
pwndbg> cyclic -l 0x61616164
pwndbg> telescope $rsp 20
pwndbg> bins / tcache / fastbins
pwndbg> rop --grep "pop rdi"
pwndbg> canary
x/NFU Format Reference
# N = count, F = format, U = unit
# Format: x=hex d=dec u=udec o=oct t=bin s=string i=instr c=char a=addr f=float
# Unit: b=byte(1) h=half(2) w=word(4) g=giant(8)
x/20xg $rsp # 20 hex QWORDS from RSP [x64 stack dump]
x/20xw $esp # 20 hex DWORDS from ESP [x86 stack dump]
x/s $rdi # string at RDI
x/10i $pc # 10 instructions from PC
Stack Frame Cheat (x64, after push rbp; mov rbp,rsp)
[rbp] = saved RBP
[rbp+8] = saved return address ← BOF target
[rbp-N] = local variables / buffer
Register Names by Arch
x86: eax ebx ecx edx esi edi esp ebp eip eflags
x64: rax rbx rcx rdx rsi rdi rsp rbp rip
r8 r9 r10 r11 r12 r13 r14 r15
pwntools GDB Integration One-Liner
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF('./binary'); context.binary = elf
p = process('./binary')
gdb.attach(p, f'break *{elf.sym["vuln"]}\ncontinue')
p.interactive()
Setup Checklist Every New Binary
1. gdb -q ./binary
2. set disassembly-flavor intel (or bake into ~/.gdbinit)
3. checksec (GEF/pwndbg)
4. start (break at main, see initial context)
5. disas main (get the lay of the land)
6. info functions (list all functions, spot 'win'/'vuln'/'flag')
Key tips for OSCP/CPTS/CTF work:
- Always run
checksecfirst — it decides your entire strategy (shellcode vs ROP, leak-needed vs not) - Use
nexti/stepi, notnext/step— stripped CTF binaries have no debug symbols for source-line stepping - GDB's stack layout is NOT identical to a standalone run — always confirm your final offset/payload against the binary run without GDB before trusting it against a remote target
set disassembly-flavor intelimmediately, every session — or bake it into~/.gdbinitand never think about it againgdb.attach()from pwntools is the fastest CTF loop: write exploit → attach → break before the trigger → step through → confirm → fire for real- Disable ASLR locally (
echo 0 > /proc/sys/kernel/randomize_va_space) only for your own local testing — never assume it's off on the real target