← Back to Writeups
CUSTOMMediumReversing

rev12

XESXOR9/2/20265 min read
#reversing#custom#medium#custom_rc4_decrypt#docker_amd64_run#key_concatenation_recovery#self_reading_binary#validator_constant_extraction

rev12

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: custom_rc4_decrypt, docker_amd64_run, key_concatenation_recovery, self_reading_binary, validator_constant_extraction

Summary

Walkthrough of the CUSTOM Reversing rev12. Task: 64-bit non-stripped PIE ELF in the train/path/station self-decrypting custom-RC4 family (sibling of RE4_rev02) but with many more struct fields (driver, passport, country, dates, durations). Sol

Description

RE4_rev12 — Reverse. Binary rev_1_2 on rev-kids20.forkbomb.ru.

A 64-bit x86-64 PIE ELF rev_1_2 (NOT stripped) is given. It is the same "train/path/station" self-decrypting custom-RC4 family as RE4_rev02 (rev_0_2), but with many more struct fields: driver name, passport, country, date of issue, trip durations, etc. The program asks you to fill all struct fields, validates each against an embedded constant, then concatenates every entered value into a single "key" used to decrypt the embedded flag. Goal: recover the key, get the flag, submit via POST flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

The binary is built from nested structs. For each struct a read_* / create_* function prompts and stores a field, and a check_*_0 validator compares the stored field against a hard-coded constant via strncmp/integer compare:

  • check_driver_name_0, check_driver_passport_0, check_passport_country_0, check_passport_date_of_issue_0
  • check_path (@0xf2d) calls check_path_begin_0, check_path_end_0, check_path_train_0, check_path_travel_time_0, check_train_driver_0 ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

If check_path passes, main (@0x1eab) prints Enter key:, reads up to 0x800 bytes, strips the newline, and calls drop_flag (@0x1c9d).

drop_flag(filename=argv[0], key) opens the program's own binary "rb":

  1. Reads the last 4 bytes of the file → ciphertext length clen = 50.
  2. Seeks to filesize - 4 - clen = 18536 and reads the 50-byte ciphertext.
  3. Runs a custom RC4: prepare_key (@0x198a) is the KSA, rc4 (@0x1a9f) is the PRGA — both standard. The key is the user-typed string.
  4. Decrypts in place. First 4 bytes of plaintext = inner length (0x2e); the rest is the flag string. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

strtoupper exists in the binary but is never called — the key is used verbatim (no case change). The check_*_0 functions only validate fields; they do not build the key. The key is the concatenation of every entered field value in entry (read traversal) order, with no separator.

Expected field values (entry order = key order)

#FieldConstantDecimal/Value
1begin station avg_trains (qword)0x5080
2begin station city (str)—Savirellia
3begin station country (str)—Ziyton
4end station avg_trains0x1723
5end city—Contiensa
6end country—Zhand
7train rate (fgets)—5
8user age (qword)0x3654
9name surname—Wenger
10name name—Matthieu
11passport number (int)0x1dc3f31950707
12passport country name—Rohan
13country age_country (int)0x5ce1486
14country population (qword)0x2a1c072759687
15date day (int)0x1622
16date month (qword)0x44
17date year (str)—2001
18train passengers_count (qword)0x3bf959
19train price (int)0x9582392
20train train_number (fgets)—H142
21duration seconds (str)—46
22duration hours (str)—27
23duration minutes (str)—33
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Note: read_station / read_country prompt labels are swapped relative to the storage offset, but the storage order (= entry order) is what feeds the key, so follow storage order, not the printed label.

Solution

  1. Reverse each check_*_0 validator (radare2 / Ghidra) to extract every expected constant.
  2. Determine read order from main and key concatenation order from prepare_key. No separators, no case change.
  3. Concatenate field values in entry order to form the RC4 key:
80SavirelliaZiyton23ContiensaZhand554WengerMatthieu1950707Rohan1486275968722420019592392H142462733

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  1. Run the binary under Docker ubuntu:22.04 (linux/amd64), feed the expected field values to each prompt, and paste the full concatenation at the Enter key: prompt → it prints "Well done!" and the flag. Alternatively reproduce the custom RC4 offline on the 50-byte tail ciphertext.

Offline decrypt (custom RC4)

python
#!/usr/bin/env python3
# Read the 50-byte appended ciphertext from rev_1_2 and decrypt with the recovered key.

KEY = b"80SavirelliaZiyton23ContiensaZhand554WengerMatthieu1950707Rohan1486275968722420019592392H142462733"
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

with open("rev_1_2", "rb") as f:
    data = f.read()

clen = int.from_bytes(data[-4:], "little")          # 50
ct = data[len(data) - 4 - clen : len(data) - 4]     # offset 18536, 50 bytes

# RC4 KSA (prepare_key) + PRGA (rc4) — standard
S = list(range(256))
j = 0
for i in range(256):
    j = (j + S[i] + KEY[i % len(KEY)]) & 0xff
    S[i], S[j] = S[j], S[i]

out = bytearray()
i = j = 0
for b in ct:
    i = (i + 1) & 0xff
    j = (j + S[i]) & 0xff
    S[i], S[j] = S[j], S[i]
    out.append(b ^ S[(S[i] + S[j]) & 0xff])
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

inner_len = int.from_bytes(out[:4], "little")        # 0x2e = 46
flag = out[4:4 + inner_len].decode()
print(flag)   # spbctf{REDACTED}

Ciphertext (50 bytes @ offset 18536):

71a45d3f7bfed449583a53caba9877cc44cad9a817cd7f38fef6daa799239898f8624b9015f7eb87d0d46b02928612387174

decrypts to \x2e\x00\x00\x00 + spbctf{REDACTED}.

Debugging lesson (failed-then-fixed)

The first run failed with "Try again!". The cause was a hex→decimal slip on the population constant 0x2a1c07, which was mis-converted to 2759175 instead of the correct 2759687. A full prompt-trace run (entering each field one at a time and watching the response) isolated the single mismatched field; correcting the conversion to 2759687 made the binary accept the key ("Well done!") and print the flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Lesson: when a multi-field RC4-key check fails, trace each prompt/response to find the single wrong field, and double-check hex→decimal conversions of numeric struct fields. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering custom_rc4_decrypt, docker_amd64_run, key_concatenation_recovery, self_reading_binary, validator_constant_extraction was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-06-15 • CUSTOM • rev12 • Difficulty: Medium