Challenge
We're given two files, file1.txt and file2.txt, each containing what looks like
a long, random blob of letters, digits, and symbols — one character per line. At a
glance the two files look identical. The hint: someone "updated" one of the files
to smuggle a message inside it.
$ ls
file1.txt file2.txt
Initial Recon
Pasting the two files side by side line-by-line makes it clear they're almost
the same, but not quite:
paste file1.txt file2.txt | nl
Most lines match. A handful don't. That's the whole challenge, really — find the
lines that differ, and figure out which differences actually carry information
versus which are just noise.
Step 1 — Diff the files properly
Rather than eyeball a few hundred lines, script it:
f1 = open("file1.txt").read().splitlines()
f2 = open("file2.txt").read().splitlines()
print(len(f1), len(f2))
for i, (a, b) in enumerate(zip(f1, f2), 1):
if a != b:
print(i, a, b)
Gotcha #1: len(f1) and len(f2) come out different — 350 vs 351 lines.
The two files aren't the same length, which means a naive zip() silently
truncates to the shorter file and — worse — anything after a genuine
insertion/deletion point will be misaligned by one position, making every
comparison from that point on unreliable.
The fix is to use a real sequence-alignment diff instead of a positional
zip(), so insertions and deletions are handled correctly:
import difflib
f1 = "".join(open("file1.txt").read().splitlines())
f2 = "".join(open("file2.txt").read().splitlines())
sm = difflib.SequenceMatcher(None, f1, f2, autojunk=False)
for tag, i1, i2, j1, j2 in sm.get_opcodes():
if tag != "equal":
print(tag, i1, i2, j1, j2, f1[i1:i2], f2[j1:j2])
This surfaces 79 total differing positions, made up of:
- Plain 1-for-1 character replacements (most of them)
- A couple of true insertions/deletions (e.g. an extra
mappears in file2 that file1 doesn't have; file1 has an extraEandXthat file2 doesn't)
Step 2 — Separate signal from noise
Looking at the 79 differences, most of them turn out to be case-only flips
of the same letter — e↔E, w↔W, l↔L, and so on. These contribute no
new information; they're decoy noise to make the diff look messier than it is.
The interesting subset is the differences that are not explainable as a
same-letter case change — a digit turning into a letter, a letter turning into
a bracket or underscore, or one letter turning into a genuinely different
letter:
interesting = []
for tag, i1, i2, j1, j2 in sm.get_opcodes():
if tag == "equal":
continue
a, b = f1[i1:i2], f2[j1:j2]
if len(a) == len(b):
for k, (x, y) in enumerate(zip(a, b)):
if x.lower() != y.lower():
interesting.append((i1 + k, x, y))
else:
interesting.append((i1, a, b))
Filtering out the case-noise leaves 29 positions where file2 introduces a
character that has no case-equivalent relationship to file1's character at
that spot. Reading those characters, in position order, off file2:
b r o n c o { y @ y y y _ Y 0 u _ f 0 u n d _ m 3 ! ! }
Step 3 — Read the message
Concatenated:
bronco{y@yyy_Y0u_f0und_m3!!}
That's the flag. The challenge author built two copies of a long random
string, then:
- Randomly flipped the case of a bunch of characters throughout — pure
visual noise, meant to make a naive
difflook overwhelming. - Substituted in the actual flag characters, one at a time, scattered through the string at positions that are not explainable as case flips.
The trick to solving it is recognizing that "the diff has 79 changes" isn't
the same as "there are 79 bits of message" — most of that is chaff, and the
job is filtering it out.
Flag
bronco{y@yyy_Y0u_f0und_m3!!}
Takeaways
- When two files "look basically the same," always check their length before diffing character-by-character — a single insertion/deletion will silently desync a positional comparison and make everything after it look wrong.
-
difflib.SequenceMatcher(ordiff/git diff --word-diff) handles insertions and deletions correctly; a rawzip()loop doesn't. - Not every difference in a diff is meaningful — case-only noise is a common and cheap way for challenge authors to bury a smaller real signal inside a larger, noisier one. Always ask "is this difference explainable by a boring transformation (case, whitespace, encoding)?" before assuming it's part of the message.
Top comments (0)