Crypto - Quantum Detanglement – UVT CTF 2025
crypto quantumOverview
Alice and Bob share a zero-noise quantum link and repeatedly prepare Bell pairs. Bob encodes a secret message by applying Pauli operators to his half of each pair (super-dense coding), and the server prints the resulting 2-qubit state vectors without collapsing them. Our goal is to reconstruct Bob’s classical message—the flag—by analyzing the printed stream of state vectors.
1. Challenge Description
-
The server prints hundreds of lines, each like: Initial state =
Matrix([[sqrt(2)/2], [0], [0], [-sqrt(2)/2]])
Matrix([[0], [-sqrt(2)/2], [sqrt(2)/2], [0]])
…
-
Each
Matrix([[a0], [a1], [a2], [a3]])
represents a 2-qubit pure state
2. Bell States
There are four maximally entangled Bell basis states:
Each printed vector has coefficients only in ${0,\pm\sqrt2/2}$, so up to a global sign it must be one of these four.
Note on global phase: Multiplying a quantum state by $-1$ does not change any measurement outcome, e.g.
is physically the same as $\Phi^+$. Thus the server’s "eight" variants collapse to four canonical labels.
3. Super-Dense Coding
Bob encodes two classical bits $(b_1,b_0)$ by applying a Pauli operator $P$ on his qubit:
Bits $(b_1,b_0)$ | Operator $P$ | Resulting Bell state |
---|---|---|
(0,0) | $I$ | $\Phi^+$ |
(0,1) | $X$ | $\Psi^+$ |
(1,0) | $Z$ | $\Phi^-$ |
(1,1) | $Y$ | $\Psi^-$ |
Alice prints the Bell state after Bob’s operation, allowing us to deduce which $P$ was applied.
4. Key Idea: Transitions
Instead of mapping each state directly to two bits (which would require guessing one of 24 possible label $\rightarrow$ bits maps), we look at pairs of successive Bell labels:
For each $(\beta_{\text{prev}},\beta_{\text{cur}})$ there is exactly one $P\in{I,X,Z,Y}$. We then translate $P\to$ its two‐bit code according to the super-dense table.
Transition table:
prev $\downarrow$ cur $\rightarrow$ | $\Phi^+$ | $\Psi^+$ | $\Phi^-$ | $\Psi^-$ |
---|---|---|---|---|
$\Phi^+$ | I | X | Z | Y |
$\Psi^+$ | X | I | Y | Z |
$\Phi^-$ | Z | Y | I | X |
$\Psi^-$ | Y | Z | X | I |
Map operators to bits:
Concatenate all 2-bit chunks in order.
5. Step-by-Step Solve
- Download the entire server output (a few KB).
-
Parse each line with a regex to extract four entries $[a_0,a_1,a_2,a_3]$.
Convert $\pm\sqrt2/2\rightarrow±1$, and $0 \rightarrow 0$ -
Classify vector $v=[a_0,a_1,a_2,a_3]$:
- If indices ${0,3}$ are non-zero ⇒ $\Phi$, else ${1,2}$ ⇒ $\Psi$.
- Sign of product of those two entries decides “+” vs “−”.
-
Compute transitions: for each consecutive pair of labels, look up $P$.
- Translate each $P$ to two bits, build a long bit string.
- Trim to a multiple of 8 bits, split into bytes, interpret as ASCII.
- Extract the flag
6. Solve
from pwn import remote
import math, re, string
HOST, PORT = "91.99.1.179", 60004
SQRT2_HALF = math.sqrt(2) / 2 # ~0.7071
r = remote(HOST, PORT)
data = r.recvall(timeout=10).decode("ascii", errors="ignore")
r.close()
print(f"got {len(data):,} bytes from server")
# turn each line into a 4 amplitude vector
def vec_from(line: str):
m = re.search(r"\[\[(.*?)\]\]", line)
if not m: return None
parts = [p.replace('[','').replace(']','').strip()
for p in m.group(1).split("],")]
if len(parts) != 4: return None
v = []
for p in parts:
if p.startswith("-sqrt"): v.append(-SQRT2_HALF)
elif p.startswith("sqrt"): v.append( SQRT2_HALF)
else: v.append(0.0)
return v
vectors = [v for v in (vec_from(l) for l in data.splitlines()) if v]
print(f"parsed {len(vectors)} vectors")
# classify vector --> Bell state
def bell(v):
# product of the two non zero amplitudes fixes the sign
if v[0] or v[3]: # phi
return "PHI+" if v[0]*v[3] > 0 else "PHI-"
else: # psi
return "PSI+" if v[1]*v[2] > 0 else "PSI-"
states = [bell(v) for v in vectors]
# transition table (prev,cur) --> operator
T = {
"PHI+": {"PHI+":'I',"PSI+":'X',"PHI-":'Z',"PSI-":'Y'},
"PHI-": {"PHI-":'I',"PSI-":'X',"PHI+":'Z',"PSI+":'Y'},
"PSI+": {"PSI+":'I',"PHI+":'X',"PSI-":'Z',"PHI-":'Y'},
"PSI-": {"PSI-":'I',"PHI-":'X',"PSI+":'Z',"PHI+":'Y'},
}
P2B = {"I":"00", "X":"01", "Z":"10", "Y":"11"}
bits = ""
for a, b in zip(states, states[1:]):
bits += P2B[T[a][b]]
print(f"extracted {len(bits)//2} operators ({len(bits)} bits)")
bits = bits[: len(bits)//8*8] # byte aligned
text = "".join(chr(int(bits[i:i+8],2)) for i in range(0,len(bits),8))
print(text)
7. Why It Works
- Global phase ignored: ±1 doesn’t change Bell type.
- Transitions give the Pauli operator directly, no brute-force over label $\rightarrow$ bits.
- Standard super-dense coding mapping recovers the original 2-bit message sequence.
This method is robust to duplicate lines (removing duplicates optional) and prints, in order, exactly the bits Bob intended to send.
Much love 💋
Ap4sh