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

8.3 KiB
Raw Blame History

Мы снова пришли к носу

Stego

На старте есть изображение kot5_nos.png. Из него извлекаем сетевой захват, затем изображения из HTTP и пакеты языка сцены. По ним восстанавливаем итоговый опубликованный рисунок. Флаг вычисляется из координат носа в этом рисунке.

Решение

Извлечение HTTP из захвата

В чанке caTs находим конверт CCAP. Его формат описан в CATD.proto: сигнатура, версия 1, длина распакованных данных, SHA-256 и zlib-поток. Проверяем конверт и распаковываем — получаем PCAPNG на 37 872 636 байт с 5788 пакетами.

В захвате есть полезные POST /upload на порт 8080 и шумовые GET /health на 8081. Соединения keep-alive, в одном потоке несколько запросов. Пакеты переупорядочены, встречаются ретрансмиты, поэтому склеивать TCP payload в порядке захвата нельзя.

Группируем TCP-сегменты по адресам и портам отправителя и получателя. Для каждого направления начинаем сборку с ISN + 1, поскольку SYN занимает один номер последовательности, и размещаем payload по sequence numbers. Совпадающие ретрансмиты отбрасываем. При конфликте байтов или пробеле в данных останавливаем сборку. В направлении от клиента к серверу разбираем запросы по заголовкам и Content-Length, оставляем POST /upload с Content-Type: image/png. Так получаем 16 PNG-тел.

В обратном направлении идут HTTP-ответы. В них пропускаем промежуточный 100 Continue, а тела финальных ответов отделяем по Content-Length или chunked-разметке. Эти ответы не добавляем к данным PNG.

Декодирование CATD

В каждом PNG есть CATD.desc: base64 и zlib скрывают дескриптор CDSC с 7240 позициями пар плиток. Каждая пара состоит из двух плиток 4 × 4, образующих блок 8 × 4. Сетка имеет размер 156 × 313. Декодируем base64, затем распаковываем zlib и проверяем CRC32 дескриптора. Его позиции используем в записанном порядке. Для индекса i получаем строку и столбец через divmod(i, 156), а левый верхний пиксель пары имеет координаты (8 * столбец, 4 * строка).

Для пары считаем среднее S = R + G слева и справа в достаточно широком числовом типе, чтобы сумма каналов не переполнилась. Знак разности кодирует бит. Если среднее слева больше, читаем 1, если меньше — 0. Разницу по модулю меньше 1.5 считаем неоднозначной и останавливаем декодирование. Байты собираем MSB-first. Контрольный байт 01010101 позволяет проверить направление сравнения на всех изображениях.

После калибровки появляется сигнатура CATD. Читаем заголовок размером 20 байт, затем payload указанной длины и четыре байта CRC32. Контрольная сумма покрывает заголовок и payload, а записана в little-endian.

Поле Смысл
version u8, значение 1
type u8, значения START=1, DATA=2, END=4
session_id Идентификатор сессии, u32 LE
sequence, total_packets Номер и количество пакетов, оба u32 LE
payload_length, payload Длина u16 LE и данные
crc32 Проверка пакета

Выбираем сессию 2085938395, наиболее частую среди валидных пакетов. Из 16 пакетов два — точные дубли, остаются 14. Если одинаковому sequence соответствуют разные байты, останавливаем сборку. Проверяем, что присутствуют все номера от 0 до total_packets - 1, и сортируем пакеты по sequence. Первый должен быть START, последний END, остальные DATA. Первые 32 байта END — SHA-256 склеенных DATA payload. Остаток END содержит правила сборки.

Исполнение сцены

START содержит спецификацию языка и контрольный пример: точка (5, 2) после ROT90 k=1 и родительского TRANSLATE(10, 0) становится (8, 5). Из примера следует, что сначала применяются преобразования своей группы, затем родительской.

После проверки SHA-256 разбираем склеенный поток DATA. В нём 544 байта с 48 командами. Язык умеет объявлять шаблоны, рисовать линии и маркеры, создавать группы и экземпляры, применять переносы, повороты и отражения, удалять объекты и публиковать группу. Координаты — i32 LE в тысячных.

Шаблон кота содержит 27 линий и три маркера. Нужен MARK id=1, нос в локальной точке (200000, 263000). Но экземпляров шаблона три:

Объект Состояние Учитываем?
oid=1 Черновая группа gid=10 Нет, группа не опубликована
oid=2 Вложенная gid=21 внутри опубликованной gid=20 Да
oid=3 В gid=20, затем DELETE 3 Нет, объект удалён

Для оставшегося носа применяем отражение по оси 1, поворот ROT90 k=2 и перенос корня:

Преобразование Координаты
Локальный маркер (200000, 263000)
REFLECT axis=1 (200000, -263000)
ROT90 k=2 (-200000, 263000)
TRANSLATE(212345, -330890) (12345, -67890)

Получаем мировые координаты (12.345, -67.890). Простое чтение локального маркера дало бы другую точку, поскольку здесь важен результат исполнения сцены, включая удаление и публикацию.

Вычисление флага

В nose.flagfn задана функция от координат в тысячных:

X, Y = 12345, -67890
N = 2000001
U, V = X + 1000000, Y + 1000000
Z = U * N + V  # 2024691944455

Подставляем Z в caplag{nose_<Z>}. С параметром --render солвер рисует только опубликованную сцену с применёнными преобразованиями и отмечает нос, чтобы результат можно было проверить визуально.

Солвер.

Флаг

caplag{nose_2024691944455}