Files
2026-09-17 00:50:07 +03:00

83 lines
8.3 KiB
Markdown
Raw Permalink 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>
На старте есть изображение `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_<Z>}`. С параметром `--render` солвер рисует только опубликованную сцену с применёнными преобразованиями и отмечает нос, чтобы результат можно было проверить визуально.
[Солвер](solve/solve.py).
## Флаг
`caplag{nose_2024691944455}`