Skip to main content

GDB Test Drive picoCTF 2022 Solution

Use a debugger to inspect a binary at runtime and manipulate execution to reveal the flag.

Published: July 20, 2023Updated: August 25, 2026

Description

Practice GDB: break at main+99, run, and jump to main+104 to skip a delay and print the flag.

Make the binary executable (chmod +x gdbme).

Drop the GDB commands into a file (drive.gdb) and load with gdb -x drive.gdb gdbme so the run is reproducible.

After jump *(main+104), execution resumes immediately from the new address; the flag prints and the program exits without any additional command.

bash
chmod +x gdbme
bash
printf 'layout asm\nbreak *(main+99)\nrun\njump *(main+104)\n' > drive.gdb
bash
gdb -x drive.gdb gdbme

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Set up the breakpoint
    Observation
    The description hands you the offsets main+99 and main+104. Break at the first to pause just before the problematic instruction, then redirect from there.
    Break at *(main+99), run, then jump past the sleep at *(main+104). The symbolic form keeps working even if the binary is PIE: GDB evaluates main against the file's own addresses before run and relocates the breakpoint once the loader picks a base, so you never have to recompute anything by hand. What does break is pasting a bare absolute address copied from an earlier session or from objdump, since that number is only valid for the base that run happened to get.
    What didn't work first

    Tried: Set the breakpoint with break main+99 (no asterisk) instead of break *(main+99).

    Without the asterisk GDB parses the argument as a linespec (a function name, or a file:line pair), not as an address expression. There is no function called main+99, so it refuses outright or offers to make the breakpoint pending on a future shared library load, and execution never stops where you wanted. The leading asterisk is what makes GDB evaluate the expression and break at that address.

    Tried: Note the address GDB prints for main before run, then hardcode break *0x...+99 from that number.

    For a PIE binary the pre-run address is the unrelocated one from the file, and the loader picks a different base at startup, so the hardcoded breakpoint lands in unmapped memory or in a library. Keep the symbolic *(main+99) form and let GDB relocate it, or read the live address with info address main after the process is running.

    Learn more

    GDB (GNU Debugger) is the standard debugger for Linux programs. It can pause execution at specific points (breakpoints), inspect registers and memory, modify values at runtime, and change the instruction pointer to jump to arbitrary locations in the code. These capabilities make it an essential tool for both software development and reverse engineering.

    A breakpoint at *(main+99) tells GDB to pause execution 99 bytes into the main function. The * is what marks the argument as an address expression to evaluate - without it, GDB treats the argument as a linespec (a function name or a file:line pair) and fails to find any function named main+99. The layout asm command switches the TUI (text user interface) to show the assembly disassembly, which is useful for understanding exactly what instruction you're stopped at.

    GDB supports scripting via here-documents (as shown in the command) or via -x script.gdb to run a file of GDB commands. Automating debugger sessions this way is powerful for CTF challenges that require repeatable interaction with a binary, and is the foundation of tools like pwndbg and pwntools which wrap GDB for exploit development.

  2. Step 2Skip the wait
    Observation
    Between those two offsets sits a sleep call long enough that waiting for the flag is impractical. GDB's jump moves the instruction pointer past it and resumes at main+104 immediately.
    Jumping to main+104 lands past the sleep call. GDB resumes execution immediately after a jump - no c needed. The flag prints as the program continues from main+104 to exit.
    What didn't work first

    Tried: Type continue (or c) after the jump command, expecting the program to need a nudge to keep running.

    jump changes the instruction pointer and resumes execution in one step; it is not a register write awaiting a separate continue. Type c afterwards and you either block on the next breakpoint or confuse things when the program has already printed the flag and exited. Nothing more is needed after jump.

    Tried: Use set $rip = *(main+104) to rewrite the instruction pointer directly instead of using jump.

    Setting the instruction pointer register changes the value without resuming, so a continue is still required. Worse, wrapping the target in a dereference reads the value stored at that address rather than the address itself. Assign the address without the dereference, or use jump, which avoids the trap entirely.

    Learn more

    The jump command in GDB changes the instruction pointer (RIP on x86-64) to a new address and resumes execution from there. This lets you skip over any instruction or block of code - in this case, a sleep() call that would otherwise make the program wait an impractically long time before printing the flag.

    Anti-debugging tricks like deliberate sleep calls, infinite loops, or timing checks are common in CTF binaries and real malware to frustrate analysis. The sleep approach is the simplest: the program is correct and will eventually print the flag, but waiting would take too long. More sophisticated techniques include checking if a debugger is attached via ptrace(PTRACE_TRACEME), detecting breakpoints by looking for 0xCC bytes in the code, or using timing side-channels.

    Knowing how to patch around such checks - either by jumping past them in GDB or by binary patching the file with a hex editor - is a core reverse engineering skill. The jump command is the lightest-weight approach since it doesn't modify the binary on disk.

Interactive tools
  • Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
  • pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{d3bugg3r_dr1v3_197c3...}

Great intro to gdb's `jump` command for skipping instructions.

Key takeaway

A debugger pauses any process, shows its full state, and redirects execution anywhere by rewriting the instruction pointer. Anti-analysis tricks like long sleeps, ptrace self-checks, and timing comparisons all aim to make dynamic analysis impractical, and a break-and-jump steps around them without touching the binary on disk. The same capability underpins exploit development, malware analysis, and license-check bypasses on every compiled platform, and scripting the session with GDB's -x flag or pwntools makes it repeatable.

Related reading

Useful tools for Reverse Engineering

Where to go next