April 12, 2026

Using GDB for CTF Reverse Engineering

A practical guide to GDB for CTF competitions: running binaries, setting breakpoints, reading registers, inspecting memory, and tracing through the Bit-O-Asm and GDB baby step challenge series.

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, and the setup needed for format string and heap challenges.

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 line numbers. 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

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 simply 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

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 code automatically after every step:

GEF (GDB Enhanced Features) is the most widely used choice for CTF work. Install it with:

bash -c "$(curl -fsSL https://gef.blah.cat/sh)"

pwndbg is an alternative plugin with a similar philosophy. Both are excellent; GEF tends to be preferred for its built-in heap analysis. After installing either one, simply run gdb ./binary as before and you will see the enhanced context panel automatically.

Tip: GEF adds commands like checksec (shows security mitigations on a binary), vmmap (shows memory layout), and pattern create / pattern search (generates De Bruijn patterns for finding buffer overflow offsets).

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 that 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

2. Bypass a password check

Set a breakpoint at the comparison instruction. Inspect the values being compared with print, or use set to force the comparison to succeed:

(gdb) break *0x4012ab # break at the cmp instruction
(gdb) run
(gdb) print $eax # actual input
(gdb) print $edx # expected value
(gdb) set $eax = $edx # force them equal to bypass the check
(gdb) continue

3. Find a return address for PIE bypass

Used in PIE TIME. With ASLR enabled, the base address of a PIE binary changes every run. GDB lets you leak it to compute the real address of target functions:

(gdb) break main
(gdb) run
(gdb) info proc mappings # shows base address of the binary
# Add the function offset (from readelf -s ./binary) to get the runtime address

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
info breakpointsList all breakpoints
delete NDelete breakpoint 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
quit / qExit GDB

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