Как создать PE-файл в C++: руководство по генерации
Пошаговое руководство по созданию исполняемых файлов PE в C++. Различия между COM и PE форматами, настройка заголовков IMAGE_DOS_HEADER, IMAGE_NT_HEADERS32 и IMAGE_SECTION_HEADER.
Как правильно генерировать исполняемые файлы в формате 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-файла и ключевые компоненты
- Различия между COM и PE форматами
- Настройка IMAGE_DOS_HEADER для корректной работы
- Конфигурация IMAGE_NT_HEADERS32 и IMAGE_SECTION_HEADER
- Практическое руководство по созданию PE-файлов в C++
Структура 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. Для корректной работы необходимо правильно настроить следующие поля:
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
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: Создание базовой структуры
// Создаем файл для 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-подписи и заголовков
// 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: Настройка опционального заголовка
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: Создание секции
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*>(§ion), sizeof(section));
Шаг 5: Запись кода и завершение файла
// Записываем код в секцию .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));
Важные замечания:
- Выравнивание: Убедитесь, что все секции правильно выравнены в соответствии с SectionAlignment и FileAlignment
- Точка входа: Адрес точки входа должен находиться в секции с флагом IMAGE_SCN_MEM_EXECUTE
- Таблицы директорий: Для полноценной работы необходимо заполнить таблицы импорта, экспорта и ресурсов
- Проверка: Всегда проверяйте создаваемый PE-файл с помощью таких инструментов как PE-bear или CFF Explorer
Создание работоспособных PE-файлов вручную - это сложный процесс, требующий глубокого понимания структуры Windows-исполняемых файлов. Однако при правильной настройке всех параметров заголовков можно создать полноценные исполняемые файлы на C++ без использования компилятора.
Источники
- Microsoft Learn - Официальная документация Microsoft по формату PE и COFF файлов: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
- corkami GitHub - Подробное описание структуры PE-файлов и практические примеры: https://github.com/corkami/docs/blob/master/PE/PE.md
- 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 сможет корректно загрузить и выполнить созданный исполняемый файл.
Для корректной генерации 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.
Для генерации 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 для импорта, экспорта и ресурсов, иначе загрузчик не сможет разрешить вызовы.

