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_:)}