Skip to main content

FactCheck picoCTF 2024 Solution

Trace the logic of a compiled binary to determine which values satisfy its conditions and form the flag.

Published: April 3, 2024Updated: August 25, 2026

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 1Understand the structure in Ghidra
    Observation
    The binary builds its data programmatically instead of storing a literal. Ghidra will show the construction pattern and the exact instruction where the string is complete, just before the 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.

    Ghidra shows main at an image-base address such as 0x001011..., but the binary is position independent, so that number is not where the code lands at runtime. Note the offset of the target instruction from the start of main instead (Ghidra's address minus main's address). Breaking on main+offset in GDB lets the debugger apply the load bias for you. The instruction you want is the one just after the final += and just before the first ~basic_string destructor call.

  2. Step 2Break in GDB and read RAX
    Observation
    Ghidra shows the flag fully assembled in RAX right after the last operator+= and before the first destructor. Break there in GDB and read RAX as a C string.
    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) disassemble main
    bash
    (gdb) break *(main+<offset_from_ghidra>)
    bash
    (gdb) run
    bash
    (gdb) x/s *(char **)$rax

    Expected output

    picoCTF{...}

    RAX holds the address of the std::string object, and a libstdc++ string keeps a pointer to its character buffer in its first eight bytes, so x/s *(char **)$rax is the dereference that prints the text. You should see the full picoCTF{...}. Plain x/s $rax works only when RAX already points at the characters; if it prints garbage, add the extra dereference, or use p *(char **)$rax to see the buffer address itself.

    What didn't work first

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

    At the entry to main the string does not exist yet, and RAX just holds whatever the last setup call returned, so you see garbage or an unrelated pointer. Step forward to just after the final append, the one adding the closing brace, before reading it.

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

    The flag is never a literal in the binary; it is concatenated character by character at runtime. strings only finds sequences already on disk, so it cannot show a value built in memory. GDB is what reads it after the final append.

    Learn more

    In the x86-64 System V calling convention, RAX holds the return value of the most recently called function. operator+= returns a reference to the string it appended to, so after the final append RAX is the address of the completed string object. x/s in GDB treats the value it is given as a pointer to a C-style string and prints bytes until the null terminator, which is why the buffer pointer inside the object is the thing to hand it.

    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

Ghidra maps a binary's structure, showing control flow and how data gets assembled; GDB reads the actual values in registers and memory as it runs. Reverse engineering usually needs both: the static pass tells you where to break, the dynamic pass tells you what the program produced. The same workflow carries into malware analysis, firmware extraction, and license-check work on any architecture.

Related reading

Useful tools for Reverse Engineering

Where to go next