Introduction
GDB (the GNU Debugger) is the most important tool in your reverse engineering and binary exploitation kit. It lets you pause a running program at any point, inspect every register and memory address, modify values on the fly, and step through assembly one instruction at a time. If you can use GDB, you can understand any Linux binary, no matter how obfuscated the source.
This guide walks through GDB from first principles, with a focus on the patterns that appear in picoCTF reverse engineering and binary exploitation challenges. By the end you will be comfortable with the GDB baby step series, GDB Test Drive, the PIE bypass technique used in PIE TIME, and the setup needed for format string and heap challenges.
cmp and lea are unfamiliar, keep the x86 assembly for CTF guide open alongside this one. When you would rather read decompiled C than step through instructions, the Ghidra reverse engineering guide is the static-analysis counterpart to GDB.Installing GDB
GDB ships with most Linux distributions. On Ubuntu or Debian:
sudo apt install gdb
To verify the install and check the version:
gdb --version
Running a binary in GDB
Start GDB with a binary as its argument. GDB opens an interactive prompt but does not run the program yet.
gdb ./challenge_binary
From the GDB prompt, use run (or r) to start the program:
(gdb) run(gdb) run arg1 arg2 # pass arguments
If the binary needs input piped from a file:
(gdb) run < /tmp/input.txt
To quit GDB at any time:
(gdb) quit
Breakpoints
A breakpoint pauses execution at a specific location. The most common targets are function names and addresses. Use break (or b) to set one:
(gdb) break main # break at the start of main(gdb) break *0x401234 # break at a specific address(gdb) break check_password # break at a named function
After setting a breakpoint, run the program. GDB pauses when the breakpoint is hit. Then use continue (or c) to resume, next (or n) to execute the next source line without entering function calls, or step (or s) to step into function calls.
(gdb) continue # run until next breakpoint or end(gdb) next # next line (step over calls)(gdb) step # next line (step into calls)(gdb) nexti # next instruction (assembly level)(gdb) stepi # step one instruction
To list all breakpoints, delete one, or disable one:
(gdb) info breakpoints(gdb) delete 1 # delete breakpoint #1(gdb) disable 1 # disable without deleting(gdb) enable 1 # re-enable
Conditional breakpoints
A conditional breakpoint only pauses the program when a given expression evaluates to true. This is invaluable when you want to stop inside a loop only on a specific iteration, or at a comparison only when the input matches a particular value.
Add a condition when you set the breakpoint:
(gdb) break *0x401234 if $eax == 42(gdb) break loop_body if i == 100(gdb) break strcmp if $rdi != 0 # break only when first arg is non-null
You can also add or change a condition on an existing breakpoint using its number:
(gdb) info breakpoints # find the breakpoint number(gdb) cond 1 $eax == 0x41 # add condition to breakpoint 1(gdb) cond 1 # remove the condition (breakpoint always hits again)
ignore N count to skip the first N hits instead: ignore 1 99 makes breakpoint 1 fire only on the 100th hit.A common CTF pattern: the binary XORs each byte of your input with a key in a loop, then compares the result to a target buffer. Set a conditional breakpoint on the comparison instruction that triggers only when the loop counter equals a specific index, and read both sides to find the key:
(gdb) break *0x4011ab if $ecx == 5 # stop on the 6th iteration(gdb) run(gdb) print/x $al # value being compared (your input, XORed)(gdb) print/x $dl # target value# key byte = your_input_byte XOR target_byte
Watchpoints
A watchpoint stops the program whenever a specific memory location is read or written, regardless of where in the code that happens. Use this when you know what changes but not where in the code the change occurs.
(gdb) watch *0x601060 # stop when the value at this address is written(gdb) rwatch *0x601060 # stop when this address is read(gdb) awatch *0x601060 # stop on either read or write# Watch a named variable (only works if debug symbols are present):(gdb) watch global_flag# Watch a local variable's address:(gdb) watch *(int*)($rbp - 0x8) # watch whatever is at rbp-8
After a watchpoint fires, GDB shows the old and new values of the watched location and the instruction that triggered the change. Watchpoints are managed like breakpoints:
(gdb) info watchpoints(gdb) delete 2 # delete watchpoint #2
Reading registers
Registers are the CPU's fast, small storage slots. On x86-64, the general-purpose registers are rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, and r8 through r15. The instruction pointer is rip.
The GDB baby step 1 and GDB baby step 2 challenges are built specifically to practice reading register values. In those challenges, the flag is the decimal value stored in eax when a specific function returns.
To read all registers at once:
(gdb) info registers(gdb) info registers eax # just one register
To print the value of a single register in different formats:
(gdb) print $eax # decimal(gdb) print/x $eax # hexadecimal(gdb) print/t $eax # binary(gdb) print/c $eax # as ASCII character
print command (abbreviated p) can evaluate any C expression, not just register names. p $eax + 1, p (int) $rax, and p &variable_name all work.Inspecting memory
The x command (examine) lets you read arbitrary memory. The format is x/NFU addr where N is the count, F is the format, and U is the unit size:
# Format letters:# x = hex, d = decimal, s = string, i = instruction, c = char# Unit letters:# b = byte, h = halfword (2), w = word (4), g = giant (8)(gdb) x/20x $rsp # 20 hex words starting at stack pointer(gdb) x/s 0x402000 # string starting at address(gdb) x/10i $rip # next 10 instructions(gdb) x/4wx $rbp-0x10 # 4 words at rbp minus 16
The disassemble command shows the assembly for a function:
(gdb) disassemble main(gdb) disassemble /m main # interleave with source if available
The backtrace (or bt) command shows the call stack, which tells you how the program arrived at the current point:
(gdb) backtrace
Patching registers and memory
GDB's set command lets you overwrite any register or memory location while the program is paused. This is one of the most powerful CTF techniques: instead of crafting an exploit that forces a comparison to succeed, you can simply reach the comparison in GDB and write the expected value yourself.
Overwriting registers
(gdb) set $rax = 0 # set rax to 0(gdb) set $eax = 1 # set the lower 32-bit alias(gdb) set $rip = 0x401234 # redirect execution to a new address(gdb) set $rsp = $rsp - 8 # adjust the stack pointer
$rip to an arbitrary address jumps to that address on the next instruction. The stack state may be inconsistent with what that function expects, so this works best for jumping to simple functions or gadgets with no stack setup.Overwriting memory
# Write an integer to an address:(gdb) set *((int*)0x601060) = 0# Alternative syntax using a type cast:(gdb) set {int}0x601060 = 42(gdb) set {long}0x601060 = 0xdeadbeef# Write a byte:(gdb) set {char}0x601060 = 0x41# Write to a stack variable (use rbp-relative addressing):(gdb) set {int}($rbp - 0x10) = 1337
Bypass a password check without cracking it
The most common use of patching in beginner CTF challenges: stop at the comparison, read the expected value, and force the check to pass without ever knowing the password.
(gdb) break *0x4012ab # break at the cmp instruction(gdb) run(gdb) print $eax # your input (after transformation)(gdb) print $edx # expected value(gdb) set $eax = $edx # force them equal(gdb) continue # the check passes
GEF and pwndbg
Vanilla GDB displays minimal context when you step through assembly. Two popular plugins greatly improve the experience by printing registers, the stack, and nearby instructions automatically after every step. Install one of them before starting any serious CTF work.
GEF (GDB Enhanced Features)
GEF is the most widely used choice for CTF binary exploitation. Install it with:
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
After installing, run gdb ./binary as usual and you will see a formatted context panel (registers, code, stack) printed automatically after every stop. Key GEF commands beyond vanilla GDB:
# Security mitigations on the binary:gef> checksec# Memory map (shows base addresses of binary, libc, stack, heap):gef> vmmap# Generate a De Bruijn cyclic pattern for finding overflow offsets:gef> pattern create 200gef> pattern search $rsp # after a crash, find where the pattern landed# Readable stack display (shows pointers resolved to symbols):gef> telescope $rsp 20# Heap chunk and bins analysis:gef> heap chunksgef> heap bins# Manually trigger the context panel at any time:gef> context
pwndbg
pwndbg is an alternative plugin with a similar philosophy. Both are excellent; GEF tends to be preferred in CTF writeups. Install it with:
git clone https://github.com/pwndbg/pwndbgcd pwndbg && ./setup.sh
pwndbg's equivalent commands: checksec, vmmap, cyclic 200 / cyclic -l VALUE, telescope, heap, bins.
~/.gdbinit wins. Most CTF players pick one and stick with it.Common CTF patterns
Most beginner CTF reverse engineering challenges fall into a small set of patterns:
1. Read a register value after a function returns
Used in the GDB baby step series. Set a breakpoint on the instruction after the function call so the function has fully executed and placed its return value in eax.
gdb ./chal(gdb) break *0x401142 # address of instruction after the call(gdb) run(gdb) print/d $eax # read the return value in decimal
2. Find a return address for PIE bypass
Used in PIE TIME. With ASLR enabled, a PIE binary's base address changes every run. GDB lets you leak the runtime base to compute the real address of any function:
(gdb) break main(gdb) run(gdb) info proc mappings # shows the base address of the binary# Example output: 0x555555554000 0x555555555000 ... /challenge# ^^^^^^^^^^^^^^^^ this is the base# Add the function's file offset (from: readelf -s ./binary | grep func_name)# to the runtime base to get the real address:(gdb) print 0x555555554000 + 0x1234 # = runtime address of target function
3. Bypass a password check
Set a breakpoint at the comparison instruction and use set to force it to succeed, or read both sides to extract the expected value:
(gdb) break *0x4012ab # break at the cmp instruction(gdb) run(gdb) print $eax # actual transformed input(gdb) print $edx # expected value(gdb) set $eax = $edx # force them equal to bypass the check(gdb) continue
4. GDB vs strace and ltrace
Before reaching for GDB, consider two faster alternatives:
# strace: print every system call (open, read, write, execve, ...)strace ./challenge# ltrace: print every library call (strcmp, printf, malloc, ...)ltrace ./challenge
ltrace is often the fastest way to solve a password cracking challenge: it will print the arguments to strcmp or memcmp in plain text, showing both the user input and the expected value side by side. Use GDB when you need to step through custom logic, the binary uses inline string comparison (not a library call), or you need to inspect register state at specific points.
Quick reference
| Command | Description |
|---|---|
| run / r | Start the program |
| run < file | Run with stdin from file |
| continue / c | Resume after a breakpoint |
| next / n | Step over one source line |
| step / s | Step into a function call |
| nexti / ni | Step one assembly instruction (no call entry) |
| stepi / si | Step one instruction (enters calls) |
| break main | Breakpoint at main |
| break *0xADDR | Breakpoint at an address |
| break *addr if expr | Conditional breakpoint |
| cond N expr | Add condition to existing breakpoint N |
| ignore N count | Skip first count hits of breakpoint N |
| watch *0xADDR | Watchpoint: stop on write to address |
| rwatch *0xADDR | Stop on read |
| awatch *0xADDR | Stop on read or write |
| info breakpoints | List all breakpoints and watchpoints |
| delete N | Delete breakpoint/watchpoint N |
| print/x $rax | Print register in hex |
| info registers | Print all registers |
| x/Ns ADDR | Examine N strings at ADDR |
| x/Ni $rip | Disassemble N instructions from rip |
| disassemble func | Show assembly for a function |
| backtrace / bt | Show the call stack |
| set $reg = val | Write a value into a register |
| set {int}ADDR = val | Write a value to a memory address |
| quit / q | Exit GDB |
Relevant challenges to practice with: GDB baby step 1, GDB baby step 2, GDB Test Drive, PIE TIME.