DEF CON Quantum CTF 2026: the qtris Walkthrough
Two years after my first Quantum Village run, I came back for the DEF CON 34 edition of the World’s First Quantum CTF, again playing solo as I did in 2024. The CTF runs the usual broad spread of categories, but one kept me busy far longer than its point value deserved: qtris?, a set of four challenges all built on a single screenshot of a “Quantum Tetris” game.
I want to walk through the whole set. It is a working example of how quantum ideas (blind and measurement-based computation, the no-cloning theorem, the Clifford versus magic distinction) get encoded into a puzzle you attack with tools every hacker already owns: PRNG seed recovery, a one-time pad, and a feedback cipher. The hardest one, No-Cloning Zone, was a 0-solve during the event; I was the only player on the entire CTF to crack it. The last one, You already solved it!, was also a 0-solve, and I will be honest about where it beat me, and about the design I uncovered along the way.
Everything in the category shares two files: an image ok.png (the game screenshot) and a tiny lcg.py, which was provided as a hint.

Three things live in that screenshot:
- The board (left): coloured blocks, each optionally carrying a small white dot, split into three stacked sub-boards separated by grey rows.
- The spawn log (right): “7-bag randomiser, oldest first”, the sequence of pieces the game generated.
- The HUD (bottom):
SCORE 29592,LEVEL 07,LINES 003,PIECES 040.
lcg.py is the game’s “randomness”:
A = 1103515245
C = 12345
MASK = 0x7FFFFFFF
class LCG:
def __init__(self, seed):
self.state = seed & MASK
def next_state(self):
self.state = (A * self.state + C) & MASK
return self.state
def rand(self):
return self.next_state()
If you have cracked a “roll-your-own random” before, the constants 1103515245 / 12345 are a fingerprint: this is the classic C rand() Linear Congruential Generator. It is fully deterministic. The whole “randomness” of the game is one 32-bit seed.
qtris? #97: Maybe Too Easy
Walkthrough
Just the image, no prompt, no hint. The title is the hint. Run strings or exiftool on the PNG and read the text chunks:
Software : TETRA replay viewer 1.0
Author : rig 03
Creation Time : 2026-08-01T16:01:41Z
qv-token : qv{that_was_3Z}
The flag is sitting in a tEXt metadata chunk under the key qv-token.
Flag:
qv{that_was_3Z}(“that was ez”). Free, and it sets the tone: the answers are in the image, in one channel or another.
qtris? #98: Seurat would be proud
Walkthrough
Georges Seurat was the father of pointillism, paintings made of dots. That is the nudge: look at the white centre-dots on the board cells, not the colours. Treat each cell as one bit, dot = 1, no dot = 0, and you have a 13x10 bitmap.
The board fills from the bottom in Tetris, so read it bottom row first (flipud), left to right, and pack 8 bits per character, MSB first:
# dot[r][c] = 1 if the cell at (r,c) has a white centre dot
bits = [dot[r][c] for r in range(12, -1, -1) for c in range(10)]
chars = [int("".join(map(str, bits[i:i+8])), 2) for i in range(0, len(bits)-7, 8)]
print("".join(chr(b) for b in chars)) # -> qv{c0nn3ct_d0ts}
The plaintext even tells you what you did: connect the dots.
Flag:
qv{c0nn3ct_d0ts}. Two channels down (metadata, dots), and the pattern is now obvious: if the dots were message #2, the colours must be message #3.
qtris? #99: No-Cloning Zone
This is the one. It was a 0-solve during the event, and it is the reason I started writing this post. If #98 was the dots, #99 is the colours, and they are encrypted.
Description
Same image, plus lcg.py, plus a trickle of hints:
- “the spawn log pad is from the OG C rand LCG… seeded with a time this month”
- “find the seed which generated the spawn log”
- “the pieces in the sidebar didn’t come from nowhere”
- “the bright rows aren’t blocks. Ask what an empty row does to what’s above it”
- “Our qTris game is inspired by blind quantum computing with a little measurement-based quantum computing.”
Walkthrough
Step 1: break the PRNG (find the seed)
The spawn log is the LCG’s output, so it is our known plaintext. The seed is “a time this month”, i.e. a Unix timestamp, a few million candidates. Brute it: for each timestamp, run the 7-bag shuffle and check whether it reproduces the six bags shown.
def bags_from(seed):
lc = LCG(seed)
out = []
for _ in range(6): # 6 bags
a = list("IOTSZJL") # base order
for i in range(6, 0, -1): # Fisher-Yates, 6 rand() per bag
j = lc.rand() % (i + 1)
a[i], a[j] = a[j], a[i]
out.append(a)
return out
# brute Aug-2026 timestamps until bags_from(t) == observed_bags
# -> unique hit: seed = 1785596927 (2026-08-01 15:08:47 UTC)
One timestamp reproduces all 42 pieces: seed = 1785596927. We now own the RNG.
Rookie trap I fell for first: submitting the seed as the flag. Rejected, in every format. The seed is the key, not the message.
What “blind” and “measurement-based” QC actually mean here
Here is what those two ideas mean in practice, in terms you can code directly:
- Blind quantum computing is, for our purposes, a one-time pad on the values. You hide a number by adding a secret pad; you recover it by subtracting the pad. Our pad is the LCG keystream.
- Measurement-based quantum computing contributes a feed-forward correction: each value’s fix-up depends on the outcomes of earlier cells. In crypto terms that is a feedback cipher (think CFB), and the “earlier outcomes” are the dots from #98, reused here.
So the decode is: colour minus pad, then a couple of dot-driven corrections.
Step 2: the decode
Each of the 8 colours is a 3-bit value (0..7). Continue the LCG past the 36 draws the shuffle consumed, and for each cell (walking the board bottom-up, boustrophedon):
theta = (state >> 16) & 7 # the pad, one value per cell
trit = (colour - theta) % 8 # 1) subtract the one-time pad
if dot_below: trit = (8 - trit) % 8 # 2) X-byproduct: a sign flip
trit ^= 4 * sZ # 3) Z-byproduct: toggle the MSB (feed-forward)
Two details worth calling out:
sZis a propagating Z-byproduct, a correction that ripples along the board depending on the dot pattern. This is the fiddly part.- The colour to number map is 8 unknowns, which naively is
8!guesses. You do not have to guess: every flag starts withqv{= 24 bits = exactly 8 cells of known plaintext, which pins the mapping algebraically. Same move you would make against a repeating-key XOR.
The trap that cost me hours
Decode it and the theme leaks out immediately: ...cl?n1ng...th?0r?m..., obviously cloning and theorem, which fits the title (the no-cloning theorem is the one quantum fact everyone half-remembers). The “obvious” answer is qv{n0_cl0n1ng_th30r3m}.
And the grader rejects it.
The lesson worth tattooing on your arm: when a decode gives a clean, on-theme string but the flag is rejected, keep decoding past the first
}. The payload did not stop attheorem}; it continued with a_ftwtail nobody was reading.
Flag:
qv{n0_cl0n1ng_th30r3m_ftw}
Full disclosure: nailing that last propagating sZ correction by hand is genuinely hard, it is a rippling, position-dependent flip. I fed the exact per-cell numbers to a strong reasoning model to close the final bit of algebra and spot the _ftw tail. Everything up to that point is doable with the steps above, and to my knowledge I was the only player to get there.
qtris? #100: You already solved it!
Its only hint is “use the hints from #99”, so it reuses the exact engine. Like #99 it was a 0-solve, and unlike #99 I did not finish it. I did work out why it is called what it is called.
The reveal is in the angles
Run the exact #99 decode across the whole board and something jumps out. The message splits by sub-board: sub-board C (the bottom 7 rows) carried #99, and the remaining region, sub-boards A and B, carries #100. Colour every cell by the parity of its decoded angle and the design draws itself:

Each cell’s decoded value is an angle in units of pi/4. In sub-board C, the angles are a mix of even and odd multiples. In sub-boards A and B, every single angle is even, a multiple of pi/2. That is not luck: it is a 2^-60 design choice.
If you have done any quantum computing, that split is the whole joke:
- Odd multiples of pi/4 (the
T-type angles) are non-Clifford, the “magic” angles. They are what makes a quantum computation genuinely hard to simulate. That is #99, the No-Cloning zone. - Even multiples, i.e. multiples of pi/2, are Clifford angles. And by the Gottesman-Knill theorem, any computation built only from Clifford operations is efficiently simulable on a classical computer.
So sub-boards A and B are a pure-Clifford computation. A Clifford circuit is one you can solve on your laptop, no quantum hardware, no magic. In other words:
“You already solved it.” The title is a Gottesman-Knill pun. A pure-Clifford program is, by construction, already classically solved.
Why it still beat me
Knowing what it is did not hand me the flag. The all-even structure means A and B encode two bits per cell, and I could prove that a straight ASCII read of those bits can never begin qv{ (the forced-even low bit collides with the q), so the flag is not a naive packing of the angles. The intended finish is to treat A+B as a small measurement-based (brickwork) pattern, simulate the Clifford computation via Gottesman-Knill, and read the logical output. I built the stabiliser simulator and ran it, but without the game’s exact resource-state geometry, which is not in ok.png or lcg.py, the readout stays ambiguous, and every construction I tried came out empty.
Honest verdict: #100 was 0-solve for the entire field, and I was the only player who got far enough on #99 to seriously attempt it. I am confident about the design, pure-Clifford, Gottesman-Knill, “already solved”, but the final extraction needs the challenge’s generator, which was never published. If Quantum Village ever releases the qtris source, I have the seed, the full decoder and a working stabiliser simulator ready; it would be a five-minute finish, and I will update this post with the flag.
Takeaways
Stripped down to its mechanics, the category rewards four moves you will reuse in any misc or stego CTF:
- Spot the weak PRNG.
1103515245is Crand(); a timestamp seed is brute-forceable in seconds. - A recovered seed is a key, not a flag. Go find what it decrypts.
- Known plaintext (
qv{) is a cheat code. It collapses8!colour mappings and keystream alignments to a couple of algebraic checks. - Do not stop at the first plausible string. Stego payloads do not announce their end; the truncated version looks right and is wrong (
_ftw).
And one quantum-specific bonus, courtesy of #100: the difference between a hard quantum program and an easy one is a single bit per angle. Clifford versus magic, Gottesman-Knill versus no-cloning. The author encoded that distinction into the geometry of a Tetris board.
Final tally for the category: #97, #98, #99 solved (the last a 0-solve, sole solver), #100 left open and honest.
Thanks to Quantum Village.
Leave a comment