5.6 KiB
У него есть прошлое
На старте есть изображение 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}