← Back to Writeups
CUSTOMMediumReversing

solidartnost

XESXOR9/2/20264 min read
#reversing#custom#medium#appjit_string_pool_recovery#docker_linux_emulation#osint_url_identification#rc4_keystream_recognition#sha1_gate_bypass#snapshot_constant_patching

solidartnost

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: appjit_string_pool_recovery, docker_linux_emulation, osint_url_identification, rc4_keystream_recognition, sha1_gate_bypass, snapshot_constant_patching

Summary

Walkthrough of the CUSTOM Reversing solidartnost. Task: a Dart 2.7.2 AppJIT snapshot with an interactive menu that gates flag decryption behind three answer checks (file size, COVID petition URL, secret SHA1). Solution: recover logic from the snapsho

Description

solidartnost.jit — установите Dart 2.7.2 из stable .deb и запустите dart solidartnost.jit.

A single artifact solidartnost.jit (4,494,096 bytes) is given, intended to be run with dart solidartnost.jit using Dart SDK 2.7.2. The program is an interactive menu themed around Russian civic petitions ("5 шагов" / "5 steps", solidarity). The goal is to recover the flag printed by menu option 3. Get flag.

The menu:

1. Verify integrity of the running file
2. Sign a petition
3. Get flag
4. Don't sign a petition
0. Exit

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

Analysis

Identifying the file

$ file solidartnost.jit
solidartnost.jit: data
$ xxd solidartnost.jit | head -1
00000000: dcdc f6f6 ...

Magic bytes dcdcf6f6 + x86-64 + the size mark it as a Dart AppJIT snapshot, SDK 2.7.2. The key property of AppJIT snapshots: they retain all class/method/string-literal names in a readable literal pool, so strings recovers almost the entire program logic.

Recon via strings

strings/grep over the snapshot reveals:

  • Source path file:///mnt/f/solidartnost/bin/main.dart
  • Functions: printGreeting, printMenu, verifyIntegrity, signAPetition, getFlag, dontSignAPetition, main
  • Option 1 (verifyIntegrity): checks full file size 4494096 and sha1 of the first 1,000,000 bytes = 8e7aa1e230a9efb83daaa1c81c4c52ebd320a6e9
  • Option 2 (signAPetition) petList URLs:
    • http://ion.ranepa.ru/school/5steps/
    • http://5steps.academy/
    • https://5steps.vote/
    • http://www.cfkadm.ru/вопрос-ответ/5steps/
  • Option 3 (getFlag): suspicious literals encFlag, state, IJ& (bytes 0x49 0x4A 0x26), plus prompt strings like ...Continuing to decrypt flag... and Flag: . ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The names encFlag, state, and IJ& (i, j, and & 0xFF) are the tell-tale signature of an RC4-style keystream XOR: an encFlag ciphertext blob, the RC4 state[256] array, the i/j indices, and the & 0xFF mask.

The getFlag gate (3 questions)

getFlag asks 3 questions before decrypting:

  1. "What was the file size checked against in menu option 1?" → 4494096
  2. "What was the choice related to COVID-19 in menu option 2?" → https://5steps.vote/ (determined by OSINT — fetching each petition URL; only 5steps.vote references коронавирус / COVID-19)
  3. "What is the secret string you need to input in menu option 3?" → checked by sha1(answer) == 88da38df84eef80de7266fb342a6c8c6c6a42c47 (literal correctSha1). This hash was NOT in rockyou and was never cracked — it turned out to be irrelevant. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Running the unmodified binary in a Dart 2.7.2 container and answering Q1=4494096, Q2=https://5steps.vote/ confirms both ([+] Correct.). Only Q3 blocks progress.

Key insight: the decryption key is hardcoded

The decisive hypothesis test: do the three answers feed the RC4 key, or do they only gate access?

Static reasoning suggested the key is a constant. This was proven empirically by patching the stored correctSha1 ASCII-hex string (40 bytes at file offset 0xcf262) to sha1(<arbitrary>), then answering Q3 with that arbitrary value. The flag printed was identical regardless of the Q3 value — proving the key is hardcoded and the gate inputs are never mixed into the keystream. This denies "key = sha1(secret)" and "key = concatenated answers", and confirms "patch the gate, dump the flag". ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Step 1 — Patch the SHA1 gate constant

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

data = bytearray(open('solidartnost.jit', 'rb').read())
off = 0xcf262                       # offset of correctSha1 (40 ASCII hex chars) in the snapshot
secret = b'verify123'               # any value we control
data[off:off+40] = hashlib.sha1(secret).hexdigest().encode()
open('verify.jit', 'wb').write(data)
print('patched -> verify.jit, Q3 answer is now:', secret.decode())

Step 2 — Run under Dart 2.7.2 (linux/amd64 Docker)

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

On a non-Linux host, run inside an ubuntu:20.04 linux/amd64 container with Dart 2.7.2 installed (image tagged dartctf):

bash
printf '3\n4494096\nhttps://5steps.vote/\nverify123\n0\n' | \
  docker run --rm -i --platform linux/amd64 -v "$PWD":/work -w /work \
  dartctf dart verify.jit

Output:

[+] Correct.        # Q1: file size 4494096
[+] Correct.        # Q2: COVID-19 petition https://5steps.vote/
[+] Correct!        # patched Q3 gate
[.] Decrypting flag
[.] Continuing to decrypt flag
[.] Proceeding to output the decrypted flag
Flag: spbctf{REDACTED}

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

The RC4 keystream uses a hardcoded key, so bypassing the gate is sufficient — no key recovery required. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering appjit_string_pool_recovery, docker_linux_emulation, osint_url_identification, rc4_keystream_recognition, sha1_gate_bypass, snapshot_constant_patching was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-06-25 • CUSTOM • solidartnost • Difficulty: Medium