Noisy Vault Challenge Scenario
Noisy Vault Challenge Scenario
Platform: HackTheBox | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-31 | Status: Solved Techniques: majority_vote_decoding, minimal_valid_circuit, qiskit_bitstring_reversal, single_query_oracle_recovery
Summary
Task: recover a 64-bit vault key from a single noisy Qiskit oracle while satisfying structural ancilla-validation constraints. Solution: submit the smallest valid CX-only circuit, aggregate 4096 measurement results with per-bit majority voting, then reverse the recovered bitstring to match Qiskit's endianness.
Noisy Vault Challenge Scenario — HackTheBox
Description
Noisy Vault Challenge Scenario
The archive contained quantum_noisy_vault/server.py. The service prepares a random 64-bit secret key on 64 data qubits and exposes 16 ancilla qubits. We are allowed exactly one oracle query and one final unlock attempt, so the whole attack must recover the secret from a single noisy measurement batch.
Summary
The key idea is to satisfy the validator with the smallest possible corrective circuit, so we do not disturb the already-encoded secret more than necessary. A chain of 16 CX gates from data qubits 0..15 into ancillas 64..79 passes validation after transpilation, while leaving the data qubits themselves unchanged in the computational basis.
The oracle then returns noisy measurement counts over 4096 shots. Because the circuit mostly preserves the prepared basis state and the noise rates are modest, each secret bit still appears more often than its flipped value. Taking a per-bit majority vote across all returned bitstrings recovers the full key, and reversing the final bitstring fixes Qiskit's classical-bit endianness.
Analysis
Reading server.py shows the important constraints:
visible_bits = 64ancilla_qubits = 16- one oracle call only:
max_oracle_calls = 1 - one unlock attempt only
- noise model uses depolarizing error on single-qubit gates, two-qubit gates, and measurement
- oracle output is
get_counts()from 4096 noisy shots
The validator does not care whether our circuit actually corrects noise. It only checks structural activity after transpile(..., optimization_level=3):
- at least
total_data_qubits // 4 = 16data-ancilla links - at least
ancilla_qubits // 4 = 4active ancillas
So the goal is not fancy error correction. The goal is to pass validation while touching the data register as little as possible.
Minimal valid circuit
This was enough:
text read-only • copy allowedCX:0,64;CX:1,65;CX:2,66;CX:3,67;CX:4,68;CX:5,69;CX:6,70;CX:7,71;CX:8,72;CX:9,73;CX:10,74;CX:11,75;CX:12,76;CX:13,77;CX:14,78;CX:15,79
Why this works:
- it creates exactly 16 data-ancilla links
- it activates 16 ancillas, which is comfortably above the minimum 4
- each operation is a single
CX - the target is an ancilla initialized to
|0⟩, so the data qubit is only used as control and its classical value is preserved
This gives the validator what it wants without adding unnecessary disturbance to the secret-bearing qubits.
Why majority vote works
The server prepares a computational-basis state corresponding to the secret key by applying X only on qubits whose secret bit is 1. If we avoid destructive transformations on the data qubits, every shot ideally measures the same 64-bit string.
Noise changes some outcomes:
- depolarizing noise can randomize qubit states during gates
- measurement noise can flip some observed bits
- the 16
CXgates add some extra two-qubit noise, but only on part of the register
Still, with 4096 shots and relatively small error probabilities, the true value of each bit remains biased above 50%. That means we do not need the most common full 64-bit string. We can recover each bit independently by counting how many shots produced 1 in that position and taking the majority.
This is more robust because different shots can have noise in different positions. Even if no single complete bitstring dominates, the correct value of each individual bit still dominates.
Qiskit endianness gotcha
result.get_counts() returns classical bitstrings in Qiskit's usual order, where the leftmost character corresponds to the highest-index classical bit. Since the server measures range(self.total_data_qubits) directly into range(self.total_data_qubits), the recovered count strings appear reversed relative to the secret key's original qubit indexing.
So after building the majority-voted bitstring, it must be reversed:
python read-only • copy allowedrecovered = measured[::-1]
Without this reversal, the unlock attempt fails even if the per-bit voting is otherwise correct.
Exploitation / Solution
- Connect to the service.
- Choose the oracle option.
- Submit the minimal valid corrective circuit with 16 data-to-ancilla
CXlinks. - Parse the JSON
countsdictionary returned from 4096 shots. - For each bit position, sum how many samples contain
1at that position. - Output
1if that count is greater than half the total shots, otherwise0. - Reverse the resulting 64-bit string because of Qiskit bit ordering.
- Send the recovered key as the one allowed unlock attempt.
Remote recovered key:
text read-only • copy allowed1000011100010101100000000100010101101000000101100011001000100111
Remote result:
text read-only • copy allowed[+] Access Granted! The vault opens: HTB{REDACTED}
Solver
Local solver path:
text read-only • copy allowedtasks/hackthebox/Noisy_Vault_Challenge_Scenario/solver.py
Minimal solver logic:
python read-only • copy allowed#!/usr/bin/env python3 import json DATA_QUBITS = 64 ANCILLA_BASE = 64 CIRCUIT = ";".join(f"CX:{i},{ANCILLA_BASE + i}" for i in range(16)) def decode_counts(counts): totals = [0] * DATA_QUBITS shots = sum(counts.values()) for bitstring, count in counts.items(): for i, bit in enumerate(bitstring): if bit == "1": totals[i] += count measured = "".join("1" if total > shots / 2 else "0" for total in totals) return measured[::-1] # Qiskit bitstring order # 1) send oracle choice # 2) send CIRCUIT # 3) parse JSON counts from server output # 4) key = decode_counts(counts) # 5) send unlock choice, then send key