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

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

@webware

t.me/webware

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


Portable EXEcutable

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


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

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

  1. Заголовки
  2. Секции

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


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

Давайте изучим по-порядку какие есть заголовки и что в них указано. Заголовками PE-файла являются следующие заголовки в указанном порядке:

  1. DOS заголовок
  2. Заглушка DOS
  3. PE заголовок
  4. Таблица секций

Начнём наше приключение с изучения DOS заголовка.


DOS-заголвок​

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


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


Вот его структура на языке 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-заголовка. Здесь мы можем увидеть байты в шестнадцатеричном представлении.


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


DOS заглушка​

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

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


Вот его структура на языке 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-заголовка в байтовом представлении:

Теперь давайте разберём каждое поле по-порядку.

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


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


На 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-битной.

Перейдём к следующему подзаголовку PE-заголовка.

  • OptionalHeader

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


Структура дополнительного заголовка представлена следующий 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++:
typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD VirtualAddress;
  DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

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

#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.

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).

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


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


Также, небольшая шпаргалка, для того, чтобы понимать какое назначение носит имя определенного заголовка секции в таблице:

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


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

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

Источник codeby.net

Report Page