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

67 lines
4.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<h1 align="center">В глазах всё написано</h1>
<p align="center">
<img src="https://img.shields.io/badge/category-Stego-blueviolet" alt="Stego"/>
</p>
На старте есть RGB-изображение `kot1_v_glazah.png` размером 1254 × 1254. Начинаем с текстовых чанков PNG. Запись `EditorNote` описывает работу редактора с блоками 2 × 2.
## Решение
Читаем метаданные через Pillow:
```python
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)` по строкам, слева направо. Для каждого пригодного блока сортируем цвета по ключу:
```text
(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`.
```python
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: остальные пригодные блоки к сообщению уже не относятся. Сверяем контрольную сумму. В тексте лежит флаг, а за ним авторская приписка: «Он ничего не потерял. Просто всё переставил. Даже взгляд.»
[Солвер](solve/solve.py).
## Флаг
`caplag{p3r3st4n0vk1_v_gl4z4h_k0t4}`