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/rei/Downloads/solve_voyager.pyTraceback (most recent call last):
File "/home/rei/Downloads/solve_voyager.py", line 147, in <module>
main()
File "/home/rei/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/rei/Downloads/solve_voyager.pyRecovered d: 0xad2f34da51dc500a96f13174ec242a42e13975f23bfdeed81f5b11cf2ae45951
Sign 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
# solve.py
#!/usr/bin/env python3.12
import hashlib
import re
import socket
from itertools import product
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from 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: 0xad2f34da51dc500a96f13174ec242a42e13975f23bfdeed81f5b11cf2ae45951
Sign branch (e1,e2,e3): (1, 1, 1)
Flag: UVT{v0y4g3r_s1gn3d_1t5_0wn_d34th}