Description
Practice GDB: break at main+99, run, and jump to main+104 to skip a delay and print the flag.
Setup
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.
chmod +x gdbmeprintf 'layout asm\nbreak *(main+99)\nrun\njump *(main+104)\n' > drive.gdbgdb -x drive.gdb gdbmeSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Set up the breakpointObservationI 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+99may not resolve before the program loads - runinfo address mainafter the firstrunto 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 ofbreak *(main+99).GDB interprets
break main+99as 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 beforerun, 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
mainhas no real address yet at that point. GDB may silently set a breakpoint at address 99, which is never hit. Runninginfo address mainafter the firstrunor usingstartito 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 themainfunction. The*dereferences the address expression - without it, GDB would interpret the argument as a line number. Thelayout asmcommand 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.gdbto 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.Step 2
Skip the waitObservationI 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 ajump- nocneeded. The flag prints as the program continues from main+104 to exit.What didn't work first
Tried: Type
continue(orc) after thejumpcommand, expecting the program to need a nudge to keep running.jumpin GDB both changes the instruction pointer and immediately resumes execution - it is not the same as setting a register and then requiringcontinue. Typingcafterjumpblocks on the next breakpoint (if any) or causes unexpected behavior if the program already printed the flag and exited. No additional command is needed afterjump.Tried: Use
set $rip = *(main+104)to rewrite the instruction pointer directly instead of usingjump.set $ripchanges the register value but does NOT resume execution - you still need to typecontinueafterward. More critically,*(main+104)in this context dereferences the address (reads the value stored there) rather than loading the address itself; you wantset $rip = main+104without the dereference operator. Usingjump *(main+104)is cleaner and avoids this pitfall.Learn more
The
jumpcommand in GDB changes the instruction pointer (RIPon 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, asleep()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 for0xCCbytes 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
jumpcommand 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.