GDB Test Drive picoCTF 2022 Solution

Published: July 20, 2023

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 1
    Set up the breakpoint
    Observation
    I noticed the challenge description explicitly gave the offsets main+99 and main+104, which suggested setting a breakpoint at main+99 to pause execution right before the problematic instruction and then redirect from there.
    Break at *(main+99), run, then jump past the sleep at *(main+104). With ASLR/PIE on, main+99 may not resolve before the program loads - run info address main after the first run to grab the live address and add 99/104 to it instead of relying on the symbolic offset.
    What didn't work first

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

    GDB interprets break main+99 as line number 99 of the source file starting at main, not an address offset. Without the * dereference, GDB errors with 'No line 99 in file' or breaks at a completely wrong location. The * is required to treat the expression as an absolute memory address.

    Tried: Set the breakpoint before the binary is loaded by running break *(main+99) at the GDB prompt before run, then observe it resolve correctly in a non-PIE build but fail in this PIE binary.

    PIE binaries have a base address of 0 until the loader maps them into memory, so main has no real address yet at that point. GDB may silently set a breakpoint at address 99, which is never hit. Running info address main after the first run or using starti to stop at the entry point gives the live base address so offsets resolve correctly.

    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 * dereferences the address expression - without it, GDB would interpret the argument as a line number. 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 2
    Skip the wait
    Observation
    I noticed the binary includes a sleep call between main+99 and main+104 that makes it impractical to wait for the flag to print naturally, which suggested using GDB's jump command to move the instruction pointer past it and resume execution immediately at main+104.
    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 in GDB both changes the instruction pointer and immediately resumes execution - it is not the same as setting a register and then requiring continue. Typing c after jump blocks on the next breakpoint (if any) or causes unexpected behavior if the program already printed the flag and exited. No additional command is needed after jump.

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

    set $rip changes the register value but does NOT resume execution - you still need to type continue afterward. More critically, *(main+104) in this context dereferences the address (reads the value stored there) rather than loading the address itself; you want set $rip = main+104 without the dereference operator. Using jump *(main+104) is cleaner and avoids this pitfall.

    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.

Flag

Reveal flag

picoCTF{d3bugg3r_dr1v3_197c3...}

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

Key takeaway

A debugger lets you pause any process, inspect its full state, and redirect execution to any address by rewriting the instruction pointer. Anti-analysis techniques like long sleeps, ptrace self-checks, and timing comparisons all try to make dynamic analysis impractical, but a debugger break-and-jump sidesteps them without modifying the binary on disk. This same capability underpins exploit development, malware analysis, and license-check bypasses across every compiled platform. Scripting the debugger session with GDB's -x flag or pwntools makes the technique repeatable and automatable.

Related reading

Want more picoCTF 2022 writeups?

Useful tools for Reverse Engineering

Do these first

What to try next