white_noise_letter: буквы в разнице кадров
GIF 480 × 270, 21 МБ, 120 кадров по 0,08 с. В описании анекдот: «Штирлиц включил телевизор. По телевизору пошёл белый шум. Штирлиц возмутился и попросил не ходить по телевизору».
- Платформа
- CTF (флаги вида DUCKERZ{…})
- Носитель
- анимированный GIF
- Приём
- буквы видны только в разнице двух соседних кадров
- Инструменты
- Python (Pillow, NumPy), ImageMagick
- Статус
- решено
Ложные срабатывания
binwalk нашёл внутри «JBOOT STAG header» и «сертификат DER», да ещё образ на 2,8 ГБ внутри файла на 21 МБ. Это мусор: 21 МБ почти случайных байт неизбежно содержат последовательности, похожие на чьи-то сигнатуры. Абсурдный размер находки — верный признак ложного срабатывания.
Подсказки
letterв имени файла — «письмо», а Штирлиц — разведчик: в шуме есть послание.- «Не ходить по телевизору» — каламбур: в шуме что-то ходит, то есть двигается.
Гипотезы и их проверка одним скриптом
Если по отдельности кадры — шум, текст может проявиться: в среднем всех кадров (если буквы неподвижны), в неподвижных пикселях или в разнице соседних кадров. Скрипт gif_noise.py проверял всё сразу. Сработала разница: каждая пара соседних кадров давала одну букву, причём каждый раз в новом месте кадра. Вот это и «ходит по телевизору».
Я собрал такой же GIF сам, только короче: 9 кадров, слово «HEXNOTES».
анимация
любой отдельный кадр — шум
Как такое делается: следующий кадр = предыдущий XOR маска буквы. Тогда кадр[i] ⊕ кадр[i+1] = маска буквы, а каждый кадр сам по себе остаётся случайным.
Скрипт: все XOR одной сеткой
import sys, os
import numpy as np
from PIL import Image, ImageSequence, ImageDraw
fr = [np.array(f.convert('L'), dtype=np.uint8) for f in ImageSequence.Iterator(Image.open(sys.argv[1]))]
tiles = []
for i in range(len(fr) - 1):
t = Image.fromarray(np.bitwise_xor(fr[i], fr[i + 1])).resize((240, 135))
ImageDraw.Draw(t).text((3, 2), str(i), fill=255) # номер пары в углу
tiles.append(t)
cols = 10
sheet = Image.new('L', (cols * 242, (len(tiles) + cols - 1) // cols * 137))
for k, t in enumerate(tiles):
sheet.paste(t, ((k % cols) * 242, (k // cols) * 137))
sheet.save('xor_sheet.png')
В задаче сетка читалась слева направо, сверху вниз. Флаг повторялся по кругу (120 кадров на 43 символа), а чёрные клетки — пары одинаковых кадров, то есть паузы между буквами.
показать флаг (спойлер)
DUCKERZ{Wh1t3_N0is3_fr0m_th3_4byss_A6F32GB}Что вынести
- Для анимаций базовый набор: задержки кадров (как ASCII), среднее, неподвижные пиксели, XOR соседних кадров.
convert … -coalesceпри ручной разборке GIF обязателен: кадр GIF может хранить только изменившийся кусок.