Files
Serdtse-Sysoly/finals/kot1/WRITEUP.md
T
2026-09-17 00:50:07 +03:00

4.0 KiB

В глазах всё написано

Stego

На старте есть RGB-изображение kot1_v_glazah.png размером 1254 × 1254. Начинаем с текстовых чанков PNG. Запись EditorNote описывает работу редактора с блоками 2 × 2.

Решение

Читаем метаданные через Pillow:

from PIL import Image

img = Image.open("public/kot1_v_glazah.png")
for key, value in img.info.items():
    print(key, value, sep="\n")

Из заметки узнаём, что редактор только переставляет четыре пикселя внутри блока, не меняя их цвета. Пригодны блоки, в которых все цвета различны, а разброс каждого канала не превышает 3. После перестановки блок остаётся пригодным по тем же признакам.

Обходим сетку от (0, 0) по строкам, слева направо. Для каждого пригодного блока сортируем цвета по ключу:

(77 * R + 150 * G + 29 * B, R, G, B)

Так получаем канонические цвета C0, C1, C2, C3. Позиции в блоке нумеруем отдельно: 0 — сверху слева, 1 — сверху справа, 2 — снизу слева, 3 — снизу справа. Перестановку записываем как позиции канонических цветов: где находится C0, затем C1, C2 и C3.

Например, если в порядке чтения блока стоят C2, C0, C3, C1, нужный кортеж — (1, 3, 0, 2). Кортеж номеров цветов (2, 0, 3, 1) задаёт обратную перестановку и даст другой полубайт.

У четырёх элементов 4! = 24 перестановки. Нумеруем их лексикографически. Первые 16 номеров кодируют значения полубайта от 0 до 15.

from itertools import permutations

catalog = list(permutations(range(4)))
# px — четыре RGB-пикселя в порядке TL, TR, BL, BR.
positions = tuple(sorted(
    range(4),
    key=lambda i: (77 * int(px[i][0]) + 150 * int(px[i][1]) + 29 * int(px[i][2]),
                   *map(int, px[i])),
))
nibble = catalog.index(positions)

Первые 16 пригодных блоков должны дать контрольный ряд 0, 1, …, 15. Сверяем его перед чтением сообщения: так проверяем отбор блоков, сортировку и направление обхода сразу.

Пропускаем 16 калибровочных блоков. Дальше объединяем полубайты, старший первым, byte = (hi << 4) | lo. В начале потока появляется конверт:

Поле Формат
Сигнатура CTP1
Версия 1
Длина payload u32 LE
Payload Текст указанной длины
Контрольная сумма CRC32 payload, u32 LE

После заголовка читаем только указанное число байт payload и следующие четыре байта CRC32: остальные пригодные блоки к сообщению уже не относятся. Сверяем контрольную сумму. В тексте лежит флаг, а за ним авторская приписка: «Он ничего не потерял. Просто всё переставил. Даже взгляд.»

Солвер.

Флаг

caplag{p3r3st4n0vk1_v_gl4z4h_k0t4}