Доброго времени суток, форумчане. Сегодня, мы немного поговорим, рассмотрим и изучим под микроскопом структуру исполняемых файлов Windows (Portable EXEcutable, или просто PE), а в следующих статьях изучим технику модифицирования (инфицирования) PE-файлов, для того, чтобы исполнять свой собственный код после запуска чужого исполняемого файла (кстати, эта техника используется многими вирусами, для собственного "паразитического" распространения) и другие "хаки" с использованием знаний о структуре PE.
Давайте проведём аналогию между квартирой и PE-файлом. У каждой квартиры есть свой этаж, своя дверь, прихожая, гостинная, кладовка, свои комнаты, также у каждой квартиры есть своя схема планировки. Вся информация о квартире и сама эта квартира хранится в PE-файле. Взглянем на структуру исполняемого файла, а потом разберём основные части.
Как мы можем увидить, исполняемый файл состоит из двух основных частей:
Если формально и без аналогий, то заголовки - структуры содержащие необходимые данные для загрузки программы, а секции - блоки данных с различным содержимым произвольного размера. Этим содержимым может быть код, ресурсы, виртуальные адреса функций и т.д..
Давайте изучим по-порядку какие есть заголовки и что в них указано. Заголовками PE-файла являются следующие заголовки в указанном порядке:
Как я и сказал, заголовки хранят необходимую информацию для загрузки PE-файла. Поэтому данный заголовок является обязательным для загрузки PE-файла, хоть и не несёт в себе большой смысловой информации.
Заголовок состоит из полей, как список состоит из пунктов свойств. Каждый пункт хранит в себе какое-либо значение. Естественно, в файле всё это представленно в байтовом представлении. Не все поля нужны для загрузки (запуска) PE-файла. Поэтому комментировать и рассматривать мы будем только поля, необходимые для загрузки файла в память.
Вот его структура на языке C/C++.
Нас интересуют только первое (e_magic) и последнее поле (e_lfanew) этого заголовка. Они является самыми важными и влияют непосредственно на загрузку PE-файла.
Я выделил самым большим красный прямоугольником область DOS-заголовка. Здесь мы можем увидеть байты в шестнадцатеричном представлении.
Вы можете поинтересоваться, для чего же другие поля? Дело в том, что система Windows построена на базе старой системы MS DOS (была до Windows). И на самом деле, это заголовок для DOS программы. Так вот, эти поля хранятся на случай, если кто-то попытается запустить PE-файл в DOS . В наше время никто практически ей не пользуется, но они остаются, так как если поменяют формат PE-файла, то перестанут работать программы с нынешним стандартом.
На самом деле, это не заголовок, а небольшая DOS программа. Просто для удобства я отнёс DOS-заглушку к заголовкам. Перед DOS-заголовком и PE-заголовком хранится небольшая DOS-программа, которая запустится, если вы попытаетесь запустить исполняемый файл Windows в MS DOS. Эта программка именуется DOS заглушкой или DOS стабом. По умолчанию, программа выведет "This program cannot be run in DOS" и завершит свою работу. Эта часть PE-файла не является обязательной.
Дальше у нас по списку PE-заголовок, который на самом деле, состоит из трёх частей: сигнатуры, файлового подзаголовка и дополнительного подзаголовка.
Вот его структура на языке C/C++:
Адрес начала PE-заголовка хранится в поле e_lfanew DOS-заголовка. Помните 08 01 00 00? Перевернём и получим адрес PE-заголовка. Это 00 00 01 08 или просто, 0x108. Перейдя по этому адресу в HEX-редакторе, мы встанем прямо перед началом PE-заголовка.
А вот структура PE-заголовка в байтовом представлении:
Теперь давайте разберём каждое поле по-порядку.
Таблица секций
Перейдём к последнему заголовку. Этот заголовок - таблица, которая содержит различную информацию о секциях. Мы уже знаем, что их количество определено в файловом заголовке в поле NumberOfSections. Проще говоря, это массив с NumberOfSections элементов. Этот массив содержит элементы типа IMAGE_SECTION_HEADER.
Давайте подробно рассмотрим основные поля.
По сути, в таблице секций просто зафиксирована информация о секциях.
Также, небольшая шпаргалка, для того, чтобы понимать какое назначение носит имя определенного заголовка секции в таблице:
Также, секциями являются и различные каталоги.
Portable EXEcutable
Portable Executabe файл (PE-файл) - это отдельный исполняемый модуль с расширением .exe (или .dll), получаемый в процессе сборки (компиляции и линковкии). В него включены код, ресурсы (иконки и другие данные), библиотеки, данные программы и т.д..
Давайте проведём аналогию между квартирой и PE-файлом. У каждой квартиры есть свой этаж, своя дверь, прихожая, гостинная, кладовка, свои комнаты, также у каждой квартиры есть своя схема планировки. Вся информация о квартире и сама эта квартира хранится в PE-файле. Взглянем на структуру исполняемого файла, а потом разберём основные части.
Как мы можем увидить, исполняемый файл состоит из двух основных частей:
- Заголовки
- Секции
Если формально и без аналогий, то заголовки - структуры содержащие необходимые данные для загрузки программы, а секции - блоки данных с различным содержимым произвольного размера. Этим содержимым может быть код, ресурсы, виртуальные адреса функций и т.д..
Давайте изучим по-порядку какие есть заголовки и что в них указано. Заголовками PE-файла являются следующие заголовки в указанном порядке:
- DOS заголовок
- Заглушка DOS
- PE заголовок
- Таблица секций
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-заголовка. Здесь мы можем увидеть байты в шестнадцатеричном представлении.
Вы можете поинтересоваться, для чего же другие поля? Дело в том, что система 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-заголовок
Дальше у нас по списку 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-заголовка в байтовом представлении:
Теперь давайте разберём каждое поле по-порядку.
- 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-битной.
Перейдём к следующему подзаголовку PE-заголовка.
- Machine
- 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
Одними из важнейших каталогов являются таблица импорта и таблица экспорта. Мы рассмотрим их в следующей статье.
- Magic
Таблица секций
Перейдём к последнему заголовку. Этот заголовок - таблица, которая содержит различную информацию о секциях. Мы уже знаем, что их количество определено в файловом заголовке в поле 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).
По сути, в таблице секций просто зафиксирована информация о секциях.
Секции
Вот и всё, мы закончили изучать заголовки. Теперь мы приступаем к изучению секций. По сути, секции являются простыми последовательными блоками данных. Они следуют друг за другом и у них нет определенного формата, так как их характеристики описаны в таблице секций. А вот формат данных, в этих секциях, зависят от типа информации, которая в них хранится. Секции, как я уже сказал, можно представить в виде комнат. Также, их можно представить и как в виде коробок с информацией. Размер каждой секции зафиксирован в таблице секций, поэтому секции должны быть определённого размера, а для этого их дополняют NULL-байтами (00). Вот и всё, что касается секций.
Также, небольшая шпаргалка, для того, чтобы понимать какое назначение носит имя определенного заголовка секции в таблице:
- .text: Код
- .data: Инициализированные данные
- .bss: Неинициализированные данные
- .rdata: Константные (рид-онли) данные
- .edata: Дескрипторы экспорта
- .idata: Дескрипторы импорта
- .reloc: Таблица релокации
- .rsrc: Ресурсы
- .tls: __declspec(thread) данные
Также, секциями являются и различные каталоги.
На этом всё. Спасибо за внимание. Если у Вас есть какие-либо вопросы или вы обнаружите неточности в статье, прошу отписаться в комментариях. Буду рад ответить на все ваши вопросы.
Следующая статья
Следующая статья
Последнее редактирование: