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

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` задана функция от координат в тысячных: ```python X, Y = 12345, -67890 N = 2000001 U, V = X + 1000000, Y + 1000000 Z = U * N + V # 2024691944455 ``` Подставляем `Z` в `caplag{nose_}`. С параметром `--render` солвер рисует только опубликованную сцену с применёнными преобразованиями и отмечает нос, чтобы результат можно было проверить визуально. [Солвер](solve/solve.py). ## Флаг `caplag{nose_2024691944455}`