Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
zipdefrag — Presented at Recon Montreal 2018 | Kitploit
Инструменты/GitHubGitHub/nccgroup/zipdefrag
Embedded Systems SecurityMemory ForensicsReverse EngineeringData RecoveryDigital ForensicsFirmware Analysis
GitHubnccgroup/zipdefrag

zipdefrag

Presented at Recon Montreal 2018

Репозиторий
748 лет назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Этот дамп — головоломка

или продвинутый парсинг методом дробовика для дураков и бунтарей

жили-были

Однажды, дорогой читатель, тёмной и бурной ночью ваш покорный автор наткнулся на озадачивающую и загадочную ситуацию — систему, в которой единственным препятствием для анализа памяти методом chip-off и реверса были неизвестная фрагментированная проприетарная файловая система в сочетании со сжатием из-за использования встроенной Java, что снижало эффективность существующих инструментов восстановления файлов.

Тогда, возможно, и было собрано какое-то ad hoc решение: вручную склеивались фрагменты, которые выглядели подходящими, с помощью чудовищной возни в консоли Python и уродливых ad hoc bash-скриптов. Этого хватало, но отнимало уйму времени.

Хотя извлечение обычных несжатых данных при анализе chip-off — довольно рутинная задача для хакера аппаратного обеспечения, сжатие создаёт серьёзные проблемы, когда его фрагменты разбросаны повсюду иррациональным и неприятным образом, даже если никакой другой реальной защиты от извлечения нет.

Но ведь должен быть способ лучше?

В zip-файлах есть кое-что интересное (это основной формат для JAR-файлов). Будучи уже довольно давно прилежным читателем International Journal of PoC||GTFO и особенно следя за работой по усечению файловых форматов Ange Albertini, я подумал, что внутри zip-файла может быть достаточно данных о самом zip-файле, чтобы прилично сшить всё обратно воедино.

Ладно, с отсылками покончено, перейдём к техническим деталям.

Прежде всего: мы можем не знать деталей файловой системы (и, с точки зрения моего исследования, независимо от системы, где я столкнулся с проблемой, я решил, что о них просто лучше не беспокоиться). Но кое-что о реализации большинства файловых систем мы знаем. В частности, они, как правило, записываются чанками. У чанков есть некий минимальный размер, известный как страницы, и мы можем определить этот размер страницы, просмотрев дамп и выявив минимальный записываемый размер блока.

Некоторые из них могут располагаться подряд, некоторые нет, и чёткой закономерности в том, когда блоки оказываются смежными, нет.

Всё это значит, что наша задача — переупорядочить страницы данных так, чтобы получить валидные (или достаточно близкие к ним) образы файлов, которые мы хотим извлечь.

Zip-файлы устроены так, что реализуют своего рода обратную иерархию. Сначала идут сжатые данные файлов (обёрнутые в локальные заголовки файлов, описывающие их). Затем — центральный каталог (перечисляющий смещения локальных заголовков файлов), а затем запись конца центрального каталога (которая, помимо прочего, описывает количество файлов в zip, смещение начала центрального каталога и его размер).

Давайте перевернём это и копнём чуть глубже:

  • Запись конца центрального каталога сообщает нам:

    • Точное расположение записи EOCD внутри zip-файла (смещение CD плюс длина CD, которые предшествуют EOCD).
    • Сколько файлов (а значит, и записей CD) искать.
    • Точное местоположение первой записи CD в zip-файле.
  • Каждая запись центрального каталога сообщает нам:

    • CRC32 сжатых данных файла
    • Метку времени
    • Множество других метаданных (метод сжатия, флаги, используемая/требуемая версия ОС...)
    • Индекс в файле для соответствующего фрагмента LF
    • Критически важно: достаточно данных для восстановления образа соответствующего фрагмента LF.
  • Каждая локальная запись файла сообщает нам:

    • Местоположение начала файла в нашем дампе
    • Если файл достаточно мал, мы получаем целиком весь файл на той же странице — или благодаря тому, что заголовок следующего файла появляется в следующем страничном фрагменте zip-файла!
    • Если достаточно маленьких файлов упаковано в достаточное количество страниц, мы можем использовать расположение страниц и известные значения каталога, чтобы выстроить порядок страниц (с известными пропусками!)

Всё вышеперечисленное приводит к тому, что подавляющая часть файла оказывается восстановленной.

Неожиданный поворот — нам придётся иметь дело более чем с одной JAR-прошивкой!

Прежде всего нам нужен этап, позволяющий отличать данные разных прошивок. Причина в том, что все смещения значимы только в пределах соответствующих zip-файлов: любое пересечение приведёт к несоответствию zip-потоков и порче данных, а мы крайне заинтересованы извлечь как можно больше неповреждённых данных. Кроме того, нам нужны веские гарантии, что, например, любые уязвимости, которые мы диагностируем в целевой прошивке, касаются именно той, которая обычно запущена, а не какого-то другого файла, просто оставленного валяться рядом.

Здесь нужно решение на основе алгоритма k-средних (он же «алгоритм Ллойда»). Вот отличное видео, объясняющее, как он работает. В SciPy была готовая хорошая версия, но мне пришлось найти/ пропатчить единственный crate для кластеризации/аналитики, реализующий этот алгоритм, чтобы заставить его работать в Rust-реализации. К счастью, мне не пришлось писать его с нуля.

После этого всё прошло как по маслу.

Для этого можно использовать ряд признаков. Поля Flags, Method и Version зависят от используемого zip-стека, которым сжимали файл. Кроме того, в заголовках есть метки времени, и в целом маловероятно, что все прошивки были скомпилированы и сжаты в одно и то же время.

Кстати, стоит отметить, что zip-файлы используют метки времени в формате MS-DOS — битовые упакованные short, представляющие год-месяц-день и час-минута-две-секунды. Если не преобразовать их в абсолютное скалярное значение до использования в качестве данных для классификации, вы можете придать разнице в год тот же вес, что и разнице в секунду, а это совсем никуда не годится!

Мы преобразуем их в евклидов вектор (это модное слово для n-мерного массива значений в ℝ, то есть вещественных координат, но видео по ссылке выше, пожалуй, объясняет всё проще), а алгоритм кластеризации делает за нас почти всё остальное, собирая все разобранные заголовки в нужное количество корзин.

Небольшое отступление о парсинге

Хотя для прототипирования этого метода я написал весьма корявый скрипт на Python, и с PoC удалось достичь примерно 70–80% восстановления содержимого JAR, я решил на этом остановиться и перейти к реализации быстрой версии на Rust.

В Rust есть crate под названием nom, который просто фантастически подходит для написания парсеров-верификаторов. Это, кстати, была одна из главных причин переписать всё на Rust. Возможность писать ясные и чрезвычайно строгие парсеры в некоторых отношениях делает задачу гораздо проще, чем попытки справиться со всем этим в Python (который, как правило, гораздо терпимее, настолько, что порой непросто быть уверенным, что ты не пропускаешь ошибочные сбои и не упускаешь граничный случай).

Если возможность писать потрясающие быстрые читабельные парсеры вам интересна, загляните в:

  • Пишем парсеры так, будто на дворе 2017-й
  • Nom Benchmarks — там кто-то написал http-парсер с нуля на Rust, который оказался немного быстрее очень быстрой C-реализации и без переполнений буфера.

К тому же выполнение такого анализа в Python само по себе медленное, и он никогда не задумывался как нечто большее, чем путь к PoC для проверки жизнеспособности этого подхода.

В общем, хватит об этом...

Двигаемся дальше

Подходы к восстановлению оставшихся фрагментов включают в себя сначала фильтрацию оставшихся страниц по кандидатам с высокой энтропией, заполнение сначала меньших пробелов (исключая как можно больше страниц из списка поиска, поскольку проверка перестановок в худшем случае — задача экспоненциальной сложности, поэтому быстрое решение лёгких случаев в приоритете и экспоненциально упрощает нам задачу по ходу дела!).

Ещё один быстрый выигрыш — находить случаи, когда локальный заголовок файла не парсится из-за границы страницы (мы должны суметь сопоставить его с аналогично выровненным аналогом, по крайней мере когда такие выравнивания уникальны и не сталкиваются с другими артефактами повреждений).

Как проверять кандидатов на недостающие страницы? У нас же есть контрольные суммы CRC32 для файлов прямо в нашем центральном каталоге! Вместо вычисления CRC32 по всему файлу, вероятно, лучший способ — вычислить CRC32 по уже известным фрагментам (в прямом направлении от данных в конце страницы перед началом пробела и в обратном — от данных (или фрагмента DataDescriptor после потока deflate) и вычислить из этого, какие промежуточные CRC32 следует ожидать для каждого блока недостающих страниц.

По сути, каждый раз, выбирая самую простую/быструю задачу, мы значительно упрощаем сложные задачи, отсеивая мусор. Именно поэтому используется энтропия Шеннона, чтобы сразу отбрасывать пустые или почти пустые страницы: нет гарантии, что у нас будут только zip-страницы с высокой энтропией, но даже при наличии выбросов это даёт огромное ускорение, позволяя с самого начала не связываться с этим осложнением.

Я просто хочу собрать эту штуковину, в чём дело?

Если хотите с ней повозиться, установите Rust (рекомендуется через замечательный rustup nightly. Затем:

root@kitploit:~
$ git clone [repo]
...
$ cd zipdefrag
...
$ cargo build --release

Можно не использовать флаг release, чтобы включить отладку.

Артефакты сборки будут в /target/{debug,release}

Документация собирается командой cargo doc (Этот crate очень подробно документирован. Я люблю писать.)

Сейчас в Rust-версии по умолчанию нет вывода в терминал; если хотите запустить CLI-обвязку, нужно установить переменную окружения RUST_LOG=zipdefrag, которая включает подробное журналирование в терминал с демонстрацией текущего анализа.

Что дальше:

  • Быстрый переносимый нативный исполняемый файл (с хуками на Python) для решения головоломок с zip-дампами из неизвестных файловых систем.

  • Демонстрационный дамп

Известные проблемы

  • Производительность Rust-реализации сейчас никуда не годится из-за расточительного поведения при поиске соответствующих фрагментов LFH. Исправлю это и усвою урок.

  • Этот метод плохо работает, когда многие файлы в JAR значительно больше размера страницы. Поскольку он опирается на активное использование структуры, присущей zip-файлам, файлы с большим объёмом данных просто не очень хорошо поддаются восстановлению.

К счастью, файлы классов для мидлетов J2ME, как правило, довольно малы, но большие бинарники внутри, вероятно, будут невосстановимы.

Кроме того, Python-PoC содержит ряд арифметических ошибок.

Скачать инструмент