Предыдущий шаг здесь. Там, кстати, имелась ошибка в коде, я ее поправил. Она проявлялась, когда у файла было больше одного TLS-коллбэка.
Появилась новая версия библиотеки для работы с PE-файлами (0.1.7). Перекачайте и пересоберите ее.
Перейдем к следующей немаловажной части многих PE-файлов - релокациям. Они используются, когда невозможно загрузить образ по указанному в заголовке базовому адресу. Преимущественно такое поведение характерно для DLL-файлов (они в принципе без релокаций не могут нормально работать). Представьте, что exe-файл грузится по адресу 0x400000. Этот exe-файл грузит DLL, которая также грузится по этому адресу. Адреса совпадают, и загрузчик будет искать релокации у DLL-файла, потому что он грузится вторым после exe. И если релокаций не будет, то загрузка не пройдет.
Сами релокации - это просто набор таблиц с указателеми на DWORD'ы, которе загрузчик должен пересчитать, если образ загружается по адресу, отличному от базового. Типов релокаций много, но реально в x86 (PE) используются только два: IMAGE_REL_BASED_HIGHLOW = 3 и IMAGE_REL_BASED_ABSOLUTE = 0, причем второй ничего не делает, а нужен только для выравнивания таблиц релокаций.
Сразу скажу, что загрузчик exe-файлы грузит практически всегда по базовому адресу, не применяя релокации. DLL наш упаковщик паковать пока не умеет, поэтому для теста упаковки релокаций мы должны создать exe-файл с некорректным базовым адресом, и тогда загрузчик будет вынужден этот файл в памяти переместить. Я тут не буду приводить исходный код проекта для теста, вы найдете его в солюшене в конце статьи. Базовый адрес загрузки (Linker - Advanced - Base Address) я выбрал 0x7F000000.
Релокации, как и все остальное, нам придется обрабатывать после распаковки файла вручную. Но перед этим необходимо дать понять загрузчику, что релокации у файла есть. Кроме того, нам нужно будет узнать новый адрес, на который загрузчик переместил файл.
Чтобы дать загрузчику знать о том, что у нашего файла есть релокации, делать ничего и не надо - у нас еще от оригинального файла остались нужные флаги, выставленные в заголовках PE-файла. Однако, нам нужно знать, по какому адресу файл загрузился.
Начнем с кода распаковщика (проект unpacker). Чтобы знать, по какому адресу файл должен был загрузиться, и по какому он реально загрузился, мы можем сделать так:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//Адрес загрузки образа (оригинальный, к нему не применяются релокации) unsigned int original_image_base_no_fixup; //... //Эти инструкции нужны только для того, чтобы //заменить в билдере распаковщика адреса на реальные __asm { mov original_image_base, 0x11111111; mov rva_of_first_section, 0x22222222; mov original_image_base_no_fixup, 0x33333333; } |
Мы добавили переменную, смысл которой полностью аналогичен добавленной в каком-то из предыдущих шагов переменной original_image_base. Отличие будет в том, что к переменной original_image_base мы применим релокации, узнав таким образом, по какому реальному адресу загрузился образ. При этом все последующие действия в распаковщике, которые мы производим с использованием этой переменной, править будет не нужно. А вот содержимое переменной original_image_base_no_fixup мы не будем модифицировать, запомнив тем самым, по какому адресу образ должен был загрузиться. Эту переменную, как и две другие, будет записывать упаковщик для распаковщика.
Модифицируем в распаковщике файл parameters.h, обновив смещения к этим трем переменным:
1 2 3 4 5 6 |
#pragma once static const unsigned int original_image_base_offset = 0x11; static const unsigned int rva_of_first_section_offset = 0x1B; static const unsigned int original_image_base_no_fixup_offset = 0x22; static const unsigned int empty_tls_callback_offset = 0x2; |
Теперь, как всегда, модифицируем структуру packed_file_info упаковщика (проект simple_pe_packer), добавив в нее два поля:
1 2 |
DWORD original_relocation_directory_rva; //Относительный адрес оригинальной директории релокаций DWORD original_relocation_directory_size; //Размер оригинальной директории релокаций |
Далее, аналогично тому, как мы делали с импортами и ресурсами:
1 2 3 4 |
//Запоминаем относительный адрес и размер //оригинальной директории релокаций упаковываемого файла basic_info.original_relocation_directory_rva = image.get_directory_rva(IMAGE_DIRECTORY_ENTRY_BASERELOC); basic_info.original_relocation_directory_size = image.get_directory_size(IMAGE_DIRECTORY_ENTRY_BASERELOC); |
После строки:
1 2 3 |
//Записываем по нужным смещениям адрес //загрузки образа *reinterpret_cast<DWORD*>(&unpacker_section_data[original_image_base_offset]) = image.get_image_base_32(); |
допишем следующую:
1 |
*reinterpret_cast<DWORD*>(&unpacker_section_data[original_image_base_no_fixup_offset]) = image.get_image_base_32(); |
которая в свежедобавленную в распаковщик переменную запишет значение базового адреса загрузки образа. На этом этапе после упаковки любого файла переменные original_image_base и original_image_base_no_fixup будут содержать одинаковые значения. Нужно натравить загрузчик на содержимое original_image_base, чтобы он при условии перемещения образа в памяти пофиксил ее. Добавим таблицу релокаций для этого. Код будем писать после следующих строк:
1 2 3 4 5 |
//... //Выставляем новую точку входа - теперь она указывает //на распаковщик, на самое его начало image.set_ep(image.rva_from_section_offset(unpacker_added_section, 0)); } |
Итак,
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 |
//Если у файла есть релокации if(image.has_reloc()) { std::cout << "Creating relocations..." << std::endl; //Создаем список таблиц релокаций и единственную таблицу pe_base::relocation_table_list reloc_tables; pe_base::relocation_table table; pe_base::section& unpacker_section = image.get_image_sections().at(1); //Устанавливаем виртуальный адрес таблицы релокаций //Он будет равен относительному виртуальному адресу второй добавленной //секции, так как именно в ней находится код распаковщика //с переменной, которую мы будем фиксить table.set_rva(unpacker_section.get_virtual_address()); //Добавляем релокацию по смещению original_image_base_offset из //файла parameters.h распаковщика table.add_relocation(pe_base::relocation_entry(original_image_base_offset, IMAGE_REL_BASED_HIGHLOW)); //Добавляем таблицу в список таблиц reloc_tables.push_back(table); //Пересобираем релокации, располагая их в конце //секции с кодом распаковщика image.rebuild_relocations(reloc_tables, unpacker_section, unpacker_section.get_raw_data().size()); } |
Тут все просто - мы просто создали таблицу релокаций из единственного элемента и добавили ее в PE-файл.
Кроме того, необходимо заменить строки:
1 2 |
//Наконец, обрежем уже ненужные нулевые байты с конца секции pe_base::strip_nullbytes(unpacker_added_section.get_raw_data()); |
на:
1 2 3 |
//Наконец, обрежем уже ненужные нулевые байты с конца секции if(!image.has_reloc()) pe_base::strip_nullbytes(unpacker_added_section.get_raw_data()); |
дабы последние байты данных, используемых для инициализации локальной памяти потока, не налезли на релокации, которые мы размещаем прямо за ними.
Осталось убрать ранее добавленную строку:
1 |
image.remove_directory(IMAGE_DIRECTORY_ENTRY_BASERELOC); |
чтобы директория релокаций не убиралась из файла (вызов image.rebuild_relocations заполняет ее таким образом, чтобы она указывала на новую директорию релокаций).
Все, что осталось сделать - обработать релокации оригинального файла в распаковщике (проект unpacker):
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 |
//Если у файла были релокации //и файл был перемещен загрузчиком if(info_copy.original_relocation_directory_rva && original_image_base_no_fixup != original_image_base) { //Указатель на первую структуру IMAGE_BASE_RELOCATION const IMAGE_BASE_RELOCATION* reloc = reinterpret_cast<const IMAGE_BASE_RELOCATION*>(info_copy.original_relocation_directory_rva + original_image_base); //Размер директории перемещаемых элементов (релокаций) unsigned long reloc_size = info_copy.original_relocation_directory_size; //Количество обработанных байтов в директории unsigned long read_size = 0; //Перечисляем таблицы перемещаемых элементов while(reloc->SizeOfBlock && read_size < reloc_size) { //Перечисляем все элементы в таблице for(unsigned long i = sizeof(IMAGE_BASE_RELOCATION); i < reloc->SizeOfBlock; i += sizeof(WORD)) { //Значение перемещаемого элемента WORD elem = *reinterpret_cast<const WORD*>(reinterpret_cast<const char*>(reloc) + i); //Если это релокация IMAGE_REL_BASED_HIGHLOW (других не бывает в PE x86) if((elem >> 12) == IMAGE_REL_BASED_HIGHLOW) { //Получаем DWORD по адресу релокации DWORD* value = reinterpret_cast<DWORD*>(original_image_base + reloc->VirtualAddress + (elem & ((1 << 12) - 1))); //Фиксим его, как PE-загрузчик *value = *value - original_image_base_no_fixup + original_image_base; } } //Просчитываем количество обработанных байтов //в директории релокаций read_size += reloc->SizeOfBlock; //Переходим к следующей таблице релокаций reloc = reinterpret_cast<const IMAGE_BASE_RELOCATION*>(reinterpret_cast<const char*>(reloc) + reloc->SizeOfBlock); } } |
Этот код я разместил в распаковщике прямо перед кодом, который производит обработку TLS. Мы действуем как загрузчик. Убедившись в том, что файл был перемещен и что он имеет таблицу релокаций, осуществляем перебор всех таблиц релокаций (или перемещаемых элементов, другими словами) и всех релокаций в пределах таблицы. Просчитываем значения по каждому адресу, на которые указывают перемещаемые элементы. Если, например, DWORD по адресу, который должен быть пересчитан, содержал значение 0x800000, базовый адрес загрузки PE-файла 0x400000, а реально он загрузился по адресу 0x500000, то мы высчитываем новое значение по формуле [0x800000 - 0x400000 + 0x500000] = 0x900000.
Забавно кстати, чуть раньше я писал о том, что в naked-функциях MSVC++ не позволяет одновременно объявлять и инициализировать переменные. Оказалось, что это так только в общей области видимости функции. Если мы сделаем новую вложенную область видимости, то все работает. То есть, код
1 2 3 4 |
void __declspec(naked) func() { int a = 0; } |
не соберется, а
1 2 3 4 5 6 |
void __declspec(naked) func() { { int a = 0; } } |
отлично сработает.
На этом работа с релокациями завершена, и любой файл, имеющий релокации и даже некорректный базовый адрес загрузки, как в солюшене, запустится. Но есть еще кое-что: если файл помимо релокаций имеет TLS, то нас ждет неудача. В директории TLS (структуре IMAGE_TLS_DIRECTORY32) адреса используются абсолютные, а не относительные, поэтому нам необходимо их перемещать, если загрузчик разместил образ по адресу, отличному от базового адреса загрузки, указанного в заголовке PE-файла. Кроме того, адреса TLS-коллбэков, если они есть, также абсолютные, и их тоже нужно править.
Перед началом работы над релокациями TLS я задался вопросом, как это все протестировать. Собирать руками бинарники, которые бы имели релокации и TLS, не было никакого желания. Поэтому я модифицировал пример для тестирования релокаций (reloc_test), о котором я говорил выше, и слинковал его с помощью бесплатного линкера UniLink. Это, пожалуй, единственный линкер, который умеет собирать TLS с коллбэками. Исходный код примера теперь такой:
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 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#include <iostream> #include <Windows.h> //Файл из комплекта линкера UniLink #include "ulnfeat.h" //Несколько TLS-переменных __declspec(thread) int a = 123; __declspec(thread) int b = 456; __declspec(thread) char c[128]; //Пара TLS-коллбэков void __stdcall tls_callback(void*, unsigned long reason, void*) { if(reason == DLL_PROCESS_ATTACH) MessageBoxA(0, "Process Callback!", "Process Callback!", 0); else if(reason == DLL_THREAD_ATTACH) MessageBoxA(0, "Thread Callback!", "Thread Callback!", 0); } void __stdcall tls_callback2(void*, unsigned long reason, void*) { if(reason == DLL_PROCESS_ATTACH) MessageBoxA(0, "Process Callback 2!", "Process Callback 2!", 0); else if(reason == DLL_THREAD_ATTACH) MessageBoxA(0, "Thread Callback 2!", "Thread Callback 2!", 0); } //Процедура потока (пустая, просто, чтобы коллбэки вызвались) DWORD __stdcall thread(void*) { ExitThread(0); } //Два TLS-коллбэка //Это объявление для линкера UniLink TLS_CALLBACK(1, tls_callback); TLS_CALLBACK(2, tls_callback2); int main() { //Выводим переменные из TLS std::cout << "Relocation test " << a << ", " << b << std::endl; c[126] = 'x'; c[127] = 0; std::cout << &c[126] << std::endl; //Спим 2 секунды Sleep(2000); //Запускаем поток и сразу закрываем его хендл CloseHandle(CreateThread(0, 0, &thread, 0, 0, 0)); //Спим 2 секунды Sleep(2000); return 0; } |
Поясню, что делает этот пример. При запуске будут вызваны два TLS-коллбэка - tls_callback и tls_callback2. Будут отображены два мессаджбокса с текстами "Process Callback!" и "Process Callback 2!". После этого в консоль будет выведено следующее:
Relocation test 123, 456
x
Наконец, через 2 секунды создастся новый поток, и TLS-коллбэки будут вызваны снова, но выдадут мессаджбоксы уже с текстами "Thread Callback!" и "Thread Callback 2!", и через 2 секунды программа завершится. Тут мы протестируем по полной программе обработку нашим упаковщиком и TLS, и релокаций. Чтобы собрать эту программу, для начала скомпилируем этот исходник (правой кнопкой мышки на файле main.cpp - Compile). Получим файл main.obj, который и скормим линкеру UniLink, набрав в консоли такую строку:
1 |
ulink.exe -B- -b:0x7F000000 main.obj, main.exe |
Эта команда говорит линкеру ulink.exe о том, что из файла main.obj нужно сделать файл main.exe, установив ему базовый адрес загрузки 0x7F000000 (чтобы наверняка применились релокации) и добавив сами релокации (опция -B-). После выполнения команды у нас будет файл с недопустимым базовым адресом загрузки, TLS с коллбэками и релокациями. Идеально для тестирования!
Переходим к проекту упаковщика (simple_pe_packer). Вынесем переменную first_callback_offset в более широкую область видимости, заменив строки
1 2 3 4 |
//Необходимо зарезервировать место //под оригинальные TLS-коллбэки //Плюс 1 ячейка под нулевой DWORD DWORD first_callback_offset = data.size(); |
на
1 2 3 4 |
//Необходимо зарезервировать место //под оригинальные TLS-коллбэки //Плюс 1 ячейка под нулевой DWORD first_callback_offset = data.size(); |
и дописав строки
1 2 3 |
//Смещение относительно начала второй секции //к абсолютному адресу TLS-коллбэка DWORD first_callback_offset = 0; |
перед
1 2 3 4 5 |
{ //Новая секция pe_base::section unpacker_section; //... |
Далее, после строк
1 2 3 |
//Добавляем релокацию по смещению original_image_base_offset из //файла parameters.h распаковщика table.add_relocation(pe_base::relocation_entry(original_image_base_offset, IMAGE_REL_BASED_HIGHLOW)); |
Дописываем код релокаций TLS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//Если у файла был TLS if(tls.get()) { //Просчитаем смещение к структуре TLS //относительно начала второй секции DWORD tls_directory_offset = image.get_directory_rva(IMAGE_DIRECTORY_ENTRY_TLS) - image.section_from_directory(IMAGE_DIRECTORY_ENTRY_TLS).get_virtual_address(); //Добавим релокации для полей StartAddressOfRawData, //EndAddressOfRawData и AddressOfIndex //Эти поля у нас всегда ненулевые table.add_relocation(pe_base::relocation_entry(static_cast<WORD>(tls_directory_offset + offsetof(IMAGE_TLS_DIRECTORY32, StartAddressOfRawData)), IMAGE_REL_BASED_HIGHLOW)); table.add_relocation(pe_base::relocation_entry(static_cast<WORD>(tls_directory_offset + offsetof(IMAGE_TLS_DIRECTORY32, EndAddressOfRawData)), IMAGE_REL_BASED_HIGHLOW)); table.add_relocation(pe_base::relocation_entry(static_cast<WORD>(tls_directory_offset + offsetof(IMAGE_TLS_DIRECTORY32, AddressOfIndex)), IMAGE_REL_BASED_HIGHLOW)); //Если имеются TLS-коллбэки if(first_callback_offset) { //То добавим еще релокации для поля AddressOfCallBacks //и для адреса нашего пустого коллбэка table.add_relocation(pe_base::relocation_entry(static_cast<WORD>(tls_directory_offset + offsetof(IMAGE_TLS_DIRECTORY32, AddressOfCallBacks)), IMAGE_REL_BASED_HIGHLOW)); table.add_relocation(pe_base::relocation_entry(static_cast<WORD>(first_callback_offset), IMAGE_REL_BASED_HIGHLOW)); } } |
Мы добавили перемещаемые элементы для всех ненулевых полей структуры IMAGE_TLS_DIRECTORY32, содержащих абсолютные адреса. Если у нас есть TLS-коллбэки, то мы добавляем релокацию и для нашего абсолютного адреса пустого TLS-коллбэка. Самое интересное - в распаковщике ничего править не нужно, потому что он обработает релокации оригинального файла, пересчитав тем самым оригинальные адреса TLS-коллбэков, и лишь после этого будет их вызывать. Единственное, что я сделал - это в очередной раз увеличил объем выделяемой распаковщиком на стеке памяти, так как ее уже начало не хватать. (Я заменил команду sub esp, 256 на sub esp, 4096, чтобы уже наверняка).
Протестировав упаковщик на созданном нами ядреном примере main.exe убеждаемся, что все прекрасно работает.
К этому моменту я уже проверил текущую версию упаковщика на главных exe-файлах следующих приложений: IrfanView, HM NIS Edit, Firefox, Notepad++, NSIS, Opera (ее нужно переименовывать в opera.exe после упаковки), Winamp, WinDjView, ResEd, Quake3, CatMario, Media Player Classic, Windows Media Player. После упаковки они работают!
Замечу напоследок, что в комментариях к исходникам UPX есть пометка о том, что если релокации и TLS находятся в одной секции, то загрузчик не будет фиксить адреса в TLS. Я, как видно, сделал именно так, и, как ни странно, все работает на Windows XP и 7 (на других не проверял).
Полный солюшен для этого шага: Own PE Packer Step 7
Круто,спасибо. Сам не собирал, жду завершения цикла, чтобы проделать с нуля.
Подскажи, а как со степенью сжатия?
Немного хуже UPX'а
Прекрасно! А что на данный момент остается нереализованным?
Экспорты и упаковка DLL. Может, что-то еще вспомню.
Упаковку драйверов и .net файлов добавь, будь мужиком
Сам добавляй, от тебя статей уже месяц нет :D