Заголовок PE-файла хранит метаданные, по которым анализатор может определить архитектуру образа, найти его основные части, понять, как они размещаются в памяти и файле, определить точку входа и найти таблицы импортов, экспортов, ресурсов, релокаций и других данных. На практике под PE-заголовком обычно понимают несколько последовательных структур, а не одну универсальную структуру.
Основная логическая последовательность выглядит так: DOS Header → DOS Stub → PE Signature → COFF File Header → Optional Header → Data Directories → Section Headers → данные секций. При этом Data Directories являются частью Optional Header, а Section Headers следуют за ним и не входят в IMAGE_OPTIONAL_HEADER. Это важно учитывать при ручном разборе бинарника и при написании собственного PE-парсера.
- Что именно называют заголовком PE-файла
- Общая схема расположения структур
- DOS Header: начало файла и поиск PE-заголовка
- DOS Stub: код между DOS Header и PE Signature
- PE Signature: граница основного PE-заголовка
- COFF File Header: архитектура и общие свойства образа
- Optional Header: почему «optional» не означает необязательный
- Ключевые поля Optional Header
- Magic и версия линкера
- Размеры кода и данных
- AddressOfEntryPoint и BaseOfCode
- ImageBase
- SectionAlignment и FileAlignment
- Версии операционной системы и подсистемы
- SizeOfImage и SizeOfHeaders
- CheckSum
- DllCharacteristics
- Размеры стека и кучи
- NumberOfRvaAndSizes
- PE32 и PE32+: что меняется
- Data Directories: где искать специальные структуры
- Section Headers: как заголовок связан с секциями
- RVA, виртуальный адрес и file offset
- Практический алгоритм чтения PE-файла
- Типичные ошибки при интерпретации PE-заголовка
- Что можно определить по PE-заголовку
- Как связаны все части PE-заголовка
- Что в первую очередь искать в PE-заголовке
- Частые вопросы о PE-заголовке
- Можно ли считать IMAGE_NT_HEADERS всем PE-заголовком?
- Почему Optional Header называется optional?
- Где находится точка входа в файле?
- Все ли Data Directories присутствуют в каждом PE-файле?
- Почему размер PE-заголовка нельзя просто считать по фиксированному числу байт?
Что именно называют заголовком PE-файла
Термин «PE-заголовок» используется в двух близких смыслах. В узком смысле речь может идти о структурах, которые Windows описывает как PE header: PE Signature, COFF File Header и Optional Header. В более практическом смысле анализатор часто рассматривает вместе с ними расположенные перед ними DOS Header и DOS Stub, а после них — таблицу заголовков секций.
Это не противоречие, а следствие того, что формат PE исторически объединяет несколько уровней. Например, структура IMAGE_NT_HEADERS содержит сигнатуру, IMAGE_FILE_HEADER и IMAGE_OPTIONAL_HEADER. DOS Header находится раньше и содержит указатель, позволяющий найти этот блок.
Поэтому вопрос «что хранится в заголовке PE-файла» правильнее понимать как вопрос о начальных структурах, которые описывают образ и позволяют перейти от начала файла к его секциям и внутренним каталогам.
Общая схема расположения структур
Упрощённая схема PE-образа выглядит следующим образом:
Это логическая схема основных структур, а не утверждение о том, что любой файл содержит только эти элементы или что каждый внутренний объект обязательно присутствует.
- DOS Header — начинается с сигнатуры MZ и содержит, среди прочего, смещение до PE Signature.
- DOS Stub — код и связанные с DOS данные между DOS Header и PE Signature.
- PE Signature — четыре байта PE\0\0.
- COFF File Header — архитектура, количество секций, размер Optional Header и набор флагов.
- Optional Header — параметры загрузки образа, адреса, выравнивание, размер образа, подсистема и другие сведения.
- Data Directories — массив пар RVA/Size внутри Optional Header, указывающий на специальные структуры.
- Section Headers — описание секций и соответствия между их представлением в файле и памяти.
- Section Data — код, данные, ресурсы и другое содержимое, на которое ссылаются заголовки.
Последовательность структур определяется полями самих заголовков. Например, e_lfanew позволяет перейти от начала файла к PE Signature, SizeOfOptionalHeader — определить размер Optional Header, а NumberOfSections — количество заголовков секций, которые нужно обработать после него.
DOS Header: начало файла и поиск PE-заголовка
Первой структурой является IMAGE_DOS_HEADER. В начале файла обычно находятся байты MZ, которым соответствует поле e_magic. В структуре это значение используется для проверки DOS-сигнатуры.
Для анализа PE особенно важен другой член структуры — e_lfanew. Он содержит файловое смещение до PE Signature. Это не виртуальный адрес и не RVA: значение отсчитывается от начала файла.
Практический смысл e_lfanew прост: анализатор не обязан предполагать, где именно расположен PE-заголовок. Он читает начало файла, получает e_lfanew и переходит к указанному смещению. Там ожидается четырёхбайтовая сигнатура PE\0\0.
Поэтому базовая проверка начинается не с поиска строки PE в произвольном месте файла. Корректнее проверить DOS Header, прочитать e_lfanew, проверить границы файла и только затем интерпретировать данные по указанному смещению как PE Signature и следующие структуры.
DOS Stub: код между DOS Header и PE Signature
DOS Stub — область DOS-совместимого кода, расположенная между DOS Header и PE Signature. В типичном исполняемом образе она исторически обеспечивала поведение файла при запуске в среде DOS. Стандартный stub мог выводить сообщение о невозможности запуска программы в DOS-режиме.
DOS Stub не следует считать частью структуры IMAGE_DOS_HEADER. Это отдельная область файла, расположенная после DOS Header и до PE Signature.
Для современного анализа Windows-образа сам DOS Stub обычно не является главным источником информации о программе. Однако его наличие важно учитывать при вычислении смещений: PE Signature не обязана находиться непосредственно после фиксированного размера IMAGE_DOS_HEADER.
PE Signature: граница основного PE-заголовка
По смещению, указанному в e_lfanew, находится четырёхбайтовая последовательность PE\0\0. Она идентифицирует расположенный далее блок как PE-образ.
Сразу после сигнатуры находится IMAGE_FILE_HEADER, то есть COFF File Header. За ним следует IMAGE_OPTIONAL_HEADER. Поэтому последовательность можно представить так:
PE\0\0 → IMAGE_FILE_HEADER → IMAGE_OPTIONAL_HEADER → IMAGE_SECTION_HEADER…
В Windows API эти элементы для 32- и 64-разрядного варианта представлены структурами IMAGE_NT_HEADERS32 и IMAGE_NT_HEADERS64. При этом сама сигнатура является отдельным четырёхбайтовым полем, а не частью IMAGE_FILE_HEADER.
COFF File Header: архитектура и общие свойства образа
COFF File Header представлен структурой IMAGE_FILE_HEADER. Он находится непосредственно после PE Signature и содержит семь полей. Его задача — дать базовую информацию о типе образа и определить размер следующей части заголовка.
| Поле | Что хранит | Практический смысл |
|---|---|---|
| Machine | Идентификатор целевой архитектуры | Позволяет определить, для какого типа процессора предназначен образ |
| NumberOfSections | Количество секций | Определяет, сколько IMAGE_SECTION_HEADER нужно обработать |
| TimeDateStamp | 32-битное значение времени | Содержит временную метку формата PE; её не следует автоматически считать достоверной датой создания файла |
| PointerToSymbolTable | Смещение COFF-таблицы символов | Имеет значение прежде всего для COFF-объектов; для обычного PE-образа обычно не используется |
| NumberOfSymbols | Количество записей таблицы символов | Связано с COFF symbol table; для обычного образа обычно равно нулю |
| SizeOfOptionalHeader | Размер Optional Header | Позволяет определить границу Optional Header перед таблицей секций |
| Characteristics | Набор флагов характеристик файла | Описывает свойства образа, например тип файла и некоторые особенности его выполнения |
Machine — один из первых параметров, который стоит проверять при анализе. Он определяет архитектуру, для которой предназначен образ. Однако одной этой проверки недостаточно для полного определения формата Optional Header: дополнительно нужно проверить его поле Magic.
NumberOfSections непосредственно связан с таблицей секций. Если это поле содержит, например, условное значение 5, анализатор ожидает обработать пять структур IMAGE_SECTION_HEADER после Optional Header. Само значение не говорит, как называются секции и какие данные находятся внутри них.
TimeDateStamp представляет временную метку в формате, определённом PE/COFF. При криминалистическом или реверс-инженерном анализе её не стоит безоговорочно трактовать как фактический момент создания файла: содержимое заголовка может быть изменено, а инструменты сборки могут использовать свои значения.
PointerToSymbolTable и NumberOfSymbols относятся к COFF-таблице символов. Для обычного исполняемого PE-образа они, как правило, не используются, поэтому нулевые значения здесь не означают отсутствие всех возможных отладочных или символьных данных в файле.
SizeOfOptionalHeader особенно важен для безопасного парсинга. Нельзя просто считать Optional Header структурой произвольной длины и без проверки переходить к Data Directories. Сначала нужно учитывать размер, объявленный COFF File Header, а затем отдельно проверить NumberOfRvaAndSizes перед чтением конкретных каталогов.
Characteristics находится именно в IMAGE_FILE_HEADER. Его не следует смешивать с DllCharacteristics, расположенным в Optional Header. Оба поля являются наборами флагов, но описывают разные свойства PE-образа.
Optional Header: почему «optional» не означает необязательный
Название IMAGE_OPTIONAL_HEADER историческое. Optional Header действительно является необязательным для COFF-объектного файла, но для обычного исполняемого PE-образа он необходим и содержит сведения, которые использует загрузчик.
Именно здесь находятся параметры, позволяющие понять, как образ должен быть представлен в памяти: предпочтительная база загрузки, точка входа, размеры образа, выравнивание, требуемая подсистема, свойства безопасности и другие характеристики.
Размер Optional Header не следует считать абстрактной константой только по названию структуры. Его границу сообщает поле SizeOfOptionalHeader в COFF File Header. Кроме того, формат определяется полем Magic, поэтому при разборе нельзя заранее интерпретировать байты как PE32 или PE32+ без проверки этого значения.
Ключевые поля Optional Header
Magic и версия линкера
Magic определяет вариант Optional Header. Значение 0x10B обозначает PE32, а 0x20B — PE32+. Это одно из принципиальных полей для корректной интерпретации последующих байтов, поскольку состав и размеры некоторых полей отличаются.
MajorLinkerVersion и MinorLinkerVersion содержат версию линкера, записанную в образ. Эти поля полезны как метаданные, но по ним не следует пытаться с абсолютной точностью определить весь набор инструментов, использованных при сборке.
Размеры кода и данных
SizeOfCode содержит размер кода, а при наличии нескольких секций с кодом — их суммарный размер согласно формату. Аналогично SizeOfInitializedData описывает инициализированные данные, а SizeOfUninitializedData — неинициализированные данные, связанные с областью BSS.
Эти значения дают общее представление об образе, но не заменяют анализ таблицы секций. Чтобы определить конкретное расположение данных, нужно смотреть IMAGE_SECTION_HEADER и соответствующие поля секций.
AddressOfEntryPoint и BaseOfCode
AddressOfEntryPoint содержит RVA точки входа. RVA, или Relative Virtual Address, — адрес относительно базы образа в виртуальном адресном пространстве.
Поэтому AddressOfEntryPoint нельзя напрямую использовать как файловое смещение. Если поле содержит условное значение 0x1234, это означает RVA 0x1234, а не «байт номер 0x1234 в файле».
BaseOfCode также представляет RVA начала области кода относительно базы образа. В PE32 присутствует дополнительное поле BaseOfData; в PE32+ его нет. Это одно из заметных структурных различий двух вариантов Optional Header.
ImageBase
ImageBase — предпочтительный виртуальный адрес, относительно которого рассчитаны RVA образа. Если образ загружается по этой базе, виртуальный адрес объекта можно концептуально получить как ImageBase + RVA.
Однако это не означает, что ImageBase всегда становится фактическим адресом загрузки. Современные механизмы загрузки и релокации позволяют образу оказаться по другой виртуальной базе. Поэтому при анализе нужно различать предпочтительную базу, указанную в заголовке, и фактический адрес отображения образа в конкретном процессе.
SectionAlignment и FileAlignment
SectionAlignment задаёт выравнивание частей образа при размещении в виртуальном адресном пространстве. FileAlignment задаёт выравнивание сырых данных секций в представлении файла.
Это два разных пространства. Условный пример: если VirtualAddress секции равен 0x2000, это не означает, что данные этой секции начинаются с байта 0x2000 в файле. Для файлового расположения используется PointerToRawData из соответствующего IMAGE_SECTION_HEADER.
В нормальном PE SectionAlignment используется для виртуального представления образа, а FileAlignment — для его дискового представления. Поэтому размеры и расположение секций на диске и в памяти могут различаться.
Версии операционной системы и подсистемы
MajorOperatingSystemVersion и MinorOperatingSystemVersion задают заявленную основную и дополнительную версии операционной системы. Отдельно указываются MajorSubsystemVersion и MinorSubsystemVersion, относящиеся к версии требуемой подсистемы.
Поле Subsystem сообщает, какая подсистема требуется образу. Среди определённых форматом вариантов есть Windows GUI, Windows CUI, native и другие специализированные варианты. Это позволяет отличать, например, образ графического приложения от консольного, не анализируя его код.
SizeOfImage и SizeOfHeaders
SizeOfImage описывает размер образа в виртуальном адресном пространстве после загрузки. Значение включает заголовки и области секций с учётом виртуального выравнивания.
SizeOfHeaders описывает размер заголовочной части образа с учётом выравнивания по FileAlignment. В расчёт входят DOS-часть, PE Signature, COFF File Header, Optional Header и заголовки секций.
Поэтому эти два поля нельзя сравнивать как два разных способа указать размер файла. SizeOfImage относится прежде всего к образу в памяти, а SizeOfHeaders — к заголовочной области файла, подготовленной для отображения.
CheckSum
CheckSum содержит контрольную сумму образа, вычисляемую по алгоритму, определённому средствами Windows. Для некоторых категорий образов Windows проверяет её при загрузке, в частности для драйверов и определённых системных компонентов.
Наличие поля не означает, что любая программа должна обязательно выполнять эту проверку или что изменение любого байта автоматически делает PE непригодным для запуска. Значение имеет смысл в контексте типа образа и сценария его загрузки.
DllCharacteristics
DllCharacteristics — набор флагов, описывающих дополнительные свойства загрузки и выполнения образа. Среди них могут быть признаки поддержки динамической базы, NX-совместимости, Control Flow Guard и другие характеристики.
Несмотря на название, поле присутствует в Optional Header исполняемых образов в целом. Его не следует понимать как набор флагов исключительно для DLL.
Размеры стека и кучи
Поля SizeOfStackReserve и SizeOfStackCommit описывают резервируемый и первоначально фиксируемый объём стека. Аналогичная пара SizeOfHeapReserve и SizeOfHeapCommit относится к параметрам кучи.
В PE32 эти размеры представлены 32-битными полями, тогда как в PE32+ соответствующие поля имеют 64-битный размер. Это одно из мест, где нельзя читать PE32+ по схеме PE32 без учёта различий структуры.
NumberOfRvaAndSizes
NumberOfRvaAndSizes указывает количество записей Data Directory, расположенных в конце Optional Header. Это число нужно учитывать при чтении каталогов: наличие места в структуре не означает, что конкретная запись доступна или содержит действительный объект.
При написании парсера недостаточно предположить фиксированное количество каталогов. Сначала проверяется граница Optional Header, затем NumberOfRvaAndSizes и только после этого читается нужная запись.
PE32 и PE32+: что меняется
PE32 и PE32+ используют общую концепцию PE, но различаются представлением ряда полей Optional Header.
| Особенность | PE32 | PE32+ |
|---|---|---|
| Magic | 0x10B | 0x20B |
| ImageBase | 32-битное поле | 64-битное поле |
| BaseOfData | Присутствует | Отсутствует |
| Размеры Stack/Heap Reserve и Commit | 32-битные поля | 64-битные поля |
При этом многие поля стандартной части Optional Header сохраняют одинаковую смысловую роль. Например, AddressOfEntryPoint остаётся RVA, а NumberOfRvaAndSizes по-прежнему определяет количество доступных записей каталогов.
Именно поэтому безопасный анализ начинается с Magic. Нельзя прочитать первые несколько полей Optional Header как PE32, а затем считать, что остальные байты будут иметь ту же структуру в PE32+.
Data Directories: где искать специальные структуры
Data Directories — массив структур IMAGE_DATA_DIRECTORY, расположенный в конце Optional Header. Каждая запись содержит два значения: адрес в виде RVA и размер в байтах. Это указатели на специальные таблицы или области данных внутри образа.
Количество записей определяется NumberOfRvaAndSizes. Кроме того, конкретная запись может иметь нулевой размер или нулевой адрес, а некоторые каталоги могут отсутствовать в конкретном образе.
| Каталог | Практическое назначение |
|---|---|
| Export Table | Описание экспортируемых функций и других экспортов образа |
| Import Table | Описание зависимостей от импортируемых модулей и функций |
| Resource Table | Поиск ресурсов: например, строк, изображений, описаний версий и других ресурсов |
| Exception Table | Данные, связанные с обработкой исключений, зависящие от архитектуры и формата образа |
| Certificate Table | Сведения о сертификатах и данных подписи, связанных с образом |
| Base Relocation Table | Информация для применения базовых релокаций при изменении адреса загрузки |
| Debug | Сведения о связанной отладочной информации |
| TLS | Описание TLS Directory и связанных с TLS данных |
| Load Config | Дополнительные сведения о конфигурации загрузчика и механизмах защиты |
| Bound Import | Информация, связанная с предварительно связанными импортами |
| IAT | Import Address Table, содержащая адреса импортируемых сущностей после разрешения импорта |
| CLR Runtime Header | Информация для образов, использующих инфраструктуру CLR |
Особенно важно, что первый элемент IMAGE_DATA_DIRECTORY называется VirtualAddress, но для большинства каталогов его следует понимать как RVA. Это не абсолютный виртуальный адрес и не файловое смещение.
Certificate Table имеет особую семантику. В отличие от обычных каталогов, первое поле этой записи интерпретируется как файловое смещение к таблице сертификатов, а не как RVA. Поэтому универсальное правило «VirtualAddress каждой Data Directory — это RVA» для Certificate Table применять нельзя.
Не следует также предполагать, что конкретный каталог обязательно начинается в секции с определённым именем. Формат PE связывает каталог с адресом и размером, а не с обязательным названием секции.
Section Headers: как заголовок связан с секциями
После Optional Header непосредственно следуют IMAGE_SECTION_HEADER. Они не являются частью IMAGE_OPTIONAL_HEADER, но без них невозможно полноценно интерпретировать размещение содержимого PE-файла.
Количество этих структур задаётся NumberOfSections в COFF File Header. Каждый Section Header описывает одну секцию и связывает её представление в файле с представлением в памяти.
| Поле | Назначение |
|---|---|
| Name | Имя секции фиксированного размера; оно удобно для анализа, но само по себе не определяет назначение секции |
| VirtualSize | Размер секции в виртуальном представлении |
| VirtualAddress | RVA начала секции при размещении образа в памяти |
| SizeOfRawData | Размер данных секции, хранящихся в файле |
| PointerToRawData | Файловое смещение начала сырых данных секции |
| PointerToRelocations | Смещение таблицы релокаций секции в форматах, где она используется |
| PointerToLinenumbers | Смещение таблицы номеров строк; исторические COFF-данные |
| NumberOfRelocations | Количество записей релокаций секции |
| NumberOfLinenumbers | Количество записей номеров строк |
| Characteristics | Флаги свойств секции, включая характеристики доступа и содержания |
Для обычного PE-образа некоторые COFF-поля секции, например PointerToRelocations и PointerToLinenumbers, обычно не используются. Их наличие в структуре не означает, что соответствующие таблицы обязательно присутствуют в каждом исполняемом файле.
RVA, виртуальный адрес и file offset
Одна из самых частых ошибок при анализе PE — смешивание адресов в памяти и смещений внутри файла.
File offset — позиция байта относительно начала файла на диске. RVA — адрес относительно базы образа в его виртуальном представлении. VA — виртуальный адрес, который можно рассматривать как фактический адрес в памяти при конкретной базе загрузки.
Концептуально связь между VA и RVA выглядит так: VA = ImageBase + RVA, если речь идёт о загруженном образе и соответствующей базе.
Связь RVA с file offset устроена иначе. Чтобы перевести RVA в файловое смещение, нужно определить секцию, диапазон виртуальных адресов которой содержит этот RVA, и использовать её VirtualAddress и PointerToRawData с учётом размеров и выравнивания.
Упрощённо для RVA, попадающего в область данных конкретной секции, можно рассматривать преобразование как:
file offset = PointerToRawData + (RVA − VirtualAddress)
Но эту формулу нельзя применять механически к любому числу. Сначала нужно убедиться, что RVA действительно относится к соответствующей секции и находится в области, представленной данными файла. Например, виртуальная часть секции может быть больше объёма её сырых данных на диске.
Именно поэтому AddressOfEntryPoint нельзя напрямую передавать функции чтения файла как смещение. Сначала RVA точки входа сопоставляется с секцией, после чего вычисляется file offset.
Практический алгоритм чтения PE-файла
При ручном анализе или разработке парсера удобно двигаться от общих структур к частным данным:
- Найти начало файла и проверить DOS Header. Проверяется e_magic, соответствующий сигнатуре MZ, и корректность границ структуры.
- Прочитать e_lfanew. Это файловое смещение до PE Signature. Перед переходом необходимо убедиться, что значение находится в пределах файла.
- Перейти к PE Signature. По e_lfanew должны находиться четыре байта PE\0\0.
- Прочитать IMAGE_FILE_HEADER. Из него получают Machine, NumberOfSections, SizeOfOptionalHeader и Characteristics, а также остальные поля COFF-заголовка.
- Определить вариант Optional Header. Читается Magic. Значение 0x10B означает PE32, а 0x20B — PE32+.
- Прочитать основные параметры Optional Header. Особое внимание уделяется AddressOfEntryPoint, ImageBase, SectionAlignment, FileAlignment, SizeOfImage и SizeOfHeaders.
- Определить границы Data Directories. Учитываются SizeOfOptionalHeader и NumberOfRvaAndSizes, чтобы не читать данные за пределами доступного Optional Header.
- Определить таблицу секций. NumberOfSections сообщает, сколько IMAGE_SECTION_HEADER нужно обработать после Optional Header.
- Сопоставить RVA с секциями. Для поиска содержимого в файле используется соответствующая секция и её PointerToRawData, а не простое преобразование RVA в file offset через ImageBase.
- Использовать Data Directories. По каталогам можно найти импорты, экспорты, ресурсы, релокации, отладочные сведения, TLS и другие структуры, если соответствующие записи присутствуют.
Условный пример: если AddressOfEntryPoint равен 0x2500, это означает RVA 0x2500. Если Section Header показывает, что секция начинается с VirtualAddress 0x2000, а её PointerToRawData равен 0x600, то для RVA 0x2500 соответствующее файловое смещение в этом условном примере будет 0xB00. Это именно пример расчёта, а не значение из конкретного PE-файла.
Типичные ошибки при интерпретации PE-заголовка
- Считать PE-заголовок одной структурой. На практике нужно различать DOS Header, PE Signature, IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER и IMAGE_SECTION_HEADER.
- Считать Optional Header действительно необязательным. Для PE-образа он необходим; слово Optional связано прежде всего с отличием от COFF-объектных файлов.
- Принимать AddressOfEntryPoint за file offset. Это RVA.
- Считать ImageBase фактическим адресом загрузки. Это предпочтительная база, а не гарантия фактического размещения.
- Смешивать SectionAlignment и FileAlignment. Первый относится к виртуальному размещению, второй — к файловому представлению.
- Считать NumberOfSections количеством любых областей данных. Он определяет число IMAGE_SECTION_HEADER, а не число всех логических объектов внутри файла.
- Безусловно читать фиксированное число Data Directories. Нужно учитывать NumberOfRvaAndSizes и границы Optional Header.
- Считать VirtualAddress из любой Data Directory обычным файловым смещением. В большинстве случаев это RVA, а Certificate Table является важным исключением.
- Ориентироваться только на имена секций. Имя вроде .text или .data удобно для человека, но формат не требует, чтобы конкретный каталог или тип содержимого находился только в секции с определённым именем.
- Считать значения заголовка безусловно достоверными. PE-заголовок — это данные файла. Для анализа подозрительного или повреждённого образа необходимо проверять согласованность полей и границы всех структур.
Что можно определить по PE-заголовку
Если рассматривать заголовки как единую систему, из них можно получить значительную часть базовой информации об образе ещё до анализа машинного кода.
- какая архитектура указана в Machine;
- является ли Optional Header форматом PE32 или PE32+;
- где находится точка входа в виртуальном адресном пространстве;
- какова предпочтительная база образа;
- каковы виртуальные и файловые параметры выравнивания;
- сколько секций нужно обработать и где они расположены;
- каков размер образа в памяти и размер его заголовочной части;
- какая подсистема заявлена для образа;
- какие дополнительные характеристики загрузки указаны в DllCharacteristics;
- какие Data Directories доступны и где расположены соответствующие структуры;
- какие области файла соответствуют определённым RVA;
- где искать импорты, экспорты, ресурсы, релокации, TLS, отладочную информацию и другие структуры.
При этом заголовок не содержит всей информации о программе. Например, наличие Import Table показывает, где искать сведения об импортах, но для понимания поведения программы потребуется анализ самих импортов и машинного кода. Аналогично наличие Resource Table только указывает на расположение ресурсов.
Как связаны все части PE-заголовка
Цепочка начинается с DOS Header: e_lfanew переводит анализатор от начала файла к PE Signature. Сигнатура отделяет DOS-часть от основного PE-заголовка. За ней находится IMAGE_FILE_HEADER, который сообщает архитектуру, число секций и размер Optional Header.
Optional Header содержит сведения, необходимые для представления образа в памяти: ImageBase, AddressOfEntryPoint, SectionAlignment, FileAlignment, SizeOfImage, SizeOfHeaders и параметры подсистемы. В его конце находятся Data Directories, через которые можно перейти к специализированным структурам.
После Optional Header находятся IMAGE_SECTION_HEADER. Они связывают виртуальное представление образа с его файловым представлением. Именно здесь появляются VirtualAddress, VirtualSize, PointerToRawData и SizeOfRawData, необходимые для преобразования адресов и поиска содержимого на диске.
Поэтому отдельное поле редко имеет полный смысл само по себе. AddressOfEntryPoint становится практически полезным только вместе с ImageBase и таблицей секций; NumberOfSections — вместе с расположением Section Headers; Data Directory — вместе с NumberOfRvaAndSizes и соответствующей секцией; SizeOfImage — вместе с SectionAlignment и описанием секций.
Что в первую очередь искать в PE-заголовке
Если задача состоит в быстрой первичной оценке бинарника, достаточно последовательно пройти несколько ключевых точек: MZ и e_lfanew, затем PE\0\0, Machine и NumberOfSections, после чего проверить Magic и прочитать AddressOfEntryPoint, ImageBase, размеры и параметры выравнивания.
Следующий уровень — таблица секций и Data Directories. Они позволяют перейти от общей информации к конкретным областям файла: определить, где находятся данные секций, найти импорты и экспорты, проверить наличие ресурсов и релокаций, а также сопоставить виртуальные адреса с физическими смещениями внутри файла.
Главное при таком анализе — не воспринимать «заголовок PE-файла» как один набор полей. Это последовательность взаимосвязанных структур: IMAGE_DOS_HEADER → PE Signature → IMAGE_FILE_HEADER → IMAGE_OPTIONAL_HEADER с Data Directories → IMAGE_SECTION_HEADER. DOS Stub находится между DOS Header и PE Signature и относится к DOS-части образа, а данные секций располагаются уже после заголовков.
Такое представление позволяет читать PE-файл не как набор случайных шестнадцатеричных значений, а как связанную систему описаний. При этом RVA, виртуальный адрес и file offset нужно постоянно различать: именно их смешение чаще всего приводит к неверному поиску точки входа, импортов или другого содержимого в файле.
Частые вопросы о PE-заголовке
Можно ли считать IMAGE_NT_HEADERS всем PE-заголовком?
В контексте Windows API IMAGE_NT_HEADERS объединяет PE Signature, IMAGE_FILE_HEADER и IMAGE_OPTIONAL_HEADER. Но в более широком практическом употреблении PE-заголовок может включать также DOS Header, DOS Stub и связанные с ним начальные данные, а при рассмотрении структуры файла — и следующую таблицу секций. Поэтому при чтении документации важно смотреть, в каком именно смысле употребляется термин.
Почему Optional Header называется optional?
Потому что формат COFF допускает объектные файлы без этой структуры. Для исполняемого PE-образа Optional Header обязателен: именно в нём находятся сведения, необходимые для загрузки и представления образа.
Где находится точка входа в файле?
В поле AddressOfEntryPoint находится RVA, а не file offset. Чтобы получить положение соответствующего байта на диске, нужно определить секцию, содержащую этот RVA, и сопоставить его с PointerToRawData и VirtualAddress этой секции.
Все ли Data Directories присутствуют в каждом PE-файле?
Нет. Количество доступных записей определяется NumberOfRvaAndSizes, а конкретный каталог может отсутствовать или иметь нулевой размер. Наличие самого поля в определении структуры не означает наличие соответствующего объекта в каждом образе.
Почему размер PE-заголовка нельзя просто считать по фиксированному числу байт?
Потому что размер Optional Header определяется SizeOfOptionalHeader, а его содержимое зависит в том числе от варианта PE32 или PE32+. Количество доступных Data Directories дополнительно определяется NumberOfRvaAndSizes. Для корректного парсинга необходимо проверять эти значения и границы файла, а не полагаться только на предполагаемую стандартную длину.
