In love with space
In love with space
Platform: crypto-kids20 | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-09 | Status: Solved Techniques: image_based_substitution, known_plaintext_attack, frequency_analysis, context_deduction
Summary
- PDF file with a grid of cat images (158 images, 35 unique) - Known plaintext: message starts with "письмо от" (letter from) - Flag format: spbctf{russianletters}
In love with space — crypto-kids20 (forkbomb.ru)
Description
"Мы получили письмо от Ыжика, но не смогли его расшифровать! Помоги нам."
- PDF file with a grid of cat images (158 images, 35 unique)
- Known plaintext: message starts with "письмо от" (letter from)
- Flag format: spbctf{russianletters}
Analysis
This is a substitution cipher where each unique cat image represents a Russian letter or space.
Key observations:
- 158 images in a 12-column grid (13 full rows + 1 partial)
- 35 unique images = 33 Russian alphabet letters + space + possibly ё
- Known text start: "письмо от"
Solution
Step 1: Extracting images from PDF
Using PyMuPDF to extract all images:
python read-only • copy allowed#!/usr/bin/env python3 import fitz # PyMuPDF import hashlib from collections import defaultdict # Open PDF doc = fitz.open("lovespace.pdf") page = doc[0] # Extract images images = [] for img_index, img in enumerate(page.get_images()): xref = img[0] base_image = doc.extract_image(xref) image_bytes = base_image["image"] img_hash = hashlib.md5(image_bytes).hexdigest() images.append(img_hash) print(f"Total images: {len(images)}") print(f"Unique images: {len(set(images))}")
Step 2: Building the correspondence map
Creating a mapping of image hashes to cipher IDs:
python read-only • copy allowed# Assign ID to each unique image unique_hashes = [] hash_to_id = {} cipher_sequence = [] for h in images: if h not in hash_to_id: hash_to_id[h] = len(unique_hashes) unique_hashes.append(h) cipher_sequence.append(hash_to_id[h]) print(f"Cipher sequence: {cipher_sequence[:20]}...")
Step 3: Known-plaintext attack
Known that text starts with "письмо от":
python read-only • copy allowedknown_plaintext = "письмо от" # First 9 characters give us mapping for: п, и, с, ь, м, о, space, т # From cipher_sequence[:9] we get correspondences: # cipher[0] -> п # cipher[1] -> и # cipher[2] -> с # cipher[3] -> ь # cipher[4] -> м # cipher[5] -> о # cipher[6] -> space # cipher[7] -> о (already known) # cipher[8] -> т
Step 4: Frequency analysis
Analyzing the frequency of each image:
python read-only • copy allowedfrom collections import Counter freq = Counter(cipher_sequence) # Most frequent (20 times) = space # Second most frequent (16 times) = о (most common vowel in Russian)
Step 5: Context analysis
Gradually recovering letters from context:
- "космос" → к (seeing pattern о_мо)
- "доступ" → д, у (context "пароль доступа")
- "получили" → л, ч (context "мы получили")
- "запрос" → з, а, р
- "информации" → ф, ц, н
- "грифом" → г
- "ваш" → в, ш
- "ноября" → я, б
- "день" → е
- "добрый" → ы, й
- "ждет"/"любит" → ж, ю
Step 6: Full decryption
python read-only • copy allowed# Final mapping cipher_id -> letter mapping = { 0: 'п', 1: 'и', 2: 'с', 3: 'ь', 4: 'м', 5: 'о', 6: ' ', 7: 'т', 8: 'ё', 9: 'ж', 10: 'н', 11: 'я', 12: 'б', 13: 'р', 14: 'к', 15: 'д', 16: 'е', 17: 'ы', 18: 'й', 19: 'л', 20: 'ч', 21: 'в', 22: 'а', 23: 'ш', 24: 'з', 25: 'у', 26: 'ф', 27: 'ц', 28: 'г', 29: 'ю', # etc. } plaintext = ''.join(mapping[c] for c in cipher_sequence) print(plaintext)
Decrypted text:
письмо от ёж ноября ёжикдобрый день ыжикмы получили ваш запрос на доступ к информации с грифом секретной пароль доступа сменили на REDACTED
Step 7: Extracting the password
The text says "пароль доступа сменили на" (password changed to) followed by: REDACTED
This means: космос нас ждет космос нас любит (space awaits us space loves us)
Full solve script
python read-only • copy allowed#!/usr/bin/env python3 """ Solve script for "In love with space" (lovespace) crypto-kids20 @ forkbomb.ru """ import fitz import hashlib from collections import Counter def solve(pdf_path): # Extract images doc = fitz.open(pdf_path) page = doc[0] images = [] for img in page.get_images(): xref = img[0] base_image = doc.extract_image(xref) img_hash = hashlib.md5(base_image["image"]).hexdigest() images.append(img_hash) # Map to cipher IDs unique_hashes = [] hash_to_id = {} cipher = [] for h in images: if h not in hash_to_id: hash_to_id[h] = len(unique_hashes) unique_hashes.append(h) cipher.append(hash_to_id[h]) # Known plaintext: "письмо от" known = "письмо от" mapping = {} for i, char in enumerate(known): mapping[cipher[i]] = char # Frequency analysis freq = Counter(cipher) most_common = freq.most_common() # Most frequent = space (already mapped from position 6) # Second most = 'о' (already mapped from position 5) # Manual analysis to complete mapping... # (In practice, iterate and deduce from context) # Final decryption plaintext = ''.join(mapping.get(c, '?') for c in cipher) # Find password after "сменили на" idx = plaintext.find("сменили на") if idx != -1: password = plaintext[idx + len("сменили на"):].strip() print(f"Flag: spbctf{{{password}}}") return plaintext if __name__ == "__main__": solve("lovespace.pdf")