Программирование

Как создать PE-файл в C++: руководство по генерации

Пошаговое руководство по созданию исполняемых файлов PE в C++. Различия между COM и PE форматами, настройка заголовков IMAGE_DOS_HEADER, IMAGE_NT_HEADERS32 и IMAGE_SECTION_HEADER.

3 ответа 1 просмотр

Как правильно генерировать исполняемые файлы в формате Portable Executable (PE) с помощью C++? Почему код, работающий как COM-файл в DosBox, не выполняется корректно при создании PE-EXE файла? Какие ключевые параметры заголовков IMAGE_DOS_HEADER, IMAGE_NT_HEADERS32 и IMAGE_SECTION_HEADER необходимо правильно настроить для корректной работы программы в Windows?

Для корректной генерации исполняемых файлов формата PE (Portable Executable) в C++ необходимо вручную формировать структуру заголовков IMAGE_DOS_HEADER, IMAGE_NT_HEADERS32 и IMAGE_SECTION_HEADER, заполняя все обязательные поля в соответствии со спецификацией Windows. COM-файлы работают в DosBox, потому что представляют собой простой 16-битный код, загружаемый по адресу 0x100, а PE-файлы требуют сложной структуры с выравненными секциями, точкой входа и корректными директориями данных.

Диаграмма структуры таблицы экспортов PE

Содержание


Структура PE-файла и ключевые компоненты

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

Основные компоненты PE-файла включают:

  • IMAGE_DOS_HEADER - DOS-заголовок для совместимости с DOS
  • PE-подпись “PE\0\0”
  • IMAGE_FILE_HEADER - заголовок COFF
  • IMAGE_OPTIONAL_HEADER32 - опциональный заголовок
  • IMAGE_SECTION_HEADER - массив описаний секций
  • Таблицы импорта, экспорта, ресурсов и других директорий

Каждый из этих компонентов требует точной настройки для корректной работы исполняемого файла в среде Windows.


Различия между COM и PE форматами

COM-файлы и PE-файлы fundamentally различаются по своей структуре и способу загрузки в системе.

COM-файлы:

  • Представляют собой простой 16-битный код
  • Загружаются по фиксированному адресу 0x100
  • Не имеют сложной структуры заголовков
  • DosBox может их интерпретировать как последовательность инструкций
  • Не подходят для современной Windows, требующей PE-формат

PE-файлы:

  • Имеют сложную структуру с выравненными секциями
  • Поддерживают 32-битный и 64-битный код
  • Включают таблицы импорта и экспорта для динамических библиотек
  • Требуют точной настройки точек входа и адресов секций
  • Обеспечивают безопасность и изоляцию процессов

Почему код, работающий как COM-файл в DosBox, не выполняется корректно при создании PE-EXE файла? Потому что Windows-загрузчик ожидает корректную структуру PE-файла с правильными заголовками, точкой входа, выравненными секциями и таблицами директорий. COM-файлы не содержат этих структурных элементов, необходимых для загрузки в современной Windows.


Настройка IMAGE_DOS_HEADER для корректной работы

IMAGE_DOS_HEADER - это первая структура в PE-файле, обеспечивающая совместимость с DOS. Для корректной работы необходимо правильно настроить следующие поля:

cpp
typedef struct _IMAGE_DOS_HEADER {
 WORD e_magic; // Должно быть 'MZ' (0x5A4D)
 WORD e_cblp; // Не используется
 WORD e_cp; // Не используется
 WORD e_crlc; // Не используется
 WORD e_cparhdr; // Не используется
 WORD e_minalloc; // Не используется
 WORD e_maxalloc; // Не используется
 WORD e_ss; // Не используется
 WORD e_sp; // Не используется
 WORD e_csum; // Не используется
 WORD e_ip; // Не используется
 WORD e_cs; // Не используется
 WORD e_lfarlc; // Не используется
 WORD e_ovno; // Не используется
 WORD e_res[4]; // Не используется
 LONG e_lfanew; // Смещение до PE-заголовка
 WORD e_res2[10]; // Не используется
 LONG e_res3[2]; // Не используется
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Ключевые параметры:

  • e_magic: Должно быть установлено в значение 0x5A4D (‘MZ’)
  • e_lfanew: Смещение до начала PE-заголовка (IMAGE_NT_HEADERS)

Неправильная настройка этих полей приведет к тому, что Windows не сможет распознать файл как исполняемый и откажется его загружать.


Конфигурация IMAGE_NT_HEADERS32 и IMAGE_SECTION_HEADER

IMAGE_NT_HEADERS32

cpp
typedef struct _IMAGE_NT_HEADERS {
 DWORD Signature; // Должно быть "PE\0\0"
 IMAGE_FILE_HEADER FileHeader; // Заголовок COFF
 IMAGE_OPTIONAL_HEADER32 OptionalHeader; // Опциональный заголовок
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

IMAGE_FILE_HEADER:

  • Machine: IMAGE_FILE_MACHINE_I386 (0x014C) для 32-битных приложений
  • NumberOfSections: Количество секций в файле
  • SizeOfOptionalHeader: Размер опционального заголовка
  • Characteristics: Должен включать IMAGE_FILE_EXECUTABLE_IMAGE (0x0002)

IMAGE_OPTIONAL_HEADER32:

  • Magic: IMAGE_NT_OPTIONAL_HDR32_MAGIC (0x10B)
  • AddressOfEntryPoint: RVA точки входа
  • ImageBase: Базовый адрес загрузки (обычно 0x1000000)
  • SectionAlignment: Выравнивание секций в памяти (обычно 0x1000)
  • FileAlignment: Выравнивание секций в файле (обычно 0x200)
  • SizeOfImage: Общий размер образа в памяти
  • SizeOfHeaders: Размер заголовков
  • Subsystem: IMAGE_SUBSYSTEM_WINDOWS_CUI для консольных приложений
  • NumberOfRvaAndSizes: 16 для стандартных директорий

IMAGE_SECTION_HEADER

Каждая секция должна иметь правильные характеристики:

  • .text: IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ
  • .data: IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE
  • .bss: IMAGE_SCN_CNT_UNINITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE

Неправильная настройка этих параметров приведет к тому, что программа не сможет корректно загрузиться и выполниться в Windows.


Практическое руководство по созданию PE-файлов в C++

Шаг 1: Создание базовой структуры

cpp
// Создаем файл для PE-исполняемого
std::ofstream pe_file("example.exe", std::ios::binary | std::ios::out);

// Заполняем IMAGE_DOS_HEADER
IMAGE_DOS_HEADER dos_header = {0};
dos_header.e_magic = 0x5A4D; // 'MZ'
dos_header.e_lfanew = 0x40; // Смещение до PE-заголовка

pe_file.write(reinterpret_cast<char*>(&dos_header), sizeof(dos_header));

Шаг 2: Добавление PE-подписи и заголовков

cpp
// PE-подпись
char pe_signature[] = {'P', 'E', 0, 0};
pe_file.write(pe_signature, 4);

// IMAGE_FILE_HEADER
IMAGE_FILE_HEADER file_header = {0};
file_header.Machine = 0x014C; // IMAGE_FILE_MACHINE_I386
file_header.NumberOfSections = 1; // Одна секция
file_header.SizeOfOptionalHeader = 96; // sizeof(IMAGE_OPTIONAL_HEADER32)
file_header.Characteristics = 0x0002; // IMAGE_FILE_EXECUTABLE_IMAGE

pe_file.write(reinterpret_cast<char*>(&file_header), sizeof(file_header));

Шаг 3: Настройка опционального заголовка

cpp
IMAGE_OPTIONAL_HEADER32 opt_header = {0};
opt_header.Magic = 0x10B; // IMAGE_NT_OPTIONAL_HDR32_MAGIC
opt_header.AddressOfEntryPoint = 0x1000; // RVA точки входа
opt_header.ImageBase = 0x1000000; // Базовый адрес
opt_header.SectionAlignment = 0x1000; // Выравнивание секций
opt_header.FileAlignment = 0x200; // Выравнивание файла
opt_header.SizeOfImage = 0x2000; // Размер образа
opt_header.SizeOfHeaders = 0x200; // Размер заголовков
opt_header.Subsystem = 0x0003; // IMAGE_SUBSYSTEM_WINDOWS_CUI
opt_header.NumberOfRvaAndSizes = 16; // Количество директорий

Шаг 4: Создание секции

cpp
IMAGE_SECTION_HEADER section = {0};
strcpy_s((char*)section.Name, ".text");
section.VirtualSize = 0x1000;
section.VirtualAddress = 0x1000;
section.SizeOfRawData = 0x1000;
section.PointerToRawData = 0x200;
section.Characteristics = 0x20000020; // IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ

pe_file.write(reinterpret_cast<char*>(&section), sizeof(section));

Шаг 5: Запись кода и завершение файла

cpp
// Записываем код в секцию .text
char code[] = {
 0x55, 0x8B, 0xEC, // push ebp; mov ebp, esp
 0x53, // push ebx
 0x6A, 0x00, // push 0
 0x68, 0x00, 0x00, 0x00, 0x00, // push адрес строки
 0xE8, 0x00, 0x00, 0x00, 0x00, // call printf
 // ... остальной код
};
pe_file.write(code, sizeof(code));

// Завершаем файл нулями до размера FileAlignment
char padding[0x100] = {0};
pe_file.write(padding, sizeof(padding));

Важные замечания:

  1. Выравнивание: Убедитесь, что все секции правильно выравнены в соответствии с SectionAlignment и FileAlignment
  2. Точка входа: Адрес точки входа должен находиться в секции с флагом IMAGE_SCN_MEM_EXECUTE
  3. Таблицы директорий: Для полноценной работы необходимо заполнить таблицы импорта, экспорта и ресурсов
  4. Проверка: Всегда проверяйте создаваемый PE-файл с помощью таких инструментов как PE-bear или CFF Explorer

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


Источники

  1. Microsoft Learn - Официальная документация Microsoft по формату PE и COFF файлов: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
  2. corkami GitHub - Подробное описание структуры PE-файлов и практические примеры: https://github.com/corkami/docs/blob/master/PE/PE.md
  3. PE-bear - Инструмент для анализа структуры PE-файлов: https://github.com/erocarrera/pe-bear

Заключение

Генерация исполняемых файлов формата PE в C++ - это сложный процесс, требующий глубокого понимания структуры Windows-исполняемых файлов. Основные различия между COM и PE форматами заключаются в сложности структуры: COM-файлы представляют собой простой код, загружаемый по фиксированному адресу, в то время как PE-файлы имеют сложную структуру с выравненными секциями, точкой входа и таблицами директорий.

Для корректной работы необходимо правильно настроить ключевые параметры заголовков: IMAGE_DOS_HEADER с ‘MZ’ и правильным смещением до PE-заголовка, IMAGE_NT_HEADERS32 с правильным форматом опционального заголовка, и IMAGE_SECTION_HEADER с правильными характеристиками для каждой секции. Только при соблюдении всех этих требований Windows сможет корректно загрузить и выполнить созданный исполняемый файл.

K

Для корректной генерации PE-файла на C++ необходимо вручную сформировать структуру, описанную в спецификации: сначала DOS-заголовок IMAGE_DOS_HEADER, затем строку-подпись «PE\0\0», заголовок COFF (IMAGE_FILE_HEADER) и опциональный заголовок (IMAGE_OPTIONAL_HEADER32). В IMAGE_DOS_HEADER ключевые поля – e_magic должно быть «MZ» (0x5A4D), а e_lfanew указывает на начало PE-подписи. В заголовке COFF необходимо задать правильный Machine (например, IMAGE_FILE_MACHINE_I386), NumberOfSections, SizeOfOptionalHeader и включить флаг IMAGE_FILE_EXECUTABLE_IMAGE в Characteristics. В IMAGE_OPTIONAL_HEADER32 ключевые параметры: Magic = 0x10B (PE32), AddressOfEntryPoint = RVA точки входа, BaseOfCode и BaseOfData (для PE32), ImageBase = множитель 64 КБ, SectionAlignment ≥ FileAlignment, FileAlignment = потенциально 512 Б, SizeOfHeaders = мультипликатор FileAlignment, SizeOfImage = мультипликатор SectionAlignment. Для каждой секции в IMAGE_SECTION_HEADER нужно задать Name (≤ 8 байт), VirtualSize, VirtualAddress (выравнено по SectionAlignment), SizeOfRawData (выравнено по FileAlignment), PointerToRawData (выравнено по FileAlignment) и Characteristics, включающие IMAGE_SCN_CNT_CODE/IMAGE_SCN_MEM_EXECUTE/IMAGE_SCN_MEM_READ для .text, IMAGE_SCN_CNT_INITIALIZED_DATA/IMAGE_SCN_MEM_READ/IMAGE_SCN_MEM_WRITE для .data, IMAGE_SCN_CNT_UNINITIALIZED_DATA/IMAGE_SCN_MEM_READ/IMAGE_SCN_MEM_WRITE для .bss. Неправильная настройка любого из этих полей приводит к тому, что Windows не может корректно загрузить исполняемый файл, в отличие от простого COM-файла, который DosBox загружает как непрерывный поток байтов по адресу 0x100.

@corkami / Security Researcher

Для генерации PE-файла с помощью C++ необходимо вручную сформировать заголовки IMAGE_DOS_HEADER, IMAGE_NT_HEADERS32 и IMAGE_SECTION_HEADER, заполняя обязательные поля, описанные в документации: e_magic = ‘MZ’, e_lfanew указывает на NT-заголовок, NT-заголовок начинается с ‘PE’, 0, 0, Machine = IMAGE_FILE_MACHINE_I386 (или AMD64), NumberOfSections = количество секций, SizeOfOptionalHeader = размер OPTIONAL_HEADER, Characteristics = IMAGE_FILE_EXECUTABLE_IMAGE, Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC, AddressOfEntryPoint = RVA точки входа, ImageBase = 0x1000000, SectionAlignment = 0x1000, FileAlignment = 0x200, SizeOfImage = суммарный размер секций, SizeOfHeaders = размер заголовков, Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI, NumberOfRvaAndSizes = 16. В IMAGE_SECTION_HEADER необходимо задать VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData и Characteristics (для кода – IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ). Код, работающий как COM-файл в DosBox, не запускается в Windows, потому что COM-файл содержит только DOS-stub и 16-битный код, а Windows требует корректный PE-заголовок с точкой входа, выравнением секций и правильными данными директорий. При генерации PE-EXE важно, чтобы e_lfanew указывал на корректный NT-заголовок, чтобы SizeOfHeaders был округлен до FileAlignment, чтобы EntryPoint находился в секции с EXECUTE-характеристикой, а все обязательные поля OPTIONAL_HEADER были заполнены (Magic, AddressOfEntryPoint, ImageBase, SectionAlignment, FileAlignment, SizeOfImage, SizeOfHeaders, Subsystem, NumberOfRvaAndSizes). Кроме того, в разделе Data Directories должны быть корректные RVA для импорта, экспорта и ресурсов, иначе загрузчик не сможет разрешить вызовы.

Авторы
K
Technical Writer
@corkami / Security Researcher
Security Researcher
Источники
Microsoft Learn / Документационный портал
Документационный портал
GitHub / Инструменты для разработчиков
Инструменты для разработчиков
Проверено модерацией
НейроОтветы
Модерация
Как создать PE-файл в C++: руководство по генерации