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 1
Understand the structure of both binariesObservationI noticed the challenge provided two versions of the same binary (breadth.v1 and breadth.v2) with the hint 'Go wide, not deep,' which suggested the key difference between them was buried across an enormous function landscape and that understanding the scale of the problem was necessary before choosing a search strategy.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 contain thousands of decoy picoCTF strings that are identical between v1 and v2, so a text diff of grep output produces no meaningful signal. The real flag lives at a byte offset where the string literal itself changed, which only a binary-level diff can isolate. Text diffing string output also loses position information needed to navigate to the function.
Tried: Run each binary and capture its stdout to find which flag string it prints.
The binaries are not simple flag-printing programs - they contain tens of thousands of functions that never get called from main, so executing them produces no useful output. The flag is stored as a string literal in a non-called function, meaning runtime execution cannot surface it. Static analysis (diffing, disassembly) is required.
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 2
Diff the two binaries to find the changed offsetObservationI noticed that two binary versions were provided and that only one function differed between them, which suggested using a byte-level binary diffing tool like radiff2 or cmp -bl to pinpoint the exact file offset of the changed function rather than inspecting thousands of functions manually.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
difftreats binary files as opaque blobs and reports only 'Binary files differ' without any offset or byte details. It has no ability to show which bytes changed or at what positions. You needcmp -blfor POSIX byte-level output orradiff2for a structured binary diff with offsets.Tried: Use
radiff2 -c breadth.v1 breadth.v2to count differences and assume only one byte changed.radiff2 -coutputs a count of differing bytes but not their locations, so you still cannot navigate to the changed function. The-uflag (unified diff) is needed to actually see which offsets differ and what the new bytes are. Without the offsets, you are back to manually searching tens of thousands of functions.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 3
Open the changed function in Ghidra and read the real flagObservationI noticed radiff2 reported a specific byte offset where breadth.v1 and breadth.v2 differ, which suggested navigating directly to that offset in Ghidra or radare2 to read the string literal in the one function that was modified to contain the real flag.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>.The file offset reported by
cmp -blis relative to the start of the file on disk, not the virtual address the binary is loaded at in memory. Radare2 maps the binary into a virtual address space, so the correct seek address is the file offset plus the segment load base (typically 0x400000 for non-PIE or shown byiSin r2). Seeking to the raw file offset lands in the wrong location and disassembles garbage bytes.Tried: Read the flag from breadth.v1 at the differing offset instead of breadth.v2.
The whole point of the diff is that v1 contains the decoy (wrong) flag string at that offset and v2 contains the real flag. Opening v1 at the same offset shows the fake picoCTF string that was replaced, not the answer. Always read the string from breadth.v2, the corrected version.
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.