Пишем упаковщик по шагам. Шаг второй. Пакуем.

Предыдущий шаг здесь

Сразу скажу, что по мере написания этого цикла статей я кое-что правлю и дорабатываю в своей библиотеке для работы с PE-файлами. Поэтому вам стоит ее перекачать и пересобрать - сейчас уже есть версия 0.1.3.

И мы продолжаем написание собственного упаковщика. В этом шаге пора переходить непосредственно к упаковке PE-файла. Я достаточно давно выкладывал простенький упаковщик, который был малоэффективным по двум причинам: во-первых, он использовал стандартные Windows-функции для упаковки и распаковки данных, обладающие достаточно низкой степенью сжатия и скоростью, во-вторых, паковались все секции PE-файла по отдельности, что не очень-то оптимально. В этот раз я сделаю по-другому. Мы будем считывать данные всех секций сразу, слеплять их в один кусок и упаковывать. В результирующем файле, таким образом, будет только одна секция (на самом деле две, потом поясню, почему), в которой мы сможем разместить и ресурсы, и код распаковщика, и сжатые данные, и вспомогательные таблицы. Мы получаем некоторый выигрыш, потому что не нужно тратить размер на файловое выравнивание, кроме того, алгоритм LZO явно более эффективен, чем RtlCompressBuffer, во всех отношениях.

Таким образом, алгоритм действий упаковщика будет примерно таким: считывание всех секций, слепление их данных в один буфер и его упаковка, расположение упакованного буфера в новой секции, удаление всех остальных имеющихся секций. Нам придется сохранить все параметры существовавших в оригинальном файле секций, чтобы потом распаковщик смог их восстановить. Напишем для этого специальную структуру:

#pragma pack(push, 1)//Структура, хранящая информацию об упакованной секцииstruct packed_section
{char name[8];//Имя секции
  DWORD virtual_size;//Виртуальный размер
  DWORD virtual_address;//Виртуальный адрес (RVA)
  DWORD size_of_raw_data;//Размер "сырых" данных
  DWORD pointer_to_raw_data;//Файловое смещение сырых данных
  DWORD characteristics;//Характеристики секции};

Эту структуру мы будем записывать в какое-либо место упакованного файла для каждой секции, а код распаковщика будет эти структуры считывать. В этих структурах будет храниться вся необходимая информация для восстановления секций PE-файла.

Кроме того, нам пригодится структура, которая хранит различную полезную информацию об оригинальном файле, которая также понадобится распаковщику. Пока что в ней будет всего три поля, и скорее всего, я буду ее со временем расширять:

//Структура, хранящая информацию об упакованном файлеstruct packed_file_info
{
  BYTE number_of_sections;//Количество секций в оригинальном файле
  DWORD size_of_packed_data;//Размер упакованных данных
  DWORD size_of_unpacked_data;//Размер оригинальных данных};#pragma pack(pop)

Обратите внимание, что обе структуры имеют выравнивание 1. Это нужно для того, чтобы они занимали как можно меньше места. Кроме того, явное указание величины выравнивания избавляет от всяческих проблем при считывании структур из файла во время распаковки.

Идем дальше. Перед упаковкой желательно бы просчитать энтропию секций файла, чтобы определить, есть ли смысл его упаковывать, или он уже сжат по максимуму. Моя библиотека предоставляет такую возможность. Кроме того, стоит проверить, не передали ли нам .NET-бинарник - такие мы упаковывать не будем.

...
  try{//Пытаемся открыть файл как 32-битный PE-файл//Последние два аргумента false, потому что нам не нужны//"сырые" данные привязанных импортов файла и //"сырые" данные отладочной информации//При упаковке они не используются, поэтому не загружаем эти данные
    pe32 image(file, false, false);
 
    //Проверим, не .NET ли образ нам подсунулиif(image.is_dotnet()){
      std::cout<<".NEt image cannot be packed!"<< std::endl;return-1;}
 
    //Просчитаем энтропию секций файла, чтобы убедиться, что файл не упакован{
      std::cout<<"Entropy of sections: ";double entropy = image.calculate_entropy();
      std::cout<< entropy << std::endl;//На wasm.ru есть статья, в которой говорится,//что у PE-файлов нормальная энтропия до 6.8//Если больше, то файл, скорее всего, сжат//Поэтому (пока что) не будем упаковывать файлы//с высокой энтропией, в этом мало смыслаif(entropy >6.8){
        std::cout<<"File has already been packed!"<< std::endl;return-1;}}
...

Перейдем к упаковке секций. Добавим в начало main.cpp строку #include <string> - строки нам пригодятся для формирования блоков данных (они располагают данные последовательно, и мы сможем прямо из строки записывать их в файл). Можно было использовать и векторы (vector), однако особой разницы нет.

Для начала необходимо произвести инициализацию библиотеки LZO:

//Инициализируем библиотеку сжатия LZOif(lzo_init()!= LZO_E_OK){
      std::cout<<"Error initializing LZO library"<< std::endl;return-1;}

Переходим к считыванию секций файла:

    std::cout<<"Reading sections..."<< std::endl;
 
    //Получаем список секций PE-файлаconst pe_base::section_list& sections = image.get_image_sections();if(sections.empty()){//Если у файла нет ни одной секции, нам нечего упаковывать
      std::cout<<"File has no sections!"<< std::endl;return-1;}

Переходим непосредственно к упаковке файла.

//Структура базовой информации о PE-файле
    packed_file_info basic_info ={0};//Получаем и сохраняем изначальное количество секций
    basic_info.number_of_sections= sections.size();
 
    //Строка, которая будет хранить последовательно//структуры packed_section для каждой секции
    std::string packed_sections_info;
 
    {//Выделим в строке необходимое количество памяти для этих стркуткр
      packed_sections_info.resize(sections.size()*sizeof(packed_section));
 
      //"Сырые" данные всех секций, считанные из файла и слепленные воедино
      std::string raw_section_data;//Индекс текущей секцииunsignedlong current_section =0;
 
      //Перечисляем все секцииfor(pe_base::section_list::const_iterator it = sections.begin(); it != sections.end();++it, ++current_section){//Ссылка на очередную секциюconst pe_base::section& s =*it;
 
        {//Создаем структуру информации//о секции в строке и заполняем ее
          packed_section& info
            =reinterpret_cast<packed_section&>(packed_sections_info[current_section *sizeof(packed_section)]);
 
          //Характеристики секции
          info.characteristics= s.get_characteristics();//Указатель на файловые данные
          info.pointer_to_raw_data= s.get_pointer_to_raw_data();//Размер файловых данных
          info.size_of_raw_data= s.get_size_of_raw_data();//Относительный виртуальный адрес секции
          info.virtual_address= s.get_virtual_address();//Виртуальный размер секции
          info.virtual_size= s.get_virtual_size();
 
          //Копируем имя секции (оно максимально 8 символов)memset(info.name, 0, sizeof(info.name));memcpy(info.name, s.get_name().c_str(), s.get_name().length());}
 
        //Если секция пустая, переходим к следующейif(s.get_raw_data().empty())continue;
 
        //А если не пустая - копируем ее данные в строку//с данными всех секций
        raw_section_data += s.get_raw_data();}
 
      //Если все секции оказались пустыми, то паковать нечего!if(raw_section_data.empty()){
        std::cout<<"All sections of PE file are empty!"<< std::endl;return-1;}
 
      //Будем упаковывать оба буфера, слепленные вместе//(читайте ниже)
      packed_sections_info += raw_section_data;}

Немного поясню по коду выше. Мы создали два буфера - packed_sections_info и raw_section_data. Не обращайте внимания, что это строки (std::string), они могут хранить бинарные данные. Первый буфер хранит идущие подряд структуры packed_section, создаваемые и заполняемые нами для всех имеющихся в PE-файле секций. Второй хранит сырые данные всех секций, слепленные вместе. Мы сможем эти данные после распаковки разделить и распихать по секциям заново, потому что информация о размере файловых данных секций хранится у нас в первом буфере и будет доступна распаковщику. Идем дальше - нужно полученный буфер raw_section_data упаковать. Можно вместе с ним упаковать и буфер packed_sections_info - пожалуй, так и сделаем. Для этого конкатенируем строки (читай: бинарные буферы) packed_sections_info и raw_section_data - это сделано в предыдущем блоке кода.

Далее мы займемся созданием новой секции PE-файла, в которой разместим наши упакованные данные:

//Новая секция
    pe_base::section new_section;//Имя - .rsrc (пояснение ниже)
    new_section.set_name(".rsrc");//Доступна на чтение, запись, исполнение
    new_section.readable(true).writeable(true).executable(true);//Ссылка на сырые данные секции
    std::string& out_buf = new_section.get_raw_data();

Итак, мы создали новую секцию (но пока не добавили ее к PE-файлу). Почему я назвал ее .rsrc? Это сделано по одной простой причине. Все файлы, имеющие ресурсы, располагают их в секции с именем .rsrc. Главная иконка файла и информация о версии также хранятся в ресурсах. Увы, проводник Windows умеет считывать иконку файла и отображать ее ТОЛЬКО в том случае, если секция, хранящая ресурсы, называется .rsrc. Эту штуку вроде бы как поправили в последних версиях и сервис-паках Windows, но лучше перестраховаться. Мы пока что ресурсами не занимаемся, поэтому название дано на будущее.

Следующий шаг - сжатие данных. Немного низкоуровневый момент... И тут нам понадобится библиотека Boost. У вас ее еще нет? Пора скачать, установить и собрать! Тем более, делается это очень просто. Но для того класса из этой библиотеки, который я дальше собираюсь использовать, даже и собирать ее не надо. Просто скачайте библиотеку, распакуйте ее в какую-нибудь директорию, например, C:\boost, и укажите в include-директориях в проекте путь к заголовочным файлам буста, например C:\boost\boost. Если мне в дальнейшем из буста потребуется класс, требующий сборки, я поясню, как это делается.

Добавим к заголовкам main.cpp строку #include <boost/scoped_array.hpp>. Далее упаковываем данные.

//Создаем "умный" указатель//и выделяем необходимую для сжатия алгоритму LZO память//Умный указатель в случае чего автоматически//эту память освободит//Мы используем тип lzo_align_t для того, чтобы//память была выровняна как надо//(из документации к LZO)
    boost::scoped_array<lzo_align_t> work_memory(new lzo_align_t[LZO1Z_999_MEM_COMPRESS]);
 
    //Длина неупакованных данных
    lzo_uint src_length = packed_sections_info.size();//Сохраним ее в нашу структуру информации о файле
    basic_info.size_of_unpacked_data= src_length;
 
    //Длина упакованных данных//(пока нам неизвестна)
    lzo_uint out_length =0;
 
    //Необходимый буфер для сжатых данных//(длина опять-таки исходя из документации к LZO)
    out_buf.resize(src_length + src_length /16+64+3);
 
    //Производим сжатие данных
    std::cout<<"Packing data..."<< std::endl;if(LZO_E_OK !=
      lzo1z_999_compress(reinterpret_cast<constunsignedchar*>(packed_sections_info.data()),
      src_length,
      reinterpret_cast<unsignedchar*>(&out_buf[0]),
      &out_length,
      work_memory.get())){//Если что-то не так, выйдем
      std::cout<<"Error compressing data!"<< std::endl;return-1;}
 
    //Сохраним длину упакованных данных в нашу структуру
    basic_info.size_of_packed_data= out_length;//Ресайзим выходной буфер со сжатыми данными по//результирующей длине сжатых данных, которая//теперь нам известна
    out_buf.resize(out_length);//Собираем буфер воедино, это и будут//финальные данные нашей новой секции
    out_buf =//Данные структуры basic_info
      std::string(reinterpret_cast<constchar*>(&basic_info), sizeof(basic_info))//Выходной буфер+ out_buf;
 
    //Проверим, что файл реально стал меньшеif(out_buf.size()>= src_length){
      std::cout<<"File is incompressible!"<< std::endl;return-1;}

Теперь осталось удалить уже ненужные нам секции PE-файла и добавить в него нашу новую секцию:

{//Сначала получим ссылку на самую первую//существующую секцию PE-файлаconst pe_base::section& first_section = image.get_image_sections().front();//Установим виртуальный адрес для добавляемой секции (читай ниже)
      new_section.set_virtual_address(first_section.get_virtual_address());
 
      //Теперь получим ссылку на самую последнюю//существующую секцию PE-файлаconst pe_base::section& last_section = image.get_image_sections().back();//Посчитаем общий размер виртуальных данных
      DWORD total_virtual_size =//Виртуальный адрес последней секции
        last_section.get_virtual_address()//Выровненный виртуальный размер последней секции+ pe_base::align_up(last_section.get_virtual_size(), image.get_section_alignment())//Минус виртуальный размер первой секции- first_section.get_virtual_address();
 
      //Удаляем все секции PE-файла
      image.get_image_sections().clear();
 
      //Изменяем файловое выравнивание, если вдруг оно было//больше, чем 0x200 - это минимально допустимое//для выровненных PE-файлов
      image.realign_file(0x200);
 
      //Добавляем нашу секцию и получаем ссылку на//уже добавленную секцию с пересчитанными адресами и размерами
      pe_base::section& added_section = image.add_section(new_section);//Устанавливаем для нее необходимый виртуальный размер
      image.set_section_virtual_size(added_section, total_virtual_size);}

Что же здесь произошло? Поясню подробнее. Сначала мы определили виртуальный адрес самой первой секции в PE-файле (об этом ниже). После этого мы определили общий виртуальный размер всех секций. Так как виртуальный размер последующей секции равен виртуальному адресу + выровненному виртуальному размеру предыдущей, то, узнав виртуальный адрес и размер последней в файле секции, мы получили виртуальный суммарный размер всех секций плюс адрес самой первой секции. Вычтя из этого числа тот самый виртуальный адрес первой секции, получаем чистый виртуальный размер всех секций вместе взятых. Это, кстати, можно было сделать гораздо проще - вызвав функцию image.get_size_of_image(), которая вернула бы, по сути, то же самое, но из заголовка PE-файла, ну да ладно. Далее мы удалили все существующие секции PE-файла. После этого добавили нашу секцию в PE-файл и получили ссылку на добавленную секцию с пересчитанными адресами и размерами (после добавления мы работаем именно с этой ссылкой). Далее мы должны оставить себе достаточное количество памяти, чтобы потом в нее распаковать все секции - поэтому мы и меняем виртуальный размер свежедобавленной секции на общий размер всех ранее существовавших секций. Виртуальный адрес добавленной секции будет вычислен автоматически по умолчанию. Нас это не очень устраивает - нам необходимо, чтобы область в памяти, занимаемая нашей секцией, полностью совпала с областью, которую занимали все секции оригинального файла. Моя библиотека позволяет явно указать виртуальный адрес секции, если она будет первой в файле (т.е. до ее добавления никаких других секций не существует). Это как раз наша ситуация. Именно поэтому мы и определили виртуальный адрес первой секции и установили его для нашей новой секции.

Тут же мы изменили и файловое выравнивание на минимально допустимое для выровненных файлов, пока у файла не было ни одной секции, чтобы все прошло быстрее.

Однако, одной секцией мы не обойдемся и нам придется создать и добавить еще одну. Зачем? - спросите вы. Ответ прост: первая секция после распаковки будет содержать данные всех секций оригинального файла. А нам еще надо где-то разместить распаковщик. Вы скажете: ну так помести его в конец секции. Но тогда он будет перезаписан при распаковке данными оригинального файла! Можно, конечно, действительно разместить его в той же самой секции, и перед самой распаковкой выделить память (с помощью VirtualAlloc или как-то еще) и скопировать туда тело распаковщика, и исполнять его уже оттуда. Но эту память потом нам нужно будет как-то освободить. И если мы это сделаем из нее самой, то произойдет падение приложения: память освобождена, и регистр процессора eip, указывающий на текущую исполняемую ассемблерную команду, указывает вникуда. Словом, без дополнительной секции не обойтись. Если вы посмотрите на тот же UPX или Upack, то увидите, что они тоже имеют по 2-3 секции.

{//Новая секция
      pe_base::section unpacker_section;//Имя - kaimi.ru
      unpacker_section.set_name("kaimi.ru");//Доступна на чтение и исполнение
      unpacker_section.readable(true).executable(true);//В будущем тут будет код распаковщика и что-то еще
      unpacker_section.get_raw_data()="Nothing interesting here...";//Добавляем и эту секцию
      image.add_section(unpacker_section);}

Переходим к следующему шагу. Немного поиздеваемся над PE-файлом:

//Удалим все часто используемые директории//В дальнейшем мы будем их возвращать обратно//и корректно обрабатывать, но пока так//Оставим только импорты (и то, обрабатывать их пока не будем)
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_BASERELOC);
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT);
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT);
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_EXPORT);
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_IAT);
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG);
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_RESOURCE);
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_SECURITY);
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_TLS);
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_DEBUG);
 
    //Урезаем таблицу директорий, удаляя все нулевые//Урезаем не полностью, а минимум до 12 элементов, так как в оригинальном//файле могут присутствовать первые 12 и использоваться
    image.strip_data_directories(16-4);//Удаляем стаб из заголовка, если какой-то был
    image.strip_stub_overlay();

Я удалил практически все более-менее используемые директории из заголовков. Это крайне неправильно, потому что большинство файлов после такого откажутся работать. Но вы же понимаете, что упаковщик мы совершенствуем шаг за шагом, поэтому пока что будет так. Оставил я только директорию импортов, и то, никак ее не обрабатывал. Импорты - первое, что нам придется корректно обрабатывать, потому что найти файл без импортов очень проблематично, а нам на чем-то надо будет проверять упаковщик.

Далее я обрезал таблицу директорий, так как у нас большинство из них теперь удалено, и удалил стаб из заголовка (обычно в нем лежит DOS stub и Rich-сигнатуры MSVC++, это нам не нужно). Таблицу директорий уменьшаем минимум до 12 элементов, не меньше. Элементы с 1 по 12 могут присутствовать в оригинальном файле и их придется восстановить. Можно было бы, конечно, оставить и самый минимум элементов в таблице, но выигрыша в размере это не даст, зато кода в распаковщике прибавится, если вдруг нам придется расширять таблицу обратно. Почему урезаем таблицу именно до 12 элементов? Потому что четыре последних точно не нужны PE-файлу для успешного запуска, и без них можно спокойно обойтись. Можно было бы еще динамически проверять, есть ли у файла 12-я (Configuration directory), 11-я (TLS directory) и т.д директории, и если нет, то еще больше урезАть таблицу директорий, но, повторюсь, смысла особого в этом нет.

Последнее, что нам остается сделать - сохранить упакованный файл под новым именем:

//Создаем новый PE-файл//Вычислим имя переданного нам файла без директории
    std::string base_file_name(argv[1]);
    std::string dir_name;
    std::string::size_type slash_pos;if((slash_pos = base_file_name.find_last_of("/\\"))!= std::string::npos){
      dir_name = base_file_name.substr(0, slash_pos +1);//Директория исходного файла
      base_file_name = base_file_name.substr(slash_pos +1);//Имя исходного файла}
 
    //Дадим новому файлу имя packed_ + имя_оригинального_файла//Вернем к нему исходную директорию, чтобы сохранить//файл туда, где лежит оригинал
    base_file_name = dir_name +"packed_"+ base_file_name;//Создадим файл
    std::ofstream new_pe_file(base_file_name.c_str(), std::ios::out| std::ios::binary| std::ios::trunc);if(!new_pe_file){//Если не удалось создать файл - выведем ошибку
      std::cout<<"Cannot create "<< base_file_name << std::endl;return-1;}
 
    //Пересобираем PE-образ//Урезаем DOS-заголовок, накладывая на него NT-заголовки//(за это отвечает второй параметр true)//Не пересчитываем SizeOfHeaders - за это отвечает третий параметр
    image.rebuild_pe(new_pe_file, true, false);
 
    //Оповестим пользователя, что файл упакован успешно
    std::cout<<"Packed image was saved to "<< base_file_name << std::endl;

В этой части кода ничего сложного не происходит, все должно быть более-менее понятно из комментариев. Итак, это все, что мы делаем в этом шаге. Шаг получился более чем насыщенным, и вам есть, о чем подумать. Естественно, упакованный файл не будет запускаться, потому что у него нет распаковщика, мы не обрабатываем импорты и не правим точку входа и еще много-много всего... Однако мы можем оценить степень сжатия и проверить в каком-нибудь просмотрщике PE-файлов (я использую CFF Explorer), что все пакуется так, как мы и задумали.

Оригинальный файл:

Пишем упаковщик по шагам. Шаг второй. Пакуем.

Упакованный файл:

Пишем упаковщик по шагам. Шаг второй. Пакуем.

Как видно, Virtual Address + Virtual Size первой секции на второй картинке совпадает с SizeOfImage на первой. Виртуальный адрес первой секции также не изменился. Это именно то, чего мы и хотели добиться. На второй картинке также видно содержимое второй секции kaimi.ru. Степень сжатия неплоха - с 1266 кб до 362 кб.

До встречи в следующей статье! Вопросы приветствуются, их можно задать в комментариях.

И, как всегда, выкладываю последний вариант проекта с последними изменениями: own PE packer step 2

Также рекомендую почитать

Пишем упаковщик по шагам. Шаг второй. Пакуем. Обсудить на форуме


Источник: http://feedproxy.google.com/~r/kaimi/dev/~3/Q4WILS9t6T8/

Читать комменты и комментировать

Добавить комментарий / отзыв



Защитный код
Обновить

Пишем упаковщик по шагам. Шаг второй. Пакуем. | | 2012-09-16 15:19:00 | | Блоги и всяко-разно | | Предыдущий шаг здесьСразу скажу, что по мере написания этого цикла статей я кое-что правлю и дорабатываю в своей библиотеке для работы с PE-файлами. Поэтому вам стоит ее перекачать и пересобрать - сейчас уже есть версия 0.1.3.И мы продолжаем написание собствен | РэдЛайн, создание сайта, заказать сайт, разработка сайтов, реклама в Интернете, продвижение, маркетинговые исследования, дизайн студия, веб дизайн, раскрутка сайта, создать сайт компании, сделать сайт, создание сайтов, изготовление сайта, обслуживание сайтов, изготовление сайтов, заказать интернет сайт, создать сайт, изготовить сайт, разработка сайта, web студия, создание веб сайта, поддержка сайта, сайт на заказ, сопровождение сайта, дизайн сайта, сайт под ключ, заказ сайта, реклама сайта, хостинг, регистрация доменов, хабаровск, краснодар, москва, комсомольск |
 
Поделиться с друзьями: