Предыдущий шаг здесь.
Пора усовершенствовать наш упаковщик. Он уже способен упаковывать и запускать самые простые бинарники, имеющие лишь таблицу импорта. Бинарники с экспортами, ресурсами, TLS, DLL с релокациями ему пока что не под силу. Нужно над этим работать. Для начала сделаем обработку второй по важности вещи после импортов - директории ресурсов.
Сначала добавим пару полей в структуру packed_file_info:
1 2 3 4 |
//... DWORD original_resource_directory_rva; //Относительный адрес оригинальной директории ресурсов DWORD original_resource_directory_size; //Размер оригинальной директории ресурсов //... |
Эти поля будут хранить информацию об оригинальной директории ресурсов. В распаковщике мы их будем записывать в заголовок PE-файла после распаковки данных секций. Но это еще не все. Ведь мы упаковываем данные всех секций, слепляя их в один большой блок, в том числе и ресурсы. У файла пропадет иконка и информация о версии, если они имелись. Он может вообще не запуститься, если у него имелись какие-то специфические манифесты в ресурсах, определяющие права или библиотеки, требуемые файлу для запуска. Нам необходимо расположить главную иконку приложения, информацию о версии и манифест не упаковывая рядом со сжатыми данными в новой директории ресурсов, выставив на нее указатель в PE-заголовке.
Сначала займемся распаковщиком (проект unpacker), тем более, изменения будут минимальными. Добавим строки аналогично коду для директории импорта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//... //Относительный виртуальный адрес директории ресурсов DWORD original_resource_directory_rva; //Виртуальный размер директории ресурсов DWORD original_resource_directory_size; //... original_resource_directory_rva = info->original_resource_directory_rva; original_resource_directory_size = info->original_resource_directory_size; //... //Указатель на директорию ресурсов IMAGE_DATA_DIRECTORY* resource_dir; resource_dir = reinterpret_cast<IMAGE_DATA_DIRECTORY*>(offset_to_directories + sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_DIRECTORY_ENTRY_RESOURCE); //Записываем значения размера и виртуального адреса в соответствующие поля resource_dir->Size = original_resource_directory_size; resource_dir->VirtualAddress = original_resource_directory_rva; |
С распаковщиком все. Переходим к упаковщику (проект simple_pe_packer). Добавим, опять-таки, аналогичные для директории импорта строки для директории ресурсов:
1 2 3 4 5 6 |
//... //Запоминаем относительный адрес и размер //оригинальной директории ресурсов упаковываемого файла basic_info.original_resource_directory_rva = image.get_directory_rva(IMAGE_DIRECTORY_ENTRY_RESOURCE); basic_info.original_resource_directory_size = image.get_directory_size(IMAGE_DIRECTORY_ENTRY_RESOURCE); //... |
В принципе, этого уже должно быть достаточно, чтобы файл с ресурсами (формами, например) мог запуститься. Проблемы возникнут, если у файла есть xp manifest или что-то подобное. Поэтому нам теперь необходимо пересобрать директорию ресурсов в упаковщике, как я описывал выше. В начало файла main.cpp добавим новый #include <pe_resource_manager.h>. Он необходим для доступа к вспомогательным классам для работы с ресурсами. Код будем добавлять в то место упаковщика, где удаляются секции исходного файла (прямо перед этим действием):
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
//Новая пустая корневая директория ресурсов pe_base::resource_directory new_root_dir; if(image.has_resources()) { std::cout << "Repacking resources..." << std::endl; //Получим ресурсы исходного файла (корневую директорию) pe_base::resource_directory root_dir = image.get_resources(); //Оборачиваем оригинальную и новую директорию ресурсов //во вспомогательные классы pe_resource_viewer res(root_dir); pe_resource_manager new_res(new_root_dir); try { //Перечислим все именованные группы иконок //и группы иконок, имеющие ID pe_resource_viewer::resource_id_list icon_id_list(res.list_resource_ids(pe_resource_viewer::resource_icon_group)); pe_resource_viewer::resource_name_list icon_name_list(res.list_resource_names(pe_resource_viewer::resource_icon_group)); //Сначала всегда располагаются именованные ресурсы, поэтому проверим, есть ли они if(!icon_name_list.empty()) { //Получим самую первую иконку для самого первого языка (по индексу 0) //Если надо было бы перечислить языки для заданной иконки, можно было вызвать list_resource_languages //Если надо было бы получить иконку для конкретного языка, можно было вызвать get_icon_by_name (перегрузка с указанием языка) //Добавим группу иконок в новую директорию ресурсов new_res.add_icon( res.get_icon_by_name(icon_name_list[0]), icon_name_list[0], res.list_resource_languages(pe_resource_viewer::resource_icon_group, icon_name_list[0]).at(0)); } else if(!icon_id_list.empty()) //Если нет именованных групп иконок, но есть группы с ID { //Получим самую первую иконку для самого первого языка (по индексу 0) //Если надо было бы перечислить языки для заданной иконки, можно было вызвать list_resource_languages //Если надо было бы получить иконку для конкретного языка, можно было вызвать get_icon_by_id_lang //Добавим группу иконок в новую директорию ресурсов new_res.add_icon( res.get_icon_by_id(icon_id_list[0]), icon_id_list[0], res.list_resource_languages(pe_resource_viewer::resource_icon_group, icon_id_list[0]).at(0)); } } catch(const pe_exception&) { //Если какая-то ошибка с ресурсами, например, иконок нет, //то ничего не делаем } try { //Получим список манифестов, имеющих ID pe_resource_viewer::resource_id_list manifest_id_list(res.list_resource_ids(pe_resource_viewer::resource_manifest)); if(!manifest_id_list.empty()) //Если манифест есть { //Получим самый первый манифест для самого первого языка (по индексу 0) //Добавим манифест в новую директорию ресурсов new_res.add_resource( res.get_resource_data_by_id(pe_resource_viewer::resource_manifest, manifest_id_list[0]).get_data(), pe_resource_viewer::resource_manifest, manifest_id_list[0], res.list_resource_languages(pe_resource_viewer::resource_manifest, manifest_id_list[0]).at(0) ); } } catch(const pe_exception&) { //Если какая-то ошибка с ресурсами, //то ничего не делаем } try { //Получим список структур информаций о версии, имеющих ID pe_resource_viewer::resource_id_list version_info_id_list(res.list_resource_ids(pe_resource_viewer::resource_version)); if(!version_info_id_list.empty()) //Если информация о версии есть { //Получим самую первую структуру информации о версии для самого первого языка (по индексу 0) //Добавим информацию о версии в новую директорию ресурсов new_res.add_resource( res.get_resource_data_by_id(pe_resource_viewer::resource_version, version_info_id_list[0]).get_data(), pe_resource_viewer::resource_version, version_info_id_list[0], res.list_resource_languages(pe_resource_viewer::resource_version, version_info_id_list[0]).at(0) ); } } catch(const pe_exception&) { //Если какая-то ошибка с ресурсами, //то ничего не делаем } } |
Итак, что же происходит в этом куске кода? Если вкратце, то мы получаем ресурсы оригинального файла и ищем в них:
1. Иконки
2. Манифест
3. Информацию о версии
Эти три вида ресурсов необходимо в неупакованном виде положить в новую директорию ресурсов. Иконку (точнее, группу иконок) из оригинального файла мы возьмем самую первую, именно ее использует Windows как основную иконку файла. Манифест всегда у файла один, и в нем сказано, какие права нужны файлу для запуска и какие DLL-файлы используются. Там может храниться и другая информация. Наконец, информация о версии - это непосредственно информация о файле, которую также можно просмотреть в проводнике Windows.
Я до этого нигде не описывал, как устроена директория ресурсов, поэтому для общего развития приведу эту информацию сейчас. Посмотрим на ресурсы какого-нибудь exe-файла в CFF Explorer'е:
Как видно, ресурсы организованы в виде дерева. В корневой директории сначала располагаются записи, указывающие на тип ресурса. В них вложены директории с записями, содержащими имя или ID ресурса, а в них, в свою очередь, вложены директории с записями, указывающими язык ресурса. Последние содержат директорию данных, которая указывает уже на данные ресурса.
Так вот, в коде выше мы перечисляем все иконки, имеющие ID или имя. Так как в PE-файлах все ресурсы сортируются, причем сначала идут именованные ресурсы, а потом только ресурсы с ID, исходя из этого, мы и ищем самую первую иконку. Она и будет иконкой приложения, и ее мы добавляем в новую директорию ресурсов, сохраняя ее имя/ID и язык. Аналогично поступаем с манифестом и информацией о версии - у всех файлов не больше одной такой записи, причем всегда неименованные, т.е. имеющие ID, поэтому берем и сохраняем первую запись манифеста и первую запись информации о версии.
Новую директорию ресурсов мы создали, теперь надо ее сохранить в создаваемом нами файле. Сначала добавим пару строк перед строкой, производящей сборку импортов:
1 2 3 4 5 6 7 8 |
//Если у нас есть ресурсы для сборки, //отключим автоматическое урезание секции после //добавления в нее импортов if(!new_root_dir.get_entry_list().empty()) settings.enable_auto_strip_last_section(false); //Пересоберем импорты image.rebuild_imports(imports, added_section, settings); |
Дело в том, что все пересборщики в моей библиотеке для работы с PE автоматически уберут все нулевые байты с конца секции, в которую пересобирают импорты/экспорты/ресурсы и т.д., при условии, что секция последняя в файле. Это совершенно адекватное поведение, позволяющее уменьшить размер файла в пределах файлового выравнивания, так как загрузчик все равно нулевые байты восстановит, но уже в памяти, в количестве [выровненный виртуальный размер секции] минус [физический размер секции]. Таким образом, если у нас какие-то структуры внутри секции заканчиваются нулевым элементом (например, завершающий дескриптор таблицы импортов), физически в файл он записан не будет, если эта таблица импортов собирается в последней секции PE-файла. С этим, кстати, связаны интересные баги в таких редакторах, как CFF Explorer, он опирается только на физические данные файла, а не на виртуальные. Поэтому он может криво отобразить ту же таблицу импорта с отрезанными нулевыми байтами в конце.
Сейчас же нам необходимо, чтобы нулевые байты в конце секции, относящиеся к таблице импорта, не обрезались, так как мы сразу после импортов разместим ресурсы. Если мы обрежем эти нулевые байты, то директория ресурсов наслоится на директорию импортов, и получится полная хрень.
Идем дальше - пересобираем ресурсы:
1 2 3 |
//Пересоберем ресурсы, если есть, что пересобирать if(!new_root_dir.get_entry_list().empty()) image.rebuild_resources(new_root_dir, added_section, added_section.get_raw_data().size()); |
Здесь все ясно - если какие-то ресурсы есть, мы их записываем в новый PE-файл. Располагаем мы их в самом конце добавленной секции, сразу за импортами. Так как секция последняя на момент пересборки ресурсов, она расширится автоматически.
Осталось убрать одну строчку, которая убирает директорию ресурсов:
1 |
image.remove_directory(IMAGE_DIRECTORY_ENTRY_RESOURCE); |
Все готово! Можно проверить упаковщик на любом exe-файле, имеющем импорты, ресурсы и даже релокации (хотя мы их пока что не обрабатываем). Давайте возьмем какой-нибудь проект на MSVC++ с MFC и посмотрим с помощью CFF Explorer'а, какие ресурсы в нем были изначально:
После упаковки имеем:
Итак, осталась одна группа иконок с несколькими иконками в ней (иконки приложения), манифест (configuration files) и информация о версии. Разумеется, упакованный файл успешно запускается!
Полный солюшен для этого шага: Own PE Packer step 5
мегареспект! При такой паковке ресурсов антивирусы (надеюсь) не будут срать кирпичами при виде пакованного твоим пакером файла. Самое интересное впереди, с нетерпением жду продолжения.
С уважением!
Можно в перспективе вкрячить что-нить в духе:
{0x01
,0x90 // NOP
,0x90 // NOP
,0x00},
{0x01
,0xF9 // STC
,0xF9 // STC
,0x00},
{0x01
,0xF8 // CLC
,0xF8 // CLC
,0x00},
{0x02
,0xC0FE // INC AL
,0xC8FE // DEC AL
,0x00},
{0x02
,0x0004 // ADD AL, 0
,0x002C // SUB AL, 0
,0x02},
{0x02
,0x002C // SUB AL, 0
,0x0004 // ADD AL, 0
,0x02},
{0x02
,0xC102 // ADD AL, CL
,0xC12A // SUB AL, CL
,0x00},
{0x02
,0xC12A // SUB AL, CL
,0xC102 // ADD AL, CL
,0x00},
{0x02
,0x0034 // XOR AL, 0
,0x0034 // XOR AL, 0
,0x02},
{0x03
,0x00C8C0 // ROR AL, 0
,0x00C0C0 // ROL AL, 0
,0x01},
{0x03
,0x00C0C0 // ROL AL, 0
,0x00C8C0 // ROR AL, 0
,0x01},
{0x03
,0xE801EB // Self modifing
,0xE801EB // Self modifing
,0x00},
{0x03
,0xE901EB // Self modifing
,0xE901EB // Self modifing
,0x00},
{0x03
,0xC201EB // Self modifing
,0xC201EB // Self modifing
,0x00}
};
Ну и потом:
{
command=*Iter;
_Data=command.Data;
switch(command.OpCode)
{
case 0xC0FE: // INC AL
_asm
{
INC _Value
}
break;
case 0x0004: // ADD AL, 0
_asm
{
MOV CL,_Data
ADD _Value,CL
}
break;
case 0x002C: // SUB AL, 0
_asm
{
MOV CL,_Data
SUB _Value,CL
}
break;
case 0xC102: // ADD AL, CL
_asm
{
MOV CL,Count
ADD _Value,CL
}
break;
case 0xC12A: // SUB AL, CL
_asm
{
MOV CL,Count
SUB _Value,CL
}
break;
case 0x0034: // XOR AL, 0
_asm
{
MOV CL,_Data
XOR _Value,CL
}
break;
case 0x00C8C0: // ROR AL, 0
_asm
{
MOV CL,_Data
ROR _Value,CL
}
break;
case 0x00C0C0: // ROL AL, 0
_asm
{
MOV CL,_Data
ROL _Value,CL
}
break;
}
}
return(_Value);
}
Это бы вообще было бы потрясно, чтобы если что - самому подправить код на выходе.
Как смотришь на это?
Я пока не думаю ни о какой антиотладке. А так - проекты выложены, все сорсы открыты, правь на здоровье)
Не, я в любом случае буду править, просто думал узнать твое мнение, и как бы ты это реализовал, а потом уже отталкиваясь от этого - плясать дальше.
Кстати не про антиотладку сейчас думал а о движке трешкода, конечно можно хитрее реализовать, можно вообще сделать, чтобы он подцеплял фейковые сигны из пейдовского user.db.
Просто мне интересно, как ты видишь готовую версию пакера. Вот и все.
Пока никак. Впереди еще TLS, релокации, экспорты, еще мб что-то по мелочам, потом какой-никакой консольный интерфейс еще надо запилить с опциями пакования...
Спасибо большое за Ваш труд. Отличный ресурс, слава богу я Вас нашел!
Explorer.exe ставит в качестве лицевой иконки - самую качественную. т.е. с самым большим разрешением.
На самом деле по моим наблюдениям он ставит ту из иконок, которая больше всего подходит по размеру. Зависит от настроек отображения папки ("маленькие значки", "большие значки", "таблица" и т.д.)
А зачем нужно восстанавливать data directories в образе (процессе) после распаковки? Какие нибудь WinAPI функции после загрузки к ним обращаются, или просто для чистоты и полноты восстановленного образа? Всегда ли распаковщик будет работать без восстановленных data directories?
Ну, например, стандартная библиотека visual c++ к ним обращалась, помню. По-моему, оттуда она доставала флаг, DLL образ или обычный исполняемый файл, чтобы поддержать соответствующую процедуру загрузки. Поэтому без восстановленных директорий образ, скорее всего, работать не будет.