Category: Cryptography
Flag: UVT{v0y4g3r_s1gn3d_1t5_0wn_d34th}
Challenge Description
Year 2387 You have established an uplink to the Voyager-X probe via an emergency telemetry relay.
Analysis
I started by pulling one full live transcript from the server so I could capture all public parameters and exactly three signatures in a single session.
timeout 8 bash -lc "printf 'SIGN 00\nSIGN 01\nSIGN 02\nQUIT\n' | nc 194.102.62.166 22869"│ Curve : secp256k1│ LCG a : 0x8d047be6d23ed97a5f8e83d6ff20b1366123dc858503be002764b531cdac5597│ Qx : 0xf29323d459059cfd3e09fc375cf0054923ce8b7b8b579328631a533d24bd145d│ Qy : 0xf167a0ca58327b757fe28893ecd75dfce809f749950f8f8345db0a291c25fcdf│ Data : 3004b7c0d22488c063e58f8b9e62eabea30befcf12554e75│ 0a0e78744099abe9592a03f86adeb2bfc56add83645a856c│ r : 0x70c62034e4eaa385710a9109d801fe2097bdc89eabc6f49e0210743f61bedb5d│ s : 0x803d4f32de48090588a8f7b334ce00b684eaa056ed4e1182ff38f896b2e01df5│ z : 0x6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d│ r : 0xbae63fd8c48a268357ed1ecaaa1d2b98014998f6857d654f5115d31d5d378c37│ s : 0xa0faa3d959162d7abc890f8c949996119f2820a3c9f2ed0227d6a7745a38e876│ z : 0x4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a│ r : 0xdb3577e944aee428d58d2f1e6d5c11d9c6ba35a290e684f6724d8e6daa7cb3f2│ s : 0x2e5417786a1197bfaa5ba8ecf7761f17c62653949269980e27eb140d8ed17948│ z : 0xdbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986Those values were enough to model the nonce fault directly. With an LCG nonce stream,
k2 = a*k1 + b and k3 = a*k2 + b, so k3 - (a+1)k2 + a*k1 = 0 (mod n).
For ECDSA, k = (z + r*d) * s^{-1} (mod n), so substituting three signatures collapses to one linear equation in d.
Because some services normalize signatures with s or n-s, I solved all 8 sign branches and accepted only the candidate where Q = d*G matched the provided public key.

My first run hit a parser bug, not a math bug: I had parsed the boxed ciphertext lines too rigidly.
python3.12 /home/LIGHT/Downloads/solve_voyager.pyTraceback (most recent call last): File "/home/LIGHT/Downloads/solve_voyager.py", line 147, in <module> main() File "/home/LIGHT/Downloads/solve_voyager.py", line 135, in main ct = parse_ciphertext(text)ValueError: Could not parse ciphertextAfter loosening ciphertext extraction to read all hex chunks in the Data box, the exact same key-recovery equations worked immediately. The solver recovered d, verified the public key match, derived AES-128-ECB key as SHA-256(d)[:16], and decrypted the final directive to the flag.
python3.12 /home/LIGHT/Downloads/solve_voyager.pyRecovered d: 0xad2f34da51dc500a96f13174ec242a42e13975f23bfdeed81f5b11cf2ae45951Sign branch (e1,e2,e3): (1, 1, 1)Flag: UVT{v0y4g3r_s1gn3d_1t5_0wn_d34th}Once d reconstructed and the AES plaintext matched the expected UVT{...} format, the challenge was done.

Solution
# !/usr/bin/env python3.12import hashlibimport reimport socketfrom itertools import product
from Crypto.Cipher import AESfrom Crypto.Util.Padding import unpadfrom ecdsa.ecdsa import generator_secp256k1
HOST = "194.102.62.166"PORT = 22869
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
def recv_until(sock: socket.socket, token: bytes, timeout: float = 10.0) -> bytes: sock.settimeout(timeout) out = b"" while token not in out: chunk = sock.recv(4096) if not chunk: break out += chunk return out
def recv_all(sock: socket.socket, timeout: float = 2.0) -> bytes: sock.settimeout(timeout) out = b"" while True: try: chunk = sock.recv(4096) except TimeoutError: break if not chunk: break out += chunk return out
def parse_field(text: str, name: str) -> int: m = re.search(rf"{name}\s+:\s+0x([0-9a-f]+)", text) if not m: raise ValueError(f"Could not parse field {name}") return int(m.group(1), 16)
def parse_ciphertext(text: str) -> bytes: m = re.search(r"Data\s+:\s*(.*?)\n\s*└", text, flags=re.DOTALL) if not m: raise ValueError("Could not parse ciphertext") hex_parts = re.findall(r"[0-9a-f]{16,}", m.group(1)) if not hex_parts: raise ValueError("Could not parse ciphertext hex chunks") return bytes.fromhex("".join(hex_parts))
def parse_signatures(text: str): sigs = re.findall( r"r\s+:\s+0x([0-9a-f]+).*?s\s+:\s+0x([0-9a-f]+).*?z\s+:\s+0x([0-9a-f]+)", text, flags=re.DOTALL, ) if len(sigs) != 3: raise ValueError(f"Expected 3 signatures, got {len(sigs)}") return [(int(r, 16), int(s, 16), int(z, 16)) for r, s, z in sigs]
def inv(x: int) -> int: return pow(x % N, -1, N)
def recover_private_key(a: int, qx: int, qy: int, sigs): (r1, s1, z1), (r2, s2, z2), (r3, s3, z3) = sigs is1, is2, is3 = inv(s1), inv(s2), inv(s3)
for e1, e2, e3 in product((1, -1), repeat=3): coeff = (e3 * r3 * is3 - (a + 1) * e2 * r2 * is2 + a * e1 * r1 * is1) % N const = (e3 * z3 * is3 - (a + 1) * e2 * z2 * is2 + a * e1 * z1 * is1) % N if coeff == 0: continue
d = (-const * inv(coeff)) % N q = d * generator_secp256k1 if q.x() == qx and q.y() == qy: return d, (e1, e2, e3)
raise ValueError("No valid private key candidate found")
def derive_flag(d: int, ct: bytes) -> str: d_forms = [d.to_bytes(32, "big"), d.to_bytes(max(1, (d.bit_length() + 7) // 8), "big")]
seen = set() for db in d_forms: if db in seen: continue seen.add(db)
key = hashlib.sha256(db).digest()[:16] pt = AES.new(key, AES.MODE_ECB).decrypt(ct)
for cand in (pt, unpad(pt, 16) if len(pt) % 16 == 0 else pt): m = re.search(rb"UVT\{[^}]+\}", cand) if m: return m.group(0).decode()
raise ValueError("Failed to derive flag from decrypted plaintext")
def main(): with socket.create_connection((HOST, PORT), timeout=10) as s: banner = recv_until(s, b"oracle@voyager-xi [3 sigs left] > ", timeout=10) s.sendall(b"SIGN 00\nSIGN 01\nSIGN 02\n") rest = recv_all(s, timeout=2)
text = (banner + rest).decode("utf-8", errors="replace")
a = parse_field(text, "LCG a") qx = parse_field(text, "Qx") qy = parse_field(text, "Qy") ct = parse_ciphertext(text) sigs = parse_signatures(text)
d, signs = recover_private_key(a, qx, qy, sigs) flag = derive_flag(d, ct)
print(f"Recovered d: {hex(d)}") print(f"Sign branch (e1,e2,e3): {signs}") print(f"Flag: {flag}")
if __name__ == "__main__": main()python3.12 solve.pyRecovered d: 0xad2f34da51dc500a96f13174ec242a42e13975f23bfdeed81f5b11cf2ae45951Sign branch (e1,e2,e3): (1, 1, 1)Flag: UVT{v0y4g3r_s1gn3d_1t5_0wn_d34th}