560 words
3 minutes
UniVsThreats 26 Quals CTF - Bro is not a space hacker - Reverse Engineering Writeup

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, stripped
checksec "/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:        Enabled

That 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 denied

This 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/airlockauth
missing seed
wc -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.enc

Running 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) = 16

To 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")                          = 16

So 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.

poke

printf "ASTRA9-BRO-1337\n" | /home/rei/Downloads/space_usb_extract/user/airlockauth
signal verified

With 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.

smile

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.py
UVT{S0m3_s3cR3tZ_4r_nVr_m3Ant_t0_B_SHRD}
UniVsThreats 26 Quals CTF - Bro is not a space hacker - Reverse Engineering Writeup
https://blog.rei.my.id/posts/57/univsthreats-26-quals-ctf-bro-is-not-a-space-hacker-reverse-engineering-writeup/
Author
Reidho Satria
Published at
2026-02-27
License
CC BY-NC-SA 4.0