Challenge file
The download is a single 64-bit Linux ELF and, unlike many rev tasks, it is not stripped.
That does not make the code readable, but it does make the important pieces easier to spot.
strings immediately shows three suspicious blobs named GAME,
KRILLYOUSELF, and TheOnePieceIsReal, plus a lot of huge function names.
main is short: it reads one string with scanf("%s"), passes it to
TheStrongDecideTheNatureOfSin, and prints yipee! only if that function returns nonzero.
So the whole check lives in that one giant function and the helpers it calls.
How the input is packed
The first useful helper is AGoodPartyRequiresABloodSacrifice. It allocates a
64 x 48 byte board and converts the user input into bitplanes. For the first
64 input bytes, bit 0 goes into row 0, bit 1 into row 1,
and so on up to row 7. The remaining rows start as zero.
In other words, column x of the first eight rows stores the byte at position x
in little-endian bit order. That matters later because once we reverse the automaton, decoding the
original input is just a matter of reassembling those eight rows back into bytes.
The worker threads
TheStrongDecideTheNatureOfSin allocates one cell structure for every board position,
wires cells together with mailbox objects, and then spawns one thread per cell using
thrd_create with BlueHouseGTechExec as the thread entry.
Statically the graph builder is ugly, so I took a quick dynamic shortcut: I ran the binary in a Linux
container with a tiny LD_PRELOAD shim that logged which cells were connected to which mailboxes.
For a center cell, the incoming edges were:
(-2,-2) (-2,0) (-2,2)
( 0,-2) ( 0,2)
( 2,-2) ( 2,0) ( 2,2)
So this is not normal Life on the immediate Moore neighborhood. Each cell samples the eight cells two steps away.
The update rule
Inside BlueHouseGTechExec, each worker gathers those eight neighbor bits, prepends the current cell bit,
and uses the resulting 9-bit index to read the GAME lookup table. The binary actually multiplies the index by two,
so only every other byte of GAME is meaningful.
If you tabulate the live outputs, the rule collapses to standard Conway Life semantics:
- dead cell with exactly 3 live neighbors becomes alive
- live cell survives with 2 or 3 live neighbors
The twist is that the binary is second-order. The next state is the Life result xor the previous state:
board[t + 1] = life(board[t]) ^ board[t - 1]
That recurrence is reversible. If you know two consecutive boards, you can recover the older one with
board[t - 1] = life(board[t]) ^ board[t + 1].
The target boards are embedded in the binary
During initialization, each cell stores two target bits taken from the large ASCII-art blobs
KRILLYOUSELF and TheOnePieceIsReal. The worker compares its final two states against those targets before returning.
The end condition is therefore:
board[0x5379] == KRILLYOUSELF
board[0x537A] == TheOnePieceIsReal
Once that clicked, the solve stopped being a simulation problem and became a reversal problem. We already know the last two boards,
because the binary ships them in plaintext as #/space art.
Running the automaton backward
I parsed both ASCII-art boards into 64 x 48 bitmaps and repeatedly applied the reverse step:
older = life(previous) ^ current
current, previous = previous, older
Repeating that 0x537A - 1 times lands on the initial board built from the flag. At that point, the picture is exactly what
AGoodPartyRequiresABloodSacrifice would have produced from the user input: the first eight rows contain packed bytes and everything below is zero.
Decoding the flag
Reassembling the first eight rows column by column gives the original input bytes directly. The recovered byte string starts with a valid flag and then zero padding:
ping_500hoursofmindglorbingaction\x00\x00\x00...
Flag: ping_500hoursofmindglorbingaction
Minimal solver
The final solver only needs the binary itself. It extracts the rule table and the two ASCII-art boards from .rodata, runs the reverse recurrence,
and decodes the packed bytes from the reconstructed initial state.
rule = GAME[::2]
previous = KRILLYOUSELF
current = TheOnePieceIsReal
for _ in range(0x537A - 1):
older = life(previous) ^ current
current, previous = previous, older
flag = unpack_first_8_rows(previous)
Downloads
solve.py
Standalone solver that extracts the lookup table and both target boards from the provided ELF, reverses the second-order automaton, and reconstructs the original input bytes.
Download solver