PE-формат. Часть 1 — Базовая информация

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

PE-формат (Portable Executable) — это формат всех 32- и 64-разрядных исполняемых файлов в ОС Windows. Такой формат имеют файлы exe, dll, ocx, sys и т.д. (Разумеется, exe под DOS не в счет). В этой статье я расскажу самую базовую информацию об устройстве этого формата и его структурах. Практически самое полное и доступно изложенное описание можно найти в статье Криса Касперски, и она является обязательной к прочтению, если вы действительно решили во всем этом разобраться. Помните — никто не говорил, что будет легко, но у вас появился отличный шанс показать, что вы настоящие мужики с железными волосатыми яйцами, разобравшись во всем этом.

Каждый исполняемый файл формата PE состоит из множества взаимосвязанных структур, содержащих информацию о самом файле, об импортируемых и экспортируемых им функциях, о перемещаемых элементах, ресурсах, Thread Local Storage и многое другое. Начнем по порядку. На рисунке ниже приведена обобщенная структура файла PE-формата:

Каждый PE-файл состоит из вышеперечисленных элементов, они являются обязательными. С самого верху находится MS-DOS-заголовок — наследие еще с тех самых времен, когда происходил переход с DOS на Windows, поддерживающий новый PE-формат. Наверняка вы замечали, что все исполняемые файлы начинаются с букв «MZ» — это сигнатура как раз характерна для структуры, названной IMAGE_DOS_HEADER и располагающейся в самом начале PE-файла. Поля этой структуры по большей части нам неинтересны, так как необходимы для запуска из-под DOS. Следует обратить внимание на следующие поля: e_magic — собственно, это поле размером два байта содержит сигнатуру ‘MZ’ (сокращение от имени Марк Збиновски, который являлся ведущим разработчиком MS-DOS и архитектором формата PE); e_lfanew — указатель на начало PE-заголовка (см. рисунок выше). Это поле должно указывать на первый байт PE-заголовка (IMAGE_NT_HEADERS), т.е. на сигнатуру «PE\0\0», причем значение этого поля должно быть выровнено по границе двойного слова. Крис Касперски в своей статье упоминает еще поле e_cparhdr, но, по всей видимости, оно по-прежнему никем не проверяется.

Весь смысл DOS-заголовка в том, чтобы передать управление на идущую далее DOS-заглушку, если вдруг кто-то запустить виндовый бинарник под досом. Обычно эта заглушка (по сути — обычная DOS-программа) выдает текст «This program cannot be run in DOS mode.», но ничто не мешает запихать туда и досовую версию программы 🙂

Далее идет уже упомянутая сигнатура PE-файла (4 байта: ‘P’, ‘E’, 0, 0), после которой начинается структура IMAGE_FILE_HEADER. Эта структура подробно описана в MSDN или в статье Криса, тем не менее, я заострю внимание на некоторых ее полях:
Machine — архитектура, на которой может запускаться файл;
NumberOfSections — количество секций в PE-файле. Допустимое значение — от 1 до 0х60. Секция — это некая область памяти, обладающая определенными характеристиками и выделяемая системой при загрузке исполняемого файла;
SizeOfOptionalHeader — размер идущего за этой структурой опционального заголовка в байтах;
Characteristics — поле флагов характеристик PE-файла. Тут содержится информация о том, имеет ли файл экспортируемые функции, перемещаемые элементы, отладочную информацию и т.д.
Остальные поля при загрузке ни на что не влияют.

Далее идет опциональный заголовок PE-файла. На самом деле, никакой он не опциональный, без него файл загружен не будет, хотя размер этого заголовка может и варьироваться. И вновь я приведу описание самых важных полей:
Magic — для 32-разрядных PE-файлов это поле должно содержать значение 0х10B, а для 64-разрядных — 0х20B. Дальше я расскажу, в чем отличие 32- и 64-разрядных версий.
AddressOfEntryPoint — адрес точки входа относительно базового адреса загрузки файла (ImageBase). О способах адресации, используемых в PE-файлах, я расскажу дальше.
ImageBase — базовый адрес загрузки PE-файла. В памяти по этому адресу после загрузки будет располагаться вышеописанная структура IMAGE_DOS_HEADER. Если у файла имеется таблица перемещаемых элементов (о ней тоже далее), то этот адрес может варьироваться, а ImageBase будет содержать лишь рекомендуемый адрес загрузки.
FileAlignment и SectionAlignment — файловое и виртуальное выравнивание секций. В обязательном порядке должны быть выполнены следующие условия:
1. SectionAlignment >= 0х1000;
2. FileAlignment >= 0х200;
3. SectionAlignment >= FileAlignment.
В Windows NT возможно создание невыровненных файлов, но в этом случае физические и виртуальные адреса каждой секции должны совпадать, и SectionAlignment должно быть равно FileAlignment.

SizeOfImage — это поле содержит размер в байтах загруженного образа PE-файла, который должен быть равен виртуальному адресу последней секции плюс ее виртуальный выровненный размер.
SizeOfHeaders — размер всех заголовков. Это поле говорит загрузчику, сколько байт считать от начала файла, чтобы получить всю необходимую информацию для загрузки файла. Значение поля не должно превышать относительного виртуального адреса первой секции.
CheckSum — контрольная сумма файла, которая проверяется загрузчиком только для самых важных системных файлов.
Subsystem — подсистема файла. Самые распространенные — IMAGE_SUBSYSTEM_WINDOWS_GUI(GUI-интерфейс Windows) и IMAGE_SUBSYSTEM_WINDOWS_CUI (консольный интерфейс). За остальными — в статью Криса или MSDN.
SizeOfStackReserve и SizeOfStackCommitSizeOfHeapReserve и SizeOfHeapCommit — размер соответственно стека и кучи, которые должны быть зарезервированы и выделены для PE-файла. 0 — значение по умолчанию. Если SizeOfStackCommit > SizeOfStackReserve или SizeOfHeapCommit > SizeOfHeapReserve, то файл загружен не будет.
NumberOfRvaAndSizes — количество элементов в таблице DATA_DIRECTORY, расположенной в самом конце опционального заголовка. Может варьироваться от 0 до 16, но все линковщики ставят значение 16, даже если не используют все элементы таблицы. Это связано с ошибками в системном загрузчике (как я понял, только в Win7 загрузчик наконец-то не содержит этих ошибок).
Остальные поля снова никому не сдались, в том числе и системному загрузчику.

Перед тем, как рассматривать таблицу DATA_DIRECTORY, я расскажу о способах адресации, которые наиболее часто используются в PE-файлах.
1. Виртуальная адресация, VA. Такие адреса отсчитываются от начала адресного пространства (т.е. от 0) и являются абсолютными.
2. Относительная виртуальная адресация (RVA). Эти адреса отсчитываются от базового адреса загрузки образа исполняемого файла, т.е. от того адреса, по которому был загружен исполняемый файл.
3. Сырые адреса, т.е. адреса непосредственно от начала файла формата PE на диске, а не в памяти.
Некоторые структуры используют и другие типы адресации.
RVA и VA легко преобразуются друг в друга: VA = RVA + базовый адрес загрузки.
Как я уже говорил выше, базовый адрес загрузки содержится в поле ImageBase опционального заголовка PE, но может варьироваться, если файл имеет таблицу перемещаемых элементов (relocations).

Теперь перейдем к разбору DATA_DIRECTORY. Каждый элемент в этой таблице (IMAGE_DATA_DIRECTORY), располагающейся в конце опционального заголовка, имеет собственное назначение. Лучше всего про это прочесть в MSDN или в статье Криса.
Каждый элемент таблицы представляет из себя структуру, содержащую два поля — виртуальный адрес тех данных, на которые указывает данный элемент, и их размер. Например, первый элемент таблицы (IMAGE_DIRECTORY_ENTRY_IMPORT) указывает на таблицу импортируемых функций из различных модулей (как правило, DLL-файлов).
Некоторые из подобных таблиц я разберу в следующих статьях, но, если не терпится узнать побольше прямо сейчас, читайте опять-таки статью Касперски, хотя и там не все эти таблицы описаны.

За DATA_DIRECTORY (т.е. после конца опционального заголовка) начинается таблица секций. Между концом опционального заголовка и таблицей секций могут присутствовать неиспользуемые байты. Существует макрос, позволяющий найти начало таблицы секций, называется он IMAGE_FIRST_SECTION и определен в WinNT.h. Каждая секция описывается структурой IMAGE_SECTION_HEADER, и идут эти структуры друг за другом. Их количество содержится в поле NumberOfSection файлового заголовка.
Как обычно, опишу только реально используемые загрузчиком поля этой структуры.
Name — имя секции. Предоставляется только для удобства и может содержать что угодно. Единственное, что нужно знать — имя секции, содержащей ресурсы файла, должно всегда быть равно «.rsrc», иначе Проводник Windows не сможет отобразить информацию о версии файла и его иконку. Разумеется, это косяк разработчиков Windows.
VirtualAddress — VA секции в памяти, должен быть выровнен на величину Section Alignment.
PointerToRawData — указатель на данные в файле, которые будут использоваться для инициализации памяти секции, должен быть выровнен на величину File Alignment.
VirtualSize и SizeOfRawData — виртуальный и физический размер секции, соответственно. Значение VirtualSize может быть невыровненным, но при загрузке образа всегда автоматически выравнивается по границе SectionAlignment. Значение SizeOfRawData обязано быть выровненным на границу FileAlignment, иначе файл загружен не будет. Впрочем, SizeOfRawData для последней секции может быть и невыровненным. Если SizeOfRawData <= VirtualSize, то оставшиеся (VirtualSize — SizeOfRawData) байт будут заполнены нулями. В противном случае поведение загрузчика не определено, потому такие PE лучше не делать. Если VirtualSize = 0 и SizeOfRawData = 0, то файл не загрузится. Если VirtualSize = 0 и SizeOfRawData != 0, то файл загрузится, а в памяти под секцию будет выделено VirtualSize, выровненное на Section Alignment, байтов. Characteristics — поле характеристик секции. Реально тут используются только флаги IMAGE_SCN_MEM_EXECUTE, IMAGE_SCN_MEM_READ, IMAGE_SCN_MEM_WRITE, настраивающие атрибуты памяти секции (исполнение, чтение, запись соответственно), а также IMAGE_SCN_MEM_DISCARDABLE — секция может быть выгружена после использования (таким свойством обладает таблица перемещаемых элементов), IMAGE_SCN_MEM_SHARED — секция совместно используется несколькими PE-файлами.

Остальные поля, как всегда, загрузчиком игнорируются.

Осталось сказать про отличия PE32 и PE64. Единственное отличие в структурах этих форматов заключается в том, что все виртуальные адреса (VA) — это 64-разрядные числа (8-байтовые). Все относительные адреса (RVA) так и остались 32-разрядными.

Вот и все, что я хотел рассказать в первой статье про формат исполняемых файлов Portable Executable. Будут еще статьи, в которых я опишу специальные таблицы (экспорт, импорт, перемещаемые элементы и т.д.).

Оставьте первый комментарий

Оставить комментарий

Ваш электронный адрес не будет опубликован.


*