Attentive Reader
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:
сallisthenesbegins with Cyrillicс, not Latinc
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 read-only • copy allowedREDACTED
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 read-only • copy allowedwget 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 read-only • copy allowedREDACTED
Final flag:
text read-only • copy allowedCODEBY{REDACTED}
Short reproducible extractor
python read-only • copy allowed#!/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 read-only • copy allowedREDACTED CODEBY{REDACTED}