Доброго времени суток, читатели. В прошлой статье, мы поверхностно изучили строение и формат PE-файла. В этой статье, мы изучим виртуальную память, познакомимся с процессом загрузки PE-файла в память, изучим его структуру в памяти, а также изучим процесс импорта. Если вы не прочитали предыдующую статью, то обязательно прочитайте.
Виртуальная память
У каждого исполняемого файла, загруженного в память есть своё адресное пространство. Такая память называется виртуальной. Виртуальная память - это максимально доступное адресное пространство для процесса. Размер виртуальной памяти зависит от ОС и от архитектуры. Для 32-битной программы Windows - это 4 GB. Эта память делится на 2 части. Первая используется программой, а вторая системой.
А после этого происходит преобразование (транслирование) адресов физической памяти (ОЗУ), к адресам в виртуальной памяти. Этот процесс изображён ниже.
Выше, я умолчал об одной важной детали. О том, что виртуальная память может состоять не только из страниц ОЗУ, а может состоять (и состоит) из страниц более медленного хранилища, например жёсткого диска. В этом и кроется смысл виртуальной памяти - в объединении ОЗУ с жёстким диском, чтобы увеличить размер рабочего множества.
Другими словами, диск является дополнением для ОЗУ. Задачей виртуальной памяти является борьба с нехваткой памяти. Как я говорил выше, у каждого загруженного исполняемого файла, есть своя выделенная ОС, виртуальная память.
Структура процесса в виртуальной памяти
Образ исполняемого файла в памяти, в загруженном виде, называется виртуальным образом. У виртуального образа есть 2 основных свойства, характеризующее его:
- ImageBase - адрес базовой загрузки
- ImageSize - размер образа.
Так зачем же ImageBase процессу? Почему он не может быть, скажем, равен 0? Почему по умолчанию он равен 0x400000? Так же было бы гораздо проще!
На самом деле, ответ на этот вопрос очень прост. В промежутке адрессов от 0x00000000 до ImageBase находятся 2 области памяти, называемые кучей и стэком. Они используются программой в своих целях. Структура процесса в виртуальной памяти выглядит примерно так:
Также в виртуальной памяти хранятся загруженные Dynamic Linked Libraries (DLLs) и различные структуры, а именно, PEBs (Process Environment Blocks) и TEBs (Thread Environment Blocks). Эти структуры хранят различную информацию о процессе и потоках.
А теперь, небольшой читщит по аббревиатурам, связанным с виртуальной адресацией:
- VA (Virtual Address) - адрес ячейки в виртуальной памяти
- RVA (Relative Virtual Address) - относительный виртуальный адрес
RVA = VA - ImageBase
VA = RVA + ImageBase
Вот и всё, что я хотел рассказать о структуре процесса в памяти. Давайте теперь перейдём к загрузке исполняемого файла в эту самую виртуальную память.
Загрузка исполняемого файла
Условно процесс загрузки можно разделить на 5 этапов:
- Разбор заголовков
- Разбор таблицы секций
- Проецирование файла в память
- Разбор таблицы импорта
- Запуск и исполнение
Разбор заголовков
Конкретно этот этап можно разделить на 2 подэтапа:
- Разбор DOS заголовка
1.1. Сначала, происходит проверка сигнатуры e_magic. Она должна быть равна "MZ".
1.2. После этого, происходит считывание поля e_lfanew и переход к разбору PE-заголовка
- Разбор PE заголовка
2.1. Далее, проверяется сигнатура PE-заголовка (поле Signature). Сигнатура должна быть равна "PE\x00\x00".
2.2. Далее происходит разбор файлового подзаголовка. А именно считываются следующие поля: Machine (архитектура процессора), NumberOfSections (количество секций), SizeOfOptionalHeader (размер дополнительного подзаголовка), Characteristics (характеристика файла).
2.3. После файлого подзаголовка загрузчик начинает разбор дополнительного подзаголовка. А именно, следующие поля:- Magic (битность)
- AddressOfEntryPoint (Relative Virtual Address (RVA) точки входа в программу)
- ImageBase (предпочтительный адрес в виртуальной памяти, куда следует загружать виртуальный образ)
- SectionAlignment (RVA начала секций в памяти будет дополнено до этого этого значения)
- FileAlignment (смещение начала cекций в файле)
- MajorSubsystemVersion (2 старших байта необходимой версии Windows)
- MinorSubsystemVersion (2 младщих байта необходимой версии Windows)
- SizeOfImage (размер виртуального образа в памяти)
- SizeOfHeaders (размер заголовков образа в памяти)
- Subsystem (тип подсистемы)
- NumberOfRvaAndSizes (количество таблицы директорий)
- DataDirectory (таблица директорий)
Разбор таблицы секций
Для каждой секции из PE-файла считывается блок размером SizeOfRawData (секция) по смещению PointerToRawData, после этого этот блок будет загружен в виртуальную память по адресу ImageBase + VirtualAddress с различными характеристиками.
Проецирование
После разбора всех заголовков, происходит непосредственно загрузка заголовков и секций.
Сначала, по адресу базовой загрузки (ImageBase) происходит проецирование всех заголовков PE-файла.
После заголовков, начинается проецирование секций. Каждая секция будет загружена по адресу ImageBase + VirtualAddress, дополнена нулями до значения VirtualSize, кстати, это называется выравниванием, и для неё будут установлены определённые характеристики. Выравнивавание нужно, в первую очередь, чтобы поддержать структуру организации секции.
Разбор таблицы импорта
Вот мы и подошли вплотную к изучению импорта. Импорт - важнейший элемент любого исполняемого файла. Импорт позволяет использовать функции из других модулей нашей программой. Таблица импорта является каталогом и секцией по совместительству (обычно носит имя .idata). Эта таблица соотносит вызовы функций из DLL с их адресами. Формат таблицы импорта зависит от режима импорта функций.
Существует 3 различных режима импорта функций, но мы внимательно изучим только первый:
- Standard import
Этот режим самый медленный, но и самый распространённый. В данном случае, информация о таблице секций заносится в элемент с индексом 1 (IMAGE_DIRECTORY_ENTRY_IMPORT) массива DataDirectory. Таблица импорта в этом случае является массивом элементов типа IMAGE_IMPORT_DESCRIPTOR, причём последний элемент должен быть нулевой. Структура IMAGE_IMPORT_DESCRIPTOR на языке C/C++ показана ниже:
C:typedef struct _IMAGE_IMPORT_DESCRIPTOR { DWORD OriginalFirstThunk; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
Рассмотрим основные поля:- OriginalFirstThunk
Это четырёхбайтовое поле содержит RVA до INT (Import Name Table) или ILT (Import Lookup Table). Этот массив содержит RVA на структуры Hint (2 байта, специальный индекс функции в DLL), Name(имя импортируемой функии). Заканчивается нулевым элементом. - TimeDateStamp
Данное поле, размером в 4 байта, содержит дату и время. - Name
Четырёхбайтовое поле содержащее RVA до строки с именем библиотеки. - FirstThunk
Это четырёхбайтовое поле содержит RVA до IAT (Import Address Table). При загрузке исполняемого файла, загрузчик загружает необходимые DLL и записывает в IAT адрес импортируемой функции.
- OriginalFirstThunk
- Bound Import
В этом случае, в виртуальную память проецируются библиотеки, из которых нужно импортировать функции, а адреса на эти функции уже вшиты в таблице импорта. Это быстрый механизм, но требует константность DLL. Для того, чтобы указать загрузчику, чтобы он использовал этот механизм, в TimeDateStamp и ForwardChain заносится значение -1, а информация о связывании находится в элементе с индексом 11 (IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT) DataDirectory.
- Delay Import
В этом случае, адрес требуемой функции получается из DLL только по мере необходимости. Указатель находится в элементе c индексом 15 DataDirectory.
Запуск программы
После разбора всех заголовков, разбора таблицы секций, проецирования файла в память и импорта функций начинается исполнение программы по адресу ImageBase + AddressOfEntryPoint. Обычно, AddressOfEntryPoint указывает на секцию кода (.text), которая содержит машинные инструкции, но это всего лишь соглашение и оно может быть несоблюдено.
Ну, на этом всё. Если есть какие-либо вопросы или поправки, буду рад если вы напишите комментарий. В следующей статье мы "вручную" создадим свой EXE-файл с использованием лишь одного Hex-редактора. После окончательного изучения PE мы начнём изучать его с точки зрения информационной безопасности.
Следующая статья
Последнее редактирование: