Пишем упаковщик по шагам. Шаг четвертый. Запускаем.

Предыдущий шаг: здесь.

Появилась новая версия библиотеки для работы с PE-файлами (0.1.4). Перекачайте и пересоберите ее.

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

Добавим несколько полей в нашу структуру packed_file_info:

Мы добавили 4 поля, которые нам пригодятся в распаковщике. Теперь необходимо их заполнить в коде упаковщика:

Здесь все просто. Во втором уроке, если вы помните, я вручную считал общий виртуальный размер всех секций исходного файла и пояснял, что он эквивалентен значению, возвращаемому функцией get_size_of_image. Здесь мы этим воспользовались. С упаковщиком на этом все. Переходим к распаковщику (проект unpacker). Нам необходимо вкомпилировать в него алгоритм разархивирования LZO1Z. Я сделал просто и по-тупому - перенес в проект unpacker все файлы, необходимые для компиляции функции lzo1z_decompress (а именно, lzo1z_d1.c, lzo1x_d.ch, config1z.h, config1x.h, lzo_conf.h, lzo_ptr.h, lzo1_d.ch, miniacc.h). Кроме того, я прописал дополнительную include-директорию в проект: ../../lzo-2.06/include. Далее пришлось еще поковыряться с настройками проекта. Visual C++ при использовании функций memset, memcpy и подобных (а мы их использовать будем не раз) может по своему желанию встроить в получившийся exe-файл целую CRT, которая для нас совершенно лишняя. Пришлось отключить intrinsic (внутренние) функции (C/C++ - Optimization - Enable Intrinsic Functions - No) и полную оптимизацию (C/C++ - Optimization - Whole Program Optimization - No), на всякий случай добавить libcmt.lib в список игнорируемых библиотек (Linker - Input - Ignore Specific Default Libraries - libcmt.lib) и отключить генерацию кода на этапе линкования (Linker - Optimization - Link Time Code Generation - Default). А раз мы отключили все внутренние функции (среди них memset и memcpy), нам теперь нужна их собственная имплементация. Добавим два файла к проекту: memcpy.c и memset.c. В эти файлы я скопировал исходный код одноименных функций из CRT:

Нас поджидает еще одна проблема. У нас в коде теперь аж четыре модуля (четыре файла с исходным кодом, .c и .cpp), то после компиляции мы будем иметь четыре объектных (obj) файла. Далее линкер должен все это как-то слепить в единый exe-файл, и он это сделает. Но он расположит эти модули в exe-файле в одному ему известном порядке. Нам же необходимо, чтобы функция unpacker_main располагалась в самом начале кода распаковщика. Мы ведь ее в упаковщике патчим, помните? Эта проблема легко решается. Создадим текстовый файл с таким содержанием:

Назовем его link_order.txt и расположим его в папке с исходниками проекта unpacker. Этот файл скажет линкеру, в каком порядке должны располагаться функции в результирующем файле. Укажем этот файл в настройках проекта: Linker - Optimization - Function Order - link_order.txt. Все, с настройками покончено, начинаем писать код распаковщика!

Во-первых, я увеличил количество данных, выделяемых на стеке до 256 байтов (sub esp, 256). Переменных локальных много, поэтому перестрахуемся, а то вдруг 128 не хватит.

Пропишем прототип функции распаковки в начало файла unpacker.cpp:

Теперь мы сможем ее использовать в коде. Далее нам понадобятся функции VirtualAlloc (для выделения памяти), VirtualProtect (для изменения атрибутов страниц памяти) и VirtualFree (для освобождения выделенной памяти). Давайте импортируем их из kernel32.dll:

Этот кусок кода аналогичен коду в шаге 3, где мы загружали user32.dll и получали в ней адрес функции MessageBoxA, так что пояснять не буду. Далее следует перенести в локальную область видимости необходимые переменные, которые для нас запас упаковщик:

Мы это сделали потому, что скоро структура packed_file_info, находящаяся в самом начале первой секции упакованного файла, будет затерта реальными распакованными данными. Теперь выделим память и распакуем в нее упакованный блок данных:

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

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

Приступим к восстановлению заголовков секций:

Заголовки секций восстановили, теперь восстановим их данные:

И, почти все готово. Чтобы упакованный файл запустился, остается лишь пофиксить его таблицу импорта, снова выступив в роли PE-загрузчика. Для начала пофиксим виртуальный адрес и размер таблицы импорта в PE-заголовке:

Заполняем таблицу импорта:

Вот и все, мы, как PE-загрузчик, заполнили PE-файлу таблицу импорта. Осталась пара мелочей:

Теперь вы поняли, зачем нам нужны были собственные пролог и эпилог функции на ассемблере. Вместо инструкции ret, которая раньше располагалась в самом конце кода распаковщика, мы поставили инструкцию jmp eax, осуществляющую переход на код оригинального файла.

Итак, распаковщик теперь сможет запустить простейший PE-файл, имеющий только таблицу импорта. Любой файл с ресурсами, TLS, экспортами работать не будет, и этим мы займемся в следующих шагах. Но мы уже можем запаковать сами себя и запустить запакованный вариант!

Как видно, мы запаковали сами себя, получив бинарник packed_simple_pe_packer.exe, и он работает!

Полный солюшен со всеми проектами для данного шага: Own PE Packer Step 4

Пишем упаковщик по шагам. Шаг четвертый. Запускаем.: 8 комментариев

  1. > Во-первых, я увеличил количество данных, выделяемых на стеке до 256 байтов (sub esp, 256). Переменных локальных много, поэтому перестрахуемся, а то вдруг 128 не хватит.

    По этому поводу хотелось бы добавить и своих 5 копеек... А точнее МелкоМягких:
    > The compiler provides a symbol, __LOCAL_SIZE, for use in the inline assembler block of function prolog code. This symbol is used to allocate space for local variables on the stack frame in custom prolog code.

    1. С x64 особо не работал, поэтому не подскажу ничего конкретного, но по идее отличий не так много должно быть. Читать доки и изучать)

Добавить комментарий для dx Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *