FactCheck picoCTF 2024 Solution

Published: April 3, 2024

Description

This binary is putting together some important piece of information... Can you uncover that information? Examine this file. Do you understand its inner workings?

Dynamic reversing

Download the binary, make it executable, and load it in Ghidra to understand the overall structure.

Have GDB available for dynamic analysis to read the flag at runtime.

bash
wget https://artifacts.picoctf.net/c_titan/187/bin && \
chmod +x bin

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Understand the structure in Ghidra
    Observation
    I noticed the binary assembles data programmatically rather than storing a literal string, which suggested static analysis in Ghidra would reveal the construction pattern and pinpoint the exact instruction where the flag is complete before any destructors run.
    In Ghidra, find main and look at the decompiled output. It performs many C++ string concatenations (the += operator calls) to build the flag character by character. Near the end of main, there is one final append of a closing curly brace, followed immediately by the first destructor call. The string's value is complete at that point and lives in the object returned in RAX.
    Learn more

    Ghidra is a free, open-source reverse engineering framework from the NSA. Its decompiler shows C++ string concatenation as operator+= calls, which is verbose but identifies where characters are being assembled. The key forensic marker here is the append of the closing } character: after that call the flag string is complete, and the first destructor call immediately follows. That boundary is your breakpoint target in GDB.

    The binary address for main starts around 0x001011... in Ghidra. ASLR changes the high bytes at runtime, but the last four hex digits are stable; use those to find the matching instruction in GDB. Look for the address just after the final += and just before the first ~basic_string destructor call.

  2. Step 2
    Break in GDB and read RAX
    Observation
    I noticed that Ghidra showed the flag string is fully assembled in RAX just after the final operator+= call and right before the first destructor, which suggested setting a GDB breakpoint at that exact address and reading RAX as a C-string to capture the complete flag at runtime.
    Run the binary in GDB. Set a breakpoint at the address of the instruction immediately after the last operator+= (the closing-brace append) and before the destructor call. When execution stops, examine RAX as a pointer to a C++ string; the flag bytes are readable there.
    bash
    gdb ./bin
    bash
    (gdb) break *0x<last_four_from_ghidra>
    bash
    (gdb) run
    bash
    (gdb) x/s $rax

    Expected output

    picoCTF{...}

    x/s $rax prints RAX as a null-terminated ASCII string. You should see the full picoCTF{...}. If you see a pointer address rather than the string, try x/s *$rax to dereference one level, or examine the object members with p (char *)$rax.

    What didn't work first

    Tried: Set a breakpoint on main and immediately run x/s $rax without stepping to the right instruction.

    Stopping at the entry of main means the flag string has not been assembled yet - RAX holds the return value of whatever setup function just ran, not the completed C++ string. You will see garbage bytes or an unrelated pointer. You need to step forward to the instruction just after the final operator+= (the closing curly-brace append) before RAX contains the fully built flag.

    Tried: Use strings on the binary to extract the flag directly without running GDB.

    The flag is never stored as a literal string in the binary. It is built character by character at runtime via C++ string concatenation. The strings utility only finds null-terminated sequences already present in the file on disk, so it will not reveal a value that is constructed dynamically in memory. GDB is required to observe the assembled value after execution reaches the final append.

    Learn more

    In the x86-64 calling convention, RAX holds the return value of the most recently called function. After the final operator+= call, the C++ string object (or a pointer to it) lives in RAX. x/s in GDB interprets the address as a pointer to a C-style string and prints characters until the null terminator.

    The flag bytes are ASCII text stored contiguously in heap memory allocated by the C++ string's internal buffer. Even without knowing the exact memory address ahead of time, reading RAX immediately after the last string operation reliably lands on the completed flag.

    Dynamic analysis (running the binary with a debugger) complements static analysis (reading disassembly). Static analysis maps the structure; dynamic analysis reads runtime values. For binaries that construct data programmatically, dynamic analysis is usually the fastest path to the answer.

Interactive tools
  • 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.
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • File Magic IdentifierIdentify file types from magic numbers. Paste hex bytes or drop a file to detect PNG, JPEG, ZIP, PDF, ELF, PCAP, SQLite, and dozens of other formats.

Flag

Reveal flag

picoCTF{wELF_d0N3_mate_...}

Per-instance binary (artifact URL includes instance ID c_titan/187). The prefix picoCTF{wELF_d0N3_mate_ is consistent but the 8-character hex suffix varies per binary. Multiple sources confirmed: e9da2c0e, 97750d5f, fd65ee4e, 2394045a all seen across different participants.

Key takeaway

Static analysis tools like Ghidra map the structure of a binary, revealing control flow and how data is assembled, while dynamic analysis tools like GDB let you read actual values in registers and memory at runtime. Combining both is the standard approach to reverse engineering: static analysis identifies where to set breakpoints, and dynamic analysis confirms what the program produces. This workflow transfers directly to malware analysis, firmware extraction, and license-check bypasses across any compiled language and architecture.

Related reading

Want more picoCTF 2024 writeups?

Useful tools for Reverse Engineering

What to try next