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.
Find the target
The challenge gives us an .exe file. The binary presents a 10-stage challenge. Each stage accepts a text input, validates it internally, and reveals the next stage if correct.
Reverse
Stage 1
At step 1, the binary prints "Stage 1/10: recover and enter the base prefix (4 chars)" then reads a line from stdin. It validates the 4 bytes against the hardcoded string UVT{ using a sequential byte-compare loop. Here is the validate process:
movzx eax, byte ptr [rax] ; load expected byte 0 from binary constant
cmp [rcx], al ; compare with input byte 0
jnz short loc_fail ; fail if mismatch
movzx eax, byte ptr [rax+1]
cmp [rcx+1], al
jnz short loc_fail
movzx eax, byte ptr [rax+2]
cmp [rcx+2], al
jnz short loc_fail
movzx eax, byte ptr [rax+3] ; 4th byte for Stage 1
cmp [rcx+3], al
jnz short loc_fail
rax→ pointer to the expected constant bytes inside the binaryrcx→ pointer to the user-supplied input buffer (read from stdin) The expected bytes for Stage 1 (visible instringsoutput and confirmed by the compare):'U' (0x55) 'V' (0x56) 'T' (0x54) '{' (0x7B)So, the answer of the Stage 1 is the flag format
UVT{.
Stage 2
At this stage, the binary uses function sub_140115860. The process is:
- Prints
"enter fragment (3 chars): "→ reads 3-char input - Calls
sub_140115AA0to build the expected fragment in a local buffer - Compares the input byte-by-byte against the built fragment (3 comparisons)
Specifically, looking at the assembly source code:
sub_140115860- Input reading:- First it prints
"enter fragment (3 chars): ":lea rdx, aEnterFragment3 ; "enter fragment (3 chars): " lea rcx, qword_14043A5E0 call sub_14010C790 ; print the prompt - Then reads input and checks length == 3:
cmp [rbp+var_18], 3 ; was exactly 3 chars entered? jz short loc_140115947 ; yes → go validate xor sil, sil ; no → fail (return 0) jmp loc_140115A32
- First it prints
sub_140115AA0:- First, it builds the expected fragment:
mov byte ptr [rcx], 4Bh ; 'K' ... mov byte ptr [rcx+1], 72h ; 'r' ... mov byte ptr [rcx+2], 34h ; '4' - Then it validate user input via a checker:
movzx eax, byte ptr [rax] cmp [rcx], al ; input[0] == 'K'? jnz short loc_1401159E8 ; fail movzx eax, byte ptr [rax+1] cmp [rcx+1], al ; input[1] == 'r'? jnz short loc_1401159E8 movzx eax, byte ptr [rax+2] cmp [rcx+2], al ; input[2] == '4'? jnz short loc_1401159E8So, the answer of Stage 2 is
Kr4.
- First, it builds the expected fragment:
Stage 3
At stage 3, the binary uses function sub_140115B80. The process is:
- Prints
"enter stage2 token (8 chars): "and reads 8-char input - Verifies
length == 8:cmp [rbp+60h+var_B0], 8/jnz loc_1401161FD - Embeds two hardcoded 4-byte constants (the “scrambled” expected result) on the stack
- Runs a per-byte transform loop on the input and compares each transformed byte against the stored constant
Checkout the stage process in the assembly source:
- First, embedding expected constant (
sub_140115B80)mov dword ptr [rbp+60h+var_98], 0FADC2431h mov dword ptr [rbp+60h+var_98+4], 0C5E42C25hThese 8 bytes (little-endian) hold the expected transformed values:
31 24 DC FA 25 2C E4 C5. - Next, it do a validation loop for each byte:
loc_140115C80: lea rcx, [rbp+60h+var_C0] cmp r10, 0Fh cmova rcx, r9 ; rcx → input buffer movzx eax, r8b ; i = loop index (0..7) imul edx, eax, 11h ; edx = i * 0x11 add dl, 6Dh ; dl += 0x6d ('m') xor dl, [rcx+r8] ; dl ^= input[i] movzx eax, r8b imul ecx, eax, 7 ; ecx = i * 7 add dl, 13h ; dl += 0x13 add dl, cl ; dl += (i*7) & 0xFF cmp dl, byte ptr [rbp+r8+60h+var_98] ; compare with expected[i] jnz loc_1401161FD ; fail if mismatch inc r8 cmp r8, 8 jb short loc_140115C80 ; loop for 8 bytes- Transform per byte
i:(i*0x11 + 0x6d) XOR input[i] + 0x13 + (i*7)must equalexpected[i]. - We got a python reverse source code here:
expected = [0x31, 0x24, 0xDC, 0xFA, 0x25, 0x2C, 0xE4, 0xC5] result = [] for i in range(8): e = expected[i] # e = ((i*0x11 + 0x6d) XOR input[i]) + 0x13 + (i*7) # => (i*0x11 + 0x6d) XOR input[i] = (e - 0x13 - i*7) & 0xFF transformed = (e - 0x13 - i*7) & 0xFF result.append(transformed ^ ((i*0x11 + 0x6d) & 0xFF)) print(bytes(result)) # => b'st4rG4te'Run the python source and we will find the answer for Stage 3:
st4rG4te.
- Transform per byte
Stage 4
Overall, the process of Stage 4 looks similarly to Stage 3. Here is the assembly source to find the answer for Stage 4:
- First, embedding expected constant:
mov [rsp+340h+var_300], 0EDA7D1D7h mov [rsp+340h+var_2FC], 49683954hExpected bytes (little-endian 8 bytes):
D7 D1 A7 ED 54 39 68 49. - Next, it do a validation loop for each byte:
loc_140116620: lea rdx, [rbp+240h+var_110] cmp r11, 0Fh cmova rdx, r10 ; rdx → input buffer movzx eax, r9b ; i = loop index (0..7) imul ecx, eax, 0Bh ; ecx = i * 11 mov r8d, 0A7h sub r8b, cl ; r8b = 0xA7 - (i*11) xor r8b, [rdx+r9] ; r8b ^= input[i] movzx eax, r9b add al, al ; al = i*2 lea ecx, [rax+r9] ; ecx = i*2 + i = i*3 add r8b, cl ; r8b += i*3 cmp r8b, byte ptr [rsp+r9+340h+var_300] ; compare with expected[i] jnz loc_140116D12 ; fail if mismatch inc r9 cmp r9, 8 jb short loc_140116620 ; loop for 8 bytes- Transform per byte
i:(0xA7 - i*0xB) XOR input[i] + i*3must equalexpected[i]. - So we have the same python reverse source code here:
expected = [0xD7, 0xD1, 0xA7, 0xED, 0x54, 0x39, 0x68, 0x49] result = [] for i in range(8): e = expected[i] # e = ((0xA7 - i*0xB) XOR input[i]) + i*3 # => (0xA7 - i*0xB) XOR input[i] = (e - i*3) & 0xFF transformed = (e - i*3) & 0xFF result.append(transformed ^ ((0xA7 - i*0xB) & 0xFF)) print(bytes(result)) # => b'pR0b3Z3n'Run the python script and we will find the correct answer:
pR0b3Z3n.
- Transform per byte
Stage 5-6
Stages 5 and 6 require no input. The binary contains an embedded blob tagged with:
- Marker string:
uvt::stage2blob::v4/UVTBLOB4
Verify with:
strings startfield/Startfield/crackme.exe | egrep "uvt::|Stage|UVTBLOB4|zen_void|pings"
When stage 5 triggers, the binary runs an internal VM/extractor that decompresses and writes the payload directory to:
uvt_crackme_work/stage2/
starfield_pings/pings.txt ← Stage 7 data
logs/system.log ← Stage 8 data
void/zen_void.bin ← Stage 9/10 data
void/zen_void_readme.txt ← key hints for Stage 9/10
probe_extender/probe_extender.py
Stage 6 verifies the hash of the extracted payload. The program prints:
stage5: payload already extracted (hash match)
stage5: continue in: Z:\...\uvt_crackme_work\stage2
Great! Stage 6/10 done.
Stage 7
At this stage, the binary points to uvt_crackme_work/stage2/starfield_pings/pings.txt and asks for the decoded fragment.
Moving to the decoding process, pings.txt contains two hex maps (labeled “even” and “odd”) and parity hints. The encoding steps (which must be reversed) are:
- The original fragment bytes were split into two groups by index parity (even/odd index).
- Even-indexed bytes were XORed with
0x52. - Odd-indexed bytes were XORed with
0x13, then the resulting sub-array was reversed. - The two maps were stored separately in the file.
We have the python reverse source for this Stage:
import json
with open("uvt_crackme_work/stage2/starfield_pings/pings.txt") as f:
data = json.load(f)
even_map = bytes.fromhex(data["even"]) # even-indexed encoded bytes
odd_map = bytes.fromhex(data["odd"]) # odd-indexed encoded bytes (reversed)
# Undo: reverse odd_map, then XOR each byte back
odd_decoded = bytes(b ^ 0x13 for b in reversed(odd_map))
even_decoded = bytes(b ^ 0x52 for b in even_map)
# Interleave: even[0], odd[0], even[1], odd[1], ...
fragment = []
for a, b in zip(even_decoded, odd_decoded):
fragment.append(a)
fragment.append(b)
print(bytes(fragment).decode()) # => "uR_pR0b3Z_xTND-"
The answer is uR_pR0b3Z_xTND
Stage 8
At this stage, the binary points to uvt_crackme_work/stage2/logs/system.log and asks for the decoded fragment.
Moving to the decoding process, system.log contains JSON-formatted telemetry_rollup entries. Each entry has two fields:
"k": a one-byte XOR key (integer)"fragx": a hex-encoded byte string (fragment piece)
Base on the analyzing, the reverse source code is:
import json, base64
results = []
with open("uvt_crackme_work/stage2/logs/system.log") as f:
for line in f:
entry = json.loads(line.strip())
k = entry["k"] # one-byte key
frag = bytes.fromhex(entry["fragx"]) # raw fragment bytes
dec = bytes(b ^ k for b in frag) # XOR each byte with k
results.append(dec)
combined = b"".join(results)
# Add padding if needed and base64-decode
padding = (4 - len(combined) % 4) % 4
decoded = base64.b64decode(combined + b"=" * padding)
print(decoded.decode()) # => "I_h1D3_in_l0Gz_"
The answer is I_h1D3_in_l0Gz_
Stage 9
At this stage, the binary references uvt_crackme_work/stage2/void/zen_void.bin. This is a large, mostly-zero binary file with several small non-zero “islands” scattered at known offsets. The zen_void_readme.txt (also extracted) documents the decryption keys.
To finding islands, first we create probe_extender.py (included in the extracted payload):
fn = 'uvt_crackme_work/stage2/void/zen_void.bin'
data = open(fn, 'rb').read()
# Islands identified by scanning for runs of non-zero bytes:
islands = [
(0x2345, 0x234b),
(0x234d, 0x2350),
(0x9550, 0x9557),
(0x9d20, 0x9d27),
(0xa1b2, 0xa1b9),
(0xe3c4, 0xe3ca),
]
Next, in zen_void_readme.txt states, we can find the key 0x2a decodes the Stage 8 island.
key = 0x2a
for s, e in islands:
block = data[s:e+1]
dec = bytes(b ^ key for b in block)
if all(32 <= c < 127 for c in dec):
print(hex(s), "->", dec.decode())
# Output: 0xa1b2 -> "1n_v01D_"
So the Stage 9 answer is 1n_v01D_
Stage 10
At this stage, the readme states: Stage 10 key = sum(bytes(stage9_text)) % 256
Base on this, we have a source code for stage 10:
stage9_text = b'1n_v01D_'
key10 = sum(stage9_text) % 256 # = 0x?? (computed at runtime)
for s, e in islands:
if s == 0xa1b2: # skip the Stage 9 island
continue
block = data[s:e+1]
dec = bytes(b ^ key10 for b in block)
if all(32 <= c < 127 for c in dec):
print(hex(s), "->", dec.decode())
# Output: 0xe3c4 -> "iN_ZEN}"
The stage 10 answer is iN_ZEN}
Get flag
After solving all 10 step, run the binary with wine to get the flag:
printf "UVT{\nKr4\nst4rG4te\npR0b3Z3n\nuR_pR0b3Z_xTND-\nI_h1D3_in_l0Gz_\n1n_v01D_\niN_ZEN}\n" | wine startfield/Startfield/crackme.exe
Here is the shell run:
$ printf "UVT{\nKr4\nst4rG4te\npR0b3Z3n\nuR_pR0b3Z_xTND-\nI_h1D3_in_l0Gz_\n1n_v01D_\niN_ZEN}\n" | wine startfield/Startfield/crackme.exe
Stage 1/10: recover and enter the base prefix (4 chars)
enter base prefix (4 chars): Great! Stage 1/10 done.
Next:
Stage 2/10: enter the 3-char fragment
enter fragment (3 chars): Great! Stage 2/10 done.
Next:
Stage 3/10: enter the stage2 token (8 chars)
enter stage2 token (8 chars): Great! Stage 3/10 done.
Next:
Stage 4/10: enter the token (8 chars)
enter token (8 chars): Great! Stage 4/10 done.
Next:
Stage 5/10: execute the VM (no input)
Great! Stage 5/10 done.
Next:
Stage 6/10: extract the embedded stage2 payload (no input)
stage5: payload already extracted (hash match)
stage5: continue in: Z:\home\kali\Desktop\wargame\uvt_crackme_work\stage2
Great! Stage 6/10 done.
Next:
Stage 7/10: decode starfield pings and enter the recovered fragment
file: Z:\home\kali\Desktop\wargame\uvt_crackme_work\stage2\starfield_pings\pings.txt
enter fragment: Great! Stage 7/10 done.
Next:
Stage 8/10: recover the hidden hint from logs and enter the decoded fragment
file: Z:\home\kali\Desktop\wargame\uvt_crackme_work\stage2\logs\system.log
enter fragment: Great! Stage 8/10 done.
Next:
Stage 9/10: find the island in the void container and enter the extracted fragment
file: Z:\home\kali\Desktop\wargame\uvt_crackme_work\stage2\void\zen_void.bin
enter fragment: Great! Stage 9/10 done.
Next:
Stage 10/10: final: compute the last fragment and enter it
file: Z:\home\kali\Desktop\wargame\uvt_crackme_work\stage2\void\zen_void.bin
enter fragment: Great! Stage 10/10 done.
UVT{Kr4cK_M3_N0w-cR4Km3_THEN-5T4rf13Ld_piNgS_uR_pR0b3Z_xTND-I_h1D3_in_l0Gz_1n_v01D_iN_ZEN}
The flag is UVT{Kr4cK_M3_N0w-cR4Km3_THEN-5T4rf13Ld_piNgS_uR_pR0b3Z_xTND-I_h1D3_in_l0Gz_1n_v01D_iN_ZEN}