April 12, 2026

Using GDB for CTF Reverse Engineering

GDB for CTF: run binaries, set breakpoints, read registers and memory, use conditional breakpoints and watchpoints, patch values, and add the GEF plugin.

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.

Note: GDB shows you the assembly, but you still need to read it. If registers and instructions like 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
Tip: For WSL (Windows Subsystem for Linux) users: GDB works inside WSL exactly the same as on native Linux. Open a WSL terminal and install it with the command above.

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
Note: The binary runs in GDB the same way it would run outside GDB. If it asks for interactive input, type it directly. If it crashes, GDB will catch the signal and let you inspect the state at the point of the crash.

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)
Tip: Conditional breakpoints are slower than unconditional ones because GDB evaluates the expression every time the address is reached. For a tight inner loop, use 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
Note: Hardware watchpoints (the default on x86) are fast and supported by the CPU. Most x86 systems support at most 4 simultaneous hardware watchpoints. If you need more, GDB falls back to software watchpoints, which are much slower because they single-step the entire program.
Tip: A watchpoint on a stack variable is only valid while that stack frame is alive. Once the function returns and the stack frame is deallocated, the watchpoint is automatically deleted.

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
Tip: The 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
Warning: Setting $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 200
gef> 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 chunks
gef> 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/pwndbg
cd pwndbg && ./setup.sh

pwndbg's equivalent commands: checksec, vmmap, cyclic 200 / cyclic -l VALUE, telescope, heap, bins.

Note: You can only have one plugin active at a time. If you install both GEF and pwndbg, the last one to write to ~/.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

CommandDescription
run / rStart the program
run < fileRun with stdin from file
continue / cResume after a breakpoint
next / nStep over one source line
step / sStep into a function call
nexti / niStep one assembly instruction (no call entry)
stepi / siStep one instruction (enters calls)
break mainBreakpoint at main
break *0xADDRBreakpoint at an address
break *addr if exprConditional breakpoint
cond N exprAdd condition to existing breakpoint N
ignore N countSkip first count hits of breakpoint N
watch *0xADDRWatchpoint: stop on write to address
rwatch *0xADDRStop on read
awatch *0xADDRStop on read or write
info breakpointsList all breakpoints and watchpoints
delete NDelete breakpoint/watchpoint N
print/x $raxPrint register in hex
info registersPrint all registers
x/Ns ADDRExamine N strings at ADDR
x/Ni $ripDisassemble N instructions from rip
disassemble funcShow assembly for a function
backtrace / btShow the call stack
set $reg = valWrite a value into a register
set {int}ADDR = valWrite a value to a memory address
quit / qExit GDB

Relevant challenges to practice with: GDB baby step 1, GDB baby step 2, GDB Test Drive, PIE TIME.

Try it on these picoCTF challenges

Walkthroughs that put this technique to work, grouped by event.

Keep reading

Guides that build on the same ideas, plus the roadmap this topic sits under.