83 lines
8.3 KiB
Markdown
83 lines
8.3 KiB
Markdown
<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}`
|