Распаковка исполняемых файлов

Привет, хабровчане. В рамках курса «Reverse-Engineering. Basic» Александр Колесников (специалист по комплексной защите объектов информатизации) подготовил авторскую статью.

Также приглашаем всех желающих на открытый вебинар по теме «Эксплуатация уязвимостей в драйвере. Часть 1». Участники вебинара вместе с экспертом разберут уязвимости переполнения в драйверах и особенности разработки эксплойтов в режиме ядра.

Статья расскажет о подходах к анализу запакованных исполняемых файлов с помощью простых средств для обратной разработки. Будут рассмотрены некоторые пакеры, которые применяются для упаковки исполняемых файлов. Все примеры будут проведены в ОС Windows, однако изучаемые подходы можно легко портировать на любую ОС.

Инструментарий и настройка ОС

Для тестов будем использовать виртуальную машину под управлением ОС Windows. Инструментарий будет содержать следующие приложения:

  • отладчик x64dbg;

  • установленный по умолчанию плагин x64dbg Scylla;

  • hiew Demo;

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

  • uncompile6 проект, который позволяет разобрать байткод виртуальной машины Python;

  • pyinstallerExtractor инструмент для распаковки архива pyInstaller.

Общие методы снятия паковки

Разберемся, что же такое паковка. В большинстве случаев исполняемые файлы современных языков программирования имеют довольно большой размер при минимальном наборе функций. Чтобы оптимизировать данную величину, можно применить паковку или сжатие. Наиболее распространенный на сегодняшний день пакер — UPX. Ниже приведен пример того, как пакер проводит сжатие исполняемого файла.

85f1635ab79736bd4a5a18eca9bb58c0.png

На картинке может показаться, что файл стал по размеру больше, однако это не всегда так. Большинство файлов за счет такой модификации могут уменьшить свой размер до 1.5 раз от исходного объема.

Что же от этого реверс-инженеру? Почему знать и уметь определять, что файл упакован? Приведу наглядный пример. Ниже приведен снимок файла, который не запакован:

ecc5fdde6a55073b7ca6bb72f9c5bbb5.png

И файл, который был пропущен через алгоритм UPX:

f1ea2a22ec8ded5ae5a5997301c86654.png

Изменения коснулись в этом случае двух основных точек исполняемого файла:

  1. Точка входа — в случае с упакованным файлом это начало алгоритма распаковки, настоящий алгоритм программы будет работать только после того, как будет распакован оригинальный файл;

  2. Код оригинального файла: теперь не найти паттернов, которые можно сразу разбирать как команды.

Итак, чтобы снова анализировать оригинальный файл, нужно найти настоящую или оригинальную точку входа. Для этого нужно разбить алгоритм на основные этапы:

  1. Этап подготовки исполнения файла — загрузчик ОС настраивает окружение, загружает файл в оперативную память;

  2. Сохранение контекста — упаковщик сохраняет контекст исполнения файла (набор значений регистров общего назначения, которые были установлены загрузчиком ОС);

  3. Распаковка оригинального файла;

  4. Передача управления оригинальному файлу.

Все описанные выше этапы можно легко отследить в отладчике. Особенно может выделяться процедура сохранения контекста. Для нее в разных архитектурах могут быть использованы команды pushad/popad или множественное использование команды push. Поэтому всегда приложение трассируют до первого изменения регистра ESP/RSP, и ставят «Hardware Breakpoint» на адрес, который был помещен в регистр в первый раз. Второе обращение этому адресу будет в момент восстановления контекста, который заполнил загрузчик ОС. Без него приложение завершится с ошибкой.

Пример UPX

Попробуем с помощью отладчика найти оригинальную точку входа для приложения. Запечатлим оригинальную точку входа до упаковки UPX:

b4c137168ca61c9073f84d39ab027034.png

Как та же точка входа выглядит после упаковки:

9f8e1984d0ffe24980259477e0435eb8.png

Запустим отладчик и попробуем найти место сохранения контекста:

7a2e28ff46cb1423e454d82c327ae56e.png

Ждем первого использования ESP — в отладчике при этом значение регистра подсветится красным цветом. Затем устанавливаем точку останова на адрес и просто запускаем приложение:

8f5d0b296d7aec92f9ac1ffa2b1c1a3a.png

В результате попадаем на оригинальную точку входа:

003e9e3906b17fbdacbd28c9b241d540.png

Вот так просто, теперь используя плагин Scylla Hide можно сохранить результирующий файл на жесткий диск и продолжить его анализ.

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

Пример PyInstaller

Не всегда подобный подход работает для приложений, которые используют более сложную структуру исполняемого файла. Рассмотрим файл, который был создан с помощью PyInstaller — пакет, который позволяет преобразовать Python скрипт в исполняемый файл. При генерации исполняемого файла создается архив, который содержит виртуальную машину Python и все необходимые библиотеки. Сам исходный код приложения при этом преобразуется в байт код и его нельзя дезассемблировать.

Попробуем все же получить что-то читаемое. Создадим простое приложение на Python и упакуем с помощью PyInstaller. Исходный код приложения:

def main():
    print("Hello World!")

if __name__ == '__main__':
    main()

Установим пакет pyInstaller и создадим exe файл:

pip install pyinstaller
pyinstaller -F hello.py #-F создать один файл

Итак, проведем сбор информации о том, что в итоге получилось. У нас есть архив, который должен запустить виртуальную машину, и код, который мы записали в виде скрипта. Попробуем восстановить исходник и просто его прочесть даже без запуска.

После выполнения команд выше, у вас должна создаться директория ./dist/test.exe. Откроем последовательно файл с помощью pyinstallerextractor и uncompile3:

bacf549bc397e1d1ae6d68518688dedd.png

Наш скрипт находится в директории, которая создается в результате распаковки. Наименование файла должно соответствовать названию exe файла. В нашем случае это test.pyc. Откроем его в hiew:

03154385de98850cef653a87a1e99899.png

Декомпиляция стандартными средствами невозможна, так как инструменты просто не умеют работать с байткодом Python. Применим специализированный инструмент — uncompile6.

328951bdcf9ecae564826cd5d885cce9.png

Таким образом можно снова получить исходный код.

Узнать подробнее о курсе «Reverse-Engineering. Basic».

Смотреть открытый вебинар по теме «Эксплуатация уязвимостей в драйвере. Часть 1».

© Habrahabr.ru