Новости из Блогов Пишем упаковщик PE-файлов по шагам.

Discussion in 'Мировые новости. Обсуждения.' started by Suicide, 12 Sep 2012.

  1. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,373
    Likes Received:
    6,619
    Reputations:
    693
    Пишем упаковщик PE-файлов по шагам.
    Пишем упаковщик PE-файлов по шагам. Шаг первый.
    Пишем упаковщик по шагам. Шаг второй. Пакуем.
    Пишем упаковщик по шагам. Шаг третий. Распаковываем.
    Пишем упаковщик по шагам. Шаг четвертый. Запускаем.
    Пишем упаковщик по шагам. Шаг пятый. Ресурсы.
    Пишем упаковщик по шагам. Шаг шестой. TLS.
    Пишем упаковщик по шагам. Шаг седьмой. Релокации.
    Пишем упаковщик по шагам. Шаг восьмой. DLL и экспорты.

    Пишем упаковщик PE-файлов по шагам. Шаг первый.
    Среда, 12. Сентябрь 2012
    автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-1/
    http://kaimi.ru/



    Раз уж я закончил разработку библиотеки на C++ для работы с PE-файлами, грех не использовать ее в каком-то более-менее серьезном проекте. Поэтому я разработаю с ее помощью упаковщик, поясняя по шагам, что я делаю, а либа на C++ сильно упростит нам жизнь. Итак, с чего же начать разработку упаковщика? Наверное, с выбора какого-нибудь несложного бесплатного алгоритма сжатия. После непродолжительных поисков таковой был мной найден: LZO. Он поддерживает множество различных видов сжатия (можно считать, разновидностей), и LZO1Z999 - самая эффективная по стемени сжатия из всех доступных. Это, конечно, не ZIP, но приближается к нему по эффективности: 550-килобайтный файл был сжат zip'ом с максимальной степенью сжатия в 174 килобайта, в то время как LZO сжал тот же файл до 185 килобайтов. Однако у LZO гораздо более быстрый распаковщик. Он также оказался базонезависимым, то есть, его можно разместить по любому виртуальному адресу, и он будет работать без всяких корректировок адресов. Размер распаковщика приятно удивил: Visual Studio с оптимизациями по размеру и отключением исключений и проверок буферов дала результат в 613 байтов кода! Этот алгоритм нам подойдет.

    Я начну написание с самых простых упаковщика и распаковщика, постепенно усложняя их. Для начала просто напишем программку, которая загружает PE-файл с помощью моей библиотеки. Упаковщик будем делать для x86-файлов, т.е. за PE+ пока что браться не будем. Итак, сначала вам необходимо будет скачать и скомпилировать в Visual Studio 2008 или 2010 мою библиотеку для работы с Portable Executable. После того, как вы это сделаете, следует создать новый проект. Я назвал его simple_pe_packer и положил в ту же папку, где лежит библиотека:

    [​IMG]

    Настройки компиляции проекта должны совпадать с настройками компиляции библиотеки, иначе не слинкуется:

    [​IMG]

    Для Debug-конфигурации, соответственно, выставим Multi-threaded Debug (/MTd). Теперь необходимо добавить в солюшен проект библиотеки LZO, чтобы было, чем упаковывать данные. Я скачал с сайта автора библиотеку lzo-2.06, распаковал ее в папку с таким же именем в каталоге с моей библиотекой для работы с PE (см. самый первый скриншот), после чего добавил в солюшен simple_pe_packer проект lzo-2.06, добавив в него все *.c и *.h-файлы из каталога lzo-2.06. Не забываем снова выставить настройки компиляции, как на втором скриншоте. Установим у проекта simple_pe_packer зависимость от проекта lzo-2.06 (правой кнопкой мыши - Project Dependencies, если вы используете английскую студию, конечно). Далее, чтобы lzo-2.06 собралось, необходимо добавить include-директорию:

    [​IMG]

    Эту директорию добавляем и в Release, и в Debug-конфигурацию, разумеется. Теперь вернемся к проекту simple_pe_packer. Здесь мы тоже добавим include-директорию:

    [​IMG]

    Она указывает на место, где лежат заголовочные файлы моей библиотеки для работы с PE-файлами. Если вы разложили всё так же, как и я, то у вас пути совпадут с моими. Если нет, то смотрите, как всё лежит у вас.

    Теперь мы полностью переходим к проекту simple_pe_packer. Добавляем к файлам исходных кодов новый файл main.cpp, в котором будет код нашего упаковщика. Для начала его код будет таким:
    Code:
    //Заголовки для работы с файлами и консолью
    #include <iostream>
    #include <fstream>
    //Заголовочный файл библиотеки для работы с PE-файлами
    #include <pe_32_64.h>
    //Заголовочный файл алгоритма LZO1Z999
    #include "../../lzo-2.06/include/lzo/lzo1z.h"
     
    //Директивы для линкования с собранными библиотеками PE и LZO
    #ifndef _M_X64
    #ifdef _DEBUG
    #pragma comment(lib, "../../Debug/pe_lib.lib")
    #pragma comment(lib, "../Debug/lzo-2.06.lib")
    #else
    #pragma comment(lib, "../../Release/pe_lib.lib")
    #pragma comment(lib, "../Release/lzo-2.06.lib")
    #endif
    #else
    #ifdef _DEBUG
    #pragma comment(lib, "../../x64/Debug/pe_lib.lib")
    #pragma comment(lib, "../x64/Debug/lzo-2.06.lib")
    #else
    #pragma comment(lib, "../../x64/Release/pe_lib.lib")
    #pragma comment(lib, "../x64/Release/lzo-2.06.lib")
    #endif
    #endif
     
    //Пока что пустая функция main
    int main(int argc, char* argv[])
    {
      return 0;
    }
    Это программа, которая совершенно ничего не делает. Однако, следует ее скомпилировать, чтобы убедиться, что все пути настроены верно. Если компиляция прошла успешно, идем дальше. Далее я буду приводить только обновляемый код функции main или его части. Сделаем еще одно небольшое действие - откроем x86 PE-файл:
    Code:
    int main(int argc, char* argv[])
    {
      //Говорим пользователю, как использовать наш упаковщик
      //На текущем шаге никаких опций упаковки не будет, просто
      //необходимо будет запускать упаковщик, передав через командную строку
      //имя файла, который мы хотим упаковать
      if(argc != 2)
      {
        std::cout << "Usage: simple_pe_packer.exe PE_FILE" << std::endl;
        return 0;
      }
     
      //Открываем файл - его имя хранится в массиве argv по индексу 1
      std::ifstream file(argv[1], std::ios::in | std::ios::binary);
      if(!file)
      {
        //Если открыть файл не удалось - сообщим и выйдем с ошибкой
        std::cout << "Cannot open " << argv[1] << std::endl;
        return -1;
      }
     
      try
      {
        //Пытаемся открыть файл как 32-битный PE-файл
        //Последние два аргумента false, потому что нам не нужны
        //"сырые" данные привязанных импортов файла и 
        //"сырые" данные отладочной информации
        //При упаковке они не используются, поэтому не загружаем эти данные
        pe32 image(file, false, false);
     
        //Оповестим пользователя, что файл считан успешно
        std::cout << "File OK" << std::endl;
      }
      catch(const pe_exception& e)
      {
        //Если по какой-то причине открыть его не удалось
        //Выведем текст ошибки и выйдем
        std::cout << e.what() << std::endl;
        return -1;
      }
     
      return 0;
    }
    Осталось скомпилировать этот код и запустить его для проверки. Запустим полученный exe-файл будущего упаковщика в консоли, передав ему его же имя для теста:

    [​IMG]

    Как видим, переданный PE-файл успешно открылся и считался. В следующем уроке мы перейдем непосредственно к простейшей упаковке и напишем на MASM32 (или на Си, я еще не решил) стаб распаковщика. А сейчас двинемся дальше. В начале статьи я написал, что алгоритм распаковки LZO1Z999 базонезависим и занимает всего 613 байтов. Как же получить бинарный вариант алгоритма? Давайте создадим новую конфигурацию проектов и назовем ее ReleaseDecompressor - она будет предназначена исключительно для того, чтобы собрать процедуру распаковщика. Делается это через меню Configuration Manager, в левом меню выбираем New..., вводим имя, выбираем Copy settings from: Release и ставим галку Create new project configurations:

    [​IMG]

    Далее переходим к свойствам проекта lzo-2.06. Во вкладке Configuration Properties - General меняем тип исполняемого файла (Configuration type) на Application (.exe). Далее выделяем все .c-файлы в проекте, кроме lzo1z_d1.c (именно в нем содержится реализация нужного нам распаковщика), и заходим в их свойства. Исключаем их из сборки:

    [​IMG]

    Должна получиться такая картина:

    [​IMG]

    Теперь заходим в настройки файла lzo1z_d1.c - того, который мы оставили в сборке. На вкладке C/C++ - Optimization выбираем оптимизацию по размеру (Optimization - Minimize size (/O1)), далее выбираем Favor Size Or Speed - Favor Small code (/Os). Вернемся теперь снова к настройками проекта lzo-2.06, перейдем на вкладку C/C++ - Code Generation. Отключим C++-исключения (Enable C++ Exceptions - No), отключим проверку буферов (Buffer Security Check - No (/GS-)). Далее, на вкладке Linker - Manifest File отключим генерацию манифеста (Generate Manifest - No (/MANIFEST:NO)). На вкладке Linker - Debugging отключим генерирование отладочной информации (Generate Debug Info - No). На вкладке Linker - System можно выставить подсистему Windows (SubSystem - Windows), но это не играет особой роли. На вкладке Linker - Advanced ставим точку входа (Entry Point - lzo1z_decompress), чтобы никакие CRT к результирующему бинарнику не подключались. На этом все, теперь можно собрать проект lzo-2.06. В результате получим маленький (размером 1.5 кб) exe-файл. Открыв его в каком-нибудь PE-просмотрщике, например, в CFF Explorer'е, увидим, что у него нет ни одной директории, что не может не радовать. Нет импортов, нет релокаций (хотя мы их не отключали) - алгоритм полностью базонезависим! Можно увидеть, что виртуальный размер единственной секции с кодом у файла - 0x265 (или 613 байтов):

    [​IMG]

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

    На этом все, до следующего шага!

    Для желающих выкладываю готовый проект со всеми установленными настройками и необходимыми файлами (однако, библиотеку для работы с PE вам придется скачать и собрать самостоятельно, расположив проект библиотеки так, как описано в начале статьи): own-packer-step1
     
    #1 Suicide, 12 Sep 2012
    Last edited: 27 Sep 2012
  2. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,373
    Likes Received:
    6,619
    Reputations:
    693
    Пишем упаковщик по шагам. Шаг второй. Пакуем.
    Воскресенье, 16. Сентябрь 2012
    автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-2/
    http://kaimi.ru/



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

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

    Таким образом, алгоритм действий упаковщика будет примерно таким: считывание всех секций, слепление их данных в один буфер и его упаковка, расположение упакованного буфера в новой секции, удаление всех остальных имеющихся секций. Нам придется сохранить все параметры существовавших в оригинальном файле секций, чтобы потом распаковщик смог их восстановить. Напишем для этого специальную структуру:
    Code:
    #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-файла.

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

    Идем дальше. Перед упаковкой желательно бы просчитать энтропию секций файла, чтобы определить, есть ли смысл его упаковывать, или он уже сжат по максимуму. Моя библиотека предоставляет такую возможность. Кроме того, стоит проверить, не передали ли нам .NET-бинарник - такие мы упаковывать не будем.
    Code:
    ...
      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:
    Code:
      //Инициализируем библиотеку сжатия LZO
        if(lzo_init() != LZO_E_OK)
        {
          std::cout << "Error initializing LZO library" << std::endl;
          return -1;
        }
    Переходим к считыванию секций файла:
    Code:
     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;
        }
    
    Переходим непосредственно к упаковке файла.
    Code:
     //Структура базовой информации о 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;
          //Индекс текущей секции
          unsigned long 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-файла, в которой разместим наши упакованные данные:
    Code:
     //Новая секция
        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>. Далее упаковываем данные.
    Code:
      //Создаем "умный" указатель
        //и выделяем необходимую для сжатия алгоритму 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<const unsigned char*>(packed_sections_info.data()),
          src_length,
          reinterpret_cast<unsigned char*>(&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<const char*>(&basic_info), sizeof(basic_info))
          //Выходной буфер
          + out_buf;
     
        //Проверим, что файл реально стал меньше
        if(out_buf.size() >= src_length)
        {
          std::cout << "File is incompressible!" << std::endl;
          return -1;
        }
    Теперь осталось удалить уже ненужные нам секции PE-файла и добавить в него нашу новую секцию:
    Code:
       {
          //Сначала получим ссылку на самую первую
          //существующую секцию 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 секции.
    Code:
     {
          //Новая секция
          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-файлом:
    Code:
      //Удалим все часто используемые директории
        //В дальнейшем мы будем их возвращать обратно
        //и корректно обрабатывать, но пока так
        //Оставим только импорты (и то, обрабатывать их пока не будем)
        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) и т.д директории, и если нет, то еще больше урезАть таблицу директорий, но, повторюсь, смысла особого в этом нет.

    Последнее, что нам остается сделать - сохранить упакованный файл под новым именем:
    Code:
       //Создаем новый 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), что все пакуется так, как мы и задумали.

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

    [​IMG]

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

    [​IMG]

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

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

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

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,373
    Likes Received:
    6,619
    Reputations:
    693
    Пишем упаковщик по шагам. Шаг третий. Распаковываем.
    Понедельник, 17. Сентябрь 2012
    автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-3/
    http://kaimi.ru/



    Идем дальше! Пришло время написать распаковщик, именно этим мы начнем заниматься в этом шаге. Обрабатывать исходную таблицу импорта мы пока не будем, так как и в этом уроке нам будет, чем заняться.

    Начнем мы вот с чего. Для работы распаковщика нам стопроцентно потребуются две WinAPI-функции: LoadLibraryA и GetProcAddress. В своем старом упаковщике я писал стаб распаковщика на MASM32 и вообще не создавал таблицу импорта. Я искал адреса этих функций в ядре, что несколько сложно и хардкорно, кроме того, это может вызвать неиллюзорные подозрения у антивирусов. Давайте в этот раз создадим обычную таблицу импортов и сделаем так, чтобы загрузчик сам нам сообщил адреса этих функций! Разумеется, набор из двух этих функций в таблице импорта так же подозрителен, как и полное их отсутствие, но ничто нам не мешает в будущем добавить еще другие левые случайные импорты из различных DLL-файлов. Куда загрузчик будет записывать адреса этих двух функций? Пора расширить нашу структуру packed_file_info!
    Code:
    //Структура, хранящая информацию об упакованном файле
    struct packed_file_info
    {
      BYTE number_of_sections; //Количество секций в оригинальном файле
      DWORD size_of_packed_data; //Размер упакованных данных
      DWORD size_of_unpacked_data; //Размер оригинальных данных
     
      DWORD load_library_a; //Адрес процедуры LoadLibraryA из kernel32.dll
      DWORD get_proc_address; //Адрес процедуры GetProcAddress из kernel32.dll
      DWORD end_of_import_address_table; //Конец IAT
    };
    
    Я добавил в структуру три поля. В первые два загрузчик впишет адреса функций LoadLibraryA и GetProcAddress из kernel32.dll. Последнее поле указывает на конец адресной таблицы импорта (import address table, IAT), и в него мы запишем ноль, чтобы дать понять загрузчику, что больше никаких функций нам не надо. Про это я еще расскажу немного дальше.

    Теперь необходимо создать новую таблицу импорта. В этом нам сильно поможет моя библиотека для работы с PE. (На старую оригинальную мы пока что наплюем).
    Code:
    //....
          //Устанавливаем для нее необходимый виртуальный размер
          image.set_section_virtual_size(added_section, total_virtual_size);
    //....
     
          std::cout << "Creating imports..." << std::endl;
     
          //Создаем импорты из библиотеки kernel32.dll
          pe_base::import_library kernel32;
          kernel32.set_name("kernel32.dll"); //Выставили имя библиотеки
     
          //Создаем импортируемую функцию
          pe_base::imported_function func;
          func.set_name("LoadLibraryA"); //Ее имя
          kernel32.add_import(func); //Добавляем ее к библиотеке
     
          //И вторую функцию
          func.set_name("GetProcAddress");
          kernel32.add_import(func); //Тоже добавляем
     
          //Получаем относительный адрес (RVA) поля load_library_a
          //нашей структуры packed_file_info, которую мы расположили в самом
          //начале добавленной секции, помните?
          DWORD load_library_address_rva = pe_base::rva_from_section_offset(added_section,
            offsetof(packed_file_info, load_library_a));
     
          //Устанавливаем этот адрес как адрес
          //таблицы адресов импорта (import address table)
          kernel32.set_rva_to_iat(load_library_address_rva);
     
          //Создаем список импортируемых библиотек
          pe_base::imported_functions_list imports;
          //Добавляем к списку нашу библиотеку
          imports.push_back(kernel32);
     
          //Настроим пересборщик импортов
          pe_base::import_rebuilder_settings settings;
          //Original import address table нам не нужна (пояснения ниже)
          settings.build_original_iat(false);
          //Будем переписывать IAT именно по тому адресу,
          //который указали (load_library_address_rva)
          settings.save_iat_and_original_iat_rvas(true, true);
          //Расположим импорты прямо за концом упакованных данных
          settings.set_offset_from_section_start(added_section.get_raw_data().size());
          //Пересоберем импорты
          image.rebuild_imports(imports, added_section, settings);
    Начало кода понятно - создали импорт библиотеки, добавили к ней пару функций, создали список импортируемых библиотек из одной-единственной kernel32.dll. Поясню строку, где мы устанавливаем RVA к IAT (kernel32.set_rva_to_iat). Я уже раньше писал кое-что об импортах PE-файла. Расскажу вкратце еще разок. Для каждой импортируемой библиотеки в таблице импортов создается следующая структура:

    [​IMG]

    Загрузчик записывает адреса импортируемых функций в Import Address Table (IAT) для каждой импортируемой DLL, а имена или ординалы импортируемых функций он берет из Original Import Address Table (или, по-другому, Import Lookup Table). Можно обойтись и без последней, например, все компиляторы Borland всегда так делают, плевать они хотели на Import Lookup Table. В этом случае у нас в единственной таблице Import Address Table содержатся сразу ординалы или имена импортируемых функций, и туда же, поверх этих данных, загрузчик запишет адреса непосредственно импортированных функций. Мы тоже не будем делать Original Import Address Table, обойдемся без нее (меньше места импорт займет), поэтому отключаем эту опцию в пересборщике импортов.

    Вызов settings.save_iat_and_original_iat_rvas настраивает пересборщик таким образом, что он не будет создавать свои собственные IAT и Original IAT, а запишет все по тем адресам, которые уже указаны в каждой библиотеке (помните вызов kernel32.set_rva_to_iat?).

    Далее мы просто пересобираем таблицу импортов. В очередной раз запустим недоупаковщик, передав ему его же имя, и посмотрим, что получилось. Убедимся, что все прошло так, как и было задумано:

    [​IMG]

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

    [​IMG]

    Как видно, по адресам 0x1009 и 0x100D записались именно те адреса, которые нам нужны, значит, все сделано правильно. (Адрес точки входа пока что совершенно левый, и нет никакого распаковщика, поэтому файл по-прежнему не запустится, но мы уже достигли многого).

    Идем дальше. Необходимо подготовить наши сорсы для написания распаковщика. Вынесем все структуры из файла main.cpp в файл structs.h, его содержимое будет таким:
    Code:
    #pragma once
    #include <Windows.h>
    #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; //Характеристики секции
    };
     
    //Структура, хранящая информацию об упакованном файле
    struct packed_file_info
    {
      BYTE number_of_sections; //Количество секций в оригинальном файле
      DWORD size_of_packed_data; //Размер упакованных данных
      DWORD size_of_unpacked_data; //Размер оригинальных данных
     
      DWORD load_library_a; //Адрес процедуры LoadLibraryA из kernel32.dll
      DWORD get_proc_address; //Адрес процедуры GetProcAddress из kernel32.dll
      DWORD end_of_import_address_table; //Конец IAT
    };
    #pragma pack(pop)
    Тут пояснять ничего не нужно, мы просто перенесли код. В main.cpp, в свою очередь, подключим этот файл:
    Code:
    //Заголовочный файл с нашими структурами
    #include "structs.h"
    И наступает время хардкора! Будем писать распаковщик. Немного поразмыслив, я решил не использовать MASM32, а писать его на C с элементами C++ и ассемблерными вставками - читаемость кода будет выше. Итак, создаем новый проект в солюшене и называем его unpacker. Добавляем к нему файлы unpacker.cpp и parameters.h (создаем). Далее в настройках выставляем всё то же самое, что мы делали с проектом lzo-2.06 в самом первом шаге, чтобы сборка была самой минимальной по размеру и базонезависимой. Точку входа (Linker - Advanced - Entry Point) выставляем в unpacker_main. Далее, в Configuration Manager'е (см. шаг 1) выставляем, чтобы этот проект всегда собирался в конфигурации Release:

    [​IMG]

    Проставим у проекта simple_pe_packer зависимость от проекта unpacker (Project Dependencies, как в шаге 1) и добавим файл parameters.h в инклюды проекта упаковщика - в этот файл мы будем вписывать необходимые параметры для сборки распаковщика:
    Code:
    //Заголовочный файл с параметрами распаковщика
    #include "../unpacker/parameters.h"
    Теперь начнем писать сам распаковщик. Открываем файл unpacker.cpp...
    Code:
    //Подключаем файл со структурами из проекта упаковщика
    #include "../simple_pe_packer/structs.h"
    //Создадим функцию без пролога и эпилога
    void __declspec(naked) unpacker_main()
    {
      //Пролог вручную
      __asm
      {
        push ebp;
        mov ebp, esp;
        sub esp, 128;
      }
     
      //... описано далее ...//
     
      //Эпилог вручную
      _asm
      {
        leave;
        ret;
      }
    }
    Итак, начинаю разъяснения. Сначала мы подключили файл, содержащий объявления структур упаковщика - в распаковщике они нам пригодятся. Далее мы создаем точку входа - процедуру unpacker_main. Обратите внимание, что это особо объявленная функция - naked. Это говорит компилятору о том, что не нужно создавать для этой функции пролог и эпилог (стековый фрейм) автоматически. Нам это необходимо сделать вручную, а зачем - поясню в следующем уроке. Пока что мы создаем точь-в-точь такие же пролог и эпилог, которые делает сам компилятор MSVC++. Строка "sub esp, 128" выделяет на стеке 128 байтов - этого нам пока должно хватить для подручных нужд. В этом шаге распаковщик не будет делать чего-то серьезного. Пролог и эпилог нужны нам, чтобы мы могли выделять память на стеке без лишних проблем. В самом конце мы пишем инструкцию ret - возврат в ядро. Теперь напишем самое простое тело упаковщика. Пусть он будет просто приветствовать нас, выдавая Message Box с текстом "Hello!".
    Code:
      //Адрес загрузки образа
      unsigned int original_image_base;
      //Относительный адрес первой секции,
      //в которую упаковщик кладет информацию для
      //распаковщика и сами упакованные данные
      unsigned int rva_of_first_section;
     
      //Эти инструкции нужны только для того, чтобы
      //заменить в билдере распаковщика адреса на реальные
      __asm
      {
        mov original_image_base, 0x11111111;
        mov rva_of_first_section, 0x22222222;
      }
    Здесь мы объявили две локальные переменные. Первая будет содержать действительный адрес загрузки образа, а вторая - относительный адрес самой первой секции, в которую, как вы помните, мы кладем всю необходимую для распаковщика информацию и сами упакованные данные. Вместо чисел 0x11111111 и 0x22222222 мы с помощью упаковщика будем записывать реальные значения.
    Code:
     //Получаем указатель на структуру с информацией,
      //которую для нас заботливо приготовил упаковщик
      const packed_file_info* info;
      //Она находится в самом начале
      //первой секции упакованного файла
      info = reinterpret_cast<const packed_file_info*>(original_image_base + rva_of_first_section);
     
      //Два тайпдефа прототипов функций LoadLibraryA и GetProcAddress
      typedef HMODULE (__stdcall* load_library_a_func)(const char* library_name);
      typedef INT_PTR (__stdcall* get_proc_address_func)(HMODULE dll, const char* func_name);
     
      //Считаем их адреса из структуры packed_file_info
      //Их нам туда подложил загрузчик
      load_library_a_func load_library_a;
      get_proc_address_func get_proc_address;
      load_library_a = reinterpret_cast<load_library_a_func>(info->load_library_a);
      get_proc_address = reinterpret_cast<get_proc_address_func>(info->get_proc_address);
    
    Здесь, кажется, все понятно. В начале первой секции упакованного файла лежит структура packed_file_info, которую мы создаем в упаковщике. В ней есть еще три поля, заполняемые самим загрузчиком - мы так устроили таблицу импортов, как вы помните. Из этих полей мы получаем адреса функций LoadLibraryA и GetProcAddress. Вы еще можете спросить, зачем я сначала объявляю все переменные, и только позже присваиваю им значения, ведь я мог бы это делать одной строкой. Все дело в том, что в naked-функциях нельзя одновременно объявить переменную и сразу же присвоить ей значение.

    И последняя (пока что) часть кода распаковщика:
    Code:
     //Создаем буфер на стеке
      char buf[32];
      //user32.dll
      *reinterpret_cast<DWORD*>(&buf[0]) = 'resu';
      *reinterpret_cast<DWORD*>(&buf[4]) = 'd.23';
      *reinterpret_cast<DWORD*>(&buf[8]) = 'll';
     
      //Загружаем библиотеку user32.dll
      HMODULE user32_dll;
      user32_dll = load_library_a(buf);
     
      //Тайпдеф прототипа функции MessageBoxA
      typedef int (__stdcall* message_box_a_func)(HWND owner, const char* text, const char* caption, DWORD type);
     
      //MessageBoxA
      *reinterpret_cast<DWORD*>(&buf[0]) = 'sseM';
      *reinterpret_cast<DWORD*>(&buf[4]) = 'Bega';
      *reinterpret_cast<DWORD*>(&buf[8]) = 'Axo';
     
      //Получаем адрес функции MessageBoxA
      message_box_a_func message_box_a;
      message_box_a = reinterpret_cast<message_box_a_func>(get_proc_address(user32_dll, buf));
     
      //Hello!
      *reinterpret_cast<DWORD*>(&buf[0]) = 'lleH';
      *reinterpret_cast<DWORD*>(&buf[4]) = '!!o';
     
      //Выводим месадж бокс
      message_box_a(0, buf, buf, MB_ICONINFORMATION);
    Здесь в целом тоже все должно быть понятно, кроме странного заполнения строк. Мы выделили буфер buf на стеке. Строки все у нас также должны быть исключительно на стеке - мы ничего не можем писать в секцию данных, так как это неизбежно приведет к появлению релокаций, и код станет базозависимым. Именно поэтому мы так нелепо по 4 байта записываем строки непосредственно в стековый буфер. Нужно еще помнить про обратный порядок байтов, с которым работает архитектура x86, а мы именно под нее пишем код, поэтому буквы в кусках строк по 4 байта расположены в обратном порядке.

    Сначала мы загружаем библиотеку user32.dll, затем получаем из нее адрес процедуры MessageBoxA, а затем вызываем ее. Вот и всё с распаковщиком!

    Но осталась еще одна вещь - нам надо код распаковщика каким-то образом вставить в упакованный файл и настроить. Я решил это дело автоматизировать. Для этого добавим новый проект с именем unpacker_converter в солюшен. Цель этого проекта такова: он будет открывать получающийся после компиляции распаковщика файл unpacker.exe, считывать данные из его единственной секции (по сути, код) и преобразовывать его в h-файл, который мы заинклюдим в проекте simple_pe_packer. Пропишем в проекте unpacker_converter include-директорию как в проекте simple_pe_packer, чтобы можно было подключать h-файлы библиотеки для работы с PE-файлами, добавим в проект файл main.cpp и начнем писать код.
    Code:
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <iomanip>
    //Заголовочный файл библиотеки для работы с PE-файлами
    #include <pe_32_64.h>
     
    //Директивы для линкования с собранной библиотекой PE
    #ifndef _M_X64
    #ifdef _DEBUG
    #pragma comment(lib, "../../Debug/pe_lib.lib")
    #else
    #pragma comment(lib, "../../Release/pe_lib.lib")
    #endif
    #else
    #ifdef _DEBUG
    #pragma comment(lib, "../../x64/Debug/pe_lib.lib")
    #else
    #pragma comment(lib, "../../x64/Release/pe_lib.lib")
    #endif
    #endif
     
    int main(int argc, char* argv[])
    {
      //Подсказка по использованию
      if(argc != 3)
      {
        std::cout << "Usage: unpacker_converter.exe unpacker.exe output.h" << std::endl;
        return 0;
      }
     
      //Открываем файл unpacker.exe - его имя
      //и путь к нему хранятся в массиве argv по индексу 1
      std::ifstream file(argv[1], std::ios::in | std::ios::binary);
      if(!file)
      {
        //Если открыть файл не удалось - сообщим и выйдем с ошибкой
        std::cout << "Cannot open " << argv[1] << std::endl;
        return -1;
      }
     
      try
      {
        std::cout << "Creating unpacker source file..." << std::endl;
     
        //Пытаемся открыть файл как 32-битный PE-файл
        //Последние два аргумента false, потому что нам не нужны
        //"сырые" данные привязанных импортов файла и 
        //"сырые" данные отладочной информации
        //При упаковке они не используются, поэтому не загружаем эти данные
        pe32 image(file, false, false);
     
        //Получаем список секций распаковщика
        pe_base::section_list& unpacker_sections = image.get_image_sections();
        //Убедимся, что она одна (так как в нем нет импортов, релокаций)
        if(unpacker_sections.size() != 1)
        {
          std::cout << "Incorrect unpacker" << std::endl;
          return -1;
        }
     
        //Получаем ссылку на данные этой секции
        std::string& unpacker_section_data = unpacker_sections.at(0).get_raw_data();
        //Удаляем нулевые байты в конце этой секции,
        //которые компилятор добавил для выравнивания
        pe_base::strip_nullbytes(unpacker_section_data);
     
        //Открываем выходной файл для записи h-файла
        //Его имя хранится в argv[2]
        std::ofstream output_source(argv[2], std::ios::out | std::ios::trunc);
     
        //Начинаем формировать исходный код
        output_source << std::hex << "#pragma once" << std::endl << "unsigned char unpacker_data[] = {";
        //Текущая длина считанных данных
        unsigned long len = 0;
        //Общая длина данных секции
        std::string::size_type total_len = unpacker_section_data.length();
     
        //Для каждого байта данных...
        for(std::string::const_iterator it = unpacker_section_data.begin(); it != unpacker_section_data.end(); ++it, ++len)
        {
          //Добавляем необходимые переносы, чтобы
          //получившийся код был читаемым
          if((len % 16) == 0)
            output_source << std::endl;
     
          //Записываем значение байта
          output_source
            << "0x" << std::setw(2) << std::setfill('0')
            << static_cast<unsigned long>(static_cast<unsigned char>(*it));
     
          //И, если необходимо, запятую
          if(len != total_len - 1)
            output_source << ", ";
        }
     
        //Конец кода
        output_source << " };" << std::endl;
      }
      catch(const pe_exception& e)
      {
        //Если по какой-то причине открыть его не удалось
        //Выведем текст ошибки и выйдем
        std::cout << e.what() << std::endl;
        return -1;
      }
     
      return 0;
    }
    Не буду детально описывать этот код - многое вам уже будет знакомо. Скажу только, что он просто формирует из файла unpacker.exe файл unpacker.h вида:

    Code:
    #pragma once
    unsigned char unpacker_data[] = {
    0x55, 0x8b, 0xec, 0x81, 0xec, 0x80, 0x00, 0x00, 0x00, 0xc7, 0x45, 0xfc, 0x11, 0x11, 0x11, 0x11, 
    0xc7, 0x45, 0xf8, 0x22, 0x22, 0x22, 0x22, 0x8b, 0x45, 0xfc, 0x03, 0x45, 0xf8, 0x8b, 0x48, 0x09, 
    0x8b, 0x70, 0x0d, 0x8d, 0x45, 0xd8, 0x50, 0xc7, 0x45, 0xd8, 0x75, 0x73, 0x65, 0x72, 0xc7, 0x45, 
    0xdc, 0x33, 0x32, 0x2e, 0x64, 0xc7, 0x45, 0xe0, 0x6c, 0x6c, 0x00, 0x00, 0xff, 0xd1, 0x8d, 0x4d, 
    0xd8, 0x51, 0x50, 0xc7, 0x45, 0xd8, 0x4d, 0x65, 0x73, 0x73, 0xc7, 0x45, 0xdc, 0x61, 0x67, 0x65, 
    0x42, 0xc7, 0x45, 0xe0, 0x6f, 0x78, 0x41, 0x00, 0xff, 0xd6, 0x6a, 0x40, 0x8d, 0x4d, 0xd8, 0x51, 
    0x51, 0x6a, 0x00, 0xc7, 0x45, 0xd8, 0x48, 0x65, 0x6c, 0x6c, 0xc7, 0x45, 0xdc, 0x6f, 0x21, 0x00, 
    0x00, 0xff, 0xd0, 0xc9, 0xc3 };
    Эти данные являются шестнадцатеричным представлением данных первой и единственной секции кода распаковщика. Он у нас пока совсем простой и маленький. Как же сделать, чтобы unpacker_converter автоматически генерировал для нас такой h-файл при пересборке распаковщика? Необходимо поправить настройку проекта unpacker (Build Events - Post-Build Event):
    Code:
    "..\unpacker_converter.exe" "..\Release\unpacker.exe" "..\simple_pe_packer\unpacker.h"
    Почему я в этой настройке не использовал макрос $(Configuration)? Потому что он для проекта unpacker всегда будет раскрываться в "Release", так как и в дебаге, и в релизе этот проект собирается как Release (мы это меняли ранее в Configuration Manager'е). Поэтому мы просто будем копировать файл unpacker_converter.exe из ЕГО текущей конфигурации в корень проекта, и оттуда его уже сможет взять проект unpacker. Таким образом, последнее, что мы делаем, это правим конфигурацию проекта unpacker_converter (Build Events - Post-Build Event):
    Code:
    copy /Y "..\$(Configuration)\unpacker_converter.exe" "..\unpacker_converter.exe"
    Осталось расставить зависимости (Project Dependencies): unpacker от unpacker_converter (вохможно, это не совсем логично, ну да ладно). После этого у нас все будет собираться и в Release, и в Debug-конфигурации.

    Поясню, что мы запишем в файл parameters.h. Его содержимое будет таким:
    Code:
    #pragma once
     
    static const unsigned int original_image_base_offset = 0x0C;
    static const unsigned int rva_of_first_section_offset = 0x13;
    Мы пишем смещения относительно начала кода распаковщика (в собранном бинарном виде) двух чисел - 0x11111111 и 0x22222222. Эти числа будут перезаписываться упаковщиком, а смещения 0xC (12) и 0x13 (19) просчитываются в любом HEX-редакторе или с помощью автогенеренного файла unpacker.h. Меняться они уже вряд ли будут, так как код перед двумя ассемблерными командами mov в распаковщике мы больше дописывать не будем.

    Добавим в include проекта simple_pe_packer автогенеренный файл unpacker.h:
    Code:
    //Тело распаковщика (автогенеренное)
    #include "unpacker.h"
    Завершающим этапом урока будет вставка тела распаковщика в упаковываемый файл. В прошлом шаге мы делали так:
    Code:
      //В будущем тут будет код распаковщика и что-то еще
          unpacker_section.get_raw_data() = "Nothing interesting here...";
    Теперь будем вставлять туда код распаковщика и настраивать его:
    Code:
    //...
          {
            //Получаем ссылку на данные секции распаковщика
            std::string& unpacker_section_data = unpacker_section.get_raw_data();
            //Записываем туда код распаковщика
            //Этот код хранится в автогенеренном файле
            //unpacker.h, который мы подключили в main.cpp
            unpacker_section_data = std::string(reinterpret_cast<const char*>(unpacker_data), sizeof(unpacker_data));
            //Записываем по нужным смещениям адрес
            //загрузки образа
            *reinterpret_cast<DWORD*>(&unpacker_section_data[original_image_base_offset]) = image.get_image_base_32();
            //и виртуальный адрес самой первой секции упакованного файла,
            //в которой лежат данные для распаковки и информация о них
            //В самом начале это секции, как вы помните, лежит
            //структура packed_file_info
            *reinterpret_cast<DWORD*>(&unpacker_section_data[rva_of_first_section_offset]) = image.get_image_sections().at(0).get_virtual_address();
          }
     
          //Добавляем и эту секцию
          const pe_base::section& unpacker_added_section = image.add_section(unpacker_section);
          //Выставляем новую точку входа - теперь она указывает
          //на распаковщик, на самое его начало
          image.set_ep(image.rva_from_section_offset(unpacker_added_section, 0));
    //...
    Всё! Теперь распаковщик будет настраиваться и добавляться в упакованный файл! Давайте убедимся в этом. Как всегда, упакуем сами себя, получив на выходе файл packed_simple_pe_packer.exe. Запустим его и увидим долгожданное окошко, ради которого было проделано столько работы!

    [​IMG]

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

    Как всегда, прикладываю полный солюшен (кроме библиотеки для работы с PE-файлами) упаковщика: Own PE packer step 3
     
  4. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,373
    Likes Received:
    6,619
    Reputations:
    693
    Пишем упаковщик по шагам. Шаг четвертый. Запускаем.
    Вторник, 18. Сентябрь 2012
    автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-4/
    http://kaimi.ru



    Появилась новая версия библиотеки для работы с PE-файлами (0.1.4). Перекачайте и пересоберите ее.

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

    Добавим несколько полей в нашу структуру packed_file_info:
    Code:
    //Структура, хранящая информацию об упакованном файле
    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 поля, которые нам пригодятся в распаковщике. Теперь необходимо их заполнить в коде упаковщика:
    Code:
    //...
        //Структура базовой информации о 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:
    Code:
    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);
    }
    Code:
    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 располагалась в самом начале кода распаковщика. Мы ведь ее в упаковщике патчим, помните? Эта проблема легко решается. Создадим текстовый файл с таким содержанием:
    Code:
    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:
    Code:
    //Алгоритм распаковки
    #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:
    Code:
     //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, так что пояснять не буду. Далее следует перенести в локальную область видимости необходимые переменные, которые для нас запас упаковщик:
    Code:
    //Относительный виртуальный адрес директории импорта
      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, находящаяся в самом начале первой секции упакованного файла, будет затерта реальными распакованными данными. Теперь выделим память и распакуем в нее упакованный блок данных:
    Code:
     //Указатель на память, в которую
      //мы запишем распакованные данные
      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 перед распаковкой не нужно, для распаковки достаточно вызвать единственную функцию, что мы и сделали. Далее вычислим виртуальный адрес заголовка первой секции.
    Code:
      //Указатель на 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 */;
    Теперь мы имеем виртуальный адрес заголовков секций. Нам их необходимо перезаписать, чтобы в памяти они выглядели так, как выглядят в оригинальном файле. Перед тем, как мы будем это делать, необходимо обработать еще кое-какие мелочи:
    Code:
     //Обнулим всю память первой секции
      //эта область соответствует области памяти, которую
      //в оригинальном файле занимают все секции
      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;
    Приступим к восстановлению заголовков секций:
    Code:
     //Виртуальный адрес структуры заголовка секции
      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(&section_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), &section_header, sizeof(section_header));
     
        //Перемещаем указатель на следующий заголовок секции
        current_section_structure_pos += sizeof(section_header);
      }
    Заголовки секций восстановили, теперь восстановим их данные:
    Code:
    //Указатель на сырые данные секции
      //Необходим для разлепления сжатых данных секций
      //и распихивания их по нужным местам
      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-заголовке:
    Code:
     //Вычислим относительный виртуальный адрес
      //начала таблицы директорий
      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;
    Заполняем таблицу импорта:
    Code:
     //Если у файла имеются импорты
      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-файлу таблицу импорта. Осталась пара мелочей:
    Code:
    //Вернем атрибуты памяти заголовков, как было изначально
      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, экспортами работать не будет, и этим мы займемся в следующих шагах. Но мы уже можем запаковать сами себя и запустить запакованный вариант!

    [​IMG]

    Как видно, мы запаковали сами себя, получив бинарник packed_simple_pe_packer.exe, и он работает!

    Полный солюшен со всеми проектами для данного шага: Own PE Packer Step 4
     
  5. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,373
    Likes Received:
    6,619
    Reputations:
    693
    Пишем упаковщик по шагам. Шаг пятый. Ресурсы.
    Среда, 19. Сентябрь 2012
    автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-5/
    http://kaimi.ru/



    Пора усовершенствовать наш упаковщик. Он уже способен упаковывать и запускать самые простые бинарники, имеющие лишь таблицу импорта. Бинарники с экспортами, ресурсами, TLS, DLL с релокациями ему пока что не под силу. Нужно над этим работать. Для начала сделаем обработку второй по важности вещи после импортов - директории ресурсов.

    Сначала добавим пару полей в структуру packed_file_info:
    Code:
    //...
      DWORD original_resource_directory_rva; //Относительный адрес оригинальной директории ресурсов
      DWORD original_resource_directory_size; //Размер оригинальной директории ресурсов
    //...
    Эти поля будут хранить информацию об оригинальной директории ресурсов. В распаковщике мы их будем записывать в заголовок PE-файла после распаковки данных секций. Но это еще не все. Ведь мы упаковываем данные всех секций, слепляя их в один большой блок, в том числе и ресурсы. У файла пропадет иконка и информация о версии, если они имелись. Он может вообще не запуститься, если у него имелись какие-то специфические манифесты в ресурсах, определяющие права или библиотеки, требуемые файлу для запуска. Нам необходимо расположить главную иконку приложения, информацию о версии и манифест не упаковывая рядом со сжатыми данными в новой директории ресурсов, выставив на нее указатель в PE-заголовке.

    Сначала займемся распаковщиком (проект unpacker), тем более, изменения будут минимальными. Добавим строки аналогично коду для директории импорта:
    Code:
    //...
      //Относительный виртуальный адрес директории ресурсов
      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). Добавим, опять-таки, аналогичные для директории импорта строки для директории ресурсов:
    Code:
    //...
        //Запоминаем относительный адрес и размер
        //оригинальной директории ресурсов упаковываемого файла
        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>. Он необходим для доступа к вспомогательным классам для работы с ресурсами. Код будем добавлять в то место упаковщика, где удаляются секции исходного файла (прямо перед этим действием):
    Code:
     //Новая пустая корневая директория ресурсов
          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'е:

    [​IMG]

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

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

    Новую директорию ресурсов мы создали, теперь надо ее сохранить в создаваемом нами файле. Сначала добавим пару строк перед строкой, производящей сборку импортов:
    Code:
    //Если у нас есть ресурсы для сборки,
          //отключим автоматическое урезание секции после
          //добавления в нее импортов
          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, он опирается только на физические данные файла, а не на виртуальные. Поэтому он может криво отобразить ту же таблицу импорта с отрезанными нулевыми байтами в конце.

    Сейчас же нам необходимо, чтобы нулевые байты в конце секции, относящиеся к таблице импорта, не обрезались, так как мы сразу после импортов разместим ресурсы. Если мы обрежем эти нулевые байты, то директория ресурсов наслоится на директорию импортов, и получится полная хрень.

    Идем дальше - пересобираем ресурсы:
    Code:
     //Пересоберем ресурсы, если есть, что пересобирать
          if(!new_root_dir.get_entry_list().empty())
            image.rebuild_resources(new_root_dir, added_section, added_section.get_raw_data().size());
    Здесь все ясно - если какие-то ресурсы есть, мы их записываем в новый PE-файл. Располагаем мы их в самом конце добавленной секции, сразу за импортами. Так как секция последняя на момент пересборки ресурсов, она расширится автоматически.

    Осталось убрать одну строчку, которая убирает директорию ресурсов:
    Code:
     image.remove_directory(IMAGE_DIRECTORY_ENTRY_RESOURCE);
    Все готово! Можно проверить упаковщик на любом exe-файле, имеющем импорты, ресурсы и даже релокации (хотя мы их пока что не обрабатываем). Давайте возьмем какой-нибудь проект на MSVC++ с MFC и посмотрим с помощью CFF Explorer'а, какие ресурсы в нем были изначально:

    [​IMG]

    После упаковки имеем:

    [​IMG]

    Итак, осталась одна группа иконок с несколькими иконками в ней (иконки приложения), манифест (configuration files) и информация о версии. Разумеется, упакованный файл успешно запускается!

    Полный солюшен для этого шага: Own PE Packer step 5
     
  6. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,373
    Likes Received:
    6,619
    Reputations:
    693
    Пишем упаковщик по шагам. Шаг шестой. TLS.
    Пятница, 21. Сентябрь 2012
    автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-6/
    http://kaimi.ru/



    Появилась новая версия библиотеки для работы с PE-файлами (0.1.5). Перекачайте и пересоберите ее.

    Пришло время заняться обработкой такой важной вещи, как Thread Local Storage (TLS) - локальной памяти потока. Что она из себя представляет? Это небольшая структура, которая говорит загрузчику PE-файлов о том, где находятся данные, которые должны быть выделены в памяти для каждого потока. Загрузчиком также производится вызов функции TlsAlloc, и значение, возвращенное ей, записывается по адресу, также указанному в этой структуре (называется это индексом). Кроме того, эта же структура может содержать адрес массива, хранящего набор коллбэков (адресов функций), которые будут вызваны загрузчиком при загрузке файла в память или при создании нового потока в процессе.

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

    Но начнем по порядку. Как обычно, будем править структуру packed_file_info (файл structures.h проекта simple_pe_packer). В нее на этот раз добавятся четыре поля:
    Code:
      //Сюда загрузчик будет записывать TLS-индекс
      DWORD tls_index;
      //Относительный адрес индекса TLS в оригинальном файле
      DWORD original_tls_index_rva;
      //Оригинальный адрес массива TLS-коллбэков в оригинальном файле
      DWORD original_rva_of_tls_callbacks;
      //Относительный адрес массива TLS-коллбэков в файле
      //после нашей модификации
      DWORD new_rva_of_tls_callbacks;
    В этих полях мы будем хранить необходимые для распаковщика значения, связанные с TLS. Отдельно поясню про коллбэки. Поле AddressOfCallBacks структуры IMAGE_TLS_DIRECTORY указывает на массив абсолютных виртуальных адресов (т.е. на адреса, идущие друг за другом), которые, в свою очередь, указывают на функции, являющиеся коллбэками. Завершается этот массив нулевым элементом. Загрузчик по очереди дергает все функции в этом массиве при следующих событиях: создание процесса, создание потока, завершение потока, завершение процесса. Первый раз они вызываются даже до того, как процесс получит управление. Чтобы дать понять загрузчику, что в нашем упакованном файле есть TLS-коллбеки (разумеется, если они были в оригинальном), мы сделаем следующее: поле AddressOfCallBacks обнулять не будем, а запишем туда адрес массива, содержащий один-единственный пустой коллбэк (не нули, а реальный коллбэк, который ничего не делает). При загрузке упакованного образа в память этот коллбэк будет выполнен и загрузчик с этого момента будет знать, что у файла есть TLS-коллбэки. Если бы мы записали в поле AddressOfCallbacks ноль или указатель на пустой массив, то загрузчика впоследствии уже не смогли бы убедить в том, что коллбэки есть. А вот сам массив коллбэков можно будет впоследствии уже менять, так как загрузчик перечитывает его каждый раз, когда он ему становится нужен.

    Индекс TLS и данные, которыми инициализируется память потока, мы переместим в свою секцию (которую мы назвали kaimi.ru, помните?), дабы они не перетерлись после распаковки, а непосредственно в распаковщике уже имеющийся от загрузчика индекс запишем туда, где он должен быть в оригинальном файле. Саму структуру IMAGE_TLS_DIRECTORY (точнее, IMAGE_TLS_DIRECTORY32, мы ведь пакуем 32-разрядные бинарники) мы также запишем в свою секцию. Туда же запишем и массив наших подложных коллбэков из единственного пустого, если они есть в оригинальном файле.

    Переходим к написанию кода. После этого блока кода:
    Code:
     //Проверим, что файл реально стал меньше
        if(out_buf.size() >= src_length)
        {
          std::cout << "File is incompressible!" << std::endl;
          return -1;
        }
    допишем следующее:
    Code:
      //Если файл имеет TLS, получим информацию о нем
        std::auto_ptr<pe_base::tls_info> tls;
        if(image.has_tls())
        {
          std::cout << "Reading TLS..." << std::endl;
          tls.reset(new pe_base::tls_info(image.get_tls_info()));
        }
    
    Теперь добавим немного кода после места, где мы пересобираем ресурсы:
    Code:
      //Если у файла был TLS
          if(tls.get())
          {
            //Указатель на нашу структуру с информацией
            //для распаковщика
            //Эта структура в самом начале свежедобавленной секции,
            //мы ее туда добавили чуть раньше
            packed_file_info* info = reinterpret_cast<packed_file_info*>(&added_section.get_raw_data()[0]);
     
            //Запишем относительный виртуальный адрес
            //оригинального TLS
            info->original_tls_index_rva = tls->get_index_rva();
     
            //Если у нас были TLS-коллбэки, запишем в структуру
            //относительный виртуальный адрес их массива в оригинальном файле
            if(!tls->get_tls_callbacks().empty())
              info->original_rva_of_tls_callbacks = tls->get_callbacks_rva();
     
            //Теперь относительный виртуальный адрес индекса TLS
            //будет другим - мы заставим загрузчик записать его в поле tls_index
            //структуры packed_file_info
            tls->set_index_rva(pe_base::rva_from_section_offset(added_section, offsetof(packed_file_info, tls_index)));
          }
    Здесь мы просто сохраняем в нашу структуру с информацией об оригинальном файле всяческую необходимую информацию об оригинальном TLS. Кроме того, в нее же загрузчик теперь запишет TLS-индекс, который мы в распаковщике переложим туда, где он должен находиться.

    Далее работаем с секцией "kaimi.ru", в которую мы раньше записывали только тело распаковщика. Добавим ей, во-первых, атрибут доступа на запись, заменив строку
    Code:
    unpacker_section.readable(true).executable(true);
    на
    Code:
      //Доступна на запись, чтение и исполнение
          unpacker_section.readable(true).executable(true).writeable(true);
    
    Заменим также строку
    Code:
      const pe_base::section& unpacker_added_section = image.add_section(unpacker_section);
    
    на
    Code:
     pe_base::section& unpacker_added_section = image.add_section(unpacker_section);
    потому что с этой секцией мы теперь будем работать.

    Теперь на некоторое время перейдем к проекту распаковщика (unpacker). Опишу детально, как мы будем обрабатывать TLS, имеющий коллбеки. Мы сохраняем все оригинальные адреса TLS (это мы уже сделали). Далее мы создаем свой массив коллбэков, имеющий всего один коллбэк, да и тот пустой, чтобы просто дать понять загрузчику, что коллбэки есть. Мы в новом TLS указываем на этот массив. Далее, сразу после распаковки файла мы вручную выполняем все коллбэки оригинального файла, потому что загрузчик по понятным причинам этого не сделает - у него всего один пустой коллбэк. После этого мы правим массив коллбэков, созданный нами, записывая туда все адреса уже оригинальных функций, и с этого момента управление TLS-коллбэками переходит во власть загрузчика, нам больше делать ничего не надо. Таким образом, наша текущая задача - сделать в распаковщике пустой TLS-коллбэк. Дабы не плодить дополнительных функций, просто модифицируем пролог функции unpacker_main:
    Code:
      //Пролог вручную
      __asm
      {
        jmp next;
        ret 0xC;
    next:
     
        push ebp;
        mov ebp, esp;
        sub esp, 256;
      }
    Распаковщик, таким образом, начнет исполняться с инструкции jmp next, сразу перепрыгнув на свое основное тело. А вот тот самый пустой коллбэк, который нам нужен, выглядит как "ret 0xC", и на эту инструкцию мы сделаем указатель в массиве коллбэков. Эта инструкция просто отдает управление, предварительно убрав из стека 0xC = 12 байтов. Если кто-то не в курсе, прототип TLS-коллбэка выглядит так:
    Code:
    typedef VOID
    (NTAPI *PIMAGE_TLS_CALLBACK) (
        PVOID DllHandle,
        DWORD Reason,
        PVOID Reserved
        );
    и использует он соглашение вызовов stdcall и три четырехбайтовых параметра. Итого, мы должны убрать из стека 3 * 4 = 12 байтов. Коллбэк не возвращает значения, поэтому модифицировать регистр eax в его теле необязательно.

    Теперь заменим все эти строки:
    Code:
     //Относительный виртуальный адрес директории импорта
      DWORD original_import_directory_rva;
      //Виртуальный размер директории импорта
      DWORD original_import_directory_size;
      //Относительный виртуальный адрес директории ресурсов
      DWORD original_resource_directory_rva;
      //Виртуальный размер директории ресурсов
      DWORD original_resource_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_resource_directory_rva = info->original_resource_directory_rva;
      original_resource_directory_size = info->original_resource_directory_size;
      total_virtual_size_of_sections = info->total_virtual_size_of_sections;
      number_of_sections = info->number_of_sections;
    на одно копирование структуры, а то слишком много лишнего кода получается:
    Code:
     //Копируем все поля структуры packed_file_info, так как они нам будут
      //нужны далее, но структуру по указателю info мы скоро затрем
      packed_file_info info_copy;
      memcpy(&info_copy, info, sizeof(info_copy));
    Заменим теперь все обращения к вышеперечисленным переменным соответствующим образом, меняя, например, original_import_directory_rva на info_copy.original_import_directory_rva.

    Поправим файлик parameters.h, у нас изменились необходимые для упаковщика смещения, кроме того, добавилось еще одно:
    Code:
    #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 empty_tls_callback_offset = 0x2;
    Последнее смещение в коде распаковщика (empty_tls_callback_offset) - это как раз смещение на инструкцию ret, осуществляющую выход из TLS-коллбэка.

    Идем дальше. В отличие от директории импортов и директории ресурсов, директорию TLS мы восстанавливать не будем - это бессмысленно. Загрузчик все равно ее не будет перечитывать. Перейдем к обработке TLS. Код будем размещать в распаковщике после той части, в которой фиксим импорты. Для начала скопируем индекс, который нам предоставил загрузчик, в ячейку памяти, где он должен лежать:
    Code:
    //Скопируем TLS-индекс
    if(info_copy.original_tls_index_rva)
        *reinterpret_cast<DWORD*>(info_copy.original_tls_index_rva + original_image_base) = info_copy.tls_index;
    Далее - более сложная часть. Обработаем TLS-коллбэки:
    Code:
     if(info_copy.original_rva_of_tls_callbacks)
      {
        //Если TLS имеет коллбэки
        PIMAGE_TLS_CALLBACK* tls_callback_address;
        //Указатель на первый коллбэк оригинального массива
        tls_callback_address = reinterpret_cast<PIMAGE_TLS_CALLBACK*>(info_copy.original_rva_of_tls_callbacks + original_image_base);
        while(true)
        {
          //Если коллбэк нулевой - это конец массива
          if(!*tls_callback_address)
            break;
     
          //Скопируем в наш массив коллбэков
          //адрес оригинального
          *reinterpret_cast<PIMAGE_TLS_CALLBACK*>(info_copy.new_rva_of_tls_callbacks + original_image_base) = *tls_callback_address;
     
          //Перейдем к следующему коллбэку
          ++tls_callback_address;
        }
     
        //Вернемся на начало уже нового массива
        tls_callback_address = reinterpret_cast<PIMAGE_TLS_CALLBACK*>(info_copy.new_rva_of_tls_callbacks + original_image_base);
        while(true)
        {
          //Если коллбэк нулевой - это конец массива
          if(!*tls_callback_address)
            break;
     
          //Вызовем коллбэк
          (*tls_callback_address)(reinterpret_cast<PVOID>(original_image_base), DLL_PROCESS_ATTACH, 0);
     
          //Перейдем к следующему коллбэку
          ++tls_callback_address;
        }
      }
    Сначала мы перечисляем все адреса коллбэков в оригинальном массиве и копируем их в наш массив TLS-коллбэков, чтобы загрузчик их подхватил, когда они понадобятся в следующий раз. Однако, при создании процесса загрузчик дернул только наш единственный пустой коллбэк, а PE-файл ожидает, что будут вызваны его коллбэки с параметром DLL_PROCESS_ATTACH. Поэтому нам нужен второй цикл, в котором мы вызываем все коллбэки из оригинального массива, передав в них базовый адрес загрузки образа в первом параметре и DLL_PROCESS_ATTACH (=1) во втором. Третий параметр не используется, смотрите прототип выше. Можно было бы, конечно, скопировать адреса коллбэков и вызвать их и в одном цикле, но вдруг в коллбэках бинарник модифицирует сам себя или ожидает, чтобы массив коллбэков был сразу заполнен? В любом случае, два цикла - тоже не панацея, но это более надежно.

    С распаковщиком все, и теперь мы возвращаемся к упаковщику. Необходимо разместить директорию TLS в секции "kaimi.ru", а также скопировать туда файловые данные, использующиеся для инициализации локальных данных новых потоков.
    Code:
         //Если у файла есть TLS
          if(tls.get())
          {
            std::cout << "Rebuilding TLS..." << std::endl;
     
            //Ссылка на сырые данные секции распаковщика
            //Сейчас там есть только тело распаковщика
            std::string& data = unpacker_added_section.get_raw_data();
     
            //Изменим размер данных секции распаковщика ровно
            //по количеству байтов в теле распаковщика
            //(на случай, если нулевые байты с конца были обрезаны
            //библиотекой для работы с PE)
            data.resize(sizeof(unpacker_data));
     
            //Вычислим позицию, в которую запишем структуру IMAGE_TLS_DIRECTORY32
            DWORD directory_pos = data.size();
            //Выделим место под эту структуру
            //запас sizeof(DWORD) нужен для выравнивания, так как
            //IMAGE_TLS_DIRECTORY32 должна быть выровнена 4-байтовую на границу
            data.resize(data.size() + sizeof(IMAGE_TLS_DIRECTORY32) + sizeof(DWORD));
     
            //Если у TLS есть коллбэки...
            if(!tls->get_tls_callbacks().empty())
            {
              //Необходимо зарезервировать место
              //под оригинальные TLS-коллбэки
              //Плюс 1 ячейка под нулевой DWORD
              DWORD first_callback_offset = data.size();
              data.resize(data.size() + sizeof(DWORD) * (tls->get_tls_callbacks().size() + 1));
     
              //Первый коллбэк будет нашим пустым (ret 0xC),
              //запишем его адрес
              *reinterpret_cast<DWORD*>(&data[first_callback_offset]) =
                image.rva_to_va_32(pe_base::rva_from_section_offset(unpacker_added_section, empty_tls_callback_offset));
     
              //Запишем относительный виртуальный адрес
              //новой таблицы TLS-коллбэков
              tls->set_callbacks_rva(pe_base::rva_from_section_offset(unpacker_added_section, first_callback_offset));
     
              //Теперь запишем в структуру packed_file_info, которую мы
              //записали в самое начало первой секции,
              //относительный адрес новой таблицы коллбэков
              reinterpret_cast<packed_file_info*>(&image.get_image_sections().at(0).get_raw_data()[0])->new_rva_of_tls_callbacks = tls->get_callbacks_rva();
            }
            else
            {
              //Если нет коллбэков, на всякий случай обнулим адрес
              tls->set_callbacks_rva(0);
            }
     
            //Очистим массив коллбэков, они нам больше не нужны
            //Мы их сделали вручную
            tls->clear_tls_callbacks();
     
            //Установим новый относительный адрес
            //данных для инициализации локальной памяти потока
            tls->set_raw_data_start_rva(pe_base::rva_from_section_offset(unpacker_added_section, data.size()));
            //Пересчитываем адрес конца этих данных
            tls->recalc_raw_data_end_rva();
     
            //Пересобираем TLS
            //Указываем пересборщику, что не нужно писать данные и коллбэки
            //Мы сделаем это вручную (коллбэки уже записали, куда надо)
            //Также указываем, что не нужно обрезать нулевые байты в конце секции
            image.rebuild_tls(*tls, unpacker_added_section, directory_pos, false, false, pe_base::tls_data_expand_raw, true, false);
     
            //Дополняем секцию данными для инициализации
            //локальной памяти потока
            unpacker_added_section.get_raw_data() += tls->get_raw_data();
            //Теперь установим виртуальный размер секции "kaimi.ru"
            //с учетом SizeOfZeroFill поля TLS
            image.set_section_virtual_size(unpacker_added_section, data.size() + tls->get_size_of_zero_fill());
            //Наконец, обрежем уже ненужные нулевые байты с конца секции
            pe_base::strip_nullbytes(unpacker_added_section.get_raw_data());
            //и пересчитаем ее размеры (физический и виртуальный)
            image.prepare_section(unpacker_added_section);
          }
    Опишу этот внушительный кусок кода. Сначала мы зарезервировали место под структуру IMAGE_TLS_DIRECTORY32 в последней секции с распаковщиком ("kaimi.ru") сразу после его кода, затем выделели место под массив TLS-коллбэков по их оригинальному количеству (каждый из них занимает 4 байта, плюс последний элемент - нулевой). В новый массив коллбэков записали указатель на код в распаковщике, который ничего не делает, кроме выравнивания стека (ret 0xC). Это даст понять загрузчику, что коллбэки у файла есть. Далее пересчитали указатели на данные, которые загрузчик будет использовать для инициализации локальных данных потоков. Мы размещаем эти данные после структуры IMAGE_TLS_DIRECTORY32 и массива TLS-коллбэков. Потом мы пересобираем TLS с помощью библиотеки для работы с PE (по сути, она просто записывает структуру IMAGE_TLS_DIRECTORY32 куда надо и заполняет ее, автоматическую запись коллбэков и данных мы отключили). Наконец, мы пересчитываем виртуальный размер секции с учетом значения поля SizeOfZeroFill в TLS оригинального файла (мы это значение не меняем). Я точно не могу сказать, как это поле обрабатывается (и описаний толковых в интернете, увы, не нашел) - зануляет ли загрузчик данные после EndAddressOfRawData прямо внутри секции, либо же после инициализации локальной памяти потока, но лучше перестраховаться и выделить место прямо в секции. На размер упакованного файла это не влияет, так как мы увеличиваем виртуальный размер секции, а не физический. После всего этого мы наконец-то обрезаем с конца секции ненужные нулевые байты (она последняя, и мы можем так сделать, об этом я уже как-то писал) и пересчитываем виртуальный и физический размеры секции (реально может поменяться только физический, так как виртуальный мы сами назначили, и он больше или равен физическому).

    Уберем теперь добавленную раньше строку:
    Code:
      image.remove_directory(IMAGE_DIRECTORY_ENTRY_TLS);
    Остается протестировать работоспособность. Обработку TLS без коллбэков можно проверить на любой скомпилированной Борландом программе. Можно также собрать программу с помощью Microsoft Visual Studio любой версии, используя в коде __declspec(thread). TLS с коллбэками сделать не так просто, я пользовался примером отсюда, компилируя его в Visual C++ 6.0, хотя можно было бы собрать TLS с коллбэками руками и на MASM32. После небольшой проверки я удостоверился, что все работает, как надо!

    Честно сказать, я заметил одну особенность, которая присуща всем упаковщикам, которые я опробовал - они все не меняют значение адреса индекса TLS. Не могу пока что сказать, почему это так, но вполне вероятно, что причина такого поведения есть. В комментариях в исходных кодах UPX сказано, что массив TLS-коллбэков должен быть также, как и структура IMAGE_TLS_DIRECTORY32, выровнен на границу DWORD'а, однако я не стал этого делать, так как даже на XP PE-файл с невыровненным массивом отлично работал.

    Есть еще замечание по старому коду. Внезапно выяснилось, что Win XP плохеет, если урезаны директории данных в PE-заголовке (Data Directory), и его explorer.exe перестает отображать иконки у файлов. Поэтому придется закомментировать строку
    Code:
     //image.strip_data_directories(16 - 4); //Закомментировали из-за непереносимости в WinXP
    для сохранения совместимости.

    Полный солюшен для этого шага: Own PE Packer, step 6
     
  7. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,373
    Likes Received:
    6,619
    Reputations:
    693
    Пишем упаковщик по шагам. Шаг седьмой. Релокации.
    Вторник, 25. Сентябрь 2012
    автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-7/
    http://kaimi.ru/



    Предыдущий шаг здесь. Там, кстати, имелась ошибка в коде, я ее поправил. Она проявлялась, когда у файла было больше одного 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). Чтобы знать, по какому адресу файл должен был загрузиться, и по какому он реально загрузился, мы можем сделать так:
    Code:
     //Адрес загрузки образа (оригинальный, к нему не применяются релокации)
      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, обновив смещения к этим трем переменным:
    Code:
    #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), добавив в нее два поля:
    Code:
    DWORD original_relocation_directory_rva; //Относительный адрес оригинальной директории релокаций
      DWORD original_relocation_directory_size; //Размер оригинальной директории релокаций
    Далее, аналогично тому, как мы делали с импортами и ресурсами:
    Code:
     //Запоминаем относительный адрес и размер
        //оригинальной директории релокаций упаковываемого файла
        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);
    После строки:
    Code:
      //Записываем по нужным смещениям адрес
            //загрузки образа
            *reinterpret_cast<DWORD*>(&unpacker_section_data[original_image_base_offset]) = image.get_image_base_32();
    
    допишем следующую:
    Code:
     *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, чтобы он при условии перемещения образа в памяти пофиксил ее. Добавим таблицу релокаций для этого. Код будем писать после следующих строк:
    Code:
    //...
          //Выставляем новую точку входа - теперь она указывает
          //на распаковщик, на самое его начало
          image.set_ep(image.rva_from_section_offset(unpacker_added_section, 0));
        }
    Итак,
    Code:
     //Если у файла есть релокации
        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-файл.

    Кроме того, необходимо заменить строки:
    Code:
        //Наконец, обрежем уже ненужные нулевые байты с конца секции
            pe_base::strip_nullbytes(unpacker_added_section.get_raw_data());
    на:
    Code:
      //Наконец, обрежем уже ненужные нулевые байты с конца секции
            if(!image.has_reloc())
              pe_base::strip_nullbytes(unpacker_added_section.get_raw_data());
    дабы последние байты данных, используемых для инициализации локальной памяти потока, не налезли на релокации, которые мы размещаем прямо за ними.

    Осталось убрать ранее добавленную строку:
    Code:
      image.remove_directory(IMAGE_DIRECTORY_ENTRY_BASERELOC);
    
    чтобы директория релокаций не убиралась из файла (вызов image.rebuild_relocations заполняет ее таким образом, чтобы она указывала на новую директорию релокаций).

    Все, что осталось сделать - обработать релокации оригинального файла в распаковщике (проект unpacker):
    Code:
    //Если у файла были релокации
      //и файл был перемещен загрузчиком
      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++ не позволяет одновременно объявлять и инициализировать переменные. Оказалось, что это так только в общей области видимости функции. Если мы сделаем новую вложенную область видимости, то все работает. То есть, код
    Code:
    void __declspec(naked) func()
    {
      int a = 0;
    }
    
    не соберется, а
    Code:
    void __declspec(naked) func()
    {
      {
        int a = 0;
      }
    }
    отлично сработает.

    На этом работа с релокациями завершена, и любой файл, имеющий релокации и даже некорректный базовый адрес загрузки, как в солюшене, запустится. Но есть еще кое-что: если файл помимо релокаций имеет TLS, то нас ждет неудача. В директории TLS (структуре IMAGE_TLS_DIRECTORY32) адреса используются абсолютные, а не относительные, поэтому нам необходимо их перемещать, если загрузчик разместил образ по адресу, отличному от базового адреса загрузки, указанного в заголовке PE-файла. Кроме того, адреса TLS-коллбэков, если они есть, также абсолютные, и их тоже нужно править.

    Перед началом работы над релокациями TLS я задался вопросом, как это все протестировать. Собирать руками бинарники, которые бы имели релокации и TLS, не было никакого желания. Поэтому я модифицировал пример для тестирования релокаций (reloc_test), о котором я говорил выше, и слинковал его с помощью бесплатного линкера UniLink. Это, пожалуй, единственный линкер, который умеет собирать TLS с коллбэками. Исходный код примера теперь такой:
    Code:
    #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, набрав в консоли такую строку:
    Code:
    ulink.exe -B- -b:0x7F000000 main.obj, main.exe
    Эта команда говорит линкеру ulink.exe о том, что из файла main.obj нужно сделать файл main.exe, установив ему базовый адрес загрузки 0x7F000000 (чтобы наверняка применились релокации) и добавив сами релокации (опция -B-). После выполнения команды у нас будет файл с недопустимым базовым адресом загрузки, TLS с коллбэками и релокациями. Идеально для тестирования!

    Переходим к проекту упаковщика (simple_pe_packer). Вынесем переменную first_callback_offset в более широкую область видимости, заменив строки
    Code:
     //Необходимо зарезервировать место
              //под оригинальные TLS-коллбэки
              //Плюс 1 ячейка под нулевой DWORD
              DWORD first_callback_offset = data.size();
    на
    Code:
    //Необходимо зарезервировать место
              //под оригинальные TLS-коллбэки
              //Плюс 1 ячейка под нулевой DWORD
              first_callback_offset = data.size();
    и дописав строки
    Code:
      //Смещение относительно начала второй секции
        //к абсолютному адресу TLS-коллбэка
        DWORD first_callback_offset = 0;
    перед
    Code:
       {
          //Новая секция
          pe_base::section unpacker_section;
     
    //...
    
    Далее, после строк
    Code:
     //Добавляем релокацию по смещению original_image_base_offset из
          //файла parameters.h распаковщика
          table.add_relocation(pe_base::relocation_entry(original_image_base_offset, IMAGE_REL_BASED_HIGHLOW));
    Дописываем код релокаций TLS:
    Code:
      //Если у файла был 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
     
  8. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,373
    Likes Received:
    6,619
    Reputations:
    693
    Пишем упаковщик по шагам. Шаг восьмой. DLL и экспорты.
    Среда, 26. Сентябрь 2012
    автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-8/
    http://kaimi.ru/



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

    Пока что можно немного расслабиться - в упаковщике кода добавится совсем немного (в распаковщике, в общем-то, тоже, но он будет на ассемблере).

    Займемся сначала упаковщиком (проект simple_pe_packer). Если у файла есть экспорты, нужно их считать, поэтому сразу после строк
    Code:
    //...
          tls.reset(new pe_base::tls_info(image.get_tls_info()));
        }
    напишем:
    Code:
     //Если файл имеет экспорты, получим информацию о них
        //и их список
        pe_base::exported_functions_list exports;
        pe_base::export_info exports_info;
        if(image.has_exports())
        {
          std::cout << "Reading exports..." << std::endl;
          exports = image.get_exported_functions(exports_info);
        }
    Тут библиотека для работы с PE-файлами сильно упрощает нам жизнь, поэтому вдаваться в подробности, как устроены структуры экспортов, не нужно. Далее заменим строки
    Code:
      //Наконец, обрежем уже ненужные нулевые байты с конца секции
            if(!image.has_reloc())
              pe_base::strip_nullbytes(unpacker_added_section.get_raw_data());
    на
    Code:
     //Наконец, обрежем уже ненужные нулевые байты с конца секции
            if(!image.has_reloc() && !image.has_exports())
              pe_base::strip_nullbytes(unpacker_added_section.get_raw_data());
    Так как после распаковщика и TLS у нас будут идти либо экспорты, либо релокации, либо и то и другое, и необходимо, чтобы они не налезли на TLS или распаковщик. Кроме того, нужно перенести строки
    Code:
          //Изменим размер данных секции распаковщика ровно
            //по количеству байтов в теле распаковщика
            //(на случай, если нулевые байты с конца были обрезаны
            //библиотекой для работы с PE)
            data.resize(sizeof(unpacker_data));
    
    выше, так как следует изменять размер данных точно по количеству байтов в распаковщике непосредственно после его записи туда в случае, если у файла есть TLS либо релокации либо экспорты. Расположим этот кусок после строк
    Code:
    //...
          //Добавляем и эту секцию
          pe_base::section& unpacker_added_section = image.add_section(unpacker_section);
     
          if(tls.get() || image.has_exports() || image.has_reloc())
          {
            //Изменим размер данных секции распаковщика ровно
            //по количеству байтов в теле распаковщика
            //(на случай, если нулевые байты с конца были обрезаны
            //библиотекой для работы с PE)
            unpacker_added_section.get_raw_data().resize(sizeof(unpacker_data));
          }
    Это, кстати, нужно было сделать еще в прошлом шаге, когда мы научили упаковщик обрабатывать релокации. Теперь меняем строки
    Code:
     //Пересобираем релокации, располагая их в конце
          //секции с кодом распаковщика
          image.rebuild_relocations(reloc_tables, unpacker_section, unpacker_section.get_raw_data().size());
    на
    Code:
       //Пересобираем релокации, располагая их в конце
          //секции с кодом распаковщика
          image.rebuild_relocations(reloc_tables, unpacker_section, unpacker_section.get_raw_data().size(), true, !image.has_exports());
    
    по все той же причине - чтобы экспорты не налезли на релокации.

    Пришло время обработать экспорты, пересобрав их директорию и расположив ее во второй добавленной нами секции ("kaimi.ru"):
    Code:
       if(image.has_exports())
        {
          std::cout << "Repacking exports..." << std::endl;
     
          pe_base::section& unpacker_section = image.get_image_sections().at(1);
     
          //Пересобираем экспорты и располагаем их в секции "kaimi.ru"
          image.rebuild_exports(exports_info, exports, unpacker_section, unpacker_section.get_raw_data().size());
        }
    И вновь библиотека для работы с PE сильно упростила на жизнь. Теперь просто уберем строку, которую добавляли раньше:
    Code:
     image.remove_directory(IMAGE_DIRECTORY_ENTRY_EXPORT);
    Теперь переходим к распаковщику. Казалось бы - что в нем править? Мы пересобрали директорию экспортов, что еще нужно? Есть одна проблема. В отличие от exe-файла, у DLL точка входа может быть вызвана загрузчиком больше одного раза. Например, при создании нового потока, или когда процесс завершается. А по адресу точки входа у нас тело распаковщика, который уже выполнил свою работу и все распаковал. Если его дернуть второй раз, то просто-напросто все упадет. Поэтому в распаковщик нужно добавить проверку, был ли файл уже распакован, и если был, то управление следует передать на оригинальную точку входа распакованного файла. Я воспользовался хитростью, которую применял в своем предыдущем распаковщике. Мы прямо внутри кода распаковщика разместим переменную размером 4 байта, заполненную нулями. После распаковки в нее мы запишем адрес оригинальной точки входа. Перед распаковкой мы проверим, нулевая ли это переменная, и если нет - это значит, что файл уже был распакован, и мы просто передадим управление по адресу, содержащемуся в этой переменной. Для начала создадим саму переменную и добавим проверку на ноль:
    Code:
    //...
      __asm
      {
        mov original_image_base, 0x11111111;
        mov rva_of_first_section, 0x22222222;
        mov original_image_base_no_fixup, 0x33333333;
      }
     
     
      //Адрес переменной, говорящей о том,
      //был ли код уже распакован
      DWORD* was_unpacked;
     
      __asm
      {
        //Хитрость с получением адреса
        //следующей за call инструкции
        call next2;
        add byte ptr [eax], al;
        add byte ptr [eax], al;
    next2:
        //В eax - адрес первой инструкции
        //add byte ptr [eax], al
        pop eax;
     
        //Сохраним этот адрес
        mov was_unpacked, eax;
     
        //Посмотрим, что по нему лежит
        mov eax, [eax];
     
        //Если там ноль, то перейдем
        //на распаковщик
        test eax, eax;
        jz next3;
     
        //Если не ноль, то завершим распаковщик
        //и перейдем на оригинальную точку входа
        leave;
        jmp eax;
     
    next3:
      }
    Поясню, что делает этот код. Чтобы создать переменную внутри кода (прямо посередине), мы в MASM32 могли воспользоваться директивой dd или db или какой-то еще подобной. В инлайновом ассемблере MSVC++ такие директивы не разрешены. Но нам нужно как-то сделать переменную из 4-х байтов, содержащих нули! Я сделал это так: команда ассемблера "add byte ptr [eax], al" занимает ровно два байта и имеет опкод 00 00. Таким образом, вписав две такие команды подряд, получаем четыре подряд идущих нулевых байта - это то, что нам нужно. Осталось как-то получить адрес первой инструкции с учетом того, что располагаться она может по любому виртуальному адресу - код-то у нас базонезависимый. Это делается с помощью инструкции call next2, которая пропускает наши левые команды и заодно заталкивает в стек адрес возврата, равный адресу команды, следующей за call. За call идут как раз наши инструкции. Теперь мы имеем их адрес. Далее проверяем, что по нему лежит (mov eax, [eax]), изначально там будет ноль, и тело распаковщика начнет выполняться, так как инструкция jz next3 произведет переход на метку. Если же распаковщик уже выполнился, мы запишем в переменную по адресу was_unpacked (который указывает на наши левые команды) адрес оригинальной точки входа, и проверка jz next3 не пройдет. Произойдет завершение тела распаковщика и переход на оригинальную точку входа исходного файла. Нам остается собственно записать по адресу was_unpacked адрес точки входа:
    Code:
    //...
      info = reinterpret_cast<const packed_file_info*>(original_image_base + rva_of_first_section);
     
      //Получим адрес оригинальной точки входа
      DWORD original_ep;
      original_ep = info->original_entry_point + original_image_base;
     
      __asm
      {
        //Запишем его по адресу, содержащемуся в переменной
        //was_unpacked
        mov edx, was_unpacked;
        mov eax, original_ep;
        mov [edx], eax;
      }
    На этом все, и мы можем собрать и протестировать наш упаковщик. Для теста я сделал новый солюшен с несколькими проектами: двумя DLL-файлами и одним EXE. Одну библиотеку EXE-файл грузит статически, другую динамически, после чего вызывает у этих библиотек несколько функций. Статически загружаемая библиотека и сам DLL-файл содержат статический TLS. (В динамически загружаемых библиотеках статического TLS быть не должно, так как он не будет инициализирован). Этот солюшен я приложил в архив в конце статьи. Упаковав и обе DLL-ки, и exe-файл (дав потом обоим упакованным DLL-файлам оригинальные имена), я убедился, что все работает без изменений, как и оригинальные файлы.

    Полный солюшен для этого шага: Own PE Packer Step 8
     
  9. Solitude

    Solitude Member

    Joined:
    29 Aug 2011
    Messages:
    445
    Likes Received:
    23
    Reputations:
    1
    Пишем упаковщик по шагам. Шаг девятый. Delay-loaded DLLs и Image Config.


    Появилась новая версия библиотеки для работы с PE-файлами (0.1.8). Перекачайте и пересоберите ее.

    Сегодня мы будем заниматься теми мелочами, на которые я в свое время забил при написании старого упаковщика. Наш распаковщик уже умеет всё, но есть пара мелких нюансов, которые неплохо бы допилить. Первое - это отложенный импорт (Delay-loaded). Этот механизм позволяет загружать необходимые PE-файлу библиотеки тогда, когда они реально становятся нужны, тем самым экономя время на загрузку образа в память. Механизм этот реализуется исключительно компиляторами/линкерами и никакого отношения к загрузчику не имеет, однако в PE-заголовке есть директория IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT, указывающая на данные отложенного импорта. Не знаю, используется ли это линкером и собранной программой, но загрузчику определенно пофиг. Но лучше оставим эту директорию, не будем ее обнулять. Уберем строку
    Code:
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT);
    С отложенным импортом всё. Следующая вещь, требующая внимания - это конфигурация загрузки образа. Есть такая директория в заголовке 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.ru"). Первым делом уберем из кода упаковщика строку
    Code:
    image.remove_directory(IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG);
    В остальном нам всегда поможет моя библиотека для работы с PE. Список SE-хендлеров мы просто перенесем в нашу вторую добавленную секцию. А вот список адресов LOCK-префиксов нам придется вручную обработать в распаковщике (переносить его мы не будем, так как загрузчик на этапе распаковки не должен их фиксить - файл еще не распакован). В распаковщике добавим после строк
    Code:
        exports = image.get_exported_functions(exports_info);
        }
    
    строки
    Code:
       //Если файл имеет 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. Далее, строку
    Code:
     if(tls.get() || image.has_exports() || image.has_reloc())
    меняем на
    Code:
     if(tls.get() || image.has_exports() || image.has_reloc() || load_config.get())
    так как директорию конфигурации загрузки мы будем размещать там же, в секции "kaimi.ru". Далее, аналогично меняем строку
    Code:
       if(!image.has_reloc() && !image.has_exports())
    на
    Code:
         if(!image.has_reloc() && !image.has_exports() && !load_config.get())
    и
    Code:
          image.rebuild_relocations(reloc_tables, unpacker_section, unpacker_section.get_raw_data().size(), true, !image.has_exports());
    на
    Code:
     image.rebuild_relocations(reloc_tables, unpacker_section, unpacker_section.get_raw_data().size(), true, !image.has_exports() && !load_config.get());
    и, наконец,
    Code:
    image.rebuild_exports(exports_info, exports, unpacker_section, unpacker_section.get_raw_data().size());
    на
    Code:
      image.rebuild_exports(exports_info, exports, unpacker_section, unpacker_section.get_raw_data().size(), true, !load_config.get());
    Далее, в структуру packed_file_info (файл structs.h) добавим пару новых полей:
    Code:
    DWORD original_load_config_directory_rva; //Относительный адрес оригинальной директории конфигурации загрузки
      DWORD lock_opcode; //Фиктивный опкод команды ассемблера LOCK
    Эти поля нам потребуются в распаковщике, а пока что в упаковщике мы их заполним, дописав после строк
    Code:
      //Запоминаем относительный адрес и размер
        //оригинальной директории релокаций упаковываемого файла
        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);
    следующие строки:
    Code:
       //Запоминаем относительный адрес
        //оригинальной директории конфигурации загрузки упаковываемого файла
        basic_info.original_load_config_directory_rva = image.get_directory_rva(IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG);
    и после
    Code:
      //Получаем и сохраняем изначальное количество секций
        basic_info.number_of_sections = sections.size();
    такие строки:
    Code:
     //Опкод ассемблерной инструкции LOCK
        basic_info.lock_opcode = 0xf0;
    Поясню, зачем это нужно. Загрузчик увидит, что наша таблица LOCK-префиксов состоит из единственного элемента, который указывает на поле lock_opcode структуры basic_info (мы ее так соберем, разумеется). На однопроцессорной системе опкод команды LOCK (0xf0), который мы записали в это поле, будет заменен на опкод инструкции NOP (0x90), и в распаковщике мы сможем проверить, нужно ли обрабатывать оригинальную таблицу LOCK-префиксов. Вообще, я не уверен, что эта функциональность присутствует в загрузчиках новых систем начиная от XP (похоже, что всем системам наплевать на эти таблицы), однако, пусть будет, мало ли всплывет. На самом деле, я и файлов-то с LOCK-таблицами не видел ни разу, и может мне просто нефиг делать. Хотя видел, в исходниках Win 2000, но об этом ниже. :D

    Ладно, с правками покончено, переходим к пересборке директории конфигурации. Сразу после куска кода, ответственного за пересборку экспортов, дописываем следующий код:
    Code:
    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.ru"
          //Пересобираем автоматически таблицу 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++ собирает проект так, как ему соблаговолится, оптимизируя по размеру, поэтому минимальные изменения могут привести к тому, что ассемблерные команды будут использованы другие). Поэтому я решил их раз и навсегда зафиксировать, сделав так:
    Code:
     //Пролог вручную
      __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:

    Code:
    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, напишем следующий код:

    Code:
    //Если файл имеет директорию конфигурации загрузки
      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

    Пятница, 28. Сентябрь 2012
    Раздел: C/C++, Windows, Для новичков, автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-9/
     
  10. Solitude

    Solitude Member

    Joined:
    29 Aug 2011
    Messages:
    445
    Likes Received:
    23
    Reputations:
    1
    Пишем упаковщик по шагам. Шаг десятый. Общая архитектура.

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

    Итак, представим, что у нас есть DLL-файл, имеющий следующие директории:
    - импорты
    - экспорты
    - ресурсы (в том числе информацию о версии)
    - релокации
    - конфигурацию загрузки
    - TLS с коллбэками

    Словом, всего по максимуму. Как это все будет расположено в упакованном файле?

    [​IMG]

    Здесь нарисована структура полученного после упаковки файла. Он всегда будет иметь две секции. Первая будет содержать упакованные данные оригинального файла, структуру с информацией об оригинальном файле, которая необходима распаковщику, и ресурсы (не все, а только иконки, манифест и информацию о версии), если они были у оригинального файла. Вторая секция будет содержать тело распаковщика, настроенное для распаковки данного файла. Опционально там же будет размещены директории TLS (данные и коллбэки там же), релокации для распаковщика и TLS, оригинальная таблица экспортов и директория конфигурации загрузки.

    Вот и все, не так и сложно!

    Воскресенье, 30. Сентябрь 2012
    Раздел: Windows, Для новичков, автор: dx
    http://kaimi.ru/2012/09/pe-packer-step-by-step-10/
     
  11. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,373
    Likes Received:
    6,619
    Reputations:
    693
    Пишем упаковщик по шагам. Шаг 11. Интерфейс командной строки. Финальная версия.
    Четверг, 4. Октябрь 2012
    автор: dx
    http://kaimi.ru/2012/10/pe-packer-step-by-step-11/
    http://kaimi.ru/



    [​IMG]

    Появилась новая версия библиотеки для работы с PE-файлами (0.1.9). Никакие баги там поправлены не были, был добавлен функционал, который упаковщик не использует, так что ваше дело, перекачивать ее или нет :)

    В этом шаге мы запилим нашему упаковщику хороший интерфейс командной строки. Я возьму вариант из старого упаковщика http://kaimi.ru/2011/12/упаковщик-pe-файлов-exe-dll/ и модифицирую его.

    Сперва нам потребуется собранная библиотека Boost. Если вы разбирали предыдущие шаги, то она уже должна у вас быть. Если вы ее еще не собрали, то поясню, как это делается. Например, вы распаковали архив с библиотекой в директорию C:\boost. Заходим в эту директорию и запускаем файл bootstrap.bat. Через какое-то время в той же директории появится файл bjam.exe. Запустим консоль (cmd) и перейдем в директорию C:\boost с помощью команды cd. Наберем команду
    Code:
    bjam variant=debug link=static threading=multi runtime-link=static
    и подождем, пока соберется debug-вариант со статической линковкой, а затем наберем
    Code:
    bjam variant=release link=static threading=multi runtime-link=static
    и соберем аналогично release-вариант. Boost собран, и можно переходить к упаковщику (проект simple_pe_packer). В файл main.cpp добавим два include:
    Code:
    #include <boost/program_options.hpp>
    #include <boost/timer.hpp>
    Первый необходим для реализации интерфейса командной строки, второй мы используем для подсчета времени упаковки файла. Заменим строки
    Code:
      //Говорим пользователю, как использовать наш упаковщик
      //На текущем шаге никаких опций упаковки не будет, просто
      //необходимо будет запускать упаковщик, передав через командную строку
      //имя файла, который мы хотим упаковать
      if(argc != 2)
      {
        std::cout << "Usage: simple_pe_packer.exe PE_FILE" << std::endl;
        return 0;
      }
    на такие:
    Code:
     //Чтобы не писать всегда полное имя неймспейса
      namespace po = boost::program_options;
     
      //Таймер будет считать, сколько времени
      //ушло на упаковку файла
      boost::timer pack_timer;
     
      //Принудительная упаковка - будет упакован даже
      //потенциально некорректный файл
      bool force_mode = false;
      //Перепаковывать ли ресурсы
      bool repack_resources;
      //Перепаковывать ли директорию конфигурации загрузки
      bool rebuild_load_config;
      //Обрезать ли DOS-заголовок
      bool strip_dos_headers;
      //Файловое выравнивание после упаковки
      unsigned long file_alignment;
     
      //Путь к исходному файлу
      std::string input_file_name;
      //Путь для упакованного файла
      std::string output_file_name;
     
      //Создаем описание опций
      po::options_description visible_options("DXPack Packer 1.0\nCommand Line Options");
     
      try
      {
        //Создаем список допустимых опций
        //Добавляем для них дефолтовые значения (не для всех)
        po::options_description cmdline;
     
        //out-file,o - значит, что имя опции "--out-file"
        //и короткий ее псевдоним "-o"
        visible_options.add_options() 
          ("out-file,o", po::value<std::string>(&output_file_name), "Output file name")
          ("file-align,a", po::value<unsigned long>(&file_alignment)->default_value(512), "Packed file alignment")
          ("strip-dos,s", po::value<bool>(&strip_dos_headers)->default_value(true), "Strip DOS headers")
          ("repack-res,r", po::value<bool>(&repack_resources)->default_value(true), "Repack resources")
          ("build-load-config,l", po::value<bool>(&rebuild_load_config)->default_value(true), "Rebuild Load Config directory")
          ("force,f", "Force packing of possibly incorrect binaries")
          ;
     
        cmdline.add(visible_options);
        //Скрытая опция - имя файла для упаковки
        cmdline.add_options()
          ("image", po::value<std::string>(&input_file_name), "PE image to pack")
          ;    
     
        //Безымянная (имя файла для упаковки должно стоять на первой позиции)
        po::positional_options_description desc_pos;
        desc_pos.add("image", 1);
     
        //Парсим командную строку
        po::variables_map vm;
        po::store(po::command_line_parser(argc, argv).
          options(cmdline).positional(desc_pos).run(), vm);
        po::notify(vm);
     
        //Если не указан путь к исходному файлу
        if(input_file_name.empty())
          throw std::runtime_error("No input file specified");
     
        //Если указан режим принудительной упаковки
        if(vm.count("force"))
        {
          std::cout << "Force mode is active!" << std::endl;
          force_mode = true;
        }
      }
      catch(const std::exception& e)
      {
        //Если что-то пошло не так - выведем описание опций
        std::cout << e.what() << std::endl << std::endl;
        std::cout << visible_options << std::endl;
        system("pause");
        return 0;
      }
    Не буду подробно описывать этот кусок кода, скажу лишь, что здесь мы удобно и просто с помощью библиотеки boost::program_options обрабатываем командную строку нашего упаковщика. Все опции, доступные через командную строку (bool force_mode, bool repack_resources, bool rebuild_load_config, bool strip_dos_headers, unsigned long file_alignmen, std::string input_file_name, std::string output_file_name), я рассовал в исходник упаковщика, и не буду пояснять, что именно изменилось, так как по мелочам поменялись многие части кода. Кроме того, в конце исходника я сделал вывод затраченного на упаковку времени, просчитанного с помощью библиотеки boost::timer. Все изменения вы, как всегда, сможете оценить, скачав полный солюшен упаковщика.

    Пожалуй, эту версию упаковщика уже можно назвать законченной. Да, он создает подозрительные импорты и, возможно, имеет еще какие-то недостатки, однако он полностью работоспособен, поддерживает то, чего некоторые другие упаковщики не умеют (например, TLS с коллбэками или перепаковку конфигурации загрузки) и имеет интерфейс командной строки. Поэтому помимо полного исходного кода я выложу и exe-файл упаковщика, вдруг кому-то пригодится. На этом я закрываю цикл статей про написание упаковщика.

    Полный солюшен для этого шага: own-packer-step-11
    Упаковщик в собранном виде (EXE): DXPack.zip