Статья [0x04] Исследуем Portable Executable [Создаём программу без компилятора (часть 2)]


Доброго времени суток, друзья. В прошлой статье, мы начали создавать простейший исполняемый модуль (.exe файл) в hex-редакторе и полностью заполнили заголовки. В этой статье, мы закончим работу над нашим исполняемым файлом и получим работоспособную программу. Приступим!

Выравнивание начала секций
В прошлой статье мы заполнили заголовки, ну а в этой мы заполним секции. Как мы договорились в прошлой статье, всего у нас будет 3 секции, а именно:
  • .text - секция кода
  • .idata - секция импорта
  • .data - секция данных
Каждая секция в файле будет занимать по 0x200 (512) байт, а в памяти по 0x1000 (4096) байт. В файле они будут начинаться со смещения 0x200 (512), а в памяти по смещению 0x1000 (4096) :).
Но что делать, если у нас есть пробел между 0x1A0 (конца заголовков) и 0x200 (началом секций)? Что делать с этим пробелом в файле? Просто заполним его нулями :D!

padding.png

Теперь мы полностью готовы к заполнению секций.

Заполняем секцию .text

Первая по порядку у нас секция .text. В ней, как мы обусловились, будет храниться наш код.

Вернёмся к нашему алгоритму:
1. Показ сообщения
2. Переход к шагу 2

Как мы знаем, для показа сообщения у нас есть функция MessageBoxA. Она будет загружена загрузчиком при разборе секции импорта, а её адрес будет помещён в определённое место в памяти.

Мы пока не знаем, что это за адрес, поэтому давайте пока представим, что адрес функции будет загружен по адресу 0xAAAAAAAA (в даленьйшем мы его заменим на настоящий).

Также, мы не знаем адрес строк, которые должны быть отображены в сообщении ("Hello codeby.net!" и "Codeby.net"). Поэтому также представим, что они будут находится по адресу 0xAAAAAAAA. На самом деле эти строки будут находится в секции данных.

Программировать, как я сказал в первой статье, мы будем на языке ассемблера. Почему? Потому что именно этот язык является прямым представлением машинного языка в понятном человеку виде. Итак, на языке ассемблера наша программа будет выглядить так:
Код:
show_message:
    ; Передаём параметры
    push 0            ; MB_OK
    push 0xAAAAAAAA   ; Codeby.net
    push 0xAAAAAAAA   ; Hello codeby.net
    push 0            ; NULL

    ; Вызываем функцию
    call [0xAAAAAAAA] ; MessageBoxA

    ; Переходим на шаг 1
    jmp  show_message

Также, нужно запомнить, что параметры когда параметры передаются через стэк (с помощью инструкций push), то они передаются в обратном порядке!

Как же нам теперь превратить код на языке ассемблера в байты? Для этого переходим по этой ссылке: . Это онлайн ассемблер. Он позволяет конвертировать код на языке ассемблера в шелл-код (код в hex-представлении).

assemble_code.png

Вставляем сюда код на языке ассемблера и нажимаем на кнопку "Assemble".

shell_code.png

Теперь копируем байты из поля Raw Hex и вставляем его в hex-редактор.

text_section_1.png

Как мы помним, мы определили размер каждой секции в файле в 0x200 (512) байт. Поэтому пространство в файле от 0x200 до 0x400 должна занимать секция. Что делать с остальным пустым пространством в секции? Мы уже знаем ответ! Просто заполняем его нулями.

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

1541333574552.png


Сама таблица импорта - это по сути массив элементом IMAGE_IMPORT_DESCRIPTOR. А массив в памяти - просто последовательность элементов. Таблица импорта заканчивается нулевым элементом, т.е. элементом 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 хранит относительный виртуальный адрес указателя на элемент таблицы Import Lookup Table - IMAGE_IMPORT_BY_NAME. Его структура на языке C/C++:

C:
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD Hint;
    CHAR[] Name;
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Поле Hint хранит в себе индекс функции в DLL - это поле может быть равно нулю, а поле Name - это строка хранящая имя функции. Эта строка заканчивается NULL-символом.

Так, следующее интересное нам поле в структуре IMAGE_IMPORT_DESCRIPTOR - это Name. Это поле содержит указатель на строку с именем DLL.

Поле FirstThunk - хранит виртуальный адрес указатель на таблицу Import Address Table. В эту таблицу загрузчик помещает адрес импортируемой функции.

iat.png

Схематична эта вся система выглядит так:

1541334847512.png

Разобрались с импортом. Теперь перейдём к деталям заполнения. Мы будем импортировать только одну функцию - MessageBoxA. Она находится в DLL'ке с именем user32.dll. Поэтому у нас будет только 2 элемента в таблице импорта. Первый - отвечающий за функцию MessageBoxA, а второй - нулевой.

В этот раз я сам заполню таблицу импорта, а после этого разберём как всё там устроено :).

import_table.png

Каждый отдельный элемент на этом изобрежении я покрасил в отдельный цвет.

  • Зелёный элемент - элемент IMAGE_IMPORT_DESCRIPTOR. Его первое поле, как мы можем заметить, содержит виртуальный адрес (0x2028) на адрес (0x2030) (бежевый элемент), который указывает на элемент IMPORT_BY_NAME (серый элемент). Также, он хранит адрес (0x2046) на имя DLL'ки (белое поле) и адрес на Import Address Table (сереневое поле) .

import_table_2.png

  • Голубой элемент - нулевой элемент таблицы дескрипторов.
  • Бежевый элемент - таблица указателей на элементы IMPORT_BY_NAME (серое поле).
  • Серый элемент - элемент IMPORT_BY_NAME. Он хранит индекс и имя функции в DLL.
  • Сиреневый элемент - таблица Import Address Table. В неё будут адресы импортируемых функций.

iat.png

  • Белый элемент - имя DLL'ки.
Дальше идёт пустая область. Она нужна для выравнивания секции до её размера в файле.

Кстати, теперь мы знаем, куда загрузится адрес нашей функции. Он загрузится по адресу 0x0040203E (ImageBase + относительный виртуальный адрес Import Address Table). Поэтому заменим его значение в секции кода.

fix_code_1.png

Ах да, в прошлой статье я допустил небольшую ошибку. Я указал неправильный виртуальный адрес таблицы импорта. Давайте исправим это!

fix_code_2.png


Заполняем секцию .data
Следующая секция будет хранить наши данные. У нас это две строки - "Hello codeby.net!" и "Codeby.net". Каждая из этих строк должна заканчиваться NULL-байтом (00). Поэтому давайте просто запишем эти данные в секцию и выравним её размер до 0x200 (512) байт.

data_section.png

Вот и всё! Мы заполнили последнюю секцию. Также, теперь мы знаем виртуальные адреса двух нужных нам строк:

Для "Codeby.net" - 0x402000 (ImageBase + адрес начала секции (эта строка начинается в самом начале секции))
Для "Hello codeby.net!" - 0x40200B (ImageBase + адрес начала секции (эта строка начинается в самом начале секции) + размер первой строки + 1 (для NULL-байта первой строк))

Заменим и эти значения в секции кода (ранее было 0xAAAAAAAA).

fix_code_3.png

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

hello_codeby_net.png

Да, это так. Мы создали собственный .exe файл имея на руках лишь hex-редактор и наши знания о формате файла!

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

Следующая статья
 
Последнее редактирование:
На самом деле, на первый взгляд информация по структуре PE файла выглядит пугающе сложной, и возникает вопрос "Как вообще это может уместится в голове?".
Но благодаря вашим статьям, страх понемногу уходит и я понемногу начал ориентироваться. Особенно ценно, что на русском языке такого материала я еще не находил.
Огромное вам спасибо.
 
Спасибо за статью.

Опечатки:

Вернёмся к нашему алгоритму:
1. Показ сообщения
2. Переход к шагу 2
2. Переход к шагу 1

Также, нужно запомнить, что параметры когда параметры передаются через стэк
Также, нужно запомнить, что когда параметры передаются через стэк

Сама таблица импорта - это по сути массив элементом
Сама таблица импорта - это по сути массив элементов

Схематична эта вся система выглядит так:
Схематично эта вся система выглядит так:

Сиреневый элемент - таблица Import Address Table. В неё будут адресы импортируемых функций.
Сиреневый элемент - таблица Import Address Table. В ней будут адреса импортируемых функций.
 
Очень интересные статьи. Прочитал и первую и вторую часть.
В дополнение могу сказать, что на хабре тоже была статья по PE формату (на тот случай, если кто-то не понял)
В первый раз вижу человека-компилятора
 
  • Нравится
Реакции: Alex Trim
Для "Codeby.net" - 0x402000 (ImageBase + адрес начала секции (эта строка начинается в самом начале секции))
Для "Hello codeby.net!" - 0x40200B (ImageBase + адрес начала секции (эта строка начинается в самом начале секции) + размер первой строки + 1 (для NULL-байта первой строк))

Для "Codeby.net" - 0x403000 (ImageBase + адрес начала секции (эта строка начинается в самом начале секции))
Для "Hello codeby.net!" - 0x40300B (ImageBase + адрес начала секции (эта строка начинается в самом начале секции) + размер первой строки + 1 (для NULL-байта первой строк))

Спасибо за труд! хотя до сих пор почему то не могу понять, как непосредственно в адресном пространстве файла (т.е. в hex редакторе) увязывать с тем где оно будет в виртуальном адресном пространстве. Единственное что отметил - это смещение на PE заголовок и размер PE хэдера т.е. меняя эти значения с учетом того где это находится в hex редакторе влияет на запуск программы, а все остальные адреса это уже с учетом того как оно развернется в памяти и сколько будет там занимать.
 
ой спасибо огромное за цикл статей !! очень весело и интересно )
 
Шикарный цикл статей и особенно инфографика по структуре файла!
Хоть голове и было больно, зато понял, как конвертнуть сырой адрес ресурса (смещение в exe-файле) в виртуальный адрес, чтобы потом найти места, где этот ресурс (в моем случае строка) используются в программе.
 
Мы в соцсетях:

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