Skip to main content

breadth picoMini by redpwn Solution

Identify meaningful differences between two versions of a binary to locate the real flag among many decoys.

Published: April 2, 2026Updated: August 25, 2026

Description

Go wide, not deep.

Download both binary versions from the challenge page.

bash
wget <challenge_url>/breadth.v1
bash
wget <challenge_url>/breadth.v2

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 of both binaries
    Observation
    Two versions of the same binary arrive with a hint to go wide rather than deep, so the difference is buried somewhere in an enormous function landscape. Get a sense of the scale before choosing how to search.
    Run file on both binaries to confirm they are ELF executables. Then use strings or open one in Ghidra to observe that the binary has an enormous number of functions (tens of thousands), nearly all of which contain decoy flag strings. The real flag is hidden among them, but there is one subtle difference between the two versions.
    bash
    file breadth.v1 breadth.v2
    bash
    strings breadth.v1 | grep picoCTF | head -20

    Expected output

    breadth.v1: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped
    breadth.v2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped
    What didn't work first

    Tried: Grep all picoCTF strings from both binaries and diff the text output to find the real flag.

    Both binaries hold thousands of decoy flag strings, identical between versions, so a text diff of grep output shows nothing. The real flag sits where a string literal changed, which only a byte-level diff isolates, and text diffing throws away the position you need to navigate to.

    Tried: Run each binary and capture its stdout to find which flag string it prints.

    These are not flag-printing programs. They hold tens of thousands of functions that main never calls, so running them produces nothing useful. The flag is a string literal inside a function that never executes, which puts it out of reach of any runtime approach.

    Learn more

    The challenge ships two versions of the same binary: breadth.v1 (the original) and breadth.v2(a corrected version). The title "breadth" is a hint about the technique: rather than going deep into any single function, you need to sweep broadly across the entire binary to find what changed.

    Each binary contains tens of thousands of randomly named functions, each printing a fake picoCTF string. Trying to read every function in Ghidra would take forever. The key insight is that only one function differs between v1 and v2 - and that function holds the real flag.

  2. Step 2Diff the two binaries to find the changed offset
    Observation
    Only one function differs between the two versions. A byte-level diff, with radiff2 or cmp -bl, gives you the exact offset instead of thousands of functions to inspect.
    Use radiff2 from the radare2 suite (or cmp -bl) to compare breadth.v1 and breadth.v2 byte by byte. The output will report the file offsets where the two binaries differ. Take note of those offsets - they point directly to the one function that was changed between versions.
    bash
    # Option A: radiff2 (radare2 suite)
    bash
    radiff2 -u breadth.v1 breadth.v2
    bash
    # Option B: cmp - prints offset and byte values of every difference
    bash
    cmp -bl breadth.v1 breadth.v2
    What didn't work first

    Tried: Use diff breadth.v1 breadth.v2 (plain text diff) to find the changed location.

    Plain diff treats a binary as an opaque blob and says only that the files differ, with no offsets and no bytes. cmp -bl gives byte-level output, and radiff2 gives a structured diff with offsets.

    Tried: Use radiff2 -c breadth.v1 breadth.v2 to count differences and assume only one byte changed.

    The count flag tells you how many bytes differ and not where, which leaves you no way to navigate. The unified diff shows the offsets and the new bytes. Without those you are back to searching tens of thousands of functions by hand.

    Learn more

    Binary diffing is the practice of comparing two versions of a compiled program to find exactly what changed. Security researchers use it to analyze patches (finding the bug a vendor fixed) and CTF authors use it to hide flags - put a fake flag in v1, replace it with the real flag in v2, and the solver has to find the one function that changed.

    radiff2 -u outputs a unified diff of the raw bytes, highlighting removed (red) and added (green) bytes at each differing offset. cmp -bl is a POSIX alternative that prints the byte offset, the old byte value, and the new byte value for every differing position.

    Either tool will give you a file offset into the binary. Convert that offset to a virtual address by accounting for the segment base, or just note the raw offset and use Ghidra's "Go To" feature to jump directly to it.

  3. Step 3Open the changed function in Ghidra and read the real flag
    Observation
    radiff2 names the offset where the two differ. Navigate straight there in Ghidra or radare2 and read the string literal in the one function that changed.
    Import both binaries into Ghidra (or open just one - the offset is the same). In the CodeBrowser, use the "Go To" dialog (press G) and enter the offset or virtual address reported by radiff2. Ghidra will navigate to the changed function. Look at the string it prints or returns - that is the real flag.
    bash
    # If you prefer a command-line approach, use radare2 directly
    bash
    # Replace 0x9504c with the actual offset from your diff output
    bash
    r2 breadth.v2
    bash
    # Inside r2: seek to the differing address
    bash
    s 0x9504c
    bash
    # Disassemble the function
    bash
    pdf
    What didn't work first

    Tried: Use the raw file offset from cmp -bl directly as the seek address in radare2 with s <offset>.

    cmp reports an offset from the start of the file on disk, not a virtual address. radare2 maps the binary into an address space, so seek to the file offset plus the segment load base, which its section listing shows. Seek to the raw offset and you disassemble garbage.

    Tried: Read the flag from breadth.v1 at the differing offset instead of breadth.v2.

    The whole point of the diff is that one version holds a decoy at that offset and the other holds the real flag. Open the wrong one and you read the string that was replaced.

    Learn more

    Every function in the binary loads a string that looks like picoCTF{...} and returns it. All but one of these strings are decoys. The function at the differing offset is the one the challenge authors modified - in v1 it holds a wrong string, in v2 it holds the correct flag.

    In Ghidra's decompiler pane you will see the string literal inline. In radare2 you can use pdf (print disassembly of function) and look for the LEA or MOV instruction that loads the flag string address, then use ps @ <address> to print the string at that address.

    This technique - download a patched binary and diff it against the original to find the one changed location - is a foundational skill in vulnerability research and is exactly what security researchers do when a vendor ships a silent security fix without a CVE.

Interactive tools
  • 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.
  • 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{VnDB2LUf...}

The flag is the real picoCTF string inside the one function that differs between breadth.v1 and breadth.v2. Use radiff2 or cmp -bl to find the offset, then read the string in Ghidra or radare2.

Key takeaway

Binary diffing compares two compiled executables byte by byte and isolates exactly what changed, turning a search across thousands of functions into a single offset. Researchers point it at silently patched software to find the vulnerability a vendor fixed without publishing an advisory, then work backwards from the patch to an exploit. In a CTF, two versions of a binary is the author telling you to diff them.

Related reading

Useful tools for Reverse Engineering

Where to go next