У него есть прошлое

Stego

На старте есть изображение `kot2_proshloe.png`, в котором записан образ диска с CatFS версии 2. Образ содержит снимок каталога, журнал транзакций и сектора данных. Нужно восстановить последнюю зафиксированную версию `nose_note.txt` перед её удалением. ## Решение ### Извлечение CatFS В PNG находим `iTXt`-запись `CatFS.boot`. Она описывает геометрию носителя: плитки 32 × 32 от `(0, 0)`, обход по строкам, один бит в чётности синего канала (`B & 1`). Байты собираются старшим битом вперёд. В плитке `32 × 32 = 1024` бита, то есть один сектор на 128 байт. Для изображения 1254 × 1254 получаем `39 × 39 = 1521` полный сектор. Сектор `0` начинается с `CATF`, версия — `2`. Сверяем CRC32 первых 124 байт со значением по смещению `124` и читаем карту областей: | Область | Сектора | |---|---| | Суперблок | `0` | | Снимок каталога | `1–2` | | Журнал транзакций | `3–7` | | Данные | С сектора `8` | В суперблоке также указано `sectors_used=107` — это число занятых секторов всей файловой системы, включая служебные области. У сектора данных есть `sector_id`, `next_sector`, `used_length` и `payload_crc`. Заголовок сектора занимает 16 байт, а CRC покрывает всё следующее 112-байтное поле payload, включая неиспользованную добивку. Следуем по `next_sector` до `0xffffffff`, полезную длину учитываем при сборке файла. Запись каталога занимает 32 байта: `name[16]`, `inode u32`, `generation u16`, `reserved u8`, `flags u8`, `size u32`, `first_sector u32`. По снимку читаем живой `format.txt` — там правила журнала и точная цель восстановления. ### Снимок записки В снимке `nose_note.txt` имеет `inode=1006`, `generation=1`, `first_sector=100`. У записи выставлен бит сжатия `flags & 2`. Собираем полезные байты цепочки секторов с номерами от `100` до `106`, затем распаковываем zlib. Получаем 1504 байта текста, но флаг обрывается на `caplag{zhurn4l_p0m`. Хвост появился позже, в журнале. Записи журнала имеют формат `[type u8][reserved u8][length u16][payload][crc32]`. Многобайтные числа читаем в little-endian. CRC32 покрывает четырёхбайтный заголовок записи и её payload. Читаем до `type=0`, сверяем CRC и группируем операции между `TXN_BEGIN` и соответствующим `TXN_COMMIT`. ### Применение журнала Сначала загружаем живые объекты снимка, сохраняя для каждого inode поколение и логические байты файла. Затем читаем транзакции в порядке журнала и применяем только завершённые COMMIT. Незавершённую транзакцию отбрасываем при следующем BEGIN или конце журнала. | Транзакция | Действие | Что делаем | |---|---|---| | `t2`, есть COMMIT | `OP_WRITE` к `(1006, gen 1)`, offset `1504`, replace_len `0` | Распаковываем zlib-патч, дописываем его байты и получаем gen 2 | | `t3`, нет COMMIT | `OP_WRITE` к `(1006, gen 2)`, offset `1486`, replace_len `31` | Отбрасываем всю транзакцию | | `t4`, есть COMMIT | `OP_DELETE` к `(1006, gen 2)` | Сохраняем последнюю версию и фиксируем удаление | | `t5`, есть COMMIT | `OP_WRITE` к `(1006, gen 0)` | Создаём новую серию inode для `draft.txt` | У `t3` правильные CRC отдельных записей, но это не делает транзакцию зафиксированной. Если применить её без COMMIT, настоящий текст заменится ложным. Одного номера inode недостаточно, поскольку операции относятся к паре `(inode, generation)`, а создание с `generation=0` после удаления начинает новую серию. Поэтому `draft.txt` из `t5` не заменяет искомую историю записки. Патч `t2` дописывает недостающий хвост. Последняя зафиксированная версия перед `t4` — gen 2 длиной 1568 байт. В её конце читаем полный флаг. [Солвер](solve/solve.py). ## Флаг `caplag{zhurn4l_p0mn1t_k0t4_n0s}`