Предыдущий шаг здесь.
Наш упаковщик уже умеет все, кроме одной вещи - упаковки бинарников, имеющих экспорты. Это, в частности, абсолютное большинство DLL-файлов и OCX-компоненты. Некоторые exe-файлы также имеют экспорты. Наш упаковщик должен пересобрать таблицу экспортов и расположить ее в доступном месте, чтобы загрузчик мог ею воспользоваться.
Пока что можно немного расслабиться - в упаковщике кода добавится совсем немного (в распаковщике, в общем-то, тоже, но он будет на ассемблере).
Займемся сначала упаковщиком (проект simple_pe_packer). Если у файла есть экспорты, нужно их считать, поэтому сразу после строк
1 2 3 |
//... tls.reset(new pe_base::tls_info(image.get_tls_info())); } |
напишем:
1 2 3 4 5 6 7 8 9 |
//Если файл имеет экспорты, получим информацию о них //и их список pe_base::exported_functions_list exports; pe_base::export_info exports_info; if(image.has_exports()) { std::cout << "Reading exports..." << std::endl; exports = image.get_exported_functions(exports_info); } |
Тут библиотека для работы с PE-файлами сильно упрощает нам жизнь, поэтому вдаваться в подробности, как устроены структуры экспортов, не нужно. Далее заменим строки
1 2 3 |
//Наконец, обрежем уже ненужные нулевые байты с конца секции if(!image.has_reloc()) pe_base::strip_nullbytes(unpacker_added_section.get_raw_data()); |
на
1 2 3 |
//Наконец, обрежем уже ненужные нулевые байты с конца секции if(!image.has_reloc() && !image.has_exports()) pe_base::strip_nullbytes(unpacker_added_section.get_raw_data()); |
Так как после распаковщика и TLS у нас будут идти либо экспорты, либо релокации, либо и то и другое, и необходимо, чтобы они не налезли на TLS или распаковщик. Кроме того, нужно перенести строки
1 2 3 4 5 |
//Изменим размер данных секции распаковщика ровно //по количеству байтов в теле распаковщика //(на случай, если нулевые байты с конца были обрезаны //библиотекой для работы с PE) data.resize(sizeof(unpacker_data)); |
выше, так как следует изменять размер данных точно по количеству байтов в распаковщике непосредственно после его записи туда в случае, если у файла есть TLS либо релокации либо экспорты. Расположим этот кусок после строк
1 2 3 4 5 6 7 8 9 10 11 12 |
//... //Добавляем и эту секцию pe_base::section& unpacker_added_section = image.add_section(unpacker_section); if(tls.get() || image.has_exports() || image.has_reloc()) { //Изменим размер данных секции распаковщика ровно //по количеству байтов в теле распаковщика //(на случай, если нулевые байты с конца были обрезаны //библиотекой для работы с PE) unpacker_added_section.get_raw_data().resize(sizeof(unpacker_data)); } |
Это, кстати, нужно было сделать еще в прошлом шаге, когда мы научили упаковщик обрабатывать релокации. Теперь меняем строки
1 2 3 |
//Пересобираем релокации, располагая их в конце //секции с кодом распаковщика image.rebuild_relocations(reloc_tables, unpacker_section, unpacker_section.get_raw_data().size()); |
на
1 2 3 |
//Пересобираем релокации, располагая их в конце //секции с кодом распаковщика image.rebuild_relocations(reloc_tables, unpacker_section, unpacker_section.get_raw_data().size(), true, !image.has_exports()); |
по все той же причине - чтобы экспорты не налезли на релокации.
Пришло время обработать экспорты, пересобрав их директорию и расположив ее во второй добавленной нами секции ("kaimi.io"):
1 2 3 4 5 6 7 8 9 |
if(image.has_exports()) { std::cout << "Repacking exports..." << std::endl; pe_base::section& unpacker_section = image.get_image_sections().at(1); //Пересобираем экспорты и располагаем их в секции "kaimi.io" image.rebuild_exports(exports_info, exports, unpacker_section, unpacker_section.get_raw_data().size()); } |
И вновь библиотека для работы с PE сильно упростила на жизнь. Теперь просто уберем строку, которую добавляли раньше:
1 |
image.remove_directory(IMAGE_DIRECTORY_ENTRY_EXPORT); |
Теперь переходим к распаковщику. Казалось бы - что в нем править? Мы пересобрали директорию экспортов, что еще нужно? Есть одна проблема. В отличие от exe-файла, у DLL точка входа может быть вызвана загрузчиком больше одного раза. Например, при создании нового потока, или когда процесс завершается. А по адресу точки входа у нас тело распаковщика, который уже выполнил свою работу и все распаковал. Если его дернуть второй раз, то просто-напросто все упадет. Поэтому в распаковщик нужно добавить проверку, был ли файл уже распакован, и если был, то управление следует передать на оригинальную точку входа распакованного файла. Я воспользовался хитростью, которую применял в своем предыдущем распаковщике. Мы прямо внутри кода распаковщика разместим переменную размером 4 байта, заполненную нулями. После распаковки в нее мы запишем адрес оригинальной точки входа. Перед распаковкой мы проверим, нулевая ли это переменная, и если нет - это значит, что файл уже был распакован, и мы просто передадим управление по адресу, содержащемуся в этой переменной. Для начала создадим саму переменную и добавим проверку на ноль:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
//... __asm { mov original_image_base, 0x11111111; mov rva_of_first_section, 0x22222222; mov original_image_base_no_fixup, 0x33333333; } //Адрес переменной, говорящей о том, //был ли код уже распакован DWORD* was_unpacked; __asm { //Хитрость с получением адреса //следующей за call инструкции call next2; add byte ptr [eax], al; add byte ptr [eax], al; next2: //В eax - адрес первой инструкции //add byte ptr [eax], al pop eax; //Сохраним этот адрес mov was_unpacked, eax; //Посмотрим, что по нему лежит mov eax, [eax]; //Если там ноль, то перейдем //на распаковщик test eax, eax; jz next3; //Если не ноль, то завершим распаковщик //и перейдем на оригинальную точку входа leave; jmp eax; next3: } |
Поясню, что делает этот код. Чтобы создать переменную внутри кода (прямо посередине), мы в 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 адрес точки входа:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//... info = reinterpret_cast<const packed_file_info*>(original_image_base + rva_of_first_section); //Получим адрес оригинальной точки входа DWORD original_ep; original_ep = info->original_entry_point + original_image_base; __asm { //Запишем его по адресу, содержащемуся в переменной //was_unpacked mov edx, was_unpacked; mov eax, original_ep; mov [edx], eax; } |
На этом все, и мы можем собрать и протестировать наш упаковщик. Для теста я сделал новый солюшен с несколькими проектами: двумя DLL-файлами и одним EXE. Одну библиотеку EXE-файл грузит статически, другую динамически, после чего вызывает у этих библиотек несколько функций. Статически загружаемая библиотека и сам DLL-файл содержат статический TLS. (В динамически загружаемых библиотеках статического TLS быть не должно, так как он не будет инициализирован). Этот солюшен я приложил в архив в конце статьи. Упаковав и обе DLL-ки, и exe-файл (дав потом обоим упакованным DLL-файлам оригинальные имена), я убедился, что все работает без изменений, как и оригинальные файлы.
Полный солюшен для этого шага: Own PE Packer Step 8
Дикс, ты гений! осталось только консоль докодить (команды и тд) и все - готов пакер. Ты не надумал пару антиотладочных фишек вкрячить или движок треш-кода или поддержку фейковых сигнатур?
Кстати, сжатие на пределе или можно улучшить?
Да сколько можно уже про антиотладку и про сжатие-то спрашивать?
1. Берешь, качаешь сорс и допиливаешь себе антиотладку
2. Берешь, ищешь хороший алгоритм сжатия и меняешь его и получаешь МАКСИМУМ сжатия
какие проблемы-то :)
Ок, больше не буду доставать вопросами. Спасибо за внимание. Всего доброго..
Можешь доставать, но конкретно вопросы про сжатие и про антиотладку ты уже раза по три задал, и я уже на них отвечал
Очень интересно дикс , жаль правда что это пока что не мой уровень , но надеюсь за пару годков нагоню ;)
Дикс, я не претендую на внимание с твоей стороны, Вы оба мне интересны как личности, а т.к. я не совсем подкован в кодинге - здесь я конечно пасую, и наверное выгляжу занудой. Да и вообще - понятия не имею, зачем читаю Ваш блог, с радостью пообщался бы на жизненные темы, политические, философские, темы здоровья и тд, а тут совершенно не мое - но почему то, читаю...
В частности о:
> add byte ptr [eax], al;
> add byte ptr [eax], al;
>...
>... В инлайновом ассемблере MSVC++ такие директивы не разрешены.
Для этих целей M$ придумал(а) пседоинструкцию _emit.
А вообще, статья интересная +5 :)
Вот искал-искал что-то подобное, но не нашел. Спасибо)