DEF CON Quantum CTF 2026: the qtris Walkthrough

9 minute read

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.

The qtris board: ok.png

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:

  • sZ is 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 with qv{ = 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 at theorem}; it continued with a _ftw tail 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:

qtris angle map: A+B is all Clifford, C is mixed

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:

  1. Spot the weak PRNG. 1103515245 is C rand(); a timestamp seed is brute-forceable in seconds.
  2. A recovered seed is a key, not a flag. Go find what it decrypts.
  3. Known plaintext (qv{) is a cheat code. It collapses 8! colour mappings and keystream alignments to a couple of algebraic checks.
  4. 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