← Back to Writeups
CUSTOMMediumCrypto

In love with space

XESXOR9/2/20264 min read
#crypto#custom#medium#image_based_substitution#known_plaintext_attack#frequency_analysis#context_deduction

In love with space

Platform: CUSTOM | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: image_based_substitution, known_plaintext_attack, frequency_analysis, context_deduction

Summary

Walkthrough of the CUSTOM Crypto In love with space. - PDF file with a grid of cat images (158 images, 35 unique) - Known plaintext: message starts with "письмо от" (letter from) - Flag format: spbctf{russianletters}

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
#!/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
# 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
known_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
from 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:

  1. "космос" → к (seeing pattern о_мо)
  2. "доступ" → д, у (context "пароль доступа")
  3. "получили" → л, ч (context "мы получили")
  4. "запрос" → з, а, р
  5. "информации" → ф, ц, н
  6. "грифом" → г
  7. "ваш" → в, ш
  8. "ноября" → я, б
  9. "день" → е
  10. "добрый" → ы, й
  11. "ждет"/"любит" → ж, ю

Step 6: Full decryption

python
# 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
#!/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")

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

Key Takeaways / Lessons

  • Mastering image_based_substitution, known_plaintext_attack, frequency_analysis, context_deduction was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-03-09 • CUSTOM • In love with space • Difficulty: Medium