← Back to Writeups
CUSTOMEasyCrypto

Сокровище 2 (Treasure 2)

XESXOR9/10/20265 min read
#crypto#custom#easy#book_cipher_decryption#page_offset_calculation#pdf_text_extraction#linux

Сокровище 2 (Treasure 2)

Platform: hackerlab | Category: Crypto | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-01-06 | Status: Solved Techniques: book_cipher_decryption, page_offset_calculation, pdf_text_extraction

Summary

Task: Decrypt a message using a book cipher with coordinates (page, line, character) referencing an Eloquent JavaScript PDF. Solution: Extract text from PDF pages with correct offset, count characters without spaces to decode the flag.

Сокровище 2 (Treasure 2) — Hackerlab

Description

"Что ж, спасибо тебе! После получения огромного наследства у меня достаточное количество свободного времени. Я поделюсь с тобой богатством, но взамен возьму самое ценное у тебя - время. Конечно же, если сможешь взломать мой шифр. Думаю, что тебе понадобиться ещё вот эта книжка..."

Files:

  • treasure_2.zip contains:
    • Записка.txt — encrypted message
    • Книга JavaScript.pdf — book "Eloquent JavaScript" 4th edition

Analysis

1. Extracting the archive

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

The archive contains files with Russian names in CP866 encoding:

bash
unzip -O CP866 treasure_2.zip

2. Note contents

The file Записка.txt contains numbers in "page line character" format:

30 1 25
170 9 1
216 5 18
216 9 1
40 1 1
140 12 1 
348 5 7
3 3 16
271 7 42
6 1 3
11 2 4
22 1 4
271 7 42
24 1 26
22 1 4
24 1 29
22 1 4
218 3 1
216 1 16
271 7 42
395 5 1
277 2 4
19 5 16
348 6 1

3. Cipher identification

This is a Book Cipher — a classical cipher where each character of the message is encoded by coordinates in a book:

  • First number — page number
  • Second number — line number on the page
  • Third number — character position in the line ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Key features

  1. PDF page offset: Book page 1 starts at PDF page 13 (offset = 12)
  2. Character counting: Character position is counted WITHOUT spaces
  3. Empty lines: On some pages, empty lines need to be skipped
  4. Indexing: All numbers are 1-indexed

Solution script

python
#!/usr/bin/env python3
"""
Book Cipher Decoder for Treasure 2
Uses pdftotext to extract text from Eloquent JavaScript PDF
"""

import subprocess
import sys

# Cipher text: (page, line, char_position)
cipher = [
    (30, 1, 25),
    (170, 9, 1),
    (216, 5, 18),
    (216, 9, 1),
    (40, 1, 1),
    (140, 12, 1),
    (348, 5, 7),
    (3, 3, 16),
    (271, 7, 42),
    (6, 1, 3),
    (11, 2, 4),
    (22, 1, 4),
    (271, 7, 42),
    (24, 1, 26),
    (22, 1, 4),
    (24, 1, 29),
    (22, 1, 4),
    (218, 3, 1),
    (216, 1, 16),
    (271, 7, 42),
    (395, 5, 1),
    (277, 2, 4),
    (19, 5, 16),
    (348, 6, 1),
]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

PDF_FILE = "Книга JavaScript.pdf"
PAGE_OFFSET = 12  # Book page 1 = PDF page 13

def get_page_text(pdf_page):
    """Extract text from specific PDF page using pdftotext"""
    result = subprocess.run(
        ['pdftotext', '-layout', '-f', str(pdf_page), '-l', str(pdf_page), PDF_FILE, '-'],
        capture_output=True, text=True
    )
    return result.stdout

def decode_char(book_page, line_num, char_pos):
    """Decode single character from book coordinates"""
    pdf_page = book_page + PAGE_OFFSET
    text = get_page_text(pdf_page)
    
    # Split into lines, filter empty for certain pages
    lines = text.split('\n')
    
    # For page 271, skip empty lines
    if book_page == 271:
        lines = [l for l in lines if l.strip()]
    
    if line_num > len(lines):
        return '?'
    
    line = lines[line_num - 1]  # 1-indexed
    
    # Remove spaces for character counting
    line_no_spaces = line.replace(' ', '')
    
    if char_pos > len(line_no_spaces):
        return '?'
    
    return line_no_spaces[char_pos - 1]  # 1-indexed
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Decode the message
message = ''
for page, line, char in cipher:
    c = decode_char(page, line, char)
    message += c
    print(f"Page {page}, Line {line}, Char {char}: '{c}'")

print(f"\nDecoded message: {message}")

Decoding process

PageLineCharacterResultSource
30125C"Chapter"
17091O"Often"
216518D"DOM"
21691Eline start
4011Bline start
140121Yline start
34857{character in code
3316ufrom text
271742_underscore
613afrom word
1124rfrom word
2214efrom word
271742_underscore
24126lfrom word
2214efrom word
24129gfrom word
2214efrom word
21831nline start
216116dfrom word
271742_underscore
39551bline start
27724rfrom word
195160digit zero
34861}closing brace
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Pitfalls

  1. Page offset: In PDF, the book may not start from the first page (table of contents, preface)
  2. Character counting: May count with or without spaces
  3. Empty lines: May be counted or skipped
  4. File encoding: Russian filenames in ZIP may be in CP866
  5. PDF format: Use pdftotext -layout to preserve line structure

Alternative Approaches

  • Manual search through PDF (for short messages)
  • Using PyPDF2/pdfplumber instead of pdftotext
  • OCR if PDF contains scans ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍