← Back to Writeups
HTBN/ASteganography

Незваный гоуст

XESXOR8/23/20265 min read
#steganography#htb#n/a

Незваный гоуст

Platform: Avitoctf | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-22 | Status: Solved Techniques: cell_centroid_extraction, gaussian_smoothing, local_background_subtraction

Summary

Task: A static Ghost Font PNG hides text in a regular field of jittered dots rather than conventional pixel bit planes. Solution: Measure each 8x8 cell's vertical ink centroid, smooth the feature grid, and subtract the local background.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: avitoctf | ID: 20260722_avitoctf_nezvanyy_goust
  • Tags: png, image_steganography, dot_grid, ghost_font
  • Indicators: regular 8x8 dot-cell lattice, message encoded by coherent dot motion, static frame resembles random noise
  • Source: 20260722_avitoctf_nezvanyy_goust.md

Foothold

Vulnerability / Misconfiguration

  1. Cell_centroid_extraction
  2. Gaussian_smoothing
  3. Local_background_subtraction
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • cell_centroid_extraction
  • gaussian_smoothing
  • local_background_subtraction
  • Tags: png, image_steganography, dot_grid, ghost_font

Original Writeup

<details><summary>Click to expand original content</summary>

Description

Пчёлы с Полянки 38 поставили на всех каналах сотовой связи СОРМ: ИИ читает переписку и смотрит картинки, докладывая о каждом упоминании мёда. Медоед-разведчик нашёл свежий сервис Ghost Font https://www.mixfont.com/ghost-font, который обещает делать текст нечитаемым для ИИ, и набрал им важное послание.

Правда, разбираться в инструкции он не стал: вместо видео прислал в штаб один скриншот и сразу отключил связь. До вылазки осталось полчаса, а на картинке только шум.

Разберите послание. Текст на картинке — флаг.

https://avitoctf.ru/files/ghost_screenshot.png Флаг в этом задании не в формате avito{...}, а фраза «WRITTEN IN GHOST FONT» добавляется сервисом на каждую картинку, это не секретное послание.

The supplied artifact is a single screenshot of a Ghost Font animation. The goal is to recover the hidden text from the apparent dot noise; the visible service attribution is an explicit decoy.

Analysis

ghost_screenshot.png is a 1918×997 RGBA PNG. Container and channel checks found no useful metadata, appended payload, varying alpha, or conventional sequential LSB stream. The image therefore had to be analyzed as the rendered output of Ghost Font rather than as an ordinary PNG bit-plane challenge.

Ghost Font forms glyphs through coherent dot movement while background dots move independently. Although motion is unavailable in one screenshot, each dot's instantaneous displacement remains measurable. The white canvas begins near (97, 40) and is 1712 pixels wide, exactly 214 × 8 pixels. Thresholding dark pixels and dividing the canvas into 8×8 cells produces a 119×214 grid with approximately one dot per cell.

For every cell, compute the centroid of its dark pixels. Dot area, covariance, shape, and horizontal centroid are dominated by background jitter, but the vertical centroid forms a weak spatially coherent signal. A small Gaussian blur suppresses independent noise and reveals two text lines. Subtracting a much broader blur removes gradual page-wide drift and improves contrast:

signal = GaussianBlur(vertical_centroid, 1.2)
       - GaussianBlur(vertical_centroid, 7.0)

The result contains leetspeak-like glyphs that can be mistaken for letters. Since the task requires the text exactly as rendered, preserve the confirmed transcription rather than normalizing ambiguous characters.

Solution

  1. Crop the visible Ghost Font canvas at (97, 40, 1809, 997).
  2. Convert it to grayscale and classify pixels below intensity 100 as ink.
  3. Trim the crop to complete 8×8 cells and reshape it into a dot-cell grid.
  4. Compute each cell's vertical dark-pixel centroid.
  5. Smooth the centroid map with sigma ≈ 1.2–1.4.
  6. Optionally subtract a sigma = 7.0 background estimate.
  7. Crop the two glyph rows, enlarge with nearest-neighbor interpolation, and read the text without character normalization.

The following concise solver reproduces the high-pass proof image:

#!/usr/bin/env python3
from pathlib import Path

import cv2
import numpy as np
from PIL import Image

root = Path(__file__).resolve().parent
rgba = np.asarray(Image.open(root / "ghost_screenshot.png").convert("RGBA"))

# Ghost Font canvas: 214 complete columns of 8x8 dot cells.
x0, y0, x1, y1 = 97, 40, 1809, 997
gray = cv2.cvtColor(rgba[y0:y1, x0:x1, :3], cv2.COLOR_RGB2GRAY)
ink = (gray < 100).astype(np.uint8)

h = ink.shape[0] // 8 * 8
w = ink.shape[1] // 8 * 8
cells = ink[:h, :w].reshape(h // 8, 8, w // 8, 8).transpose(0, 2, 1, 3)

yy = np.mgrid[:8, :8][0]
area = cells.sum(axis=(2, 3)).astype(np.float32)
cy = (cells * yy).sum(axis=(2, 3)) / np.maximum(area, 1)

fine = cv2.GaussianBlur(cy.astype(np.float32), (0, 0), 1.2)
coarse = cv2.GaussianBlur(cy.astype(np.float32), (0, 0), 7.0)
decoded = (fine - coarse)[24:89, 12:207]

lo, hi = np.percentile(decoded, (3, 99))
decoded = np.clip((decoded - lo) * 255 / (hi - lo), 0, 255).astype(np.uint8)
decoded = cv2.resize(decoded, None, fx=8, fy=8, interpolation=cv2.INTER_NEAREST)
Image.fromarray(decoded).save(root / "decoded_phrase_highpass.png")

Evidence and Artifacts

  • ghost_screenshot.png — original 1918×997 RGBA challenge artifact.
  • analyze_ghost.py — complete feature extraction and proof-image generator.
  • decoded_phrase.png — vertically smoothed centroid map.
  • decoded_phrase_highpass.png — local-background-subtracted map; some glyphs remain visually ambiguous, so exact transcription must not be inferred through normalization.
  • notes.md — reconnaissance, failed families, and extraction findings.
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR