Статья [0x01] Исследуем Portable Executable (EXE-файл) [Формат PE-файла]

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

Portable EXEcutable
Portable Executabe файл (PE-файл) - это отдельный исполняемый модуль с расширением .exe (или .dll), получаемый в процессе сборки (компиляции и линковкии). В него включены код, ресурсы (иконки и другие данные), библиотеки, данные программы и т.д..

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

PE101-v1RU.png

Как мы можем увидить, исполняемый файл состоит из двух основных частей:
  1. Заголовки
  2. Секции
Если вернуться к аналогии с квартирой, то заголовок - это информация о квартире. Планировка квартиры, количество комнат, этаж, где кладовка, где гостинная, где дверь, где прихожая, кто и когда сделал квартиру, квартира ли это вообще и т.д.. А секции - это комнаты. В секциях хранится код, различные данные, строки, функции и т.д.. Также как и в комнатах есть свои жильцы.

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

Секции.png

Давайте изучим по-порядку какие есть заголовки и что в них указано. Заголовками PE-файла являются следующие заголовки в указанном порядке:
  1. DOS заголовок
  2. Заглушка DOS
  3. PE заголовок
  4. Таблица секций
Начнём наше приключение с изучения DOS заголовка.

DOS-заголвок

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

Заголовок состоит из полей, как список состоит из пунктов свойств. Каждый пункт хранит в себе какое-либо значение. Естественно, в файле всё это представленно в байтовом представлении. Не все поля нужны для загрузки (запуска) PE-файла. Поэтому комментировать и рассматривать мы будем только поля, необходимые для загрузки файла в память.

Вот его структура на языке C/C++.
C:
typedef struct _IMAGE_DOS_HEADER {
    USHORT e_magic;
    USHORT e_cblp;
    USHORT e_cp;
    USHORT e_crlc;
    USHORT e_cparhdr;
    USHORT e_minalloc;
    USHORT e_maxalloc;
    USHORT e_ss;
    USHORT e_sp;
    USHORT e_csum;
    USHORT e_ip;
    USHORT e_cs;
    USHORT e_lfarlc;
    USHORT e_ovno;
    USHORT e_res[4];
    USHORT e_oemid;
    USHORT e_oeminfo;
    USHORT e_res2[10];
    LONG   e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Нас интересуют только первое (e_magic) и последнее поле (e_lfanew) этого заголовка. Они является самыми важными и влияют непосредственно на загрузку PE-файла.
  • e_magic
    Двухбайтовое поле e_magic хранит в себе специальную сигнатуру. Эта сигнатура нужна, чтобы указать что это действительно исполняемый файл. Вот она - "MZ". Каждый PE-файл обязан начинаться с неё. Если это не так, файл просто не запустится.

  • e_lfanew
    Четырёхбайтовое поле e_magic хранит в себе смещение до заголовка PE. То есть хранит, количество байтов, которое нужно отсчитать с начала файла, для того, чтобы попасть прямо к PE-заголовку, т.е. проще говоря, адрес PE-заголовка относительно начала файла. Только этот адрес хранится в обратном порядке. Например, на изображении ниже 08 01 00 00 - это 00 00 01 08 (0x108) наоборот. Почему наоборот? Не будем углубляться, но скажу, что компьютеру так легче работать с данными.

DOS_header.png

Я выделил самым большим красный прямоугольником область DOS-заголовка. Здесь мы можем увидеть байты в шестнадцатеричном представлении.

Вы можете поинтересоваться, для чего же другие поля? Дело в том, что система Windows построена на базе старой системы MS DOS (была до Windows). И на самом деле, это заголовок для DOS программы. Так вот, эти поля хранятся на случай, если кто-то попытается запустить PE-файл в DOS :D. В наше время никто практически ей не пользуется, но они остаются, так как если поменяют формат PE-файла, то перестанут работать программы с нынешним стандартом.

DOS заглушка

На самом деле, это не заголовок, а небольшая DOS программа. Просто для удобства я отнёс DOS-заглушку к заголовкам. Перед DOS-заголовком и PE-заголовком хранится небольшая DOS-программа, которая запустится, если вы попытаетесь запустить исполняемый файл Windows в MS DOS. Эта программка именуется DOS заглушкой или DOS стабом. По умолчанию, программа выведет "This program cannot be run in DOS" и завершит свою работу. Эта часть PE-файла не является обязательной.

DOS_stub.png

PE-заголовок

Дальше у нас по списку PE-заголовок, который на самом деле, состоит из трёх частей: сигнатуры, файлового подзаголовка и дополнительного подзаголовка.

Вот его структура на языке C/C++:
C:
typedef struct _IMAGE_NT_HEADERS {
  DWORD Signature; // Сигнатура
  IMAGE_FILE_HEADER FileHeader; // Файловый заголовка
  IMAGE_OPTIONAL_HEADER32 OptionalHeader; // Дополнительный
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

Адрес начала PE-заголовка хранится в поле e_lfanew DOS-заголовка. Помните 08 01 00 00? Перевернём и получим адрес PE-заголовка. Это 00 00 01 08 или просто, 0x108. Перейдя по этому адресу в HEX-редакторе, мы встанем прямо перед началом PE-заголовка.

Pe_begin.png

А вот структура PE-заголовка в байтовом представлении:

pe_header.png

Теперь давайте разберём каждое поле по-порядку.
  • Signature
    Это четырёхбайтовое поле содержит сигнатуру, а именно значение 50 45 00 00 (или "PE\x00\x00"). Эта сигнатура указывает на то, что перед нами действительно PE-файл (Ага, ещё одна проверка).

  • FileHeader
    Это обязательный подзаголовок PE-заголовка. Он хранит в себе базовые характеристики исполняемого файла.

    На C/C++ структура данного заголовка выглядит так:

    C:
    typedef struct _IMAGE_FILE_HEADER {
      WORD  Machine; // Архитектура процессора
      WORD  NumberOfSections; // Кол-во секций
      DWORD TimeDateStamp; // Дата и время создания программы
      DWORD PointerToSymbolTable; // Указатель на таблицу символов
      DWORD NumberOfSymbols; // Число символов в таблицу
      WORD  SizeOfOptionalHeader; // Размер дополнительного заголовка
      WORD  Characteristics; // Характеристика
    } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

    Разберём и этот подзаголовок по порядку.
    • Machine
      Данное двухбайтовое поле содержит информацию о характеристике процессора, на котором может быть выполнена данная программа. Вот 3 основных значения, которые может принять этот заголовок:
      1. IMAGE_FILE_MACHINE_I386 (0x014c) - означает, что программа может выполняться на x32
      2. IMAGE_FILE_MACHINE_IA64 (0x0200) - означает, что программа может выполняться на процессорах Intel Itanium (Intel x64).
      3. IMAGE_FILE_MACHINE_AMD64 (0x8664) - означает, что программа может выполняться на процессорах AMD64 (x64).

    • NumberOfSections
      Двухбайтовоеполе NumberOfSections содержит в себе число секций (комнат) в PE-файле.

    • SizeOfOptionalHeader
      Двухбайтовое поле содержащее размер дополнительного заголовка, который идёт сразу за файловым заголовком.

    • Characteristics
      Даное двухбайтовое поле содержит характеристики PE-файла. Например, является ли это exe-файлом, или dll. Также, тут описано, является ли данная программа x64-битной или x86-битной.

      FileHeader.png


      Перейдём к следующему подзаголовку PE-заголовка.
  • OptionalHeader
    Это ещё один обязательный подзаголовок PE-файла. В нём хранится необходимая информация для загрузки PE-файла. Он имеет всего два формата PE32+ (для 64-битных программ) и PE32 (для 32-битных).

    Структура дополнительного заголовка представлена следующий C/C++ кодом.

    C:
    typedef struct _IMAGE_OPTIONAL_HEADER {
      WORD                   Magic;
      BYTE                      MajorLinkerVersion;
      BYTE                      MinorLinkerVersion;
      DWORD                SizeOfCode;
      DWORD                SizeOfInitializedData;
      DWORD                SizeOfUninitializedData;
      DWORD                AddressOfEntryPoint;
      DWORD                BaseOfCode;
      DWORD                BaseOfData;
      DWORD                ImageBase;
      DWORD                SectionAlignment;
      DWORD                FileAlignment;
      WORD                   MajorOperatingSystemVersion;
      WORD                   MinorOperatingSystemVersion;
      WORD                   MajorImageVersion;
      WORD                   MinorImageVersion;
      WORD                   MajorSubsystemVersion;
      WORD                   MinorSubsystemVersion;
      DWORD                Win32VersionValue;
      DWORD                SizeOfImage;
      DWORD                SizeOfHeaders;
      DWORD                CheckSum;
      WORD                   Subsystem;
      WORD                   DllCharacteristics;
      DWORD                SizeOfStackReserve;
      DWORD                SizeOfStackCommit;
      DWORD                SizeOfHeapReserve;
      DWORD                SizeOfHeapCommit;
      DWORD                LoaderFlags;
      DWORD                NumberOfRvaAndSizes;
      IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
    } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

    И на этот раз рассмотрим только основные поля, необходимые для загрузки PE-файла в память.
    • Magic
      Это двухбайтовое поле отвечает за битность программы (x32/x64). Оно может принимать следующие значения:

      1. IMAGE_NT_OPTIONAL_HDR32_MAGIC (0x10b) - означает, что это x32 (x86) исполняемый образ.
      2. IMAGE_NT_OPTIONAL_HDR64_MAGIC (0x20b) - означает, что это x64 исполняемый образ.
      3. IMAGE_ROM_OPTIONAL_HDR_MAGIC (0x107) - означает, что это ROM образ.

    • AddressOfEntryPoint
      Четырёхбайтовое поле AddressOfEntryPoint содержит адрес начала кода, т.е. указатель на дверь в команту кода.
      Техника инфекции, которую мы изучим в будующем, основана на изменении этого значения.

    • ImageBase
      Это четырёхбайтовое поле содержит предпочтительный адрес загрузки программы в память. В следующей статье мы разберём для чего он нужен.

    • SectionAlignment
      Это четырёхбайтовое поле содержит относительный виртуальный адрес (относительно ImageBase, т. е. сколько байтов нужно отсчитать с адреса загрузки программы, чтобы попасть к началу секций) начала секций в виртуальной памяти.

    • FileAlignment
      Это четырёхбайтовое поле содержит смещение относительной файла (сколько байтов нужно отсчитать с начала файла) начала секций в исполняемом файле.

    • MajorSubsystemVersion и MinorSubsytemVersion
      В этих двухбайтовых полях содержится необходимая версия Windows.

    • SizeOfImage
      Это четырёхбайтовое поле содержит размер (в байтах) загруженного исполняемого файла в памяти.

    • SizeOfHeaders
      Четырёхбайтовое поле SizeOfHeaders содержит размер (в байтах) заголовков файла в памяти.

    • Subsystem
      Двухбайтовое поле содержащее тип подсистемы (GUI, CLI, Driver, ...).

    • NumberOfRvaAndSizes
      Данное четырёхбайтовое поле содержит число каталогов в массиве каталогов. По умолчанию равна 16.

    • DataDirectory
      Это поле - на самом деле массив, которая содержит информацию о каталогах. Их число определено в поле NumberOfRvaAndSizes (по умолчанию (и почти всегда) 16) дополнительного заголовка. Каждый элемент информации о каталоге хранит относительный виртуальный адрес (относительно ImageBase, т. е. сколько байтов нужно отсчитать с адреса загрузки программы, чтобы попасть к началу секций) и размер какого-либо каталога (которые являются и секциями), которая определяется по её позиции в массиве.

      Вот структура каталога на языке C/C++:
      C:
      typedef struct _IMAGE_DATA_DIRECTORY {
        DWORD VirtualAddress;
        DWORD Size;
      } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

      А вот идентификаторы (порядковый номер в DataDirectory):

      C:
      #define IMAGE_DIRECTORY_ENTRY_EXPORT              0
      #define IMAGE_DIRECTORY_ENTRY_IMPORT                      1
      #define IMAGE_DIRECTORY_ENTRY_RESOURCE            2
      #define IMAGE_DIRECTORY_ENTRY_EXCEPTION           3
      #define IMAGE_DIRECTORY_ENTRY_SECURITY            4
      #define IMAGE_DIRECTORY_ENTRY_BASERELOC           5
      #define IMAGE_DIRECTORY_ENTRY_DEBUG                       6
      //      IMAGE_DIRECTORY_ENTRY_COPYRIGHT               7
      #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE        7
      #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR           8
      #define IMAGE_DIRECTORY_ENTRY_TLS                     9
      #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG        10
      #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT      11
      #define IMAGE_DIRECTORY_ENTRY_IAT                    12
      #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT       13
      #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR     14

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

Таблица секций

Перейдём к последнему заголовку. Этот заголовок - таблица, которая содержит различную информацию о секциях. Мы уже знаем, что их количество определено в файловом заголовке в поле NumberOfSections. Проще говоря, это массив с NumberOfSections элементов. Этот массив содержит элементы типа IMAGE_SECTION_HEADER.

C:
typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Давайте подробно рассмотрим основные поля.
  • Name
    Это поле, размером в 8 байт, содержит имя секции, в ASCII кодировке.

  • VirtualSize
    Это четырёхбайтовое поле содержит размер (в байтах) секции (той самой комнаты) в виртуальной памяти.

  • VirtualAddress
    А это четырёхбайтовое поле уже содержит относительный адрес секции в виртуальной памяти.

  • SizeOfRawData
    Данное четырёхбайтовое поле содержит размер секции в файле.

  • PointerToRawData
    А указатель на эти самые данные, содержаться в этом четырёхбайтовом поле.

  • Characteristics
    Это четырёхбайтовое поле содержит атрибуты секции. Например, права чтения, записи и исполнения (Read Write Execute) (RWE).

    section_table.png

По сути, в таблице секций просто зафиксирована информация о секциях.

Секции
Вот и всё, мы закончили изучать заголовки. Теперь мы приступаем к изучению секций. По сути, секции являются простыми последовательными блоками данных. Они следуют друг за другом и у них нет определенного формата, так как их характеристики описаны в таблице секций. А вот формат данных, в этих секциях, зависят от типа информации, которая в них хранится. Секции, как я уже сказал, можно представить в виде комнат. Также, их можно представить и как в виде коробок с информацией. Размер каждой секции зафиксирован в таблице секций, поэтому секции должны быть определённого размера, а для этого их дополняют NULL-байтами (00). Вот и всё, что касается секций.

Также, небольшая шпаргалка, для того, чтобы понимать какое назначение носит имя определенного заголовка секции в таблице:
  • .text: Код
  • .data: Инициализированные данные
  • .bss: Неинициализированные данные
  • .rdata: Константные (рид-онли) данные
  • .edata: Дескрипторы экспорта
  • .idata: Дескрипторы импорта
  • .reloc: Таблица релокации
  • .rsrc: Ресурсы
  • .tls: __declspec(thread) данные

Также, секциями являются и различные каталоги.

На этом всё. Спасибо за внимание. Если у Вас есть какие-либо вопросы или вы обнаружите неточности в статье, прошу отписаться в комментариях. Буду рад ответить на все ваши вопросы.

Следующая статья
 
Последнее редактирование:

PingVinich

Технарь
Green Team
19.03.2017
138
513
BIT
0
Немного расскрою содержание следующей статьи: мы поговорим о виртуальной адресации, выравнивании памяти, таблицах импорта и экспорта, и напишем "Hello, world" без помощи компилятора и линкера, а с помощью лишь наших знаний и hex-редактора, чтобы вы поняли масштаб моего фанатизма поглубже изучить и закрепить полученный материал.

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

SearcherSlava

Red Team
10.06.2017
943
1 262
BIT
224
Немного расскрою содержание следующей статьи: мы поговорим о виртуальной адресации, выравнивании памяти, таблицах импорта и экспорта, и напишем "Hello, world" без помощи компилятора и линкера, а с помощью лишь наших знаний и hex-редактора, чтобы вы поняли масштаб моего фанатизма поглубже изучить и закрепить полученный материал.

В следующих статьях мы уже будем изучать этот вопрос со стороны информационной безопасности, именно поэтому я поместил эту статью в данный раздел.
@PingVinich, здрав будь! Что сказать, силён, бро! Исследуя труды Ange Albertini в твоей интерпретации, со временем можно от патча на 2 байта перейти к нахождению и извлечению золотого ключика и замене его на свой, минуя всякие там обфускации и деобфускации исключительно с т.з информационной безопасности. Браво! Бис!
 
  • Нравится
Реакции: PingVinich и kot-gor

SearcherSlava

Red Team
10.06.2017
943
1 262
BIT
224
PE101 от Альбертини на русском.








 
A

a11x

e_lfanew
Четырёхбайтовое поле e_magic хранит в себе смещение до заголовка PE.
Приветствую всех, нашёл опечатку: e_magic в абзаце замените на e_lfanew. Материал отличный. Автору благодарности.
 

Alex Trim

Green Team
12.07.2020
18
11
BIT
0
Отлично изложено. Спасибо ОГРОМНОЕ.

Кстати, буквы MZ (0x4D и 0x5A) в dos-заголовке - это инициалы создателя формата исполняемых файлов в MS-DOS Марка Збиковски.
 

Alex Trim

Green Team
12.07.2020
18
11
BIT
0
Опечатки в статье

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

Я выделил самым большим красный прямоугольником область DOS-заголовка.
Я выделил самым большим красным прямоугольником область DOS-заголовка.

Это поле - на самом деле массив, которая содержит информацию о каталогах.
Это поле - на самом деле массив, который содержит информацию о каталогах.

Каждый элемент информации о каталоге хранит относительный виртуальный адрес (относительно ImageBase, т. е. сколько байтов нужно отсчитать с адреса загрузки программы, чтобы попасть к началу секций) и размер какого-либо каталога (которые являются и секциями), которая определяется по её позиции в массиве.
Каждый элемент информации о каталоге хранит относительный виртуальный адрес (относительно ImageBase, т. е. сколько байтов нужно отсчитать с адреса загрузки программы, чтобы попасть к началу секций) и размер какого-либо каталога (которые являются и секциями), который определяется по его позиции в массиве.
 
  • Нравится
Реакции: Сергей Попов
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!