Предыдущий шаг здесь.
Появилась новая версия библиотеки для работы с PE-файлами (0.1.8). Перекачайте и пересоберите ее.
Сегодня мы будем заниматься теми мелочами, на которые я в свое время забил при написании старого упаковщика. Наш распаковщик уже умеет всё, но есть пара мелких нюансов, которые неплохо бы допилить. Первое - это отложенный импорт (Delay-loaded). Этот механизм позволяет загружать необходимые PE-файлу библиотеки тогда, когда они реально становятся нужны, тем самым экономя время на загрузку образа в память. Механизм этот реализуется исключительно компиляторами/линкерами и никакого отношения к загрузчику не имеет, однако в PE-заголовке есть директория IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT, указывающая на данные отложенного импорта. Не знаю, используется ли это линкером и собранной программой, но загрузчику определенно пофиг. Но лучше оставим эту директорию, не будем ее обнулять. Уберем строку
1 |
image.remove_directory(IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT); |
Эта статья рассказывает о какой-то антинаучной хуйне. Если вы — физик, химик, биолог или просто слишком хорошо помните школьную программу соответствующего курса, вам лучше ее не читать. В противном случае вы рискуете умереть от смеха. Мы предупредили. |
I see what you did there. Информация в данной статье приведена по состоянию на неизвестно когда. Возможно, она уже безнадёжно устарела и заинтересует только слоупоков. |
С отложенным импортом всё. Следующая вещь, требующая внимания - это конфигурация загрузки образа. Есть такая директория в заголовке PE-файлов, IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG. Эта директория может содержать структуру IMAGE_LOAD_CONFIG_DIRECTORY32 для x86 PE-файлов (IMAGE_LOAD_CONFIG_DIRECTORY64 для PE+), которая предоставляет загрузчику информацию о том, как образ должен быть загружен. Еще там же содержится список адресов команд, имеющих префикс LOCK, который на однопроцессорных системах заменяется на NOP, а также список всех SEH-обработчиков (он используется для предотвращения SEH-хакинга и представляет собой список всех легальных и допустимых обработчиков исключений в PE-файле). Компиляторы MSVC++ последних версий иногда генерируют эту директорию, помещая в нее список SEH-обработчиков программы и указатель на свой security cookie (переменная для контроля переполнений и порчи буферов/стэка). Судя по исходникам ядра Win 2000, это все считывается загрузчиком, поэтому убивать напрочь директорию IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG не совсем правильно, хотя нарушений в работе PE-файлов после ее зануления я не наблюдал. Сохраним эту директорию, переместив ее во вторую секцию упакованного файла ("kaimi.io"). Первым делом уберем из кода упаковщика строку
1 |
image.remove_directory(IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG); |
В остальном нам всегда поможет моя библиотека для работы с PE. Список SE-хендлеров мы просто перенесем в нашу вторую добавленную секцию. А вот список адресов LOCK-префиксов нам придется вручную обработать в распаковщике (переносить его мы не будем, так как загрузчик на этапе распаковки не должен их фиксить - файл еще не распакован). В распаковщике добавим после строк
1 2 |
exports = image.get_exported_functions(exports_info); } |
строки
1 2 3 4 5 6 7 |
//Если файл имеет Image Load Config, получим информацию о ней std::auto_ptr<pe_base::image_config_info> load_config; if(image.has_config()) { std::cout << "Reading Image Load Config..." << std::endl; load_config.reset(new pe_base::image_config_info(image.get_image_config())); } |
Эти строки считывают конфигурацию загрузки образа, если она имеется. Код аналогичен считыванию TLS. Далее, строку
1 |
if(tls.get() || image.has_exports() || image.has_reloc()) |
меняем на
1 |
if(tls.get() || image.has_exports() || image.has_reloc() || load_config.get()) |
так как директорию конфигурации загрузки мы будем размещать там же, в секции "kaimi.io". Далее, аналогично меняем строку
1 |
if(!image.has_reloc() && !image.has_exports()) |
на
1 |
if(!image.has_reloc() && !image.has_exports() && !load_config.get()) |
и
1 |
image.rebuild_relocations(reloc_tables, unpacker_section, unpacker_section.get_raw_data().size(), true, !image.has_exports()); |
на
1 |
image.rebuild_relocations(reloc_tables, unpacker_section, unpacker_section.get_raw_data().size(), true, !image.has_exports() && !load_config.get()); |
и, наконец,
1 |
image.rebuild_exports(exports_info, exports, unpacker_section, unpacker_section.get_raw_data().size()); |
на
1 |
image.rebuild_exports(exports_info, exports, unpacker_section, unpacker_section.get_raw_data().size(), true, !load_config.get()); |
Далее, в структуру packed_file_info (файл structs.h) добавим пару новых полей:
1 2 |
DWORD original_load_config_directory_rva; //Относительный адрес оригинальной директории конфигурации загрузки DWORD lock_opcode; //Фиктивный опкод команды ассемблера LOCK |
Эти поля нам потребуются в распаковщике, а пока что в упаковщике мы их заполним, дописав после строк
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 |
//Запоминаем относительный адрес //оригинальной директории конфигурации загрузки упаковываемого файла basic_info.original_load_config_directory_rva = image.get_directory_rva(IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG); |
и после
1 2 |
//Получаем и сохраняем изначальное количество секций basic_info.number_of_sections = sections.size(); |
такие строки:
1 2 |
//Опкод ассемблерной инструкции LOCK basic_info.lock_opcode = 0xf0; |
Поясню, зачем это нужно. Загрузчик увидит, что наша таблица LOCK-префиксов состоит из единственного элемента, который указывает на поле lock_opcode структуры basic_info (мы ее так соберем, разумеется). На однопроцессорной системе опкод команды LOCK (0xf0), который мы записали в это поле, будет заменен на опкод инструкции NOP (0x90), и в распаковщике мы сможем проверить, нужно ли обрабатывать оригинальную таблицу LOCK-префиксов. Вообще, я не уверен, что эта функциональность присутствует в загрузчиках новых систем начиная от XP (похоже, что всем системам наплевать на эти таблицы), однако, пусть будет, мало ли всплывет. На самом деле, я и файлов-то с LOCK-таблицами не видел ни разу, и может мне просто нефиг делать. Хотя видел, в исходниках Win 2000, но об этом ниже. :D
Ладно, с правками покончено, переходим к пересборке директории конфигурации. Сразу после куска кода, ответственного за пересборку экспортов, дописываем следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
if(load_config.get()) { std::cout << "Repacking load configuration..." << std::endl; pe_base::section& unpacker_section = image.get_image_sections().at(1); //Очистим таблицу адресов LOCK-префиксов load_config->clear_lock_prefix_list(); //Добавим единственный адрес нашего левого LOCK-префикса load_config->add_lock_prefix_rva(pe_base::rva_from_section_offset(image.get_image_sections().at(0), offsetof(packed_file_info, lock_opcode))); //Пересобираем директорию конфигурации загрузки и располагаем ее в секции "kaimi.io" //Пересобираем автоматически таблицу SE Handler'ов и LOCK-префиксов image.rebuild_image_config(*load_config, unpacker_section, unpacker_section.get_raw_data().size(), true, true); } |
Мы пересобираем директорию конфигурации загрузки, располагая ее в самом конце второй добавленной в упакованный файл секции. В опциях распаковщика мы указываем, что таблицу SE-обработчиков и LOCK-префиксов нужно пересобрать. Оригинальную таблицу LOCK-префиксов мы обработаем уже в распаковщике. С упаковщиком на этом все. Переходим к проекту распаковщика (unpacker). Такое впечатление, что снова поехали смещения, указанные в файле parameters.h, и не факт, что в предыдущем шаге они правильные (MSVC++ собирает проект так, как ему соблаговолится, оптимизируя по размеру, поэтому минимальные изменения могут привести к тому, что ассемблерные команды будут использованы другие). Поэтому я решил их раз и навсегда зафиксировать, сделав так:
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 |
//Пролог вручную __asm { jmp next; ret 0xC; next: push ebp; mov ebp, esp; sub esp, 4096; mov eax, 0x11111111; mov ecx, 0x22222222; mov edx, 0x33333333; } //Адрес загрузки образа unsigned int original_image_base; //Относительный адрес первой секции, //в которую упаковщик кладет информацию для //распаковщика и сами упакованные данные unsigned int rva_of_first_section; //Адрес загрузки образа (оригинальный, к нему не применяются релокации) unsigned int original_image_base_no_fixup; //Эти инструкции нужны только для того, чтобы //заменить в билдере распаковщика адреса на реальные __asm { mov original_image_base, eax; mov rva_of_first_section, ecx; mov original_image_base_no_fixup, edx; } |
Теперь у нас смещения ассемблерных команд [mov eax, 0x11111111] и т.д. будут всегда одинаковыми, так как опкод команд [mov eax/ecx/edx, число] всегда одинаков. Поправим под новый код значения смещений в файле parameters.h:
1 2 3 |
static const unsigned int original_image_base_offset = 0x0F; static const unsigned int rva_of_first_section_offset = 0x14; static const unsigned int original_image_base_no_fixup_offset = 0x19; |
Далее перед кодом, обрабатывающим 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 |
//Если файл имеет директорию конфигурации загрузки if(info_copy.original_load_config_directory_rva) { //Получим указатель на оригинальную директорию //конфигурации загрузки const IMAGE_LOAD_CONFIG_DIRECTORY32* cfg = reinterpret_cast<const IMAGE_LOAD_CONFIG_DIRECTORY32*>(info_copy.original_load_config_directory_rva + original_image_base); //Если директория имеет таблицу LOCK-префиксов //и загрузчик переписал наш подложный LOCK-опкод //на опкод NOP (0x90) (т.е. система однопроцессорная) if(cfg->LockPrefixTable && info_copy.lock_opcode == 0x90 /* NOP opcode */) { //Получаем указатель на первый элемент таблицы //абсолютных адресов LOCK-префиксов const DWORD* table_ptr = reinterpret_cast<const DWORD*>(cfg->LockPrefixTable); //Перечисляем их while(true) { //Указатель на LOCK-префикс BYTE* lock_prefix_va = reinterpret_cast<BYTE*>(*table_ptr); if(!lock_prefix_va) break; //Меняем его на NOP *lock_prefix_va = 0x90; } } } |
Вот мы и закончили заниматься уже, по всей видимости, мало кому нужным функционалом, потому что современным одноядерным процессорам пофиг на префикс LOCK, и загрузчику пофиг на таблицу LOCK-префисков. :)
Забавно кстати, но EXE-файлы из Win 2000 нормально пакуются и работают под ней.
P.S. В Win2000 загрузчику тоже, кажется, насрать на LOCK-префиксы. Единственное, что он делает при загрузке - проверяет, чтобы по адресам LOCK-префиксов не были записаны опкоды инструкции NOP (0x90) для многопроцессорных систем. В то время Windows имела два ядра - однопроцессорное и многопроцессорное, которые подсовывались системе еще на этапе установки. С тех пор, по всей видимости, никто описанный функционал директории Load Configuration так и не реализовал, а поля с описаниями остались. Кстати, в Win2000 и сама структура другая совершенно, в ней отсутствуют некоторые поля. Моя библиотека для работы с PE-файлами ее считать не сможет. Но функционал в упаковщике я решил оставить. Теперь упаковщик самый правильный и соответствует открытой документации от Microsoft, хотя ей не соответствует их загрузчик. :) В конце-концов, пересборка самой директории конфигурации загрузки с сохранением адресов SEH-обработчиков - точно не лишнее.
Полный солюшен для этого шага: Own PE Packer Step 9