$ cat writeup.md…
$ cat writeup.md…
b01lersc
Task: craft a quantum eavesdropping attack by submitting an ancilla state and 3-qubit unitary, then survive 96 rounds of QBER and information-theoretic score checks. Solution: embed the 2D Bell logical space into a tuned two-parameter isometry near the information-disturbance frontier, measure only Bob's qubit, and retry until finite-sample checks accept the run.
quantum secret sharing is the way of the future!
Remote:
ncat --ssl qss.opus4-7.b01le.rs 8443Source:
challenge.py
English summary: the service asks us to submit a normalized 1-qubit ancilla and a 3 x 3-qubit attack unitary, then interact with a 96-round protocol containing both test rounds and information rounds. To get the flag we must leak enough information about the hidden secret bit while keeping the induced disturbance low and close to the challenge's theoretical information-disturbance frontier.
This is not a standard "break the crypto primitive" problem. The server already defines the protocol and the score function; our job is to engineer an eavesdropping attack that looks good under those metrics.
The important public ingredients are four Bell-style states:
phi_minus = [1, 0, 0, -1] / sqrt(2) psi_plus = [0, 1, 1, 0] / sqrt(2) psi_upper_plus = (phi_minus + psi_plus) / sqrt(2) phi_upper_minus = (phi_minus - psi_plus) / sqrt(2)
They are indexed as:
S0 = {|phi->, |psi+>}S1 = {|Phi->, |Psi+>}Those four states span only a 2-dimensional logical subspace. That immediately suggests reducing the problem to a BB84-like two-state attack inside the basis
|0_L> = |phi-> |1_L> = |psi+>
and treating S1 as the logical X basis obtained from superpositions of |0_L> and |1_L>.
The server fixes:
ROUNDS = 96TEST_RATIO = 0.40MAX_QBER = 0.32MIN_QBER_CHECKED = 4MIN_QBER_CHECKED_PER_BASIS = 2MIN_INFO_ROUNDS = 16MIN_MUTUAL_INFO_BITS = 0.5FRONTIER_GAP_TOL_BITS = 0.25MIN_RAW_INFO_ACCURACY = 0.60For each round the server samples:
secretset_bitz or xBefore some public disclosure, we must already announce a basis and outcome:
{"basis": "z|x", "outcome": 0|1}
Then the server reveals:
S0 or S1If it is a test round, the server only checks consistency between our pre-announced value and the hidden measurement outcome logic. If it is an information round, we may choose a measurement plan on qubit b and/or c, receive the outcomes, and then submit a secret guess.
At the end, the server computes several filters:
z and x bases.0.32.16 kept information rounds.0.5 bits.0.25 bits of the server's theoretical frontier I_max(D).0.60.So the challenge is explicitly about living near an information-disturbance tradeoff, not about achieving zero disturbance.
The most useful observation is that the four public Bell-style states all lie in the span of |phi-> and |psi+>. Therefore the attack only needs to define what happens to a 2D logical qubit tensored with a 1-qubit ancilla.
We take the ancilla to be:
|0> = [1, 0]
and build an isometry from the logical basis into three physical qubits (Alice, Bob, Eve-ancilla).
The solver uses the family
v0 = Z_A [ λ|000> + sqrt(1-λ^2)(sqrt(p)|011> + sqrt(1-p)|101>) ] v1 = Z_A [ λ|111> + sqrt(1-λ^2)(sqrt(p)|100> + sqrt(1-p)|010>) ]
where Z_A is a Pauli-Z correction on Alice's qubit.
That correction is not cosmetic. Without it, the server's secret labels do not line up with the way the challenge scores the two public bases. Applying Z on Alice makes the label convention consistent in both S0 and S1.
Intuitively:
λ term preserves stronger correlation with the original logical state,sqrt(1-λ^2) branch leaks information into Eve's side information,p tunes how that leaked amplitude is split between the two asymmetric branches,The service does not accept an isometry directly. It demands a full 8x8 unitary.
So the solver:
x0 = |phi->|0>x1 = |psi+>|0>v0 and v1.U = V W*
where W is the basis completion with first columns x0, x1 and V is the completion with first columns v0, v1.
This guarantees a valid unitary whose action on the logical subspace is exactly the chosen attack.
A more complicated two-measurement strategy was considered, but in the live solve the simple version worked:
S0, measure Bob's qubit in zS1, measure Bob's qubit in xThen guess:
if public_set == S0: secret = b if public_set == S1: secret = 1 - b
This is exactly what the final solve.py does.
The reason it works is that the chosen isometry preserves enough logical structure on Bob's subsystem that one carefully matched basis measurement already captures a useful fraction of the secret information, while still keeping the disturbance under control.
The protocol also evaluates our pre-public test-round announcement. The solver uses a deterministic pattern instead of trying to infer anything impossible in advance:
z, x, z, x, ...0This is a deliberately pragmatic choice. It ensures both bases get represented often enough to satisfy the balanced-check conditions, and the isometry parameters are tuned so the resulting aggregate disturbance/information statistics can still pass.
The successful run used:
p = 0.09 lambda = 0.84
This point gave a good empirical balance:
This is the main reason the challenge is probabilistic. Even if the underlying strategy is good, only 96 rounds are sampled, and several conditions depend on random filtering:
So a theoretically decent attack can still fail one connection and pass the next. The successful flag was obtained on attempt 2.
challenge.py and identify the 2D logical Bell subspace.|0>.v0, v1 with Alice-side Z correction.8x8 unitary by orthonormal completion.0.S0 -> ["b", "z"]S1 -> ["b", "x"]solve.py)The solver begins by defining the logical Bell states and a helper for orthonormal completion. The heart of the attack is:
def build_unitary(p: float, lam: float) -> np.ndarray: q = 1.0 - p bar = math.sqrt(max(0.0, 1.0 - lam * lam)) e = np.eye(8, dtype=np.complex128) idx = lambda bits: int(bits, 2) v0 = lam * e[idx("000")] + bar * (math.sqrt(p) * e[idx("011")] + math.sqrt(q) * e[idx("101")]) v1 = lam * e[idx("111")] + bar * (math.sqrt(p) * e[idx("100")] + math.sqrt(q) * e[idx("010")]) za = np.diag([1, 1, 1, 1, -1, -1, -1, -1]).astype(np.complex128) v0 = za @ v0 v1 = za @ v1 bell = build_bell_states() anc = np.array([1.0, 0.0], dtype=np.complex128) x0 = np.kron(bell[(0, 0)], anc) x1 = np.kron(bell[(0, 1)], anc) w = orthonormal_completion([x0, x1]) v = orthonormal_completion([v0, v1]) u = v @ w.conj().T return u
It serializes the ancilla as [1.0, 0.0] and the unitary as JSON, then interacts with the remote service over TLS.
For the pre-public announcement phase:
if round_type == "test": basis = "z" if test_counter % 2 == 0 else "x" else: basis = "z" if info_counter % 2 == 0 else "x" f.write(json.dumps({"basis": basis, "outcome": 0}) + "\n")
For information rounds:
if public_set_basis == "z": plan = [["b", "z"]] else: plan = [["b", "x"]]
and the secret guess is computed as:
bit = int(m.group(1)) current_guess = bit if public_set_basis == "z" else 1 - bit
Because the challenge is probabilistic, the script loops over multiple attempts:
for attempt in range(1, ATTEMPTS + 1): flag, summary = solve_once(HOST, PORT, ancilla_json, unitary_json)
That retry loop is essential rather than optional.
Full working solver (tasks/b01lersc/crypto/qss/solve.py):
import json import math import re import socket import ssl import sys from typing import List, Tuple import numpy as np HOST = "qss.opus4-7.b01le.rs" PORT = 8443 ATTEMPTS = 40 # Empirically good point in the isometry family. P = 0.09 LAMBDA = 0.84 def build_bell_states() -> dict[Tuple[int, int], np.ndarray]: phi_minus = np.array([1, 0, 0, -1], dtype=np.complex128) / math.sqrt(2.0) psi_plus = np.array([0, 1, 1, 0], dtype=np.complex128) / math.sqrt(2.0) return { (0, 0): phi_minus, (0, 1): psi_plus, (1, 0): (phi_minus - psi_plus) / math.sqrt(2.0), (1, 1): (phi_minus + psi_plus) / math.sqrt(2.0), } def orthonormal_completion(cols: List[np.ndarray], dim: int = 8) -> np.ndarray: basis: List[np.ndarray] = [] for vec in cols: w = vec.astype(np.complex128).copy() for b in basis: w -= np.vdot(b, w) * b n = float(np.linalg.norm(w)) if n > 1e-12: basis.append(w / n) for i in range(dim): e = np.zeros(dim, dtype=np.complex128) e[i] = 1.0 w = e.copy() for b in basis: w -= np.vdot(b, w) * b n = float(np.linalg.norm(w)) if n > 1e-12: basis.append(w / n) if len(basis) == dim: break return np.column_stack(basis) def build_unitary(p: float, lam: float) -> np.ndarray: q = 1.0 - p bar = math.sqrt(max(0.0, 1.0 - lam * lam)) e = np.eye(8, dtype=np.complex128) idx = lambda bits: int(bits, 2) v0 = lam * e[idx("000")] + bar * (math.sqrt(p) * e[idx("011")] + math.sqrt(q) * e[idx("101")]) v1 = lam * e[idx("111")] + bar * (math.sqrt(p) * e[idx("100")] + math.sqrt(q) * e[idx("010")]) # Z on Alice so the challenge's secret labels line up in both public bases. za = np.diag([1, 1, 1, 1, -1, -1, -1, -1]).astype(np.complex128) v0 = za @ v0 v1 = za @ v1 bell = build_bell_states() anc = np.array([1.0, 0.0], dtype=np.complex128) x0 = np.kron(bell[(0, 0)], anc) x1 = np.kron(bell[(0, 1)], anc) w = orthonormal_completion([x0, x1]) v = orthonormal_completion([v0, v1]) u = v @ w.conj().T if not np.allclose(u.conj().T @ u, np.eye(8), atol=1e-9): raise ValueError("generated matrix is not unitary") return u def matrix_to_jsonable(u: np.ndarray) -> list[list[float]]: u = np.real_if_close(u, tol=1_000) out: list[list[float]] = [] for row in u: out.append([float(x) for x in row]) return out def solve_once(host: str, port: int, ancilla_json: str, unitary_json: str) -> tuple[str | None, str]: ctx = ssl.create_default_context() sock = socket.create_connection((host, port), timeout=10) ssock = ctx.wrap_socket(sock, server_hostname=host) f = ssock.makefile("rw", encoding="utf-8", newline="\n") transcript: list[str] = [] test_counter = 0 info_counter = 0 round_type = None public_set_basis = None current_guess = 0 try: while True: line = f.readline() if line == "": break transcript.append(line) stripped = line.strip() if stripped == "ancilla_statevector_json>": f.write(ancilla_json + "\n") f.flush() elif stripped == "unitary_8x8_json>": f.write(unitary_json + "\n") f.flush() elif stripped.startswith("round_type:"): round_type = stripped.split(":", 1)[1].strip() public_set_basis = None elif stripped == "pre_public_announce_json>": if round_type == "test": basis = "z" if test_counter % 2 == 0 else "x" test_counter += 1 else: basis = "z" if info_counter % 2 == 0 else "x" info_counter += 1 f.write(json.dumps({"basis": basis, "outcome": 0}) + "\n") f.flush() elif stripped.startswith("public_set:"): public_set_basis = "z" if "S0" in stripped else "x" elif stripped == "measurement_plan_json>": if public_set_basis == "z": plan = [["b", "z"]] else: plan = [["b", "x"]] f.write(json.dumps(plan) + "\n") f.flush() elif stripped.startswith("measurement_outcomes:"): m = re.search(r"b:[zx]=([01])", stripped) if not m: raise ValueError(f"could not parse measurement line: {stripped!r}") bit = int(m.group(1)) current_guess = bit if public_set_basis == "z" else 1 - bit elif stripped == "secret_guess_bit>": f.write(str(current_guess) + "\n") f.flush() text = "".join(transcript) finally: try: f.close() finally: ssock.close() flag_match = re.search(r"([A-Za-z0-9_]+\{[^\n{}]+\})", text) if flag_match: return flag_match.group(1), flag_match.group(1) summary = "no summary" for line in reversed(text.splitlines()): if line.startswith("No flag yet:") or line.startswith("ABORT:") or line.startswith("qber="): summary = line break return None, summary def main() -> None: ancilla_json = json.dumps([1.0, 0.0]) unitary_json = json.dumps(matrix_to_jsonable(build_unitary(P, LAMBDA))) for attempt in range(1, ATTEMPTS + 1): print(f"[+] attempt {attempt}/{ATTEMPTS} with p={P} lambda={LAMBDA}") try: flag, summary = solve_once(HOST, PORT, ancilla_json, unitary_json) except Exception as exc: print(f"[!] attempt failed with exception: {exc}") continue if flag: print(f"[+] FLAG: {flag}") return print(f"[-] no flag this attempt ({summary})") print("[!] exhausted attempts without a flag") if __name__ == "__main__": main()
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar