← Back to Writeups
CUSTOMMediumSteganography

Attentive Reader

XESXOR9/2/20264 min read
#steganography#custom#medium#source_comparison#textual_diff_analysis#case_based_morse_decoding#homoglyph_detection#linux

Attentive Reader

Platform: hackerlab | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-08 | Status: Solved Techniques: source_comparison, textual_diff_analysis, case_based_morse_decoding, homoglyph_detection

Summary

Task: a Gutenberg-style HTML ebook hides data in tiny source-level edits rather than visible rendered content. Solution: compare the challenge file with the authentic Gutenberg original, group modified letters between inserted double spaces, and decode uppercase/lowercase patterns as Morse to recover the flag text.

Attentive Reader — Hackerlab

Description

«Сможешь ли ты увидеть то, что другие не замечают?»

English summary: the archive contains a single HTML file that renders as an ordinary Project Gutenberg ebook. The real payload is hidden in subtle source-level changes, so the goal is to compare the challenge HTML with the authentic original and recover the CODEBY{...} flag.

Analysis

The provided task.htm looked like a normal copy of The Advancement of Learning by Francis Bacon from Project Gutenberg. Viewing it in a browser did not reveal anything unusual, and basic source inspection did not show a flag string, hidden CSS payload, or an appended blob. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The challenge hint suggested that the important part was not obvious visual content but very small differences. That made this a source-comparison task rather than a rendered-page stego task.

Key insight: compare against the authentic Gutenberg file

The decisive step was fetching the original legacy Gutenberg HTML for ebook 5500 and comparing it with the challenge file:

  • challenge file: task.htm
  • reference file: authentic Gutenberg HTML / local reference original_old.htm

Once the two files were diffed, the hidden pattern became visible. The challenge author had made many tiny edits across the text: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  • selected letters were flipped between uppercase and lowercase
  • extra double spaces were inserted to separate groups
  • one word used a mixed-script homoglyph: сallisthenes begins with Cyrillic с, not Latin c

These edits are easy to miss when reading normally, but they are obvious once you inspect a diff at the source level.

How the encoding works

The modified characters between inserted double spaces form groups. Inside each group:

  • uppercase letter = .
  • lowercase letter = -

That turns each group into a Morse symbol. Decoding all groups yields: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

text
REDACTED

Wrapping it in the required format gives the final flag.

Solution

Step 1: obtain a clean reference

Download the original Gutenberg HTML for the same ebook, or use the saved local copy:

bash
wget https://www.gutenberg.org/files/5500/5500-h/5500-h.htm -O original.htm

Step 2: compare the challenge source with the original

Run a diff between task.htm and the authentic file. The important observation is that the page content is mostly unchanged, but many tiny character-level edits appear throughout the source. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

What to look for:

  • letters with unexpected case changes
  • inserted double spaces
  • suspicious non-Latin lookalikes inside English words

Step 3: split the modified stream into groups

Treat the inserted double spaces as separators. For each separated group, keep the changed letters only.

Example rule:

  • A → .
  • a → -

Convert every group from case pattern to Morse, then decode the Morse stream to text.

Step 4: decode the message and build the flag

The decoded plaintext is:

text
REDACTED

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

Final flag:

text
CODEBY{REDACTED}

Short reproducible extractor

python
#!/usr/bin/env python3
from pathlib import Path
from difflib import SequenceMatcher

MORSE = {
    ".-": "A", "-...": "B", "-.-.": "C", "-..": "D", ".": "E", "..-.": "F",
    "--.": "G", "....": "H", "..": "I", ".---": "J", "-.-": "K", ".-..": "L",
    "--": "M", "-.": "N", "---": "O", ".--.": "P", "--.-": "Q", ".-.": "R",
    "...": "S", "-": "T", "..-": "U", "...-": "V", ".--": "W", "-..-": "X",
    "-.--": "Y", "--..": "Z",
    ".----": "1", "..---": "2", "...--": "3", "....-": "4", ".....": "5",
    "-....": "6", "--...": "7", "---..": "8", "----.": "9", "-----": "0",
}
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

orig = Path("original_old.htm").read_text(encoding="utf-8")
mod = Path("task.htm").read_text(encoding="utf-8")

groups = []
current = []
sm = SequenceMatcher(a=orig, b=mod)

for tag, i1, i2, j1, j2 in sm.get_opcodes():
    if tag == "equal":
        if "  " in mod[j1:j2] and current:
            groups.append("".join(current))
            current = []
        continue

    changed = [ch for ch in mod[j1:j2] if ch.isalpha()]
    current.extend(changed)

    if "  " in mod[j1:j2] and current:
        groups.append("".join(current))
        current = []
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

if current:
    groups.append("".join(current))

symbols = ["".join("." if ch.isupper() else "-" for ch in group) for group in groups]
decoded = "".join(MORSE[s] for s in symbols)
print(decoded)
print(f"CODEBY{{{decoded}}}")

Expected output:

text
REDACTED
CODEBY{REDACTED}

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