• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Создание упаковщика/протектора с нуля с помощью C++

Artem N

(L2) cache
Пользователь
Регистрация
28.11.2020
Сообщения
329
Реакции
278
В этой статье я покажу вам как создать свой собственный упаковщик/протектор с нуля, используя только Visual Studio и C/C++ без необходимости использования ассемблера. Мы начнём с основ и рассмотрим более продвинутые моменты ближе к концу статьи. Это идеальный вариант для тех, кто хочет глубже понять Computer Science. Если вы готовы, то возьмите чашку чая и понеслась!

Вступление

Помните время, когда люди "развлекались", используя PE Detectors, такие как Pied, exeinfo, die, RDG и прочие, чтобы определить, какой упаковщик/протектор использовал разработчик?

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

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

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

Она требует весьма глубоких знаний в области низкоуровневого программирования, поэтому лишь немногие люди могут её реализовать.

Эта статья покажет вам как создать свой собственный упаковщик, используя только VC++. И хорошая новость заключается в том, что знание Ассемблера не требуется!

Справка

Вы можете спросить: зачем мне нужен собственный упаковщик, если их сотни. Чтобы получить ответ на этот вопрос, вам нужно знать как они работают.

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

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

Например, если у вас есть файл, упакованный с помощью ASPack, вы можете легко распаковать его с помощью скрипта OllyDbg или скачать распаковщик типа ASPackDie - одним щелчком мыши!

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

Мы не будем создавать упаковщик в общепринятом понимании. Вместо манипуляций с существующим PE-файлом будем создавать новый (как это делает линкер) на основе исходного файла.

Эта статья является второй частью статьи о создании шелл-кода.

Подготовка среды разработки

1. Инструменты и ПО
- Visual Studio 2019
- VC++ Build Tools (C++ 17+ Support)
- CFF Explorer (PE Viewer/Editor)
- HxD (Hex Editor)

2. Создание проектов
1. Открываем Visual Studio 2019
2. Создаём два пустых проекта C++
3. Имя первого pe_packer, имя второго unpacker_stub
4. Установите для pe_packer Тип конфигурации "Приложение (.exe)"
5. То же самое для unpacker_stub
6. Установите unpacker_stub независимым от CRT (C Runtime). Если вы не знаете как, то это рассказано в первой части статьи, Также здесь unpacker_stub является исполняемым файлом, поэтому нужно убрать опцию /NOENTRY.
7. Для обоих проектов - Конфигурацию x64 и Release.
8. Добавим в проекты два файла, один для упаковщика и один для распаковщика со следующим кодом:
C++:
// packer.cpp (pe_packer project)
#include <Windows.h>
#include <iostream>
#include <fstream>

using namespace std;

int main(int argc, char* argv[])
{
    if (argc != 3) return EXIT_FAILURE;

    char* input_pe_file     = argv[1];
    char* output_pe_file    = argv[2];

    return EXIT_SUCCESS;
}
C++:
// unpacker.cpp (unpacker_stub project)
#include <Windows.h>

// Entrypoint
void func_unpack()
{
}

Итак, всё готово и можно приступать к разработке!
Для более быстрого тестирования упаковщика вы можете создать файл pe_packer_tester.bat со следующим содержимым:
"%cd%\pe_packer.exe" "%cd%\input_pe.exe"
01_pe_packer_tutorial_starter_kit_vs16_x64.zip

Упаковщик: Парсинг + Проверка исходного PE-файла

Ок, на данный момент у нас есть input_pe_file и output_pe_file, переданные пользователем нашему упаковщику. Первый шаг - проверить входной файл и убедиться, что это корректный PE-файл и что он соответствует формату.

Для проверки нам нужно его распарсить:
C++:
// Reading Input PE File
ifstream input_pe_file_reader(argv[1], ios::binary);
vector<uint8_t> input_pe_file_buffer(istreambuf_iterator<char>(input_pe_file_reader), {});

// Parsing Input PE File
PIMAGE_DOS_HEADER in_pe_dos_header = (PIMAGE_DOS_HEADER)input_pe_file_buffer.data();
PIMAGE_NT_HEADERS in_pe_nt_header  = (PIMAGE_NT_HEADERS)(input_pe_file_buffer.data() + in_pe_dos_header->e_lfanew);

Затем проверяем следующие два поля:
C++:
bool isPE  = in_pe_dos_header->e_magic == IMAGE_DOS_SIGNATURE;
bool is64  = in_pe_nt_header->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 &&
    in_pe_nt_header->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC;
bool isDLL = in_pe_nt_header->FileHeader.Characteristics & IMAGE_FILE_DLL;
bool isNET = in_pe_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].Size != 0;

После добавления проверки и дополнительных действий код упаковщика должен выглядеть так:
C++:
// packer.cpp
#include <Windows.h>
#include <iostream>
#include <fstream>
#include <vector>

using namespace std;

// Macros
#define BOOL_STR(b) b ? "true" : "false"
#define CONSOLE_COLOR_DEFAULT   SetConsoleTextAttribute(hConsole, 0x09);
#define CONSOLE_COLOR_ERROR     SetConsoleTextAttribute(hConsole, 0x0C);

int main(int argc, char* argv[])
{
    // Setup Console
    HANDLE  hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleTitle("Custom x64 PE Packer by H.M v1.0");
    FlushConsoleInputBuffer(hConsole);
    CONSOLE_COLOR_DEFAULT;

    // Validate Arguments Count
    if (argc != 3) return EXIT_FAILURE;

    // User Inputs
    char* input_pe_file     = argv[1];
    char* output_pe_file    = argv[2];

    // Reading Input PE File
    ifstream input_pe_file_reader(argv[1], ios::binary);
    vector<uint8_t> input_pe_file_buffer(istreambuf_iterator<char>(input_pe_file_reader), {});
    
    // Parsing Input PE File
    PIMAGE_DOS_HEADER in_pe_dos_header = (PIMAGE_DOS_HEADER)input_pe_file_buffer.data();
    PIMAGE_NT_HEADERS in_pe_nt_header =  (PIMAGE_NT_HEADERS)(input_pe_file_buffer.data() + in_pe_dos_header->e_lfanew);
    
    // Validte PE Infromation
    bool isPE  = in_pe_dos_header->e_magic == IMAGE_DOS_SIGNATURE;
    bool is64  = in_pe_nt_header->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 &&
                 in_pe_nt_header->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC;
    bool isDLL = in_pe_nt_header->FileHeader.Characteristics & IMAGE_FILE_DLL;
    bool isNET = in_pe_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].Size != 0;

    // Log Validation Data
    printf("[Validation] Is PE File : %s\n", BOOL_STR(isPE));
    printf("[Validation] Is 64bit : %s\n", BOOL_STR(is64));
    printf("[Validation] Is DLL : %s\n", BOOL_STR(isDLL));
    printf("[Validation] Is COM or .Net : %s\n", BOOL_STR(isNET));

    // Validate and Apply Action
    if (!isPE)
    {
        CONSOLE_COLOR_ERROR;
        printf("[Error] Input PE file is invalid. (Signature Mismatch)\n");
        return EXIT_FAILURE;
    }
    if (!is64)
    {
        CONSOLE_COLOR_ERROR;
        printf("[Error] This packer only supports x64 PE files.\n");
        return EXIT_FAILURE;
    }
    if (isNET)
    {
        CONSOLE_COLOR_ERROR;
        printf("[Error] This packer currently doesn't support .NET/COM assemblies.\n");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Упаковщик: разработка PE-генератора

Хорошо, теперь, когда мы знаем, что наш входной PE-файл правильный, пришло время создать генератор пустого PE-файла. Для этого мы будем использовать WinAPI.

Создаём DOS-Header

Каждый PE-файл начинается с DOS-заголовка, который содержит сигнатуру или адрес таблицы релокаций, адрес в файле нового заголовка. Для создания DOS-заголовка нам нужно заполнить структуру IMAGE_DOS_HEADER следующими значениями:
C++:
// Initializing Dos Header
IMAGE_DOS_HEADER    dos_h;
memset(&dos_h, NULL, sizeof IMAGE_DOS_HEADER);
dos_h.e_magic       = IMAGE_DOS_SIGNATURE;
dos_h.e_cblp        = 0x0090;
dos_h.e_cp          = 0x0003;
dos_h.e_crlc        = 0x0000;
dos_h.e_cparhdr     = 0x0004;
dos_h.e_minalloc    = 0x0000;
dos_h.e_maxalloc    = 0xFFFF;
dos_h.e_ss          = 0x0000;
dos_h.e_sp          = 0x00B8;
dos_h.e_csum        = 0x0000; // Checksum
dos_h.e_ip          = 0x0000;
dos_h.e_cs          = 0x0000;
dos_h.e_lfarlc      = 0x0040;
dos_h.e_ovno        = 0x0000;
dos_h.e_oemid       = 0x0000;
dos_h.e_oeminfo     = 0x0000;
dos_h.e_lfanew      = 0x0040; // Address of the NT Header

Создаём NT-Header

После того, как мы создали DOS-Header, следующий заголовок должен быть NT-Header, который содержит всю важную информацию о файле:
- Сигнатруа
- Файловый заголовок
- Опциональные заголовки

Они объединены в одной структуре IMAGE_NT_HEADERS и мы просто заполняем её следующими значениями:
C++:
// Initializing Nt Header
IMAGE_NT_HEADERS    nt_h;
memset(&nt_h, NULL, sizeof IMAGE_NT_HEADERS);
nt_h.Signature                                          = IMAGE_NT_SIGNATURE;
nt_h.FileHeader.Machine                                 = IMAGE_FILE_MACHINE_AMD64;
nt_h.FileHeader.NumberOfSections                        = 2;
nt_h.FileHeader.TimeDateStamp                           = 0x00000000; // Must Update
nt_h.FileHeader.PointerToSymbolTable                    = 0x0;
nt_h.FileHeader.NumberOfSymbols                         = 0x0;
nt_h.FileHeader.SizeOfOptionalHeader                    = 0x00F0;
nt_h.FileHeader.Characteristics                         = 0x0022;     // Must Update
nt_h.OptionalHeader.Magic                               = IMAGE_NT_OPTIONAL_HDR64_MAGIC;
nt_h.OptionalHeader.MajorLinkerVersion                  = 10;
nt_h.OptionalHeader.MinorLinkerVersion                  = 0x05;
nt_h.OptionalHeader.SizeOfCode                          = 0x00000200; // Must Update
nt_h.OptionalHeader.SizeOfInitializedData               = 0x00000200; // Must Update
nt_h.OptionalHeader.SizeOfUninitializedData             = 0x0;
nt_h.OptionalHeader.AddressOfEntryPoint                 = 0x00001000; // Must Update
nt_h.OptionalHeader.BaseOfCode                          = 0x00001000;
nt_h.OptionalHeader.ImageBase                           = 0x0000000140000000;
nt_h.OptionalHeader.SectionAlignment                    = 0x00001000;
nt_h.OptionalHeader.FileAlignment                       = 0x00000200;
nt_h.OptionalHeader.MajorOperatingSystemVersion         = 0x0;
nt_h.OptionalHeader.MinorOperatingSystemVersion         = 0x0;
nt_h.OptionalHeader.MajorImageVersion                   = 0x0006;
nt_h.OptionalHeader.MinorImageVersion                   = 0x0000;
nt_h.OptionalHeader.MajorSubsystemVersion               = 0x0006;
nt_h.OptionalHeader.MinorSubsystemVersion               = 0x0000;
nt_h.OptionalHeader.Win32VersionValue                   = 0x0;
nt_h.OptionalHeader.SizeOfImage                         = 0x00003000; // Must Update
nt_h.OptionalHeader.SizeOfHeaders                       = 0x00000200;
nt_h.OptionalHeader.CheckSum                            = 0xFFFFFFFF; // Must Update
nt_h.OptionalHeader.Subsystem                           = IMAGE_SUBSYSTEM_WINDOWS_CUI;
nt_h.OptionalHeader.DllCharacteristics                  = 0x0120;
nt_h.OptionalHeader.SizeOfStackReserve                  = 0x0000000000100000;
nt_h.OptionalHeader.SizeOfStackCommit                   = 0x0000000000001000;
nt_h.OptionalHeader.SizeOfHeapReserve                   = 0x0000000000100000;
nt_h.OptionalHeader.SizeOfHeapCommit                    = 0x0000000000001000;
nt_h.OptionalHeader.LoaderFlags                         = 0x00000000;
nt_h.OptionalHeader.NumberOfRvaAndSizes                 = 0x00000010;

IMAGE_NT_HEADERS зависит от архитектуры процессора, которую вы установили в свойствах проекта. В это статье мы работаем с IMAGE_NT_HEADERS64.

Создание секций

Теперь у нас есть DOS-Header и NT-Header. Единственное, что осталось - секции! Секции содержат свои данные в PE-файле, у них тоже есть заголовки. Поэтому нам нужно инициализировать заголовки, а затем записать данные по соответствующим смещениям. Для создания заголовков используем структуру IMAGE_SECTION_HEADER:
C++:
// Initializing Section [ Code ]
IMAGE_SECTION_HEADER    c_sec;
memset(&c_sec, NULL, sizeof IMAGE_SECTION_HEADER);
c_sec.Name[0] = '[';
c_sec.Name[1] = ' ';
c_sec.Name[2] = 'H';
c_sec.Name[3] = '.';
c_sec.Name[4] = 'M';
c_sec.Name[5] = ' ';
c_sec.Name[6] = ']';
c_sec.Name[7] = 0x0;
c_sec.Misc.VirtualSize                  = 0x00001000;   // Virtual Size
c_sec.VirtualAddress                    = 0x00001000;   // Virtual Address
c_sec.SizeOfRawData                     = 0x00000600;   // Raw Size
c_sec.PointerToRawData                  = 0x00000200;   // Raw Address
c_sec.PointerToRelocations              = 0x00000000;   // Reloc Address
c_sec.PointerToLinenumbers              = 0x00000000;   // Line Numbers
c_sec.NumberOfRelocations               = 0x00000000;   // Reloc Numbers
c_sec.NumberOfLinenumbers               = 0x00000000;   // Line Numbers Number
c_sec.Characteristics                   = IMAGE_SCN_MEM_EXECUTE   |
    IMAGE_SCN_MEM_READ    |
    IMAGE_SCN_CNT_CODE    ;

// Initializing Section [ Data ]
IMAGE_SECTION_HEADER    d_sec;
memset(&d_sec, NULL, sizeof IMAGE_SECTION_HEADER);
d_sec.Name[0] = '[';
d_sec.Name[1] = ' ';
d_sec.Name[2] = 'H';
d_sec.Name[3] = '.';
d_sec.Name[4] = 'M';
d_sec.Name[5] = ' ';
d_sec.Name[6] = ']';
d_sec.Name[7] = 0x0;
d_sec.Misc.VirtualSize                  = 0x00000200;   // Virtual Size
d_sec.VirtualAddress                    = 0x00002000;   // Virtual Address
d_sec.SizeOfRawData                     = 0x00000200;   // Raw Size
d_sec.PointerToRawData                  = 0x00000800;   // Raw Address
d_sec.PointerToRelocations              = 0x00000000;   // Reloc Address
d_sec.PointerToLinenumbers              = 0x00000000;   // Line Numbers
d_sec.NumberOfRelocations               = 0x00000000;   // Reloc Numbers
d_sec.NumberOfLinenumbers               = 0x00000000;   // Line Numbers Number
d_sec.Characteristics                   = IMAGE_SCN_CNT_INITIALIZED_DATA |
    IMAGE_SCN_MEM_READ;

Создание PE-файла

Отлично! Теперь всё готово и мы можем записать PE-файл на диск, для этого используйте код:
C++:
// Create/Open PE File
fstream pe_writter;
pe_writter.open(output_pe_file, ios::binary | ios::out);

// Write DOS Header
pe_writter.write((char*)&dos_h, sizeof dos_h);

// Write NT Header
pe_writter.write((char*)&nt_h, sizeof nt_h);

// Write Headers of Sections
pe_writter.write((char*)&c_sec, sizeof c_sec);
pe_writter.write((char*)&d_sec, sizeof d_sec);

// Add Padding
while (pe_writter.tellp() != c_sec.PointerToRawData) pe_writter.put(0x0);

// Write Code Section
pe_writter.put(0xC3); // Empty PE Return Opcode
for (size_t i = 0; i < c_sec.SizeOfRawData - 1; i++) pe_writter.put(0x0);

// Write Data Section
for (size_t i = 0; i < d_sec.SizeOfRawData; i++) pe_writter.put(0x0);

// Close PE File
pe_writter.close();

Теперь запустите упаковщик и смотрите на магию!
02_pe_packer_tutorial_packer_chapter1_vs16_x64.zip

Упаковщик: основная часть

Ок, теперь у нас есть парсер и генератор, пришло время создать сам упаковщик. Для выполнения этой задачи будем используем fast-lzma2 для сжатия и AES-256 для шифрования. После запишем данные в файл.

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

Добавление необходимых библиотек

1. Клонируйте fast-lzma2 и добавьте в свой проект, используя статическую линковку;
2. Клонируйте tiny-aes-c и добавьте в свой проект.

Также вы можете использовать tiny-aes-c, рассмотренный в первой части статьи.

Добавьте библиотечные заголовки:
C++:
// Encryption Library
extern "C"
{
    #include "aes.h"
}

// Compression Library
#include "lzma2\fast-lzma2.h"
#pragma comment(lib, "lzma2\\fast-lzma2.lib")

Сжатие/кодирование данных

И, наконец, мы упаковываем и шифруем файл следующим образом:
C++:
// <----- Packing Data ( Main Implementation ) ----->
printf("[Information] Initializing AES Cryptor...\n");
struct AES_ctx ctx;
const unsigned char key[32] = {
    0xD6, 0x23, 0xB8, 0xEF, 0x62, 0x26, 0xCE, 0xC3, 0xE2, 0x4C, 0x55, 0x12,
    0x7D, 0xE8, 0x73, 0xE7, 0x83, 0x9C, 0x77, 0x6B, 0xB1, 0xA9, 0x3B, 0x57,
    0xB2, 0x5F, 0xDB, 0xEA, 0x0D, 0xB6, 0x8E, 0xA2
};
const unsigned char iv[16] = {
    0x18, 0x42, 0x31, 0x2D, 0xFC, 0xEF, 0xDA, 0xB6, 0xB9, 0x49, 0xF1, 0x0D,
    0x03, 0x7E, 0x7E, 0xBD
};
AES_init_ctx_iv(&ctx, key, iv);

printf("[Information] Initializing Compressor...\n");
FL2_CCtx* cctx = FL2_createCCtxMt(8);
FL2_CCtx_setParameter(cctx, FL2_p_compressionLevel, 9);
FL2_CCtx_setParameter(cctx, FL2_p_dictionarySize, 1024);

vector<uint8_t> data_buffer;
data_buffer.resize(input_pe_file_buffer.size());

printf("[Information] Compressing Buffer...\n");
size_t original_size = input_pe_file_buffer.size();
size_t compressed_size = FL2_compressCCtx(cctx, data_buffer.data(), data_buffer.size(),
                                          input_pe_file_buffer.data(), original_size, 9);
data_buffer.resize(compressed_size);

// Add Padding Before Encryption
for (size_t i = 0; i < 16; i++) data_buffer.insert(data_buffer.begin(), 0x0);
for (size_t i = 0; i < 16; i++) data_buffer.push_back(0x0);

printf("[Information] Encrypting Buffer...\n");
AES_CBC_encrypt_buffer(&ctx, data_buffer.data(), data_buffer.size());

// Log Compression Information
printf("[Information] Original PE Size :  %ld bytes\n", input_pe_file_buffer.size());
printf("[Information] Packed PE Size   :  %ld bytes\n", data_buffer.size());

// Calculate Compression Ratio
float ratio =
    (1.0f - ((float)data_buffer.size() / (float)input_pe_file_buffer.size())) * 100.f;
printf("[Information] Compression Ratio : %.2f%%\n", (roundf(ratio * 100.0f) * 0.01f));

Запись данных в файл и обновление выравниваний

Теперь нам нужно записать упакованные данные в созданный файл. Выполните следующие действия:

1. Добавьте эти макросы в глобальную область видимости:
C++:
#define file_alignment_size         512   // Default Hard Disk Block Size (0x200)
#define memory_alignment_size       4096  // Default Memory Page Size (0x1000)

2. Добавьте эти функции в глобальную область видимости:
C++:
inline DWORD _align(DWORD size, DWORD align, DWORD addr = 0)
{
    if (!(size % align)) return addr + size;
    return addr + (size / align + 1) * align;
}
Выравнивание - это очень важная операция при работе с PE-файлами, изучить её очень полезно!

3. Обновите код, используя выравнивание:
C++:
nt_h.OptionalHeader.SectionAlignment                    = memory_alignment_size;
nt_h.OptionalHeader.FileAlignment                       = file_alignment_size;
C++:
d_sec.Misc.VirtualSize          = _align(data_buffer.size(), memory_alignment_size);
d_sec.VirtualAddress            = c_sec.VirtualAddress + c_sec.Misc.VirtualSize;
d_sec.SizeOfRawData             = _align(data_buffer.size(), file_alignment_size);
d_sec.PointerToRawData          = c_sec.PointerToRawData + c_sec.SizeOfRawData;
C++:
// Write Data Section
size_t current_pos = pe_writter.tellp();
pe_writter.write((char*)data_buffer.data(), data_buffer.size());
while (pe_writter.tellp() != current_pos + d_sec.SizeOfRawData) pe_writter.put(0x0);

// Releasing And Finalizing
vector<uint8_t>().swap(input_pe_file_buffer);
vector<uint8_t>().swap(data_buffer);
CONSOLE_COLOR_SUCCSESS;
printf("[Information] PE File Packed Successfully.");
return EXIT_SUCCESS;

4. Соберите проект и убедитесь, что упаковщик создал корректный рабочий PE-файл, содержащий упакованные данные.
1639330738934.png


Распаковщик: реализация стаба

Отлично! Если вы всё ещё со мной, пришло время создать код распаковщика и поместить его в файл проекта. Для этого создадим стаб. Откройте файл unpacker.cpp и добавьте fast-lzma2 и tiny-aes-c, установите значения ключей. Теперь нужно добавить некоторые переменные, которые мы сможем изменять, найдя их позже по значению:
C++:
volatile PVOID data_ptr                 = (void*)0xAABBCCDD;
volatile DWORD data_size                = 0xEEFFAADD;
volatile DWORD actual_data_size         = 0xA0B0C0D0;

Почему ключевое слово volatile? Просто... чтобы не дать компилятору оптимизировать их и одновременно сохранить оптимизацию, это беспроигрышный вариант. Код должен выглядеть так:
C++:
// unpacker.cpp (unpacker_stub project)
#include <Windows.h>

// Encryption Library
extern "C"
{
    #include "aes.h"
}

// Compression Library
#include "lzma2\fast-lzma2.h"

// WARNING : If you faced error using pragma, try adding lib file in linker settings
#pragma comment(lib, "lzma2\\fast-lzma2.lib")

// Merge Data With Code
#pragma comment(linker, "/merge:.rdata=.text")

// Entrypoint
void func_unpack()
{
    // Internal Data [ Signatures ]
    volatile PVOID data_ptr                 = (void*)0xAABBCCDD;
    volatile DWORD data_size                = 0xEEFFAADD;
    volatile DWORD actual_data_size         = 0xA0B0C0D0;
    volatile DWORD header_size              = 0xF0E0D0A0;
    
    // Initializing Resolvers
    k32_init(); crt_init();

    // Getting BaseAddress of Module
    intptr_t imageBase = (intptr_t)GetModuleHandleA(0);
    data_ptr = (void*)((intptr_t)data_ptr + imageBase);

    // Initializing Cryptor
    struct AES_ctx ctx;
    const unsigned char key[32] = {
    0xD6, 0x23, 0xB8, 0xEF, 0x62, 0x26, 0xCE, 0xC3, 0xE2, 0x4C, 0x55, 0x12,
    0x7D, 0xE8, 0x73, 0xE7, 0x83, 0x9C, 0x77, 0x6B, 0xB1, 0xA9, 0x3B, 0x57,
    0xB2, 0x5F, 0xDB, 0xEA, 0x0D, 0xB6, 0x8E, 0xA2
    };
    const unsigned char iv[16] = {
        0x18, 0x42, 0x31, 0x2D, 0xFC, 0xEF, 0xDA, 0xB6, 0xB9, 0x49, 0xF1, 0x0D,
        0x03, 0x7E, 0x7E, 0xBD
    };
    AES_init_ctx_iv(&ctx, key, iv);

    // Casting PVOID to BYTE
    uint8_t* data_ptr_byte = (uint8_t*)data_ptr;

    // Decrypting Buffer
    AES_CBC_decrypt_buffer(&ctx, data_ptr_byte, data_size);

    // Allocating Code Buffer
    uint8_t* code_buffer = (uint8_t*)malloc(actual_data_size);

    // Decompressing Buffer
    FL2_decompress(code_buffer, actual_data_size, &data_ptr_byte[16], data_size - 32);
    memset(data_ptr, 0, data_size);
}

Мы не используем многопоточную распаковку lzma2, потому что использование потоков в шелл-коде - очень плохая идея!

Распаковщик: C Runtime и WinAPI Resolver

Ок, теперь, если вы попытаетесь собрать проект unpacker_stub, то столкнетесь с множеством ошибок о неразрешённых внешних символах:
1639331307604.png

Это произошло потому, что мы удалили все стандартные библиотеки, такие как msvcrt и kernel32. Но есть одно решение, которое называется Lazy Import.

Lazy Import

При "ленивом импорте" мы вызываем системные функции на лету. Чтобы использовать эту технику, вам понадобится эта удивительная библиотека, состоящая из одного заголовка от настоящего гения Justas Masiulis.
Первое, что нужно сделать - это загрузить библиотеку:
C++:
uintptr_t msvcrtLib = reinterpret_cast<uintptr_t>(LI_FIND(LoadLibraryA)(_S("msvcrt.dll")));

А затем просто вызвать функцию:
C++:
LI_GET(msvcrtLib, printf)("This is a message from dynamically loaded printf.\n");

И всё! Вы можете использовать любую библиотеку и любую функцию без каких-либо проблем в вашем файле, но проблема здесь в том, что у нас много функций в fast-lzma2, и замена всех их функцией LI_GET может занять очень много времени!

Кроме того это может вызвать множество проблем в коде библиотеки, поэтому мне пришла в голову идея: "Что, если я сделаю Resolvers"? Получилось!

Разработка Resolver

Что такое Resolver и как мы можем использовать его в качестве решения? Всё просто: мы реализуем все функции C Runtime и WinAPI внутри смоделированных msvrct.lib и kernel32.lib (можно использовать любые другие). Затем мы вызываем оригинальные функции и перенаправляем параметры в них, после возвращаем результат. Это даст нам возможность создать статическую библиотеку из любой динамической библиотеки!
Например, вот как мы реализуем memcpy:
C++:
// resolver.h
void crt_init();
void* ___memcpy(void* dst, const void* src, size_t size);
C++:
// resolver.cpp
uintptr_t msvcrtLib = 0;
#define _VCRTFunc(fn) LI_GET(msvcrtLib,fn)
void crt_init()
{
    msvcrtLib = reinterpret_cast<uintptr_t>(LI_FIND(LoadLibraryA)(_S("msvcrt.dll")));
}

// Dynamic memcpy
void* ___memcpy(void* dst, const void* src, size_t size)
{
    return _VCRTFunc(memcpy)(dst, src, size);
}
C++:
// resolver_export.cpp
#include "resolver.h"

#define RESOLVER extern "C"
RESOLVER void* __cdecl memcpy(void* dst, const void* src, size_t size)
{
    return ___memcpy(dst, src, size);
}

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

Статическая линковка и Resolvers

При линковке не используйте pragma, вместо этого используйте свойства компоновщика:

1. Перейдите в конфигурацию проекта unpacker_stub и в разделе Компоновщик > Общие > Дополнительные каталоги библиотек измените его на ".\resolvers"

2. Перейдите в Компоновщик > Ввод > Дополнительные зависимости и добавьте "msvrcrt.lib" и "kernel32.lib"

3. Перейдите в Каталоги VC++ и очистите поля "Каталоги библиотек" и "Каталоги библиотек WinRT", чтобы избежать линковки с оригинальными библиотеками.

4. Создайте заголовки функций:
C++:
// Resolvers Functions
extern "C" void crt_init();
extern "C" void k32_init();

5. Инициализируйте Resolvers перед инициализацией шифровщика:
C++:
// Initializing Resolvers
k32_init();
crt_init();

6. Обновите объединение секций, как показано ниже:
C++:
// Merge Data With Code
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/merge:.data=.text")

7. Перейдите в Компоновщик> Командная строка и в Дополнительных параметрах введите "/EMITPOGOPHASEINFO /SECTION:.text,EWR":
1639332093572.png


8. Перейдите в Компоновщик > Дополнительно и измените Внесение случайности в базовый адрес на Нет (/DYNAMICBASE:NO)

9. Перейдите в Компоновщик > Дополнительно и измените Фиксированный базовый адрес на Да (/FIXED). Эта опция предотвратит создание секции релокаций, иначе код будет зависеть от стаба.

Компилируем и происходит магия... Стаб распаковщика успешно скомпилирован!

Распаковщик: Loader/Mapper

Пришло время добавить лоадер и маппер в распаковщик и закончить работать над стабом. Для этого используем библиотеку mmLoader, разработанную на чистом C.

После добавления библиотеки и файла в проект, вставьте следующий код в конец стаба распаковщика:
C++:
// PE Loader Library
#include "mmLoader.h"

...

// Loading PE File
DWORD pe_loader_result = 0;
HMEMMODULE pe_module = LoadMemModule(code_buffer, true, &pe_loader_result);

Вот так! Теперь соберите проект, и вы должны получить файл unpacker_stub.exe, который содержит только две секции:
- .text: код распаковщика
- .pdata: содержит секцию исключений, которая нам не нужна.

Извлеките данные из .text, используя CFF Explorer или Hex Editor и преобразуйте их в массив:
C++:
// unpacker_stub.h (pe_packer project)
unsigned char unpacker_stub[175104] = {
    0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B,
    0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0,
    0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26
    ...
03_pe_packer_tutorial_packer_chapter2_vs16_x64.zip

Упаковщик: Создание стаба

Подключите unpacker_stub.h в packer.cpp сделайте следующие изменения в коде.

1. Добавляем функцию-хелпер для поиска байтов в стабе распаковщика:
C++:
#include <algorithm>

...

inline DWORD _find(uint8_t* data, size_t data_size, DWORD& value)
{
    for (size_t i = 0; i < data_size; i++)
        if (memcmp(&data[i], &value, sizeof DWORD) == 0) return i;
    return -1;
}

2. Измените заголовки секций:
C++:
// Initializing Section [ Code ]
IMAGE_SECTION_HEADER    c_sec;
memset(&c_sec, NULL, sizeof IMAGE_SECTION_HEADER);
c_sec.Name[0] = '[';
c_sec.Name[1] = ' ';
c_sec.Name[2] = 'H';
c_sec.Name[3] = '.';
c_sec.Name[4] = 'M';
c_sec.Name[5] = ' ';
c_sec.Name[6] = ']';
c_sec.Name[7] = 0x0;
c_sec.Misc.VirtualSize = _align(sizeof unpacker_stub, memory_alignment_size);
c_sec.VirtualAddress = memory_alignment_size;
c_sec.SizeOfRawData = sizeof unpacker_stub;
c_sec.PointerToRawData = file_alignment_size;
c_sec.Characteristics =
    IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ |
    IMAGE_SCN_MEM_WRITE | IMAGE_SCN_CNT_CODE;

// Initializing Section [ Data ]
IMAGE_SECTION_HEADER    d_sec;
memset(&d_sec, NULL, sizeof IMAGE_SECTION_HEADER);
d_sec.Name[0] = '[';
d_sec.Name[1] = ' ';
d_sec.Name[2] = 'H';
d_sec.Name[3] = '.';
d_sec.Name[4] = 'M';
d_sec.Name[5] = ' ';
d_sec.Name[6] = ']';
d_sec.Name[7] = 0x0;
d_sec.Misc.VirtualSize = _align(data_buffer.size(), memory_alignment_size);
d_sec.VirtualAddress = c_sec.VirtualAddress + c_sec.Misc.VirtualSize;
d_sec.SizeOfRawData = _align(data_buffer.size(), file_alignment_size);
d_sec.PointerToRawData = c_sec.PointerToRawData + c_sec.SizeOfRawData;
d_sec.Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA |
    IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE;

3. Обновите код, создающий PE-заголовки:
C++:
// Update PE Image Size
printf("[Information] Updating PE Information...\n");
nt_h.OptionalHeader.SizeOfImage =
    _align(d_sec.VirtualAddress + d_sec.Misc.VirtualSize, memory_alignment_size);

// Update PE Informations
nt_h.FileHeader.Characteristics = in_pe_nt_header->FileHeader.Characteristics;
nt_h.FileHeader.TimeDateStamp = in_pe_nt_header->FileHeader.TimeDateStamp;
nt_h.OptionalHeader.CheckSum = 0xFFFFFFFF;
nt_h.OptionalHeader.SizeOfCode = c_sec.SizeOfRawData;
nt_h.OptionalHeader.SizeOfInitializedData = d_sec.SizeOfRawData;
nt_h.OptionalHeader.Subsystem = in_pe_nt_header->OptionalHeader.Subsystem;

// Update PE Entrypoint ( Taken from .map file )
nt_h.OptionalHeader.AddressOfEntryPoint = 0x00005940;
Чтобы получить смещение EntryPoint из map-файла, просто найдите func_unpacker и вы обнаружите это смещение там. Или вы можете просто скопировать точку входа из файла unpacker_stub.exe с помощью CFF Explorer.

4. Сейчас нам нужно найти сигнатуры в стабе и пропатчить их:
C++:
// Create/Open PE File
printf("[Information] Writing Generated PE to Disk...\n");
fstream pe_writter;
pe_writter.open(output_pe_file, ios::binary | ios::out);

// Write DOS Header
pe_writter.write((char*)&dos_h, sizeof dos_h);

// Write NT Header
pe_writter.write((char*)&nt_h, sizeof nt_h);

// Write Headers of Sections
pe_writter.write((char*)&c_sec, sizeof c_sec);
pe_writter.write((char*)&d_sec, sizeof d_sec);

// Add Padding
while (pe_writter.tellp() != c_sec.PointerToRawData) pe_writter.put(0x0);

// Find Singuatures in Unpacker Stub
DWORD data_ptr_sig              = 0xAABBCCDD;
DWORD data_size_sig             = 0xEEFFAADD;
DWORD actual_data_size_sig      = 0xA0B0C0D0;
DWORD header_size_sig           = 0xF0E0D0A0;
DWORD data_ptr_offset           = _find(unpacker_stub, sizeof unpacker_stub, data_ptr_sig);
DWORD data_size_offset          = _find(unpacker_stub, sizeof unpacker_stub, data_size_sig);
DWORD actual_data_size_offset   = _find(unpacker_stub, sizeof unpacker_stub, actual_data_size_sig);
DWORD header_size_offset        = _find(unpacker_stub, sizeof unpacker_stub, header_size_sig);

// Log Singuatures Information
if (data_ptr_offset != -1)
    printf("[Information] Signature A Found at :  %X\n", data_ptr_offset);
if (data_size_offset != -1)
    printf("[Information] Signature B Found at :  %X\n", data_size_offset);
if (actual_data_size_offset != -1)
    printf("[Information] Signature C Found at :  %X\n", actual_data_size_offset);
if (header_size_offset != -1)
    printf("[Information] Signature D Found at :  %X\n", header_size_offset);

// Update Code Section
printf("[Information] Updating Offset Data...\n");
memcpy(&unpacker_stub[data_ptr_offset], &d_sec.VirtualAddress, sizeof DWORD);
memcpy(&unpacker_stub[data_size_offset], &d_sec.SizeOfRawData,  sizeof DWORD);
DWORD pe_file_actual_size = (DWORD)input_pe_file_buffer.size();
memcpy(&unpacker_stub[actual_data_size_offset], &pe_file_actual_size, sizeof DWORD);
memcpy(&unpacker_stub[header_size_offset], &nt_h.OptionalHeader.BaseOfCode, sizeof DWORD);

// Write Code Section
printf("[Information] Writing Code Data...\n");
pe_writter.write((char*)&unpacker_stub, sizeof unpacker_stub);

// Write Data Section
printf("[Information] Writing Packed Data...\n");
size_t current_pos = pe_writter.tellp();
pe_writter.write((char*)data_buffer.data(), data_buffer.size());
while (pe_writter.tellp() != current_pos + d_sec.SizeOfRawData) pe_writter.put(0x0);

// Close PE File
pe_writter.close();

Поехали, попробуем упаковщик... и... Поздравляю! Вы сделали свой первый упаковщик!
1639332609233.png

04_pe_packer_tutorial_packer_chapter3_vs16_x64.zip

Упаковщик: Поддержка динамического связывания + Создание таблицы экспорта

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

Данный этап не так прост, как предыдущие части. На самом деле он очень сложен и требует железных мозгов для его разрешения. Но не волнуйтесь, я разобрал его для вас, так что давайте начнём и добавим поддержку DLL в наш упаковщик!

A) Доделываем стаб распаковщика для поддержки DLL-файлов

На данный момент стаб распаковщика не предназначен для DllMain. Нам нужно изменить его, чтобы убедиться, что он правильно пройдет процедуру инициализации и добавить два дополнительных параметра, которые будут описаны в следующем разделе.
Новый стаб должен выглядеть так:
C++:
// unpacker.cpp (unpacker_stub project)
// WinAPI Functions
#include <Windows.h>
#include <winnt.h>
EXTERN_C IMAGE_DOS_HEADER __ImageBase;

// Resolvers Functions
EXTERN_C void crt_init();
EXTERN_C void k32_init();

// Encryption Library
extern "C"
{
    #include "aes.h"
}

// Compression Library
#include "lzma2\fast-lzma2.h"

// PE Loader Library
#include "mmLoader.h"

// Merge Data With Code
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/merge:.data=.text")

// Cross Section Value
EXTERN_C static volatile uintptr_t      moduleImageBase = 0xBCEAEFBA;
EXTERN_C static volatile FARPROC        functionForwardingPtr = (FARPROC)0xCAFEBABE;

// External Functions
EXTERN_C BOOL CallModuleEntry(void* pMemModule_d, DWORD dwReason);

// Multi-Accessing Values
HMEMMODULE pe_module = 0;

// Entrypoint (EXE/DLL)
BOOL func_unpack(void*, int reason, void*)
{
    // Releasing DLL PE Module
    if (reason == DLL_PROCESS_DETACH)
    { CallModuleEntry(pe_module, DLL_PROCESS_DETACH); FreeMemModule(pe_module); return TRUE; };

    // Handling DLL Thread Events
    if (reason == DLL_THREAD_ATTACH) return CallModuleEntry(pe_module, DLL_THREAD_ATTACH);
    if (reason == DLL_THREAD_DETACH) return CallModuleEntry(pe_module, DLL_THREAD_DETACH);

    // Internal Data [ Signatures ]
    volatile PVOID data_ptr = (void*)0xAABBCCDD;
    volatile DWORD data_size = 0xEEFFAADD;
    volatile DWORD actual_data_size = 0xA0B0C0D0;
    volatile DWORD header_size = 0xF0E0D0A0;

    // Initializing Resolvers
    k32_init(); crt_init();

    // Getting BaseAddress of Module
    intptr_t imageBase = (intptr_t)&__ImageBase;
    data_ptr = (void*)((intptr_t)data_ptr + imageBase);

    // Initializing Cryptor
    struct AES_ctx ctx;
    const unsigned char key[32] = {
    0xD6, 0x23, 0xB8, 0xEF, 0x62, 0x26, 0xCE, 0xC3, 0xE2, 0x4C, 0x55, 0x12,
    0x7D, 0xE8, 0x73, 0xE7, 0x83, 0x9C, 0x77, 0x6B, 0xB1, 0xA9, 0x3B, 0x57,
    0xB2, 0x5F, 0xDB, 0xEA, 0x0D, 0xB6, 0x8E, 0xA2
    };
    const unsigned char iv[16] = {
    0x18, 0x42, 0x31, 0x2D, 0xFC, 0xEF, 0xDA, 0xB6, 0xB9, 0x49, 0xF1, 0x0D,
    0x03, 0x7E, 0x7E, 0xBD
    };
    AES_init_ctx_iv(&ctx, key, iv);

    // Casting PVOID to BYTE
    uint8_t* data_ptr_byte = (uint8_t*)data_ptr;

    // Decrypting Buffer
    AES_CBC_decrypt_buffer(&ctx, data_ptr_byte, data_size);

    // Allocating Code Buffer
    uint8_t* code_buffer = (uint8_t*)malloc(actual_data_size);

    // Decompressing Buffer
    FL2_decompress(code_buffer, actual_data_size, &data_ptr_byte[16], data_size - 32);
    memset(data_ptr, 0, data_size);

    // Loading PE Module
    DWORD pe_loader_result = 0;
    pe_module = LoadMemModule(code_buffer, false, &pe_loader_result);

    // Set Image Base
    moduleImageBase = (uintptr_t)*pe_module;
    functionForwardingPtr = 0;

    // Call Entrypoint
    return CallModuleEntry(pe_module, DLL_PROCESS_ATTACH);
}

Теперь давайте я объясню вам что сделал ;)

1. Мы изменили тип возврата func_unpack на BOOL и добавили 3 параметра:
C++:
BOOL func_unpack(void*, int reason, void*)

2. Мы должны переписать метод получения базового адреса. В EXE мы просто используем GetModuleHandle, но не в DLL. Можно использовать первый параметр функции func_unpack (который является типом hInstance), но это работает только для DLL. Используя же __ImageBase, мы получим верное значение для любого PE-файла:
C++:
#include <winnt.h>
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
...
// Getting BaseAddress of Module
intptr_t imageBase = (intptr_t)&__ImageBase;

3. Нам нужно получить возможность контролировать точку входа нашей DLL. Мы должны внести некоторые несложные изменения в mmLoader и сделать функцию CallModuleEntry пубоичной. Потом будем использовать её для вызова вручную после загрузки нашего модуля из памяти:
C++:
// External Functions
EXTERN_C BOOL CallModuleEntry(void* pMemModule_d, DWORD dwReason);
...
// Changes in mmLoader.c
BOOL CallModuleEntry(void* pMemModule_d, DWORD dwReason)
{
  PMEM_MODULE pMemModule = pMemModule_d;
...

4. Мы должны обрабатывать события DLL, чтобы избежать утечек памяти, сбоев или потери данных при её выгрузке из памяти:
C++:
// Releasing DLL PE Module
if (reason == DLL_PROCESS_DETACH)
{ CallModuleEntry(pe_module, DLL_PROCESS_DETACH); FreeMemModule(pe_module); return TRUE; };

// Handling DLL Thread Events
if (reason == DLL_THREAD_ATTACH) return CallModuleEntry(pe_module, DLL_THREAD_ATTACH);
if (reason == DLL_THREAD_DETACH) return CallModuleEntry(pe_module, DLL_THREAD_DETACH);

5. Добавим две константы, к которым будем обращаться далее:
C++:
// Cross Section Value
EXTERN_C static volatile uintptr_t      moduleImageBase = 0xBCEAEFBA;
EXTERN_C static volatile FARPROC        functionForwardingPtr = (FARPROC)0xCAFEBABE;

6. Обновим процесс загрузки и установим некие значения, что это за значения? Читайте дальше!
C++:
// Loading PE Module
DWORD pe_loader_result = 0;
pe_module = LoadMemModule(code_buffer, false, &pe_loader_result);

// Set Image Base
moduleImageBase = (uintptr_t)*pe_module;
functionForwardingPtr = 0;

// Call Entrypoint
return CallModuleEntry(pe_module, DLL_PROCESS_ATTACH);

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

B) Добавление поиска по шаблону в упаковщик в стаб распаковщика

Так, переходим к файлу packer.cpp и добавим код поиска по шаблону сразу после строки, в которой мы обновили значение точки входа:
C++:
// Update PE Entrypoint ( Taken from .map file )
nt_h.OptionalHeader.AddressOfEntryPoint = 0x00005F10;

// Get Const Values Offset In Unpacker
DWORD imagebase_value_sig = 0xBCEAEFBA;
DWORD imageBaseValueOffset = _find(unpacker_stub, sizeof unpacker_stub, imagebase_value_sig);
memset(&unpacker_stub[imageBaseValueOffset], NULL, sizeof uintptr_t);
if (imageBaseValueOffset != -1)
    printf("[Information] ImageBase Value Signature Found at :  %X\n", imageBaseValueOffset);
DWORD forwarding_value_sig = 0xCAFEBABE;
DWORD forwarding_value_offset = _find(unpacker_stub, sizeof unpacker_stub, forwarding_value_sig);
memset(&unpacker_stub[forwarding_value_offset], NULL, sizeof FARPROC);
if (imageBaseValueOffset != -1)
    printf("[Information] Function Forwading Value Signature Found at :  %X\n", forwarding_value_offset);

C) Добавляем Секцию экспорта/Таблицы/Этап генерации кода

Пришло время сделать шаг, который определит, упаковываем ли мы DLL-файл. Добавьте следующий код сразу после поиска шаблона:
C++:
// Create Export Table ( Section [ Export ] )
IMAGE_SECTION_HEADER et_sec;
memset(&et_sec, NULL, sizeof IMAGE_SECTION_HEADER);
bool hasExports = false; vector<uint8_t> et_buffer;

if (isDLL)
{
    // We Generate Export Section, Export Table and Export Code Here
}

D) Извлекаем информацию об экспорте из исходного PE-файла

В начале нужно выяснить, есть ли экспорт у файла:
C++:
if (isDLL)
{
    uint8_t export_section_index = 0;
    int export_section_raw_addr = -1;

    // Get Export Table Information
    IMAGE_DATA_DIRECTORY ex_table =
        in_pe_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    if (ex_table.VirtualAddress != 0) hasExports = true;

    printf("[Information] Has Exports : %s\n", BOOL_STR(hasExports));
    
    if (hasExports)
    {
        printf("[Information] Creating Export Table...\n");
        // We Have Exports on Input PE File!
    }
}

Теперь мы получили RVA исходной экспортной секции и вычислили по какому виртуальному адресу она находится:
C++:
// Export Directory RVA
DWORD e_dir_rva = ex_table.VirtualAddress;
DWORD et_sec_virtual_address = d_sec.VirtualAddress + d_sec.Misc.VirtualSize;

printf("[Information] Input PE File Section Count : %d\n", in_pe_nt_header->FileHeader.NumberOfSections);

Перебираем все секции PE-файла и выясняем, какая из них является секцией экспорта:
C++:
// Get Section Macro
#define GET_SECTION(h,s) (uintptr_t)IMAGE_FIRST_SECTION(h) + ((s) * sizeof IMAGE_SECTION_HEADER)

...

// Find Export Section in Input PE File
for (size_t i = 0; i < in_pe_nt_header->FileHeader.NumberOfSections; i++)
{
    IMAGE_SECTION_HEADER* get_sec = (PIMAGE_SECTION_HEADER)(GET_SECTION(in_pe_nt_header, i));
    IMAGE_SECTION_HEADER* get_next_sec = (PIMAGE_SECTION_HEADER)(GET_SECTION(in_pe_nt_header, i + 1));

    if (e_dir_rva > get_sec->VirtualAddress &&
        e_dir_rva < get_next_sec->VirtualAddress &&
        (i + 1) <= in_pe_nt_header->FileHeader.NumberOfSections)
    {
        export_section_index = i; break;
    };
}

printf("[Information] Export Section Found At %dth Section\n", export_section_index + 1);

if (export_section_index != -1)
{
    // Actual Export Generation Happens Here
}

Хорошо, давайте поговорим о том, как мы будем выполнять процесс генерации dll-экспорта, прежде чем мы шагнем в драконью пасть...

E) Концепция DLL Export Forwarding

Что такое форвардинг функций? В программировании форвардинг означает переход от вызова одной функции к другой без изменения её параметров.

Это может быть сделано с помощью нескольких методов, известных как DLL hijacking, proxy-dll, machine code redirection и т.д. Создадим небольшой ассемблерный код (32 байта), который определяет базовый адрес загруженного модуля, суммируем его с реальным смещением функции и, наконец, переходим к нему.
1639333670627.png


Будем использовать для форвардинга функций следующий код:
Код:
PUSH RCX
PUSH RAX
MOV RAX,QWORD PTR DS:[(Image Base Address)]
MOV ECX, (Function Offset)
ADD RAX,RCX
MOV QWORD PTR DS:[(Function Offset + Image Base Address)],RAX
POP RAX                                    
POP RCX
JMP QWORD PTR DS:[(Function Offset + Image Base Address)] /* < Jump */

F) Клонирование таблицы экспорта исходного файла, внесение изменений и ребазирование

Теперь, когда вы знаете, как всё устроено, пора приступать к самому сложному. Но прежде чем продолжить, добавьте эти полезные макросы для облегчения процесса:
C++:
#define GET_SECTION(h,s) (uintptr_t)IMAGE_FIRST_SECTION(h) + ((s) * sizeof IMAGE_SECTION_HEADER)
#define RVA_TO_FILE_OFFSET(rva,membase,filebase) ((rva - membase) + filebase)
#define RVA2OFS_EXP(rva) (input_pe_file_buffer.data() +  \
    (RVA_TO_FILE_OFFSET(rva, in_pe_exp_sec->VirtualAddress, in_pe_exp_sec->PointerToRawData)))
#define REBASE_RVA(rva) ((rva - in_pe_exp_sec->VirtualAddress + et_sec_virtual_address) - \
                            (e_dir_rva - in_pe_exp_sec->VirtualAddress))

Разбираем исходную секцию экспорта и теперь у нас есть доступ к её данным:
C++:
printf("[Information] Parsing Input PE Export Section...\n");

// Get Export Directory
PIMAGE_SECTION_HEADER in_pe_exp_sec = (PIMAGE_SECTION_HEADER)(GET_SECTION(in_pe_nt_header, export_section_index));
PIMAGE_EXPORT_DIRECTORY e_dir = (PIMAGE_EXPORT_DIRECTORY)RVA2OFS_EXP(e_dir_rva);
DWORD e_dir_size = in_pe_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

printf("[Information] Export Section Name : %s\n", in_pe_exp_sec->Name);

// Extracting Input Binary Export Table
PULONG  in_et_fn_tab = (PULONG)RVA2OFS_EXP(e_dir->AddressOfFunctions);
PULONG  in_et_name_tab = (PULONG)RVA2OFS_EXP(e_dir->AddressOfNames);
PUSHORT in_et_ordianl_tab = (PUSHORT)RVA2OFS_EXP(e_dir->AddressOfNameOrdinals);
uintptr_t in_et_data_start = (uintptr_t)in_et_fn_tab;
DWORD in_et_last_fn_name_size = strlen((char*)RVA2OFS_EXP(in_et_name_tab[e_dir->NumberOfNames - 1])) + 1;
uintptr_t in_et_data_end = (uintptr_t)(RVA2OFS_EXP(in_et_name_tab[e_dir->NumberOfNames - 1]) + in_et_last_fn_name_size);

Потом мы просто ребазируем с помощью макроса следующим образом:
C++:
// Rebase Export Table Addresses
printf("[Information] Rebasing Expor Table Addresses...\n");
e_dir->AddressOfFunctions = REBASE_RVA(e_dir->AddressOfFunctions);
e_dir->AddressOfNames = REBASE_RVA(e_dir->AddressOfNames);
e_dir->AddressOfNameOrdinals = REBASE_RVA(e_dir->AddressOfNameOrdinals);
for (size_t i = 0; i < e_dir->NumberOfNames; i++) in_et_name_tab[i] = REBASE_RVA(in_et_name_tab[i]);

После этого копируем данные секции экспорта в наш новый PE-файл:
C++:
// Generate Export Table Direcotry Data
et_buffer.resize(e_dir_size);
memcpy(et_buffer.data(), e_dir, sizeof IMAGE_EXPORT_DIRECTORY);

G) Создание кода для экспорта

Добавьте этот небольшой фрагмент кода в ваш исходный код упаковщика сразу после // Helpers:
C++:
// Machine Code
unsigned char func_forwarding_code[32] =
{
    0x51, 0x50,                                         // PUSH RCX, PUSH RAX
    0x48, 0x8B, 0x05,   0x00, 0x00, 0x00, 0x00,         // MOV RAX,QWORD PTR DS:[OFFSET]
    0xB9,               0x00, 0x00, 0x00, 0x00,         // MOV ECX,VALUE
    0x48, 0x03, 0xC1,                                   // ADD RAX,RCX
    0x48, 0x89, 0x05,   0x00, 0x00, 0x00, 0x00,         // MOV QWORD PTR DS:[OFFSET],RAX
    0x58, 0x59,                                         // POP RAX, POP RCX
    0xFF, 0x25,         0x00, 0x00, 0x00, 0x00,         // JMP QWORD PTR DS:[OFFSET]
};

После этого нам нужно выделить временный буфер, вычислить Image base RVA, RVA текущего блока кода и смещение. Затем просто устанавливаем значения в массиве и добавляем в этот временный буфер. Далее создаём в секцию экспорта:
C++:
// Generate Export Table Codes
printf("[Information] Generating Function Forwarding Code...\n");
DWORD ff_code_buffer_size = sizeof func_forwarding_code * e_dir->NumberOfFunctions;
uint8_t* ff_code_buffer = (uint8_t*)malloc(ff_code_buffer_size);
DWORD image_base_rva = c_sec.VirtualAddress + imageBaseValueOffset;
DWORD ff_value_rva = c_sec.VirtualAddress + forwarding_value_offset;
for (size_t i = 0; i < e_dir->NumberOfFunctions; i++)
{
    DWORD func_offset = in_et_fn_tab[in_et_ordianl_tab[i]];
    DWORD machine_code_offset = i * sizeof func_forwarding_code;
    DWORD machine_code_rva = et_buffer.size() + machine_code_offset + et_sec_virtual_address;

    // Machine Code Data
    int32_t* offset_to_image_base       = (int32_t*)&func_forwarding_code[5];
    int32_t* function_offset_value      = (int32_t*)&func_forwarding_code[10];
    int32_t* offset_to_func_addr        = (int32_t*)&func_forwarding_code[20];
    int32_t* offset_to_func_addr2       = (int32_t*)&func_forwarding_code[28];

    offset_to_image_base[0]     = (image_base_rva - machine_code_rva) - (5 + sizeof int32_t);
    function_offset_value[0]    = func_offset;
    offset_to_func_addr[0]      = (ff_value_rva - machine_code_rva) - (20 + sizeof int32_t);
    offset_to_func_addr2[0]     = (ff_value_rva - machine_code_rva) - (28 + sizeof int32_t);
    memcpy(&ff_code_buffer[machine_code_offset], func_forwarding_code, sizeof func_forwarding_code);

    // Update Function Address
    in_et_fn_tab[i] = et_sec_virtual_address + et_buffer.size() + (i * sizeof func_forwarding_code);
}

// Copy Updated Export Table Data
DWORD et_data_size = in_et_data_end - in_et_data_start;
memcpy(&et_buffer.data()[sizeof IMAGE_EXPORT_DIRECTORY], (void*)in_et_data_start, et_data_size);

// Merge Export Table and Export Data Buffers
DWORD size_of_export_table = et_buffer.size();
et_buffer.resize(size_of_export_table + ff_code_buffer_size);
memcpy(&et_buffer.data()[size_of_export_table], (void*)ff_code_buffer, ff_code_buffer_size);
free(ff_code_buffer);

Это всё! Теперь нужно создать новую секцию для экспорта:
C++:
// Generate Export Table Section
et_sec.Name[0] = '[';
et_sec.Name[1] = ' ';
et_sec.Name[2] = 'H';
et_sec.Name[3] = '.';
et_sec.Name[4] = 'M';
et_sec.Name[5] = ' ';
et_sec.Name[6] = ']';
et_sec.Name[7] = 0x0;
et_sec.Misc.VirtualSize = _align(et_buffer.size(), memory_alignment_size);
et_sec.VirtualAddress = et_sec_virtual_address;
et_sec.SizeOfRawData = _align(et_buffer.size(), file_alignment_size);
et_sec.PointerToRawData = d_sec.PointerToRawData + d_sec.SizeOfRawData;
et_sec.Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_CNT_CODE;

Обновим таблицу экспорта:
C++:
// Update Export Table Directory
nt_h.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress = et_sec.VirtualAddress;
nt_h.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size = e_dir_size;

Обновим счётчик секций и размер образа:
C++:
// Update PE Headers
nt_h.FileHeader.NumberOfSections = 3;

// Update PE Image Size
nt_h.OptionalHeader.SizeOfImage =
    _align(et_sec.VirtualAddress + et_sec.Misc.VirtualSize, memory_alignment_size);

H) Запись экспорта DLL в PE-файл

Конечно, нужно сделать некоторые изменения для записи экспорта в файл. После записи секции данных запишем секцию экспорта, используя значение current_pos:
C++:
// Write Export Section
if (et_buffer.size() != 0 && hasExports)
{
    printf("[Information] Writing Export Table Data...\n");
    current_pos = pe_writter.tellp();
    pe_writter.write((char*)et_buffer.data(), et_buffer.size());
    while (pe_writter.tellp() != current_pos + et_sec.SizeOfRawData) pe_writter.put(0x0);
}

Итак, полный код создания файла:
C++:
// Create/Open PE File
printf("[Information] Writing Generated PE to Disk...\n");
fstream pe_writter;
size_t current_pos;
pe_writter.open(output_pe_file, ios::binary | ios::out);

// Write DOS Header
pe_writter.write((char*)&dos_h, sizeof dos_h);

// Write NT Header
pe_writter.write((char*)&nt_h, sizeof nt_h);

// Write Headers of Sections
pe_writter.write((char*)&c_sec, sizeof c_sec);
pe_writter.write((char*)&d_sec, sizeof d_sec);
if(nt_h.FileHeader.NumberOfSections == 3) pe_writter.write((char*)&et_sec, sizeof et_sec);

// Add Padding
while (pe_writter.tellp() != c_sec.PointerToRawData) pe_writter.put(0x0);

// Find Singuatures in Unpacker Stub
DWORD data_ptr_sig              = 0xAABBCCDD;
DWORD data_size_sig             = 0xEEFFAADD;
DWORD actual_data_size_sig      = 0xA0B0C0D0;
DWORD header_size_sig           = 0xF0E0D0A0;
DWORD data_ptr_offset           = _find(unpacker_stub, sizeof unpacker_stub, data_ptr_sig);
DWORD data_size_offset          = _find(unpacker_stub, sizeof unpacker_stub, data_size_sig);
DWORD actual_data_size_offset   = _find(unpacker_stub, sizeof unpacker_stub, actual_data_size_sig);
DWORD header_size_offset        = _find(unpacker_stub, sizeof unpacker_stub, header_size_sig);

...

// Update Code Section
printf("[Information] Updating Offset Data...\n");
memcpy(&unpacker_stub[data_ptr_offset], &d_sec.VirtualAddress, sizeof DWORD);
memcpy(&unpacker_stub[data_size_offset], &d_sec.SizeOfRawData,  sizeof DWORD);
DWORD pe_file_actual_size = (DWORD)input_pe_file_buffer.size();
memcpy(&unpacker_stub[actual_data_size_offset], &pe_file_actual_size, sizeof DWORD);
memcpy(&unpacker_stub[header_size_offset], &nt_h.OptionalHeader.BaseOfCode, sizeof DWORD);

// Write Code Section
printf("[Information] Writing Code Data...\n");
current_pos = pe_writter.tellp();
pe_writter.write((char*)&unpacker_stub, sizeof unpacker_stub);
while (pe_writter.tellp() != current_pos + c_sec.SizeOfRawData) pe_writter.put(0x0);

// Write Data Section
printf("[Information] Writing Packed Data...\n");
current_pos = pe_writter.tellp();
pe_writter.write((char*)data_buffer.data(), data_buffer.size());
while (pe_writter.tellp() != current_pos + d_sec.SizeOfRawData) pe_writter.put(0x0);

// Write Export Section
if (et_buffer.size() != 0 && hasExports)
{
    printf("[Information] Writing Export Table Data...\n");
    current_pos = pe_writter.tellp();
    pe_writter.write((char*)et_buffer.data(), et_buffer.size());
    while (pe_writter.tellp() != current_pos + et_sec.SizeOfRawData) pe_writter.put(0x0);
}

// Close PE File
pe_writter.close();

Теперь наш упаковщик поддерживает и DLL-файлы!
05_pe_packer_tutorial_packer_chapter4_vs16_x64.zip

Упаковщик: Создаём информацию о Версии файла

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

Вы можете использовать библиотеки с GitHub, я же использую свою собственную библиотеку.
1. Ссылка на utilities\hmrclib64_vc16.lib, которая находится в zip-файле с исходным кодом следующей части.
2. Добавьте определения функций в packer.cpp сразу после заголовков:
C++:
// PE Info Ediotr
void  HMResKit_LoadPEFile(const char* peFile);
void  HMResKit_SetFileInfo(const char* key, const char* value);
void  HMResKit_SetPEVersion(const char* peFile);
void  HMResKit_ChangeIcon(const char* iconPath);
void  HMResKit_CommitChanges(const char* sectionName);
3. Добавьте информацию и иконку:
C++:
// Post-Process [ Add Information & Icon ]
printf("[Information] Adding File Information and Icon...\n");
HMResKit_LoadPEFile(output_pe_file);
HMResKit_SetFileInfo("ProductName", "Custom PE Packer");
HMResKit_SetFileInfo("CompanyName", "MemarDesign™ LLC.");
HMResKit_SetFileInfo("LegalTrademarks", "MemarDesign™ LLC.");
HMResKit_SetFileInfo("Comments", "Developed by Hamid.Memar");
HMResKit_SetFileInfo("FileDescription", "A PE File Packed by HMPacker");
HMResKit_SetFileInfo("ProductVersion", "1.0.0.1");
HMResKit_SetFileInfo("FileVersion", "1.0.0.1");
HMResKit_SetFileInfo("InternalName", "packed-pe-file");
HMResKit_SetFileInfo("OriginalFilename", "packed-pe-file");
HMResKit_SetFileInfo("LegalCopyright", "Copyright MemarDesign™ LLC. © 2021-2022");
HMResKit_SetFileInfo("PrivateBuild", "Packed PE");
HMResKit_SetFileInfo("SpecialBuild", "Packed PE");
HMResKit_SetPEVersion("1.0.0.1");
if (!isDLL) HMResKit_ChangeIcon("app.ico");
HMResKit_CommitChanges("[ H.M ]");

Здесь мы не рассматриваем извлечение иконки и информации о файле из исходного файла. Это легко делается, но так или иначе нам нужно распарсить секцию ресурсов. Я не хочу удлинять статью, поэтому будет достаточно одного примера. Ознакомьтесь с этой статьёй.
06_pe_packer_tutorial_packer_chapter_final_vs16_x64.zip

Упаковщик: Дополнения + Советы по улучшению

Несколько советов и дополнений по улучшению упаковщика, которые вы можете использовать.

Совет 1: Обновление контрольной суммы

Последний шаг после всех наших действий - обновление контрольной суммы PE-файла, которая находится по адресу:
C++:
OptionalHeader.CheckSum = 0xFFFFFFFF;
Правильная контрольная сумма очень важна для получения лучших результатов при антивирусном сканировании. Вы можете ознакомиться с этой статьёй как считается контрольная сумма для PE-файлов.

Совет 2: Добавление цифровой подписи

Наш упакованный файл не похож на созданный каким-нибудь общеизвестным компилятором, что может вызвать некоторые проблемы с антивирусами. Если это ваша программа, то её подпись поможет решить эту проблему. Получите сертификат и используйте signtool.exe.

Совет 3: Поддержка файла манифеста

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

Вы должны распарсить каталог ресурсов и извлечь его оттуда.

Совет 4: Поддержка .Net

Для добавления поддержки .Net вы можете пойти сложным путем (манипулируя c .Net PE) или использовать нативный CLR-хостинг, который я рекомендую. Вы можете посмотреть мою статью о CLR-хостинге. Кстати, она старая и сейчас можно всё сделать намного лучше. Возможно, я напишу статью об этом, кто знает?

Упакуйте .Net-сборку в секцию данных и используйте CLR-хостинг в стабе распаковщика, чтобы загрузить её в памяти, также вы можете использовать .Net Core Hosting.

Совет 5: Многократная упаковка

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

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

Еще одна интересная информация об упаковщике: вы можете упаковать уже упакованный PE-файл ещё раз, неограниченное количество раз снова и снова. И каждый раз менять ключ и вектор инициализации!

Совет 6: Более высокая степень сжатия

Не забывайте, что упаковщики могут уменьшать файлы только большого размера. Стаб распаковщика тоже занимает место. Например, если вы упаковываете 1KB DLL, то полученный файл будет больше исходного, но если вы упакуете 100MB DLL, то получите маленький упакованный файл с высокой степенью сжатия!

В любом случае, даже в этой ситуации вы можете использовать UPX на сжатом файле, чтобы "скрыть" наш код распаковки.

Заметка для настоящих безумцев: если вы хотите пойти дальше, то возьмите исходный код UPX и добавьте дополнительный уровень шифрования прямо в него!

Совет 7: Виртуализация кода

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

Дополнительно: Релоки и нестандартные PE-файлы

Упаковщик нуждается в доработке, например, в части обработки релоков и экспорта non-ordinal функций. Настоятельно рекомендую не использовать упаковщик на нестандартных или подписанных PE-файлах, таких как d3dcompiler_47.dll.

Вы можете добавлять новые функции в упаковщик и закоммитить в HMPacker-репозиторий.

Дополнительно: Многоязыковые PE-файлы

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

Дополнительно: Испытание в реальных условиях на Marmoset Toolbag 3

Попробуем наш упаковщик на программе Marmoset Toolbag 3! Marmoset 3 имеет четырк PE-файла:
1. toolbag.exe: главный исполняемый файл размером 19,763,288 байт
2. substance_linker.dll: библиотека, размер 378,368 байт
3. substance_sse2_blend.dll: ещё одна библиотека размером 958,976 байт
4. python36.dll: библиотека Python, размер 3,555,992 байт

Ок, теперь давайте попробуем на них наш упаковщик...
Код:
pack_marmoset.bat :
"%cd%\pe_packer.exe" "%cd%\toolbag.exe" "%cd%\toolbag_packed.exe"
"%cd%\pe_packer.exe" "%cd%\substance_sse2_blend.dll" "%cd%\substance_sse2_blend_packed.dll"
"%cd%\pe_packer.exe" "%cd%\substance_linker.dll" "%cd%\substance_linker_packed.dll"
"%cd%\pe_packer.exe" "%cd%\python36.dll" "%cd%\python36_packed.dll"

Результат:
1639335947360.png


Потрясающе! Наш упаковщик уменьшил размер toolbag.exe с 19,763,288 байт до 5,169,152 байт! Давайте проверим программу, чтобы узнать, работает она правильно или нет...
1639336006735.png


Она работает идеально! Никаких сбоев, никакого падения производительности и очень чистый...

Дополнительно: Проверяем упакованный файл антивирусами

VirusTotal

Вот проверка 67 AVs с помощью VirusTotal, только 2 AV обнаружили упакованный toolbag в виде ложного срабатывания. Оно может быть исправлено добавлением фейкового кода в поддельную секцию. Некоторые AV (особенно с ИИ, такие как SecureAge APEX), отмечают любой PE-файл без понятного кода (а наш файл сильно сжат и зашифрован) флагом, однако добавление произвольного C++-кода уберёт этот флаг.
1639336134642.png

БУДЬТЕ МИЛЫМ. Этот трюк не работает на тех программах, которые содержат настоящий зловредный код. Будьте хорошим человеком и не используйте технику против других, это некрасиво.

AntiScan

Вот проверка 26 AVs с помощью AntiScan, ни один из них не обнаружил упакованный файл!
1639336289431.png


Дополнительно: взглянем поближе на упакованный файл

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

- Ни один из популярных детекторов не распознал упакованный файл:
1639336359427.png


- Наш файл состоит из нестандартных имён секций, без таблицы импорта и не имеет зависимостей:
1639336387827.png


- Энтропия 99% и это означает, что он сильно упакован.
1639336420682.png


- Файл всего лишь на ~12.5 мегабайт потребляет больше памяти, по сравнению с исходным:
1639336458026.png


---
Автор статьи - The Ænema. Оригинал тут - https://www.codeproject.com/Articles/5317556/Creating-Your-Very-Own-x64-PE-Packer-Protector-fro
Переведено специально для xss.pro.
 

Вложения

  • 06_pe_packer_tutorial_packer_chapter_final_vs16_x64.zip
    3.8 МБ · Просмотры: 79
Пожалуйста, обратите внимание, что пользователь заблокирован
Спасибо за статью, полезная матчасть.
Этот трюк не работает на тех программах, которые содержат настоящий зловредный код.
Чтоб усовершенствовать упаковщик до криптора, нужно
1. уменьшить энтропию, "разбавив" код/данные разным мусором. Цель упаковщика - упаковать (К.О.), уменьшить размер, ну а криптору размер не важен.
2. дать осмысленные имена секциям, также поиграться с энтропией.
3. пейлоад (оригинальный файл) разбить на части, часть хранить в ресурсах, часть в данных и т.д
ну и добавить иконку, манифест, описание, чтобы закосить под легит. прогу.
 
Благодарю переводчика за проявленную при переводе статьи выдержку :). В оригинале я бы не осилил её дочитать, информация малость неоднозначная и местами меня триггерит.

Те моменты, которые касаются генерации Portable Executable, трогать не буду - понимать их необходимо, без них гарантировано хождение по граблям, но в итоге покажу, как можно без них обойтись.

Немного критики. Начну с очевидного:
C:
// Getting BaseAddress of Module
intptr_t imageBase = (intptr_t)GetModuleHandleA(0);
...
// Getting BaseAddress of Module
intptr_t imageBase = (intptr_t)&__ImageBase;
Автор, пока писал, кое-чему научился.

без необходимости использования ассемблера.
...
Будем использовать для форвардинга функций следующий код:
Код:
PUSH RCX
PUSH RAX
MOV RAX,QWORD PTR DS:[(Image Base Address)]
MOV ECX, (Function Offset)
ADD RAX,RCX
MOV QWORD PTR DS:[(Function Offset + Image Base Address)],RAX
POP RAX
POP RCX
JMP QWORD PTR DS:[(Function Offset + Image Base Address)] /* < Jump */
Необходимости нет, но асм есть. При этом RCX тут, похоже, можно было бы и убрать (заменив его на ADD RAX,(Function Offset) ).
Но если модифицировать код трамплина, остальное поплывёт, поскольку автор фанат магических чисел и лишних действий:
C++:
 // Machine Code
unsigned char func_forwarding_code[32] =
{
0x51, 0x50,                                         // PUSH RCX, PUSH RAX
0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00, // MOV RAX,QWORD PTR DS:[OFFSET]
0xB9,               0x00, 0x00, 0x00, 0x00,         // MOV ECX,VALUE
0x48, 0x03, 0xC1,                                   // ADD RAX,RCX
0x48, 0x89, 0x05,   0x00, 0x00, 0x00, 0x00,         // MOV QWORD PTR DS:[OFFSET],RAX
0x58, 0x59,                                         // POP RAX, POP RCX
0xFF, 0x25,         0x00, 0x00, 0x00, 0x00,         // JMP QWORD PTR DS:[OFFSET]
};
/// ...
    // Machine Code Data
    int32_t* offset_to_image_base       = (int32_t*)&func_forwarding_code[5];
    int32_t* function_offset_value      = (int32_t*)&func_forwarding_code[10];
    int32_t* offset_to_func_addr        = (int32_t*)&func_forwarding_code[20];
    int32_t* offset_to_func_addr2       = (int32_t*)&func_forwarding_code[28];

    offset_to_image_base[0]     = (image_base_rva - machine_code_rva) - (5 + sizeof int32_t);
    function_offset_value[0]    = func_offset;
    offset_to_func_addr[0]      = (ff_value_rva - machine_code_rva) - (20 + sizeof int32_t);
    offset_to_func_addr2[0]     = (ff_value_rva - machine_code_rva) - (28 + sizeof int32_t);
    memcpy(&ff_code_buffer[machine_code_offset], func_forwarding_code, sizeof func_forwarding_code);
Как это можно сделать иначе? Вот такой псевдокод (пишу без проверки, под WinE гонять подобное мало смысла).
C++:
uint8_t* mc = &ff_code_buffer[machine_code_offset];
size_t i = 0;
mc[i++] = 0x51;                                         // PUSH RCX
mc[i++] = 0x50;                                         // PUSH RAX
mc[i++] = 0x48; mc[i++] = 0x8B; mc[i++] = 0x05;         // MOV RAX, QWORD PTR DS:[OFFSET]
(int32_t*)(mc + i) = (image_base_rva - machine_code_rva) - (i + sizeof int32_t); i += sizeof int32_t;
mc[i++] = 0xB9;                                         // MOV ECX, VALUE ...
(int32_t*)(mc + i) = func_offset; i +=4;
mc[i++] = 0x48; mc[i++] = 0x03; mc[i++] = 0xC1;         // ADD RAX, RCX
mc[i++] = 0x48; mc[i++] = 0x89; mc[i++] = 0x05;         // MOV QWORD PTR DS:[OFFSET], RAX
(int32_t*)(mc + i) = (ff_value_rva - machine_code_rva) - (i + sizeof int32_t); i += sizeof int32_t;
mc[i++] = 0x58;                                         // POP RAX
mc[i++] = 0x59;                                         // POP RCX
mc[i++] = 0xFF; mc[i++] = 0x25;                         // JMP QWORD PTR DS:[OFFSET] ...
(int32_t*)(mc + i) = (ff_value_rva - machine_code_rva) - (i + sizeof int32_t); i += sizeof int32_t;
Был некий шелкодес, который модифицировали и копировали, получился написанный на коленке JIT-ассемблер. Попробуем убрать RCX:
C++:
uint8_t* mc = &ff_code_buffer[machine_code_offset];
size_t i = 0;
mc[i++] = 0x50;                                         // PUSH RAX
mc[i++] = 0x48; mc[i++] = 0x8B; mc[i++] = 0x05;         // MOV RAX, QWORD PTR DS:[OFFSET]
(int32_t*)(mc + i) = (image_base_rva - machine_code_rva) - (i + sizeof int32_t); i += sizeof int32_t;
mc[i++] = 0x48; mc[i++] = 0x05;                         // ADD RAX, imm32
(int32_t*)(mc + i) = func_offset; i +=4;
mc[i++] = 0x48; mc[i++] = 0x89; mc[i++] = 0x05;         // MOV QWORD PTR DS:[OFFSET], RAX
(int32_t*)(mc + i) = (ff_value_rva - machine_code_rva) - (i + sizeof int32_t); i += sizeof int32_t;
mc[i++] = 0x58;                                         // POP RAX
mc[i++] = 0xFF; mc[i++] = 0x25;                         // JMP QWORD PTR DS:[OFFSET] ...
(int32_t*)(mc + i) = (ff_value_rva - machine_code_rva) - (i + sizeof int32_t); i += sizeof int32_t;
Повторю, может быть этот код сразу не заработает, но идея, надеюсь, ясна. Это компактнее, быстрее и проще поддерживать.

Идём дальше. tiny-aes-c в чуть больше 500 строчек? Предвычесленный s-box. Номально для тех, кто пишет 2k21. Оказывается, таблицы можно генерировать, как было ещё в DiskCryptor.

---

Теперь конструктив. Автор статьи обещал без ассемблера же? Потом я добавил, что код для генерации PE не нужен. В самом деле, если мы что-то упаковываем, значит у нас есть exe файл? Его для нас создал линкер при трансляции исхолдного текста. Ну пусть создаёт и остальное.
C++:
uint8_t payload[] = {
#include "payload.exe.c"
}

int main() {
  // аллоцируем память для запускаемого образа.
  void *p = VirtualAlloc(...);

  // расшифровываем массив в аллоцированное пространство
  decipher(payload, p, sizeof(payload));

  // распаковываем
  unpack(p);

  // тут осталось самая малость:
  // - настроить защиту секций;
  // - обработать релоки;
  // - связать импорт;
  // - и выполнить точку входа (балансировать стек)
  // наверное, я что-то забыл, но Вы же внимательно прочли статью и всё поняли?  
}
Что такое payload.exe.c и откуда он взялся? Это так же должно быть понятно из статьи, надо лишь преобразовать упакованный и зашифрованный бинарник в пригодный для включения в единицу трансляции вид. То есть первый байт становится '0x4d', второй - '0x5a' и так далее. У этого метода есть недостатки, но для упаковки своего исполняемого файла прятать от себя исходники не обязательно.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх