← Back to Writeups
CUSTOMMediumReversing

rev02

XESXOR9/2/20264 min read
#reversing#custom#medium#check_constant_extraction#rc4_ksa_prga_reimplementation#self_reading_binary#key_concatenation_recovery#tail_ciphertext_carving

rev02

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: check_constant_extraction, rc4_ksa_prga_reimplementation, self_reading_binary, key_concatenation_recovery, tail_ciphertext_carving

Summary

Walkthrough of the CUSTOM Reversing rev02. Task: 64-bit non-stripped PIE ELF that asks for train-route fields, concatenates them into an RC4 key, and decrypts a flag appended to its own file. Solution: extract the expected field values from ea

Description

Hello! In order to complete the challenge you need to fill some info about path to flag Enter value of <field> field please: Now concatenate all data you entered and place here as key Enter key: Decrypting flag and second part Flag: %s

A 64-bit x86-64 PIE ELF (rev_0_2, not stripped) presents a series of prompts asking you to fill in fields describing a "path to flag" (a train/route record). It then asks you to concatenate everything you typed into a single "key" which is used to decrypt the flag. The flag is submitted via POST to the flag endpoint on rev-kids20.forkbomb.ru. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

The binary is a self-decrypting RC4 challenge. Relevant functions (symbols are present because the binary is not stripped):

  • read_user / read_train / read_station / read_path + create_* — build a nested struct hierarchy: path -> train -> user, two stations, and a travel duration.
  • check_path and its helpers check_path_begin_0, check_path_end_0, check_path_train_0, check_train_driver_0 — each entered field is validated against a hardcoded constant. Those constants ARE the intended field values; reversing the comparisons recovers every value directly.
  • prepare_key @ 0x1382 — RC4 KSA (key scheduling).
  • rc4 @ 0x1497 — RC4 PRGA.
  • drop_flag @ 0x1695 — opens the program's own binary file (fopen(argv[0], "rb")), reads the last 4 bytes as a little-endian ciphertext length (44), reads the ciphertext at filesize - 4 - 44, and runs the custom RC4 keyed by the raw user-typed key string. The plaintext is a 4-byte little-endian length prefix followed by the flag.
  • strtoupper @ 0x15c5 — exists in the binary but is NOT applied to the key (an intentional red herring; the key stays mixed-case). ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key insight: the RC4 key is the concatenation of ALL entered field values in entry/read order with no separator and no case change.

Recovered field values (from the check_*_0 validators)

FieldValue
rate4
train_numberS146
age60
nameSebastiano Riese
countryRohan
price2306
passengers_count955
begin station city / f2 / f372 / Mona / Mosapsa
end station city / f2 / f387 / Isontol / Hrotiecka
travel_time9912
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Concatenated key (entry order, no separator):

4S14660Sebastiano RieseRohan230695572MonaMosapsa87IsontolHrotiecka9912

Solution

Two equivalent paths were used:

(a) Dynamic replay — run the binary under Docker ubuntu:22.04 (amd64), enter exactly the expected field values, then at Enter key: paste the full concatenation and read Flag: ....

(b) Static offline — carve the 44-byte ciphertext from the tail of the binary (filesize - 4 - 44) and reproduce the custom RC4 (standard KSA + PRGA) against it, then strip the 4-byte length prefix. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

python
#!/usr/bin/env python3
import struct

def rc4(key, ct):
    key = key.encode() if isinstance(key, str) else key
    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]
    i = j = 0; out = bytearray()
    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])
    return bytes(out)

data = open("rev_0_2", "rb").read()
n = len(data)
clen = struct.unpack("<I", data[n-4:n])[0]          # 44
ct   = data[n-4-clen : n-4]                          # tail ciphertext
key  = "4S14660Sebastiano RieseRohan230695572MonaMosapsa87IsontolHrotiecka9912"
pt   = rc4(key, ct)
inner_len = struct.unpack("<I", pt[:4])[0]           # 40
print(pt[4:4+inner_len].decode())
# spbctf{REDACTED}

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

Both paths yield the same flag. The inner value REDACTED is the MD5 of the empty string — a nice sanity-check tell that the key concatenation and RC4 reproduction were correct. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering check_constant_extraction, rc4_ksa_prga_reimplementation, self_reading_binary, key_concatenation_recovery, tail_ciphertext_carving was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

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