Статья [0x02] Исследуем Portable Executable [Загрузка PE-файла]


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

loading.png


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

У каждого исполняемого файла, загруженного в память есть своё адресное пространство. Такая память называется виртуальной. Виртуальная память - это максимально доступное адресное пространство для процесса. Размер виртуальной памяти зависит от ОС и от архитектуры. Для 32-битной программы Windows - это 4 GB. Эта память делится на 2 части. Первая используется программой, а вторая системой.

virtual_memory.png

Виртуальная память управляются устройством управления памяти (Memory Managment Unit) (MMU). MMU делит физическую память на "куски" памяти одинакового размера. Эти "куски" называются страницами. MMU является компонентном аппаратного обеспечения. Со стороны ОС виртуальная память управляется Virtual Memory Manager'ом (VMM).

А после этого происходит преобразование (транслирование) адресов физической памяти (ОЗУ), к адресам в виртуальной памяти. Этот процесс изображён ниже.

ozu_virutal_memory.png

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

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

virtual_memory_2.png

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

Структура процесса в виртуальной памяти

Образ исполняемого файла в памяти, в загруженном виде, называется виртуальным образом. У виртуального образа есть 2 основных свойства, характеризующее его:
  • ImageBase - адрес базовой загрузки
  • ImageSize - размер образа.
Эти поля описаны в заголовках PE-файла.

Так зачем же ImageBase процессу? Почему он не может быть, скажем, равен 0? Почему по умолчанию он равен 0x400000? Так же было бы гораздо проще!

На самом деле, ответ на этот вопрос очень прост. В промежутке адрессов от 0x00000000 до ImageBase находятся 2 области памяти, называемые кучей и стэком. Они используются программой в своих целях. Структура процесса в виртуальной памяти выглядит примерно так:

memory_map.png

Также в виртуальной памяти хранятся загруженные 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 этапов:
  1. Разбор заголовков
  2. Разбор таблицы секций
  3. Проецирование файла в память
  4. Разбор таблицы импорта
  5. Запуск и исполнение
Давайте последовательно изучим каждый этап.

Разбор заголовков
Конкретно этот этап можно разделить на 2 подэтапа:
  1. Разбор DOS заголовка
    1.1. Сначала, происходит проверка сигнатуры e_magic. Она должна быть равна "MZ".

    1.2. После этого, происходит считывание поля e_lfanew и переход к разбору PE-заголовка

  2. Разбор 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 с различными характеристиками.

Проецирование

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

mapping.png

Сначала, по адресу базовой загрузки (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 адрес импортируемой функции.
  • Bound Import
    В этом случае, в виртуальную память проецируются библиотеки, из которых нужно импортировать функции, а адреса на эти функции уже вшиты в таблице импорта. Это быстрый механизм, но требует константность DLL. Для того, чтобы указать загрузчику, чтобы он использовал этот механизм, в TimeDateStamp и ForwardChain заносится значение -1, а информация о связывании находится в элементе с индексом 11 (IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT) DataDirectory.

  • Delay Import
    В этом случае, адрес требуемой функции получается из DLL только по мере необходимости. Указатель находится в элементе c индексом 15 DataDirectory.
Ну так вот. Мы изучили, что такое импорт. Вернёмся к загрузке PE-файла в память. Загрузчик, на этом этапе, изучает тип импорта, а после этого, в зависимости от него, загружает DLL в память опеределённым способом и в определённый момент и импортирует функцию.

Запуск программы
После разбора всех заголовков, разбора таблицы секций, проецирования файла в память и импорта функций начинается исполнение программы по адресу ImageBase + AddressOfEntryPoint. Обычно, AddressOfEntryPoint указывает на секцию кода (.text), которая содержит машинные инструкции, но это всего лишь соглашение и оно может быть несоблюдено.



Ну, на этом всё. Если есть какие-либо вопросы или поправки, буду рад если вы напишите комментарий. В следующей статье мы "вручную" создадим свой EXE-файл с использованием лишь одного Hex-редактора. После окончательного изучения PE мы начнём изучать его с точки зрения информационной безопасности.

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

explorer

Platinum
05.08.2018
1 080
2 475
BIT
0
Всё прекрасно, правда не увидел выполненного обещания:
Немного расскрою содержание следующей статьи: мы поговорим о виртуальной адресации, выравнивании памяти, таблицах импорта и экспорта, и напишем "Hello, world" без помощи компилятора и линкера

Где "Hello, world" ?
 
  • Нравится
Реакции: Tihon49 и PingVinich

PingVinich

Технарь
Green Team
19.03.2017
138
513
BIT
0
Всё прекрасно, правда не увидел выполненного обещания:


Где "Hello, world" ?

Ну, на этом всё. Если есть какие-либо вопросы или поправки, буду рад если вы напишите комментарий. В следующей статье мы "вручную" создадим свой EXE-файл с использованием лишь одного Hex-редактора. После окончательного изучения PE мы начнём изучать его с точки зрения информационной безопасности.

Решил оставить на следующую статью, чтобы не слишком раздувать эту. Постараюсь, чтобы завтра вышла новая статья.
 
  • Нравится
Реакции: Tihon49

explorer

Platinum
05.08.2018
1 080
2 475
BIT
0
PingVinich
Уже вовсю читаю) Хотя статья не закончена, уже есть вопрос - почему столь малюсенькая программа так криво исполнена? Всё разъезжается - кнопка вправо, надпись влево. По центру всё уж поставили бы )
 

PingVinich

Технарь
Green Team
19.03.2017
138
513
BIT
0
PingVinich
Уже вовсю читаю) Хотя статья не закончена, уже есть вопрос - почему столь малюсенькая программа так криво исполнена? Всё разъезжается - кнопка вправо, надпись влево. По центру всё уж поставили бы )
Это окно сообщения (MessageBox). Нет возможности изменить позицию его элементов. Это отдельный элемент системы Windows. Оно вызывается функцией MessageBoxA. Почитать о ней можно здесь: .
 
  • Нравится
Реакции: Tihon49

Alex Trim

Green Team
12.07.2020
18
11
BIT
0
Спасибо за статью. Нашёл несколько опечаток.

Виртуальная память управляются устройством управления памяти
Виртуальная память управляется устройством управления памяти

MMU является компонентном аппаратного обеспечения.
MMU является компонентом аппаратного обеспечения.

Каждая ячейка на изображении, эта страница памяти.
Каждая ячейка на изображении – это страница памяти.
 
Мы в соцсетях:

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