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

5.6 KiB
Raw Blame History

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

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 байт. В её конце читаем полный флаг.

Солвер.

Флаг

caplag{zhurn4l_p0mn1t_k0t4_n0s}