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

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

Наш упаковщик уже умеет все, кроме одной вещи - упаковки бинарников, имеющих экспорты. Это, в частности, абсолютное большинство DLL-файлов и OCX-компоненты. Некоторые exe-файлы также имеют экспорты. Наш упаковщик должен пересобрать таблицу экспортов и расположить ее в доступном месте, чтобы загрузчик мог ею воспользоваться.

Пока что можно немного расслабиться - в упаковщике кода добавится совсем немного (в распаковщике, в общем-то, тоже, но он будет на ассемблере).

Займемся сначала упаковщиком (проект simple_pe_packer). Если у файла есть экспорты, нужно их считать, поэтому сразу после строк

напишем:

Тут библиотека для работы с PE-файлами сильно упрощает нам жизнь, поэтому вдаваться в подробности, как устроены структуры экспортов, не нужно. Далее заменим строки

на

Так как после распаковщика и TLS у нас будут идти либо экспорты, либо релокации, либо и то и другое, и необходимо, чтобы они не налезли на TLS или распаковщик. Кроме того, нужно перенести строки

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

Это, кстати, нужно было сделать еще в прошлом шаге, когда мы научили упаковщик обрабатывать релокации. Теперь меняем строки

на

по все той же причине - чтобы экспорты не налезли на релокации.

Пришло время обработать экспорты, пересобрав их директорию и расположив ее во второй добавленной нами секции ("kaimi.io"):

И вновь библиотека для работы с PE сильно упростила на жизнь. Теперь просто уберем строку, которую добавляли раньше:

Теперь переходим к распаковщику. Казалось бы - что в нем править? Мы пересобрали директорию экспортов, что еще нужно? Есть одна проблема. В отличие от exe-файла, у DLL точка входа может быть вызвана загрузчиком больше одного раза. Например, при создании нового потока, или когда процесс завершается. А по адресу точки входа у нас тело распаковщика, который уже выполнил свою работу и все распаковал. Если его дернуть второй раз, то просто-напросто все упадет. Поэтому в распаковщик нужно добавить проверку, был ли файл уже распакован, и если был, то управление следует передать на оригинальную точку входа распакованного файла. Я воспользовался хитростью, которую применял в своем предыдущем распаковщике. Мы прямо внутри кода распаковщика разместим переменную размером 4 байта, заполненную нулями. После распаковки в нее мы запишем адрес оригинальной точки входа. Перед распаковкой мы проверим, нулевая ли это переменная, и если нет - это значит, что файл уже был распакован, и мы просто передадим управление по адресу, содержащемуся в этой переменной. Для начала создадим саму переменную и добавим проверку на ноль:

Поясню, что делает этот код. Чтобы создать переменную внутри кода (прямо посередине), мы в MASM32 могли воспользоваться директивой dd или db или какой-то еще подобной. В инлайновом ассемблере MSVC++ такие директивы не разрешены. Но нам нужно как-то сделать переменную из 4-х байтов, содержащих нули! Я сделал это так: команда ассемблера "add byte ptr [eax], al" занимает ровно два байта и имеет опкод 00 00. Таким образом, вписав две такие команды подряд, получаем четыре подряд идущих нулевых байта - это то, что нам нужно. Осталось как-то получить адрес первой инструкции с учетом того, что располагаться она может по любому виртуальному адресу - код-то у нас базонезависимый. Это делается с помощью инструкции call next2, которая пропускает наши левые команды и заодно заталкивает в стек адрес возврата, равный адресу команды, следующей за call. За call идут как раз наши инструкции. Теперь мы имеем их адрес. Далее проверяем, что по нему лежит (mov eax, [eax]), изначально там будет ноль, и тело распаковщика начнет выполняться, так как инструкция jz next3 произведет переход на метку. Если же распаковщик уже выполнился, мы запишем в переменную по адресу was_unpacked (который указывает на наши левые команды) адрес оригинальной точки входа, и проверка jz next3 не пройдет. Произойдет завершение тела распаковщика и переход на оригинальную точку входа исходного файла. Нам остается собственно записать по адресу was_unpacked адрес точки входа:

На этом все, и мы можем собрать и протестировать наш упаковщик. Для теста я сделал новый солюшен с несколькими проектами: двумя DLL-файлами и одним EXE. Одну библиотеку EXE-файл грузит статически, другую динамически, после чего вызывает у этих библиотек несколько функций. Статически загружаемая библиотека и сам DLL-файл содержат статический TLS. (В динамически загружаемых библиотеках статического TLS быть не должно, так как он не будет инициализирован). Этот солюшен я приложил в архив в конце статьи. Упаковав и обе DLL-ки, и exe-файл (дав потом обоим упакованным DLL-файлам оригинальные имена), я убедился, что все работает без изменений, как и оригинальные файлы.

Полный солюшен для этого шага: Own PE Packer Step 8

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

  1. Дикс, ты гений! осталось только консоль докодить (команды и тд) и все - готов пакер. Ты не надумал пару антиотладочных фишек вкрячить или движок треш-кода или поддержку фейковых сигнатур?
    Кстати, сжатие на пределе или можно улучшить?

    1. Да сколько можно уже про антиотладку и про сжатие-то спрашивать?
      1. Берешь, качаешь сорс и допиливаешь себе антиотладку
      2. Берешь, ищешь хороший алгоритм сжатия и меняешь его и получаешь МАКСИМУМ сжатия

      какие проблемы-то :)

        1. Можешь доставать, но конкретно вопросы про сжатие и про антиотладку ты уже раза по три задал, и я уже на них отвечал

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

  3. В частности о:
    > add byte ptr [eax], al;
    > add byte ptr [eax], al;
    >...
    >... В инлайновом ассемблере MSVC++ такие директивы не разрешены.

    Для этих целей M$ придумал(а) пседоинструкцию _emit.

    А вообще, статья интересная +5 :)

Добавить комментарий

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