Pe файлы что это

Обновлено: 02.07.2024

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

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


Рис. 2.3. PE-файл на диске и в оперативной памяти

В секциях размещаются код и данные исполняемого файла, а также служебная информация, необходимая загрузчику (например, секция ". reloc " на схеме содержит таблицу релокаций). Секции в оперативной памяти должны быть выровнены по границам страниц, поэтому загрузчик отображает каждую секцию, начиная с новой страницы адресного пространства процесса. Это приводит к тому, что в памяти секции, как правило, располагаются менее компактно, чем в файле (и это отражено на схеме).

Так как расположение элементов PE-файла в памяти и на диске отличаются, для их локализации приходится вводить два понятия: относительный виртуальный адрес элемента в памяти (Relative Virtual Address - RVA ) и смещение элемента в файле (file offset).

RVA некоторого элемента PE-файла - это разность виртуального адреса данного элемента и базового адреса, по которому PE-файл загружен в память. Например, если файл загружен по адресу 0x400000, и некоторый элемент в нем располагается по адресу 0x402000, то RVA этого элемента равен (0x402000 - 0x400000) = 0x2000.

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

Загрузчик формирует образ PE-файла в памяти таким образом, что соблюдается следующее правило: пусть ox и oy - смещения каких-то элементов в файле, а rx и ry - RVA этих элементов, тогда если ox < oy , то rx < ry .

Секции

Секция в PE-файле представляет либо код, либо некоторые данные (глобальные переменные, таблицы импорта и экспорта, ресурсы, таблица релокаций). Каждая секция имеет набор атрибутов, задающий ее свойства. Атрибуты секции определяют, доступна ли секция для чтения и записи, содержит ли она исполняемый код, должна ли она оставаться в памяти после загрузки исполняемого файла, могут ли различные процессы использовать один экземпляр этой секции и т.д.

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

Каждая секция имеет имя. Оно не используется загрузчиком и предназначено главным образом для удобства человека. Разные компиляторы и компоновщики дают секциям различные имена. Например, компоновщик от Microsoft размещает код в секции ".text", константы - в секции ".rdata", таблицы импорта и экспорта - в секциях ".idata" и ".edata", таблицу релокаций - в секции ". reloc ", ресурсы - в секции ".rsrc". В то же время компоновщик фирмы Borland использует имена "CODE" для секций, содержащих код, и "DATA" для секций с данными.

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

Выбор базового адреса образа PE-файла в памяти

Давайте обсудим, каким образом загрузчик определяет базовый адрес, по которому нужно загрузить PE-файл. Для exe-файлов это тривиальная задача: в заголовках файла присутствует поле ImageBase , содержащее значение базового адреса. Так как для выполнения exe-файла создается отдельный процесс со своим виртуальным адресным пространством, то обычно не возникает никаких проблем с тем чтобы отобразить файл в это адресное пространство по заданному адресу. Как правило, все exe-файлы содержат в поле ImageBase значение 0x400000.

Импорт функций

В PE-файле существует специальная секция ".idata", описывающая функции, который этот файл импортирует из динамических библиотек. Описание импортируемых функций в секции ".idata" приводит к тому, что библиотеки загружаются загрузчиком операционной системы еще до запуска программы. В принципе, необязательно описывать каждую импортируемую функцию в этой секции, так как динамические библиотеки можно загружать с помощью функции LoadLibrary из Win32 API прямо во время выполнения программы.

В процессе загрузки программы осуществляется связывание (binding) функций, импортируемых из динамических библиотек. Связывание подразумевает загрузку соответствующих динамических библиотек и составление таблицы адресов импорта (Import Address Table - IAT). Адрес каждой импортируемой функции заносится в эту таблицу и в дальнейшем используется для вызова данной функции.

Экспорт функций

Информация об экспортируемых функциях хранится внутри PE-файла в специальной секции ".edata". При этом каждой функции присваивается уникальный номер, и с этим номером связывается RVA тела функции, и, возможно, имя функции. Не всякая экспортируемая функция имеет имя, так как имена служат, главным образом, для удобства программистов.

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

Несмотря на то, что исполняемые файлы Windows устроены достаточно просто, мне не удалось найти сколь-нибудь полного их описания. И официальные документы Microsoft, и многочисленные статьи, посвященные этой теме, грешат, во-первых, неполнотой и многочисленными неясностями, во-вторых, трудно обнаруживаемыми ошибками или неточностью описаний.

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

Несмотря на то, что изложенная здесь информация прошла проверку временем (она используется во многих написанных мной утилитах, начиная с 1999 г.), гарантировать отсутствие ошибок я не берусь. Дело в том, что очень трудно различить, какая информация, содержащаяся в PE-файлах, является существенно важной для процесса загрузки и исполнения программ, а какая не используется вообще. По мере возможности я расставлял пометы "несущественно" или "загрузчиком не используется", но какие-то из этих помет могут оказаться неверными. Буду рад за указания на замеченные неточности или ошибки.

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

1.1. Стандарты COFF и PE

Во всех 32-разрядных ветках ОС Windows объектные (.OBJ), библиотечные (.LIB) и исполняемые (.EXE и .DLL) файлы хранятся в едином формате COFF (Common Object File Format), который используется некоторыми системами семейства Unix и ОС VMS.

На сегодняшний день существует два формата PE-файлов: PE32 и PE32+. Оба они ограничивают адресное пространство программы размеров в 4 Гб (0xFFFFFFFF), но PE32 использует 32-битовые адреса (архитектура Win32), а PE32+ – 64-битовые адреса (архитектура Win64).

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

1.2. Общая структура файла

Любой PE-файл состоит из нескольких заголовков и нескольких (от 1 до 96) секций. Заголовки содержат служебную информацию, описывающую различные свойства исполняемого файла и его структуру. Секции содержат данные, которые размещаются в адресном пространстве процесса во время загрузки исполняемого файла в память.

PE-файлы являются файлами с относительной загрузкой, т. е. теоретически могут размещаться в пространстве адресов 0x00000000 - 0xFFFFFFFF с любого адреса, называемого базовым адресом. Поскольку базовый адрес заранее неизвестен, структура PE-файлов основана на понятия RVA (relative virtual address, относительный виртуальный адрес). RVA представляет собой смещение от базового адреса исполняемого файла до данного адреса. Иными словами, для получения линейного адреса в виртуальной памяти процесса нужно сложить RVA с базовым адресом.

Следует особо подчеркуть, что RVA не имеют ничего общего общего со смещениями относительно начала файла. В процессе загрузки файла каждая его секция размещается в памяти со своего RVA и при необходимости дополняется нулями до заданного размера. При этом RVA секции и ее размер в памяти, вообще говоря, никак не связаны с ее местоположением и размером в исходном файле.

Общая структура PE-файла такова:

Подробно каждая из этих структур описана в следующих разделах.

1.3. Проецирование файла в память

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

1.4. Общее описание загрузки PE-файла

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

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

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

Данная самораспространяющаяся программа не содержит в себе никакого вредоносного кода, но ее с легкостью можно дописать до вполне боевого вируса. Поэтому хочу заметить, что
ВСЕ ПРИВЕДЕННОЕ В ЭТОЙ СТАТЬЕ МОЖЕТ БЫТЬ ИСПОЛЬЗОВАНО ТОЛЬКО В УЧЕБНО-ПОЗНАВАТЕЛЬНЫХ ЦЕЛЯХ. Автор не несет никакой ответственности за любой ущерб, нанесенный применением полученных знаний.

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

Кратко о формате PE

Поскольку наш вирус будет заражать именно PE-файлы, мы, естественно, должны иметь представление об этом формате. Это формат исполняемых в операционной системе Windows файлов. Кстати, раз уж мы заговорили об ОС, то заметим, что наш вирус должен нормально исполняться на как можно большем числе ОС данного семейства. Программа тестировалась на работоспособность в Win
9x\Me\NT\2000\XP\2K3.

Итак, если заглянуть внутрь типичного исполняемого файла, мы увидим следующую структуру в упрощенном виде:


PE-файл в самом своем начале (MZ-заголовок) содержит программу для ОС DOS. Эта программа называется stub и нужна для совместимости со старыми ОС. Если мы запускаем PE-файл под ОС DOS или OS/2, она выводит на экран консоли текстовую строку, которая информирует пользователя, что данная программа не совместима с данной версией ОС. Программист при линковке может указать любую программу DOS, любого размера. После этой DOS-программы идет структура, которая называется IMAGE_NT_HEADERS (PE-заголовок). Эта структура описывается следующим образом:

Первый элемент IMAGE_NT_HEADERS – сигнатура PE-файла. Для PE-файлов она должна иметь значение "PE\0\0". Далее идет структура, которая называется файловым заголовком и определенная как IMAGE_FILE_HEADER. Файловый заголовок содержит наиболее общие свойства для данного PE-файла. После файлового заголовка идет опциональный заголовок - IMAGE_OPTIONAL_HEADER32. Он содержит специфические параметры данного PE-файла. После опционального заголовка начинается таблица секций (Object Table). В ней содержится информация о каждой секции. После таблицы секций идут исходные данные для секций. В конец PE-файла можно записать любую информацию и от этого функционирование программы не изменится (если там не присутствует проверка контрольной суммы или что-то подобное).

Способы заражения PE

До сих пор мы ничего не сказали о том, каким способом будем заражать файл, ведь их несколько:

  • Внедрение в PE-заголовок
  • Расширение последней секции
  • Добавление новой секции

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

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


Вычисляем дельта-смещение

Итак, мы приняли решение дописываться в конец исполняемого файла. Прежде, чем приводить код, отмечу, что для начала нам нужно найти адреса необходимых API-функций. Разберемся сейчас с наиболее важными понятиями: Virtual Address (VA) и Relative Virtual Address
(RVA).

VA - это адрес чего-нибудь в оперативной памяти. RVA - это смещение на что-то относительно того места, куда проецирован файл. А если просто сказать, то VA = RVA + база.

Чтобы наш вирус работал, он должен быть написан в базонезависимом коде. В связи с этим появляется еще одно понятие - дельта-смещение.

Что это такое? Все очень просто. Когда вирус находится в чистом виде (так называемое первое поколение), т.е. не записан еще ни в какой файл, когда он работает, он обращается к переменным как есть относительно прописанного в его заголовке адреса, куда файл проецирован системой. Теперь представим, что наш вирус заразил программу. И там начинает работать. Но теперь он работает не там, куда его загрузил загрузчик, а из того места, где находится загруженная зараженная программа. Получается, что переменные теперь указывают на абсолютно другое место. Поэтому обратившись к своим данным по заданным адресам, вирус прочитает совсем не те данные, которые ему необходимы. Для того, чтобы решить эту проблему, вычисляется
дельта-смещение. Это смещение относительно начала вируса, а не той программы, которая была им заражена.

Как видим, при входе в вирусный код мы вызываем call. Но call после вызова помещает в стек адрес возврата. Вычитаем из него адрес метки VirDelta и получаем нужное нам смещение относительно начала файла. Далее сохраняем дельта-смещение для дальнейшего использования (прибавляя его к адресам переменных, последние принимают корректные значения).

Ищем адреса API-функций

Следующая проблема состоит в поиске этих самых адресов. Но для начала нужно найти адрес библиотеки kernel32.dll в памяти, т.к. самые необходимые функции находятся именно в ней. Если нам потребуется использовать функции из других библиотек, мы просто используем LoadLibrary и GetProcAddress, которые находятся в kernel32.dll.

Существует множество методов поиска базы kernel32, один из которых - использование механизма структурной обработки исключений SEH (Structured Exception
Handling).

SEH представляет собой цепочку обработчиков - ячейки памяти, в которых содержатся адреса на процедуры обработки исключений. Эта цепочка начинается с fs:0000 и заканчивается последним обработчиком, который содержит значение 0FFFFFFFFh. Ну и что это нам дает? А то, что адрес последнего обработчика - это и есть адрес kernel32.dll в памяти.

Итак, дельта-смещение мы определили. Приведем теперь код поиска базы kernel32:

Здесь мы сканируем память (с того адреса, который мы только что получили) на наличие сигнатуры MZ (4D5Ah). Если она присутствует, значит, все сделано верно. Далее по смещению 3Ch находится смещение начала PE-заголовка. Сравниваем значение 2х байтов по этому смещению на сигнатуру PE (5045h) (на случай, если мы чисто случайно попали на ту область памяти, где нам встретились символы MZ). Если значение этих байт равно PE, то kernel32.dll несомненно найдена.

Теперь рассмотрим некоторые поля PE-заголовка, необходимые нам:


Чтобы найти адрес необходимой нам API-функции в kernel32, нам нужно добраться до секции экспорта этой библиотеки. По смещению 78h от начала PE-заголовка находится RVA адрес этой секции. Но не забудем, что нам нужен не RVA, а VA. Для этого нужно сложить этот RVA со значением Image Base (адрес в области памяти, куда файл проецирован системой). Тогда мы получим реальный адрес секции экспорта.

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

Теперь нам нужно получить адрес таблицы экспорта из секции экспорта. Рассмотрим некоторые интересные нам поля секции экспорта:


Первое поле содержит базу ординалов функций. Второе поле содержит число указателей на имена. Третье поле содержит RVA таблицы экспорта. Эта таблица содержит адреса экспортируемых функций (их точки входа) или данных в формате DWORD RVA (по 4 байта на элемент). Четвертое поле - RVA таблицы указателей на имена. Последнее поле - RVA на таблицу ординалов. Для доступа к данным используется ординал функции с коррекцией на базу ординалов (Ordinal
Base).

Итак, теперь мы знаем адрес таблицы имен и адрес таблицы адресов всех функций библиотеки kernel32.dll. Чтобы найти адрес конкретной функции, мы должны сравнить ее имя с каждым именем в таблице имен экспортируемых функций, и если очередное сравниваемое имя совпало с искомым, мы смотрим в таблицу ординалов по соответствующему индексу и извлекаем таким образом адрес функции. Далее нам этот адрес остается где-то сохранить (в нашем случае – в стеке) для дальнейшего использования и перейти к поиску адреса другой нужной нам функции и так далее.

Чтобы не хранить в коде вируса имена функций (ведь они бывают иногда длинные), нам достаточно хранить 4-байтовые хеш-значения имен. Заодно и при просмотре тела вируса в HEX-редакторе не бросаются в глаза имена функций, содержащиеся в коде вируса:

А при поиске нужной нам функции мы будем сравнивать не имена, а хеш-значения имен (подсчитав предварительно это значение для каждой нужной нам функции). Т.е., допустим, что мы нашли какое-то имя в таблице имен kernel32. Вычисляем хеш-значение этого имени и сравниваем это значение с искомым из нашей таблицы хешей HashTable. Если совпадают – значит, нашли. Если нет – ищем
дальше:

Но как нам вычислить заранее хеш-значение определенного имени? Для этого я написал небольшую программку на Visual C++ с ассемблерной вставкой, ссылку на которую можно найти в конце статьи (с исходником).

После выполнения приведенного кода адреса всех функций будут находиться в стеке:

Общая структура вирусного кода

Все вышесказанное было лишь прелюдией в процессе написания нашего вируса. Теперь начнется самое интересное. Будем писать вирус на MASM. Почему я отдаю предпочтение этому пакету? Просто он мне нравится.

Напишем общий файл main.asm, который будет включать отдельные части кода:

В файле start_code.inc содержится весь приведенный ранее код по определению дельта-смещения, поиску базы kernel и адресов функций. Содержимое остальных файлов будет ясно из дальнейшего изложения. Но в любом случае в конце статьи есть ссылки с полностью рабочими примерами и исходниками.

Смотря на этот код, можно задать как минимум два вопроса:

  • зачем нам макрос szText?
  • зачем подключать библиотеку kernel32.lib и вызывать функцию ExitProcess перед начальной меткой?

Хитрый макрос позволяет нам не хранить текст в переменной, а сразу заталкивать его адрес в стек перед вызовом какой-либо функции, имеющей одним из своих параметров текстовую строку. Например, в функцию
LoadLibrary:

Что же касается вызова функции ExitProcess, то здесь проблема кроется в системах старше Windows XP (Win9x\Me\NT\2000). При попытке запустить код без такого вызова программа попросту не запускалась в перечисленых системах. Причем молча. Скорее всего, это связано с тем, что в данных системах загрузчик не хочет загружать программы без секций импорта. Не будем отвлекаться от нашей темы, поскольку исследование данного вопроса выходит за рамки этой статьи.


Данная статья является первым маленьким шажком на пути к написанию собственного несложного упаковщика. Знаний предстоит получить весьма большое количество, так что садитесь поудобнее, запасайтесь попкорном и готовьтесь к чтению. В статье большое количество отсылок к 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 и SizeOfStackCommit, SizeOfHeapReserve и 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-файлов).
Некоторые из подобных таблиц я разберу в следующих статьях, но, если не терпится узнать побольше прямо сейчас, читайте опять-таки статью Касперски, хотя и там не все эти таблицы описаны.

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

Еще небольшой вопрос. А размеры полей в PE-заголовке для 64-х битных программ отличаются? Или там просто флаг, дескать прога 64-х битная. а остальное - как обычно? Насколько я понимаю, там нет смысла удваивать размеры полей.

Я же в статье написал, в чем отличие. В PE64 все поля, которые содержат VA, имеют размер QWORD, т.е. 64 бита.

И действительно. Моя ошибка. За инфу спасибо.

Спасибо автору статьи. Давно хотел всё это вспомнить, повторить, а тут всё как на ладони))))

>Между концом опционального заголовка и таблицей секций могут присутствовать неиспользуемые байты.
У других авторов написано, что нет, она сразу следует за заголовком. На практике я не встречал, чтобы там был пробел.

Вот цитата из статьи Криса. По всей видимости, ничто мешает SizeOfOptionalHeader сделать больше, чем реальный размер OptionalHeader. Например, можно урезать NumberOfRvaAndSizes, не изменяя при этом SizeOfOptionalHeader, и получить некоторое количество неиспользуемых байтов. Сам не пробовал так делать (или пробовал, но уже не помню об этом), но не думаю, что это сделает бинарник неработоспособным.

Вот, сделал бинарник с урезанным NumberOfRvaAndSizes (разумеется, он запускается и работает). В нем, если в hex-редакторе глянуть, между опциональными заголовками и таблицей секций написано, что Вася Пупкин передает привет Москве :)

Да, вынужден согласиться. Оф-стандарт подтверждает Криса:
This table immediately follows the optional header, if any. This positioning is required because the file header does not contain a direct pointer to the section table. Instead, the location of the section table is determined by calculating the location of the first byte after the headers. Make sure to use the size of the optional header as specified in the file header.

В WinXP'овом загрузчике проблем с отсутствием неких неиспользуемых DataDirectories нет в целом, мой бинарник, который я ради примера сделал, запускается и на ней. Но там есть проблема, например, если будет отсутствовать столько DataDirectory'ий, что Debug Directory тоже не будет (не помню уже, до какого числа надо урезать NumberOfRvaAndSizes, чтобы такое получилось). В таком случае, даже если Debug-директория была пустой (заполненной нулями), в XP файл не загрузится. В Win Nt (и, кажется, при каких-то условиях и в XP) файл не загрузится, если у него нет таблицы импорта. Были и еще какие-то ошибки, не помню уже. В Vista какие-то из них поправили, в семерке вроде как совсем всё хорошо.

На мой взгляд самая адекватная статья в Руненте про формат PE. Мыщъх хоть и пишет много, пытается дать как можно больше инфы, но мягкоговоря описывает всё не совсем внятно. На статью наткнулся случайно, остановился почитать - не зря потратил время, сам хоть и кодер, но время потратил не зря. DX, ты молодец ;)

Статья хорошая, дебильную рожу в начале статьи можно убрать - все портит.

"Практически самое полное и доступно изложенное описание можно найти в статье Криса Касперски". - а название статьи не подскажете?


Kaimi :

Видимо речь о: "Путь воина – внедрение в pe/coff-файлы"

Большое спасибо) Я ее нашел, но есть и еще одна (кому интересно из тех, кто будет читать комменты: "Техника внедрения и удаления кода из PE-файлов"). Вот и гадал, какая из двух подразумевается. Не считая его книги "Компьютерные вирусы изнутри и снаружи".

Дебильную рожу в начале лучше оставить т.к. она "отфильтровывает" совсем дибилов которые напрягаются от ее вида. Ради Вашего же блага, ептель.

Читайте также: