Challenge file
The download is a single stripped 64-bit ELF. The import table is tiny:
printf, scanf, strlen, and puts.
That already suggests the whole check is self-contained.
Running strings does not leak much beyond the obvious user-facing
text:
Wrong flag!
Correct flag!
Enter the flag:
On macOS I could not execute it directly, so I used static analysis in radare2 and only verified the recovered flag later inside an amd64 Linux container.
Finding the real check
The first important function is the final comparator at 0x403544.
It loops over a generated table at 0x41a480 and compares it against a
constant table at 0x419460:
generated = *(0x41a480 + index * 4)
expected = *(0x419460 + index * 4)
if (generated != expected) {
puts("Wrong flag!")
crash()
}
puts("Correct flag!")
crash()
The crash is deliberate. The binary prints the verdict and then immediately hits a divide-by-zero, which is why it helps to run it unbuffered when verifying a candidate flag.
The helper table
Near the end of main there is a 128-entry function pointer table at
0x419060. For normal ASCII input, each character is used as an index
into that table and the pointed-to helper gets called.
Each helper is almost identical. The only real difference is one immediate value,
which is why the compiled main explodes into a huge wall of nearly
identical blocks. That was enough to show the generated words were not a cryptographic
hash or anything similarly annoying: they were just structured 32-bit values built from
constants, the current position, and a tiny bit of state.
The easiest way to solve the challenge is not to perfectly model that whole state machine. It is to dump the final table that reaches the comparator and recover the effective recurrence from there.
The table is interleaved
At first I tried to decode the constant table as if every word came from those
helpers. That fails immediately. The reason is that main also writes
its own words into the same output buffer between helper calls.
The expected table alternates between two generators: even indices are length-only
values written by main, odd indices are character-dependent values
written by the helper functions.
The even entries follow a very simple pattern:
expected[2 * i] = 0xDEADBEEF + 726 * (length + 1 - i) # 0 <= i < length
So the very first entry gives the flag length immediately:
length = ((expected[0] - 0xDEADBEEF) / 726) - 1
For this binary that works out to 51 characters.
Recovering the flag
Once you ignore the even indices, the odd indices fall out directly from the
final generator state that reaches the comparator. For the i-th
character:
state = 0x539 if i == 0 else i - 1
flag[i] = expected[2 * i + 1]
^ 0xDEADBEEF
^ (i * 0x1337)
^ (state * 0xABCD)
Running that over the first 51 odd entries decodes clean ASCII and
immediately produces the whole flag. So even though the control flow is intentionally
ugly, the finished comparison table is straightforward to invert.
Minimal solver
The challenge-specific solver I saved with the writeup just reads the constant table from the ELF and applies those two observations:
length = ((table[0] - 0xDEADBEEF) // 726) - 1
prev = 0x539
flag = []
for i in range(length):
word = table[2 * i + 1]
ch = word ^ 0xDEADBEEF ^ (i * 0x1337) ^ (prev * 0xABCD)
flag.append(chr(ch))
prev = ch
print("".join(flag))
Verification
I verified the decoded flag by running the binary in a Linux container with stdout unbuffered:
printf '%s\n' 'ping{n0_c0mp1l3r_w45_hur7_dur1ng_m4k1ng_7h15_ch4ll}' \
| stdbuf -o0 ./chall
Enter the flag: Correct flag!
After printing that line the program crashes on purpose with a floating point exception, which matches the control flow in the verdict function.
Downloads
recover_flag.py
Standalone script that extracts the comparison table from the provided ELF, checks the length-only words, and decodes the final odd-slot recurrence.
Download solverFlag
ping{n0_c0mp1l3r_w45_hur7_dur1ng_m4k1ng_7h15_ch4ll}