BMP с монологом Гамлета: ключ в байтах выравнивания

стеганография · задача 13   bmppaddingпалитра

Маленький BMP 22 × 34 с палитрой. Выглядит как разноцветный шум.

Платформа
CTF (флаги вида DUCKERZ{…})
Носитель
BMP, 8 бит, палитра 256 цветов
Приём
ключ-алфавит в байтах выравнивания строк, текст — индексами в пикселях
Инструменты
xxd, Python
Статус
решено

Проверка заголовка: подмен нет

ПроверкаРасчёт
строка пикселей22 байта → выравнивание до 24 (кратно 4)2 лишних байта на строку
размер данных24 × 34 = 816✓ как в заголовке
смещение пикселей54 + 256 × 4 = 1078✓
размер файла1078 + 816 = 1894✓, хвоста нет

Что подозрительно

  • Случайная палитра (ca3d59, e9d841, 047f67…). В нормальной 8-битной картинке палитра упорядочена. Случайная означает, что автору было всё равно, как картинка выглядит: смысл в номерах цветов, а не в цветах.
  • Картинка крошечная: 748 пикселей — ровно размер короткого текста.
  • 2 байта выравнивания × 34 строки = 68 «ничейных» байт, которые просмотрщик никогда не показывает. В ASCII-колонке xxd каждые 24 байта видны пары ab, cd, ef…
131910160550110b11000142008805500061a62b10 байт — номера цветов (пиксели)+ выравнивание
Фрагмент строки: пиксели 19, 16, 5, 1, 11 — это s, p, e, a, k по алфавиту; в выравнивании лежат «a», «b»

Скрипт: отделить пиксели от выравнивания

import sys
d = open(sys.argv[1], 'rb').read()
off, w, h = 1078, 22, 34
row = (w + 3) // 4 * 4                                    # 24 байта на строку с выравниванием
rows = [d[off + i*row : off + (i+1)*row] for i in range(h)]

alphabet = b''.join(r[w:] for r in rows).decode()        # байты выравнивания
pixels = b''.join(r[:w] for r in rows[::-1])             # BMP хранится снизу вверх
print('алфавит:', alphabet)
print(''.join(' ' if p == 0 else alphabet[p - 1] for p in pixels))

Из выравнивания собирается алфавит abcdefghijklmnopqrstuvwxyz_{}ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789. Пиксель n — это n-я буква, 0 — пробел. Строки разворачиваем, потому что BMP хранит их снизу вверх. В порядке файла текст шёл кусками задом наперёд.

Получается наставление Гамлета актёрам («Speak the speech, I pray you…», акт 3, сцена 2), и посреди монолога вставлен флаг. Проверка гипотезы по формату флага: байты 21 32 20 28 22 2F 37 1C = номера 33, 50, 32, 40, 34, 47, 55, 28 → D U C K E R Z {.

показать флаг (спойлер)DUCKERZ{bmp_sh4k3sp34r_l33t_sp33ch}

Что вынести

  • В BMP есть «ничейные» байты: выравнивание строк, зарезервированные поля заголовка, промежуток между палитрой и пикселями.
  • Нашлась строка-алфавит — ищи числа, которые могут быть номерами в ней.
  • Формат флага (DUCKERZ{) — лучший способ проверить гипотезу на первых символах.