solidartnost
solidartnost
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-25 | Status: Solved Techniques: appjit_string_pool_recovery, docker_linux_emulation, osint_url_identification, rc4_keystream_recognition, sha1_gate_bypass, snapshot_constant_patching
Summary
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 snapshot literal pool, prove the RC4 decryption key is hardcoded (gate inputs do not feed the key), patch the stored correctSha1 constant to bypass the SHA1 gate, and run under Dart 2.7.2 in a linux/amd64 Docker container to dump the flag.
solidartnost — SPbCTF
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 size4494096andsha1of the first 1,000,000 bytes =8e7aa1e230a9efb83daaa1c81c4c52ebd320a6e9 - Option 2 (
signAPetition)petListURLs:http://ion.ranepa.ru/school/5steps/http://5steps.academy/https://5steps.vote/http://www.cfkadm.ru/вопрос-ответ/5steps/
- Option 3 (
getFlag): suspicious literalsencFlag,state,IJ&(bytes0x49 0x4A 0x26), plus prompt strings like...Continuing to decrypt flag...andFlag:.
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:
- "What was the file size checked against in menu option 1?" →
4494096 - "What was the choice related to COVID-19 in menu option 2?" →
https://5steps.vote/(determined by OSINT — fetching each petition URL; only5steps.votereferences коронавирус / COVID-19) - "What is the secret string you need to input in menu option 3?" → checked by
sha1(answer) == 88da38df84eef80de7266fb342a6c8c6c6a42c47(literalcorrectSha1). 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 read-only • copy allowed#!/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 read-only • copy allowedprintf '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.