Skip to main content

GDB baby step 2 picoGym Exclusive Solution

Use a debugger to pause execution and read a register's final value to recover the flag.

Published: March 5, 2024Updated: August 25, 2026

Description

Continue practicing with debugger0_b by reporting the value in EAX right before main returns. Convert the result to decimal for the final flag.

Debugger practiceDownload debugger0_b

Make the binary executable and load it into gdb with layout asm so you can watch instructions in context.

Place a breakpoint after the final arithmetic instruction (main+59) to read registers at the exact moment main is about to return.

bash
wget https://artifacts.picoctf.net/c/520/debugger0_b
bash
chmod +x debugger0_b
bash
gdb --args ./debugger0_b

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Break after the math
    Observation
    The binary ships without debug info or source, so there are no line numbers to break on. Function symbols like main do survive, so find the last instruction writing EAX in the disassembly and break at that offset from main.
    Inside gdb, set b *(main+59). This lands execution immediately after the last modification of EAX so the register holds its final value.
    bash
    b *(main+59)
    bash
    run
    What didn't work first

    Tried: Setting a breakpoint with b main or a source line number like b 59 instead of the offset-based b *(main+59)

    b main resolves, but it stops at the prologue, long before the arithmetic that sets EAX. b 59 needs debug line info the binary does not carry, so it errors out or lands somewhere unrelated. The asterisk-offset form breaks on a raw instruction address, which is the only reliable anchor here.

    Tried: Breaking at b *main to stop at the very start of main and then stepping through instructions with ni until EAX looks interesting

    Stepping through 60 bytes of arithmetic one instruction at a time is slow and easy to overshoot, landing at the return where EAX may already be clobbered. Breaking at the right offset lands you immediately after the last arithmetic operation, with an unambiguous snapshot.

    Learn more

    Breakpoints are the primary mechanism for pausing execution at a specific point in GDB. The syntax b *(main+59) sets a breakpoint at the memory address that is 59 bytes into the main function's machine code. The asterisk dereferences the address expression - without it, GDB would try to find a source line rather than an instruction offset.

    Using offset-based breakpoints (rather than source line numbers) is necessary when you do not have debug symbols or source code - exactly the situation in CTF binary challenges. The offset +59 must be determined from the disassembly: look for the last instruction that writes to EAX, count its byte offset from the start of main, and break immediately after it.

    GDB breakpoints are powerful because they support conditions (break main if i == 5), ignore counts (ignore 1 3 skips the first three hits), and commands (commands 1 ... end runs GDB commands automatically when the breakpoint fires). These features let you automate complex debugging scenarios - essential when analyzing loops or deeply nested call chains in real targets.

  2. Step 2Print EAX and convert
    Observation
    The answer is the EAX value just before main returns, in decimal. Print the register at the breakpoint and convert.
    Once the breakpoint hits, run print $eax to capture the register contents. Convert that hexadecimal (if needed) into decimal and wrap it with picoCTF{...}.
    bash
    print $eax

    Expected output

    picoCTF{<eax_decimal>}
    What didn't work first

    Tried: Running p/x $eax to print EAX in hexadecimal and submitting the hex value directly as the flag

    The challenge explicitly asks for the decimal representation. p/x shows a hex string prefixed with 0x, which is a different number when pasted as-is. Running plain p $eax (or p/d $eax) gives the decimal integer that must be wrapped in picoCTF{...}.

    Tried: Using info registers to read EAX before setting a breakpoint and running the binary, reading whatever value appears in the register listing

    At startup, the register view shows uninitialized state left over from the shell, not from the binary. The value you want exists only after execution reaches the breakpoint.

    Learn more

    In GDB, registers are accessed with a $ prefix: $eax, $rbp, $rip, and so on. The print command (alias p) evaluates and displays an expression. By default it shows the result in decimal, but you can specify a format: p/x $eax for hex, p/t $eax for binary, p/c $eax for the character representation.

    The info registers command (alias i r) dumps all general-purpose registers at once, which is useful when you are not sure which register holds the value you need. For x86-64, the full set includes RAX/RBX/RCX/RDX/RSI/RDI/RSP/RBP and R8-R15, plus the instruction pointer RIP and the flags register EFLAGS.

    After capturing EAX's value, the conversion to decimal is the same as in earlier challenges. The consistent workflow - set breakpoint, run, print register, convert - is deliberately repetitive across the GDB Baby Step series. Repetition builds muscle memory, so that these actions become automatic before the challenges increase in complexity.

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{<eax_decimal>}

Your decimal value depends on the constant embedded in debugger0_b-replace <eax_decimal> with the number you observe in gdb.

Key takeaway

A breakpoint pauses execution at a precise instruction and hands you the register and memory state at that moment, with no stepping through everything before it. Offset-based breakpoints matter when there is no source and no symbols, because the offset comes from the disassembly rather than from line numbers. That is the entry point to dynamic analysis, and the same move intercepts license-check comparisons, bypasses authentication gates, and catches the instant a vulnerability fires.

Related reading

Useful tools for Reverse Engineering

Where to go next