Предыдущий шаг: здесь.
Появилась новая версия библиотеки для работы с PE-файлами (0.1.4). Перекачайте и пересоберите ее.
Итак, из прошлых шагов мы имеем работающий упаковщик и базовый распаковщик, который пока что ничего не делает. В этом шаге мы добьемся запуска простых упакованных программ (которые не имеют ничего, кроме таблицы импорта и, возможно, релокаций). Первое, что нужно сделать в распаковщике помимо разархивирования данных - это поправить таблицу импорта оригинального файла. Обычно это делает загрузчик, но сейчас для сжатого файла роль загрузчика играем мы.
Добавим несколько полей в нашу структуру packed_file_info:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//Структура, хранящая информацию об упакованном файле struct packed_file_info { BYTE number_of_sections; //Количество секций в оригинальном файле DWORD size_of_packed_data; //Размер упакованных данных DWORD size_of_unpacked_data; //Размер оригинальных данных DWORD total_virtual_size_of_sections; //Полный виртуальный размер всех секций оригинального файла DWORD original_import_directory_rva; //Относительный адрес оригинальной таблицы импорта DWORD original_import_directory_size; //Размер оригинальной таблицы импорта DWORD original_entry_point; //Оригинальная точка входа DWORD load_library_a; //Адрес процедуры LoadLibraryA из kernel32.dll DWORD get_proc_address; //Адрес процедуры GetProcAddress из kernel32.dll DWORD end_of_import_address_table; //Конец IAT }; |
Мы добавили 4 поля, которые нам пригодятся в распаковщике. Теперь необходимо их заполнить в коде упаковщика:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//... //Структура базовой информации о PE-файле packed_file_info basic_info = {0}; //Получаем и сохраняем изначальное количество секций basic_info.number_of_sections = sections.size(); //Запоминаем относительный адрес и размер //оригинальной таблицы импорта упаковываемого файла basic_info.original_import_directory_rva = image.get_directory_rva(IMAGE_DIRECTORY_ENTRY_IMPORT); basic_info.original_import_directory_size = image.get_directory_size(IMAGE_DIRECTORY_ENTRY_IMPORT); //Запоминаем его точку входа basic_info.original_entry_point = image.get_ep(); //Запоминаем общий виртуальный размер всех секций //упаковываемого файла basic_info.total_virtual_size_of_sections = image.get_size_of_image(); |
Здесь все просто. Во втором уроке, если вы помните, я вручную считал общий виртуальный размер всех секций исходного файла и пояснял, что он эквивалентен значению, возвращаемому функцией 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void * __cdecl memset ( void *dst, int val, unsigned int count ) { void *start = dst; while (count--) { *(char *)dst = (char)val; dst = (char *)dst + 1; } return(start); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void * __cdecl memcpy ( void * dst, const void * src, unsigned int count ) { void * ret = dst; /* * copy from lower addresses to higher addresses */ while (count--) { *(char *)dst = *(char *)src; dst = (char *)dst + 1; src = (char *)src + 1; } return(ret); } |
Нас поджидает еще одна проблема. У нас в коде теперь аж четыре модуля (четыре файла с исходным кодом, .c и .cpp), то после компиляции мы будем иметь четыре объектных (obj) файла. Далее линкер должен все это как-то слепить в единый exe-файл, и он это сделает. Но он расположит эти модули в exe-файле в одному ему известном порядке. Нам же необходимо, чтобы функция unpacker_main располагалась в самом начале кода распаковщика. Мы ведь ее в упаковщике патчим, помните? Эта проблема легко решается. Создадим текстовый файл с таким содержанием:
1 2 3 4 |
unpacker_main@0 lzo1z_decompress memset memcpy |
Назовем его link_order.txt и расположим его в папке с исходниками проекта unpacker. Этот файл скажет линкеру, в каком порядке должны располагаться функции в результирующем файле. Укажем этот файл в настройках проекта: Linker - Optimization - Function Order - link_order.txt. Все, с настройками покончено, начинаем писать код распаковщика!
Во-первых, я увеличил количество данных, выделяемых на стеке до 256 байтов (sub esp, 256). Переменных локальных много, поэтому перестрахуемся, а то вдруг 128 не хватит.
Пропишем прототип функции распаковки в начало файла unpacker.cpp:
1 2 3 4 5 6 7 |
//Алгоритм распаковки #include "lzo_conf.h" /* decompression */ LZO_EXTERN(int) lzo1z_decompress ( const lzo_bytep src, lzo_uint src_len, lzo_bytep dst, lzo_uintp dst_len, lzo_voidp wrkmem /* NOT USED */ ); |
Теперь мы сможем ее использовать в коде. Далее нам понадобятся функции VirtualAlloc (для выделения памяти), VirtualProtect (для изменения атрибутов страниц памяти) и VirtualFree (для освобождения выделенной памяти). Давайте импортируем их из kernel32.dll:
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 |
//kernel32.dll *reinterpret_cast<DWORD*>(&buf[0]) = 'nrek'; *reinterpret_cast<DWORD*>(&buf[4]) = '23le'; *reinterpret_cast<DWORD*>(&buf[8]) = 'lld.'; *reinterpret_cast<DWORD*>(&buf[12]) = 0; //Загружаем библиотеку kernel32.dll HMODULE kernel32_dll; kernel32_dll = load_library_a(buf); //Тайпдеф прототипа функции VirtualAlloc typedef LPVOID (__stdcall* virtual_alloc_func)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect); //Тайпдеф прототипа функции VirtualProtect typedef LPVOID (__stdcall* virtual_protect_func)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect); //Тайпдеф прототипа функции VirtualFree typedef LPVOID (__stdcall* virtual_free_func)(LPVOID lpAddress, SIZE_T dwSize, DWORD dwFreeType); //VirtualAlloc *reinterpret_cast<DWORD*>(&buf[0]) = 'triV'; *reinterpret_cast<DWORD*>(&buf[4]) = 'Alau'; *reinterpret_cast<DWORD*>(&buf[8]) = 'coll'; *reinterpret_cast<DWORD*>(&buf[12]) = 0; //Получаем адрес функции VirtualAlloc virtual_alloc_func virtual_alloc; virtual_alloc = reinterpret_cast<virtual_alloc_func>(get_proc_address(kernel32_dll, buf)); //VirtualProtect *reinterpret_cast<DWORD*>(&buf[0]) = 'triV'; *reinterpret_cast<DWORD*>(&buf[4]) = 'Plau'; *reinterpret_cast<DWORD*>(&buf[8]) = 'etor'; *reinterpret_cast<DWORD*>(&buf[12]) = 'tc'; //Получаем адрес функции VirtualProtect virtual_protect_func virtual_protect; virtual_protect = reinterpret_cast<virtual_protect_func>(get_proc_address(kernel32_dll, buf)); //VirtualFree *reinterpret_cast<DWORD*>(&buf[0]) = 'triV'; *reinterpret_cast<DWORD*>(&buf[4]) = 'Flau'; *reinterpret_cast<DWORD*>(&buf[8]) = 'eer'; //Получаем адрес функции VirtualFree virtual_free_func virtual_free; virtual_free = reinterpret_cast<virtual_free_func>(get_proc_address(kernel32_dll, buf)); |
Этот кусок кода аналогичен коду в шаге 3, где мы загружали user32.dll и получали в ней адрес функции MessageBoxA, так что пояснять не буду. Далее следует перенести в локальную область видимости необходимые переменные, которые для нас запас упаковщик:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//Относительный виртуальный адрес директории импорта DWORD original_import_directory_rva; //Виртуальный размер директории импорта DWORD original_import_directory_size; //Оригинальная точка входа DWORD original_entry_point; //Общий размер всех секций файла DWORD total_virtual_size_of_sections; //Количество секций в оригинальном файле BYTE number_of_sections; //Копируем эти значения из структуры packed_file_info, //которую для нас записал упаковщик original_import_directory_rva = info->original_import_directory_rva; original_import_directory_size = info->original_import_directory_size; original_entry_point = info->original_entry_point; total_virtual_size_of_sections = info->total_virtual_size_of_sections; number_of_sections = info->number_of_sections; |
Мы это сделали потому, что скоро структура packed_file_info, находящаяся в самом начале первой секции упакованного файла, будет затерта реальными распакованными данными. Теперь выделим память и распакуем в нее упакованный блок данных:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//Указатель на память, в которую //мы запишем распакованные данные LPVOID unpacked_mem; //Выделяем память unpacked_mem = virtual_alloc( 0, info->size_of_unpacked_data, MEM_COMMIT, PAGE_READWRITE); //Выходной размер распакованных данных //(эта переменная, в принципе, не нужна) lzo_uint out_len; out_len = 0; //Производим распаковку алгоритмом LZO lzo1z_decompress( reinterpret_cast<const unsigned char*>(reinterpret_cast<DWORD>(info) + sizeof(packed_file_info)), info->size_of_packed_data, reinterpret_cast<unsigned char*>(unpacked_mem), &out_len, 0); |
Инициализировать алгоритм LZO перед распаковкой не нужно, для распаковки достаточно вызвать единственную функцию, что мы и сделали. Далее вычислим виртуальный адрес заголовка первой секции.
1 2 3 4 5 6 7 8 9 10 11 12 |
//Указатель на DOS-заголовок файла const IMAGE_DOS_HEADER* dos_header; //Указатель на файловый заголовок IMAGE_FILE_HEADER* file_header; //Виртуальный адрес начала заголовков секций DWORD offset_to_section_headers; //Просчитываем этот адрес dos_header = reinterpret_cast<const IMAGE_DOS_HEADER*>(original_image_base); file_header = reinterpret_cast<IMAGE_FILE_HEADER*>(original_image_base + dos_header->e_lfanew + sizeof(DWORD)); //Вот по такой формуле offset_to_section_headers = original_image_base + dos_header->e_lfanew + file_header->SizeOfOptionalHeader + sizeof(IMAGE_FILE_HEADER) + sizeof(DWORD) /* Signature */; |
Теперь мы имеем виртуальный адрес заголовков секций. Нам их необходимо перезаписать, чтобы в памяти они выглядели так, как выглядят в оригинальном файле. Перед тем, как мы будем это делать, необходимо обработать еще кое-какие мелочи:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//Обнулим всю память первой секции //эта область соответствует области памяти, которую //в оригинальном файле занимают все секции memset( reinterpret_cast<void*>(original_image_base + rva_of_first_section), 0, total_virtual_size_of_sections - rva_of_first_section); //Изменим атрибуты блока памяти, в котором //расположены заголовки PE-файла и секций //Нам необходим доступ на запись DWORD old_protect; virtual_protect(reinterpret_cast<LPVOID>(offset_to_section_headers), number_of_sections * sizeof(IMAGE_SECTION_HEADER), PAGE_READWRITE, &old_protect); //Теперь изменим количество секций //в заголовке PE-файла на оригинальное file_header->NumberOfSections = number_of_sections; |
Приступим к восстановлению заголовков секций:
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 |
//Виртуальный адрес структуры заголовка секции DWORD current_section_structure_pos; current_section_structure_pos = offset_to_section_headers; //Перечислим все секции for(int i = 0; i != number_of_sections; ++i) { //Создаем структуру заголовка секции IMAGE_SECTION_HEADER section_header; //Обнуляем структуру memset(§ion_header, 0, sizeof(section_header)); //Заполняем важные поля: //Характеристики section_header.Characteristics = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->characteristics; //Смещение файловых данных section_header.PointerToRawData = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->pointer_to_raw_data; //Размер файловых данных section_header.SizeOfRawData = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->size_of_raw_data; //Относительный виртуальный адрес секции section_header.VirtualAddress = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->virtual_address; //Виртуальный размер секции section_header.Misc.VirtualSize = (reinterpret_cast<packed_section*>(unpacked_mem) + i)->virtual_size; //Копируем оригинальное имя секции memcpy(section_header.Name, (reinterpret_cast<packed_section*>(unpacked_mem) + i)->name, sizeof(section_header.Name)); //Копируем заполненный заголовок //в память, где находятся заголовки секций memcpy(reinterpret_cast<void*>(current_section_structure_pos), §ion_header, sizeof(section_header)); //Перемещаем указатель на следующий заголовок секции current_section_structure_pos += sizeof(section_header); } |
Заголовки секций восстановили, теперь восстановим их данные:
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 |
//Указатель на сырые данные секции //Необходим для разлепления сжатых данных секций //и распихивания их по нужным местам DWORD current_raw_data_ptr; current_raw_data_ptr = 0; //Восстановим указатель на заголовки секций current_section_structure_pos = offset_to_section_headers; //Снова перечисляем все секции for(int i = 0; i != number_of_sections; ++i) { //Заголовок секции, который мы только что сами записали const IMAGE_SECTION_HEADER* section_header = reinterpret_cast<const IMAGE_SECTION_HEADER*>(current_section_structure_pos); //Копируем данные секции в то место памяти, //где они должны располагаться memcpy(reinterpret_cast<void*>(original_image_base + section_header->VirtualAddress), reinterpret_cast<char*>(unpacked_mem) + number_of_sections * sizeof(packed_section) + current_raw_data_ptr, section_header->SizeOfRawData); //Перемещаем указатель на данные секции //в распакованном блоке данных current_raw_data_ptr += section_header->SizeOfRawData; //Переходим к следующему заголовку секции current_section_structure_pos += sizeof(IMAGE_SECTION_HEADER); } //Освобождаем память с распакованными данными, //она нам больше не нужна virtual_free(unpacked_mem, 0, MEM_RELEASE); |
И, почти все готово. Чтобы упакованный файл запустился, остается лишь пофиксить его таблицу импорта, снова выступив в роли PE-загрузчика. Для начала пофиксим виртуальный адрес и размер таблицы импорта в PE-заголовке:
1 2 3 4 5 6 7 8 9 10 11 |
//Вычислим относительный виртуальный адрес //начала таблицы директорий DWORD offset_to_directories; offset_to_directories = original_image_base + dos_header->e_lfanew + sizeof(IMAGE_NT_HEADERS32) - sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_NUMBEROF_DIRECTORY_ENTRIES; //Указатель на директорию импорта IMAGE_DATA_DIRECTORY* import_dir = reinterpret_cast<IMAGE_DATA_DIRECTORY*>(offset_to_directories + sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_DIRECTORY_ENTRY_IMPORT); //Записываем значения размера и виртуального адреса в соответствующие поля import_dir->Size = original_import_directory_size; import_dir->VirtualAddress = original_import_directory_rva; |
Заполняем таблицу импорта:
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 |
//Если у файла имеются импорты if(original_import_directory_rva) { //Виртуальный адрес первого дескриптора IMAGE_IMPORT_DESCRIPTOR* descr; descr = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(original_import_directory_rva + original_image_base); //Перечисляем все дескрипторы //Последний - нулевой while(descr->Name) { //Загружаем необходимую DLL HMODULE dll; dll = load_library_a(reinterpret_cast<char*>(descr->Name + original_image_base)); //Указатели на таблицу адресов и lookup-таблицу DWORD* lookup, *address; //Учтем, что lookup-таблицы может и не быть, //как я говорил в предыдущем шаге lookup = reinterpret_cast<DWORD*>(original_image_base + (descr->OriginalFirstThunk ? descr->OriginalFirstThunk : descr->FirstThunk)); address = reinterpret_cast<DWORD*>(descr->FirstThunk + original_image_base); //Перечисляем все импорты в дескрипторе while(true) { //До первого нулевого элемента в лукап-таблице DWORD lookup_value = *lookup; if(!lookup_value) break; //Проверим, импортируется ли функция по ординалу if(IMAGE_SNAP_BY_ORDINAL32(lookup_value)) *address = static_cast<DWORD>(get_proc_address(dll, reinterpret_cast<const char*>(lookup_value & ~IMAGE_ORDINAL_FLAG32))); else *address = static_cast<DWORD>(get_proc_address(dll, reinterpret_cast<const char*>(lookup_value + original_image_base + sizeof(WORD)))); //Переходим к следующему элементу ++lookup; ++address; } //Переходим к следующему дескриптору ++descr; } } |
Вот и все, мы, как PE-загрузчик, заполнили PE-файлу таблицу импорта. Осталась пара мелочей:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//Вернем атрибуты памяти заголовков, как было изначально virtual_protect(reinterpret_cast<LPVOID>(offset_to_section_headers), number_of_sections * sizeof(IMAGE_SECTION_HEADER), old_protect, &old_protect); //Эпилог вручную _asm { //Переходим на оригинальную точку входа mov eax, original_entry_point; add eax, original_image_base; leave; //Вот так jmp eax; } |
Теперь вы поняли, зачем нам нужны были собственные пролог и эпилог функции на ассемблере. Вместо инструкции ret, которая раньше располагалась в самом конце кода распаковщика, мы поставили инструкцию jmp eax, осуществляющую переход на код оригинального файла.
Итак, распаковщик теперь сможет запустить простейший PE-файл, имеющий только таблицу импорта. Любой файл с ресурсами, TLS, экспортами работать не будет, и этим мы займемся в следующих шагах. Но мы уже можем запаковать сами себя и запустить запакованный вариант!
Как видно, мы запаковали сами себя, получив бинарник packed_simple_pe_packer.exe, и он работает!
Полный солюшен со всеми проектами для данного шага: Own PE Packer Step 4
По сколько часов в сутки ты спишь? По три?!)
Спасибо за еще одну часть, и очень оперативно!
Написание статьи не много времени занимает, у меня просто было готово сразу несколько частей)
Дико круто! Сколько всего частей планируется?
Хз, не планировал. Пока более-менее завершенное что-то не получится.
> Во-первых, я увеличил количество данных, выделяемых на стеке до 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.
Про это не знал, спасибо
dx, а нет идей случаем, по созданию x64 шелл-кода?
Там же, релоки... пробовал не выходит.
С x64 особо не работал, поэтому не подскажу ничего конкретного, но по идее отличий не так много должно быть. Читать доки и изучать)