← Back to Writeups
CUSTOMMediumSteganography

Вокруг шум (Noise Around)

XESXOR9/2/20264 min read
#steganography#custom#medium#bit_position_analysis#image_diff_lsb#sparse_grid_sampling#stride_discovery_autocorrelati#utf8_bit_packing

Вокруг шум (Noise Around)

Platform: bug-makers | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2025-10-04 | Status: Solved Techniques: bit_position_analysis, image_diff_lsb, sparse_grid_sampling, stride_discovery_autocorrelation, utf8_bit_packing

Summary

Task: a noise PNG with LSB-hidden song lyrics and flag; organizers also provided the original clean image. Solution: diff the two images to find ±1 LSB changes confined to the Red channel on a 18-px column grid, then use autocorrelation to discover a 7-cell stride and decode every 7th grid LSB as big-endian UTF-8.

Вокруг шум (Noise Around) — bug-makers

Description

Вокруг шум. Где-то тут спрятан текст песни, ну и флаг заодно.

English: "Noise around. Somewhere here the lyrics of a song are hidden, and the flag too." A modified PNG (noise_around.png, 2160×2160 RGB) that looks like pure visual noise is provided. An update note explained that the article hosting the original picture was deleted, so the organizers attached the ORIGINAL clean image separately (original_noise.zip → unnamed_large.png, same 2160×2160 noise base). The goal is to recover the hidden song lyrics and the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

The cover image is genuine noise, so blind LSB extraction over the whole image is hopeless (≈50% of LSBs are random by construction). The decisive clue is that both the modified and the original images are given — this is image-diff steganography: the hidden data lives in the per-pixel difference between the two images.

Computing modified - original per channel revealed a very structured pattern:

  • Differences occur only in the Red channel.
  • Every difference is exactly ±1 (classic LSB modification).
  • Changed pixels appear only at columns where x ≡ 0 mod 18 — a regular grid of 120 columns (x = 0, 18, 36, …, 2142), across all 2160 rows.
  • Total changed pixels: 18703. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

So the embedding uses a sparse sampling grid: the Red-channel LSB of every 18th column, for every row, giving a 2160 × 120 = 259200-bit grid in row-major order.

Finding the stride (the breakthrough)

Decoding all 259200 grid LSBs directly as 7-bit or 8-bit ASCII gave ~75% printable but pure gibberish. The diff bitstream itself (modified_LSB XOR original_LSB) held the answer:

  • Reshaping the diff bitstream to width 7 showed ones only in bit position 0 — columns 1–6 of every 7-group were always zero.
  • Equivalently, the autocorrelation of the diff bitstream peaked strongly at lag 7 (and at its multiples 14, 21, 35, 84, 98, 105, …). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This proves the message bits are written into every 7th grid cell in raster order. Slicing the grid LSB stream with ::7 yields 259200 / 7 ≈ 37029 message bits. Packed 8-per-byte big-endian (MSB-first) and decoded as UTF-8, they produce the full Cyrillic lyrics — the song "Вокруг шум" by the band Каста (Kasta) — with the flag embedded at the start.

Solution

  1. Recognize image-diff LSB stego because both modified and original images are provided.
  2. Diff them: changes are Red-channel only, always ±1, only at columns x % 18 == 0.
  3. Sample the modified Red-channel LSB on that 18-px column grid, row-major → 259200 bits.
  4. Discover the 7-cell stride via width-7 reshape / autocorrelation of the diff stream (peak at lag 7).
  5. Take every 7th grid LSB (::7) → ~37029 bits.
  6. Pack 8 bits/byte big-endian, decode UTF-8 → Cyrillic lyrics + flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Minimal solver

python
#!/usr/bin/env python3
import re
import numpy as np
from PIL import Image

a = np.array(Image.open('noise_around.png'))

# Red-channel LSB sampled every 18 columns, row-major, then every 7th bit.
bits = (a[:, 0:2160:18, 0] & 1).flatten()[::7]
data = np.packbits(bits[: len(bits) // 8 * 8]).tobytes()  # MSB-first / big-endian

text = data.decode('utf-8', 'ignore')
print(text)

m = re.search(rb'BugCTF\{[^}]+\}', data)
print(m.group().decode())

The flag also pops out directly via the regex rb'BugCTF\{[^}]+\}' over the decoded bytes. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Failed approaches (lessons learned)

  • Whole-image LSB over every pixel/all channels, both bit orders → noise. The cover is a real noise image with ~50% random LSBs.
  • Diff MASK (changed=1 / unchanged=0) as the bitstream → ~14% printable, not text.
  • Diff SIGN (+1→1, −1→0) at changed positions as the message → noise.
  • Modified grid LSB decoded directly as 7-bit/8-bit ASCII without the ::7 stride → ~75% printable but gibberish.
  • The unlock was the width-7 / autocorrelation analysis of the diff stream, which exposed the every-7th-cell stride. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍