Description
Go wide, not deep.
Setup
Download both binary versions from the challenge page.
wget <challenge_url>/breadth.v1wget <challenge_url>/breadth.v2Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Understand the structure of both binaries
ObservationTwo 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.Runfileon both binaries to confirm they are ELF executables. Then usestringsor 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.bashfile breadth.v1 breadth.v2bashstrings breadth.v1 | grep picoCTF | head -20Expected 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) andbreadth.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
picoCTFstring. 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.Step 2Diff the two binaries to find the changed offset
ObservationOnly 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.Useradiff2from the radare2 suite (orcmp -bl) to comparebreadth.v1andbreadth.v2byte 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)bashradiff2 -u breadth.v1 breadth.v2bash# Option B: cmp - prints offset and byte values of every differencebashcmp -bl breadth.v1 breadth.v2What 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.v2to 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 -uoutputs a unified diff of the raw bytes, highlighting removed (red) and added (green) bytes at each differing offset.cmp -blis 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.
Step 3Open the changed function in Ghidra and read the real flag
Observationradiff2 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 directlybash# Replace 0x9504c with the actual offset from your diff outputbashr2 breadth.v2bash# Inside r2: seek to the differing addressbashs 0x9504cbash# Disassemble the functionbashpdfWhat 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 theLEAorMOVinstruction that loads the flag string address, then useps @ <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.