The challenge is a stripped binary implementing a game of tetris in the command line. Reversing the binary reveals that the rules deviate from “guideline tetris” in several ways:
Notably, the game’s implementation of piece rotations does follow guideline tetris, including mechanics such as kicks. There is also no 180-degree rotation option unlike other popular tetris clones.
The board is stored as a bitmask uint8_t board[48]
. A 1
bit means the cell
is filled while a 0
bit means the cell is empty. board[0]
respresents the
left half of the bottom row with LSB on the left side of the board, board[1]
is the right half of the bottom row, board[2]
is the left half of the second
row and so on.
The game ends once the user locks a piece in and the next piece that would have spawned in collides with any existing piece. After this, the binary executes the board as shellcode. We know some information about registers when this happens:
r11 = r15 = <board[] on heap>
r13 = "%02hhx" @ 0x302c
r14 = ".o" @ 0x3033
rdi = <jump_table> @ 0x3044
if a piece was recently rotatedrsp[0] = <main+0x529> @ 0x1709 (return address)
There a win()
in the binary, so jumping to <win+0xe> @ 0x1a0e
is enough to
spawn a shell. A typical shellcode will involve selecting one of the registers
above, deriving the address to win()
and finally jumping to/calling win()
.
The ultimate goal is to read the flag file off the local filestystem.
In an ideal scenario, someone’s recipe for a solution might look something like this:
win()
While discussing solutions after the game ended, it turns out this ideal scenario was far from reality for the solutions that FMC and friends came up with. People would forget one or two steps and overcompensate in some other part of their exploit, usually ending up in playing more tetris than they need to.
Here’s a sampler of the exploits that people (over)cooked:
0: 49 81 ee 25 16 00 00 sub r14,0x1625
7: 41 ff e6 jmp r14
For FMC’s own solution, while our cracked pwn players were bashing heads on the other challenges, sleep-deprived zzoru and myself looked at 64-bit math and went “heh this is doable”. Unfortunately, to craft this shellcode we needed to build two massive overhangs in the 4th and 5th rows over the course of an extremely long game of tetris. Solving for the moves by hand order took us over 3h, and was the slowest exploit by far of the ones we saw.
On the bright side, it’s not like the two of us had other challenges to work on at the time 🥴.
0: 58 pop rax
1: 66 2d fb fc sub ax,0xfcfb
5: 50 push rax
6: ff e0 jmp rax
I did start with 69 and proceeded to 70 because 69 was bad
tchen from [:] (the new merger on the block) also had a bit too much free time on his hands and decided that it would be fun to play the game honestly without any RNG manipulation. Impressively, he still ended up with a shorter exploit than others with similar shellcode.
0: 4c 87 fe xchg rsi,r15
3: b2 ff mov dl,0xff
5: 31 ff xor edi,edi
7: 0f 05 syscall
Honestly 4 of us playing tetris to race who gets it first was the best part of this […] ctf
There was a [win()]?
Wait, did i actually not notice i already had it and spent like 100 extra moves looking for a piece i didnt even need
zeski from organizers gets extra style points
for reading the wrong question and still getting the right answer. Instead of
jumping to win()
, he triggers a read()
into board memory which lets him send
his own execve("/bin/sh")
as a second stage via standard input. Though the
swiss-cheese garbage pile on the left takes away a tiny bit from the elegance of
it all.
0: 66 81 ef 36 16 sub di,0x1636
5: 90 nop
6: ff e7 jmp rdi
This solution from soon_haari of Cold Fusion unfortunately wasn’t the one that flagged for his team because he was 2 minutes late. However, the remarkably clean garbage pile with its crazy overhangs are [*chef’s kiss].
0: 66 81 ef 36 16 sub di,0x1636
5: 57 push rdi
6: c3 ret
And finally, this solution coming from T1d and RiK of RePokemonedCollections is the most “platonic ideal” of the ones we saw. The exploit itself was the shortest by a long short, taking only 140 moves, 27 pieces and 2 line clears to get to a shell.