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

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

Подготовка
Для начала, если у вас нету hex-редактора, то скачайте его. Я же, использую бесплатный и удобный hex-редактор, который носит название HxD Hex Editor. Скачать его можно по этой ссылке: .

Также, при создании нам понадобится схема различных структуры, поэтому мы вооружимся следующей замечательной схемой ниже:

PE102.png

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

hello.png

Программа будет 32-ух битной, для облегчения задачи.

Алгоритм программы будет выглядеть вот так:
  1. Выводим окошко с сообщением
  2. Переходим к шагу 1
Для вывода окон в Windows существует специальная функция (Windows API) под названием MessageBoxA. Первым параметром передаётся идентификатор окна (можно 0), вторым параметром сообщение, третьим параметром заголовок сообщения, а четвёртым стиль окна (можно 0). Эта функция находится в библиотеке user32.dll.

На языке C данная программа будет выглядеть вот так:

C:
int main()
{
    while (true)
    {
        MessageBoxA(0, "Hello codeby.net!", "Codeby.net", 0);
    }
}

Но нам придётся писать не на языке C или C++, а на машинном языке (да, на тех самых 0 и 1, но в HEX-представлении). К счастью, мы можем облегчить данную задачу с помощью языка ассемблера. Мы напишем нашу программу на языке ассемблера, а после этого превратим код на языке ассемблера в байтовое представление.

Теперь давайте определим количество секций. Нам нужны только 3 секции:
  • .text - в этой секции будет храниться код.
  • .idata - в этой секции будет храниться таблица импорта.
  • .data - в этой секции будут храниться данные.
Основа заложена, на счёт остального соорентируемся на местности.

Начало
Для начала, откроем Hex-редактор и создадим новый файл:

new_file.png


Теперь мы начнём попорядку заполнять нужные нам структуры. Начнём с заголовков, а после них начнём заполнять секции.

Вспомним какие существуют заголовки
  1. DOS-заголовок
  2. DOS-заглушка
  3. PE-заголовок
    1. Файловый подзаголовок
    2. Дополнительный подзаголовок
  4. Таблица секций
Приступим к последовательному заполнению заголовков.

DOS-заголовок

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

1540710816640.png

Необходимыми полями для загрузки у DOS-заголовка являются, как мы знаем, e_magic и e_lfanew. Поле e_magic хранит в себе специальную сигнатуру MZ, а поле e_lfanew смещение до PE-заголовка. Давайте заполним только эти поля в DOS заголовке, а остальные забьём нулями. Так как мы пока не знаем смещения до PE заголовка, заполним это поле байтами AA (почему AA? Для того, чтобы это поле выделялось. Позже мы поменяем его на смещение до PE-заголовка).

1540711062926.png


DOS заглушка
DOS заглушка не является обязательной частью PE-файла, поэтому мы можем просто проигнорировать и не заполнять данную структуру :).

PE-сигнатура
Вот мы и подошли к началу PE-заголовка. Давайте запишем сюда сигнатуру PE файла ("PE\x00\x00") и изменим поле e_lfanew (смещение до PE-заголовка) в DOS-заголовке (cмещением до PE-заголовка будет число 0x40 (не забывайте записывать числа в обратном порядке!)).

1540711381660.png

Сразу после PE-сигнатуры идёт файловый заголовок. Приступим к его заполнению.

Файловый заголовок

1540711612201.png

Первое поле файлового заголовка содержит архитектуру процессора. Мы сюда запишем значение 0x014c (опять же в обратном порядке), что означает что мы указали 32-ух битную архитектуру процессора. Вот список возможных значений:

1540711765808.png

Вторым двухбайтовым полем является число секций (2 байта). Мы обусловились в начале на 3 поля. Поэтому запишем сюда число 3.

Далее идут 3 четырёхбайтовых поля: TimeDateStamp, PointerToSymbolTable и NumberOfSymbols. Так как эти поля не являются обязательными, мы забьём их нулями. В дальнейшем будем поступать так со всеми некритичными полями.

Теперь нам нужно указать размер дополнительного заголовка (2 байта). Мы пока его не знаем, поэтому заполним также байтами AA.

После размера дополнительного заголовка следуют характеристики файла. Укажем здесь 0x102, что будет означать что это 32-ух битный исполняемый файл.
Вот мы и закончили заполнение файлового заголовка. Мы к нему ещё вернёмся, чтобы указать размер дополнительного заголовка, но позже. К концу заполнения заголовок должен выглядеть вот так:

file_header_1.png


Дополнительный заголовк

1540712936019.png

Мы заполним только поля Magic, AddressOfEntryPoint, ImageBase, SectionAlignment, FileAlignment, MajorSubsystemVersion, SizeOfImage, SizeOfHeaders, SubSystem, NumberOfRvaAndSizes и таблицу информации о каталогах. Остальные заполним нулями.

Поле Magic: тут всё просто. Это поле хранит битность программы. Вот список принимаемых значений:

1540713119896.png

Мы укажем 0x01b, так как наша программа 32-ух битная.

Поле AddressOfEntryPoint, как мы знаем, хранит относительный виртуальный адрес точки входа в программу. Так как мы пока его не знаем заполним это поле байтами AA.

Поле ImageBase хранит предпочитаемый адрес базовой выгрузки файла. Запишем сюда значение по-умолчанию, т.е. 0x00400000.

Далее у нас идёт поле SectionAlignment. Это поле содержит смещение начала заголовков программы в виртуальной памяти. Пока заполним байтами AA.

FileAlignment содержит смещение начала заголовков относительно начала файла. Его значение должно быть равно числу 2 в натуральной степени (от 512 до 65 536). По умолчанию это значение 512 или 0x200 в шестнадцатеричном виде. Так и запишем.

На данный момент наш файл выглядит так:

optional_header_1.png

Двигаемся дальше.

В MajorSubsystemVersion мы поместим цифру 4, обозначающее Windows NT.

Поле SizeOfImage хранит размер образа в памяти. Его значение должно быть кратно значению SectionAlignment, поэтому также заполним байтами AA.

Поле SizeOfHeaders содержит размер всех заголовков. Оно должно быть кратно значению FileAlignment (или равно). Заполним байтами BB.

Поле Subsystem, содержит тип подсистемы. Доступны следующие значения:

1540714386796.png


Укажем 2, так как наша программа является графической программой Windows.

Поле NumberOfRvaAndSizes содержит число элементов в массиве DataDirectory. По умолчанию 16 или 0x10. Так и укажем.

На данный момент наш заголовок выглядит так:

optional_header_2.png

Теперь приступаем к заполнению DataDirectory. DataDirectory - массив элементов VirtualAddress, Size. Так как NumberOfRvaAndSizes 0x10, т.е. 16, то и элементов в массиве будет столько же. Вспомним об индексах элементов:

C:
#define IMAGE_DIRECTORY_ENTRY_EXPORT              0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT                      1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE            2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION           3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY            4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC           5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG                       6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT               7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE        7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR           8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS                     9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG        10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT      11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT                    12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT       13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR     14   // COM Runtime descriptor

Из всех этих индексов нас интересует только один - IMAGE_DIRECTORY_ENTRY_IMPORT. Элемент с этим индексом содержит информацию о таблице импорта. Так нас интересует только элемент с данным индексом, мы заполним только его, а остальные заполним NULL байтами (00).

Структура элемента хранящегося в этом массиве выглядит так (на языке C/C++):

C:
typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD VirtualAddress;
  DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

Элемент VirtualAddress содержит адрес, куда будет выгружена секция, а Size - размер секции. Поле Size в данном случае можно не заполнять.

Давайте заполним массив DataDirectory. Так как мы не определились с адресом таблицы импорта, заполним его байтами AA.

data_directory.png

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

size.png

0x137 - 0x58 + 0x1 = 0xE0

Получившееся значение и запишем в поле SizeOfOptionalHeader файлового заголовка

1540722122211.png

Вот мы и закончили заполнение PE заголовка. Из заголовков осталось заполнить только таблицу секций. К ней мы сейчас и перейдём.

Таблица секций

Как мы узнали в прошлых статьях, таблица секций это массив элементов IMAGE_SECTION_HEADER. Вспомним строение элемента:

C:
typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  DWORD VirtualSize;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Существенными элементами являются лишь Name, VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData, Characteristics. Ну что же, вспомнили строение элемента, приступим к заполнению.

Как мы договорились ранее, у нас будет 3 секции:
  • .text
  • .idata
  • .data
Начнём с заполнения информации о секции .text.

Сначала заполним восьмибайтовое поле Name именем секции (.text).
Далее, заполним поле VirtualSize. Давайте договоримся, что размер каждой секции в виртуальной памяти будет 0x1000 (в байтах), а в файле размер будет 0x200 (в байтах). Поэтому для каждой секции мы заполним поле VirtualSize значением 0x1000.

Дальше идёт поле VirtualAddress, пусть также будет заполнено значением 0x1000. Это поле содержит относительный виртуальный адрес секции.

Поле SizeOfRawData хранит значение размера секций в файле. Как мы договорились ранее, размер каждой секции будет равен 0x200. Значение поля SizeOfRawData должно быть кратно полю FileAlignment (!!!).

Поле PointerToRawData содержит значение адреса секции в файле. Оно также должно быть кратно FileAlignment. Поэтому также укажем здесь 0x200.

В поле Characteristics мы установим значение 0x60000020 (IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ). Ознакомиться со значением характеристики можно здесь: .

Остальные поля заполним нулями.

А пока, взглянем на структуру элемента данных о секции в файле:

section.png

Остальные элементы заполним по такому же принципу, запомнив, что VirtualAddress каждого элемента равен сумме VirtualAddress и VirtualSize прошлого элемента, а PointerToRawData равен сумме PointerToRawData и SizeOfRawData прошлого элемента. Как мы и обсуловились, размер каждой секции в виртуальной памяти будет равен 0x1000, а в файле 0x200.

sections.png

Теперь, когда мы знаем виртуальный адрес (0x1000) загрузки секции кода (.text) в виртуальную память, мы можем изменить значения AddressOfEntryPoint, SectionAlignment, SizeOfImage, SizeOfHeaders, а также виртуальный адрес таблицы импорта.

AddressOfEntryPoint будет равен 0x1000, так как секция кода загрузится по этому адресу, а в секции кода будет находится наш код (логично!). (Поле AddressOfEntryPoint хранит адрес точки входа в программу).

SectionAlignment будет также равен 0x1000, так как секция кода является первой и она будет загружена по этому адресу (Поле SectionAlignment хранит адрес начала секций в виртуальной памяти).

Давайте договоримся, что SizeOfImage будет равен 0x4000, так как размер файла не будет превышать данного значения. (Почему? Потому что последняя секция файла будет находиться по смещению 0x600 и иметь размер 0x800) (Поле SizeOfImage хранит размер виртуального образа).

Поле SizeOfHeaders будет равен 0x200, так как секции в файле будут начинаться именно с этого значения, а как мы помним секции следуют сразу после заголовков (Поле SizeOfHeaders хранит размер всех заголовков (в байтах)).

Адрес таблицы импорта будет равен 0x400 (PointerToRawData таблицы .idata), так как таблица импорта загрузится именно по этому адресу.

final.png

Мы заполнили все заголовки! Настало очередь секций, но увы и ах, мы начнём заполнять их только в следующей статье.

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

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

PingVinich

Технарь
Green Team
19.03.2017
138
512
BIT
0
Исполняемый модуль на данном этапе:
 

Вложения

  • hello_1.zip
    216 байт · Просмотры: 594

Debug

Red Team
07.07.2017
217
445
BIT
5
Я надеюсь эти знание мне пригодятся)) Спасибо вам большое
 
  • Нравится
Реакции: PingVinich

Tihon49

Green Team
06.01.2018
193
120
BIT
0
Капец! Очень круто!!! Если честно, мало что понял (в технической части) )))) но написано так, что при желании можно во всём разобраться (чём сейчас и занимаюсь).
 
  • Нравится
Реакции: PingVinich

Fox007

One Level
07.12.2018
2
1
BIT
0
Жалко что таким годным контентом интересуются так мало людей. Статья написано очень хорошо, все понятно и разьяснено, нету лишней воды. Четко! респект
 
  • Нравится
Реакции: Trimsky

Aleks Binakril

Green Team
04.12.2019
38
5
BIT
0
гораздо информативнее чем у тех же индусов которые писали хелп для визуал студио
 

Alex Trim

Green Team
12.07.2020
18
11
BIT
0
Спасибо. Отличная статья. Сейчас в GeekBrains как раз проходим реверс-инжиниринг и бинарные уязвимости. Ваши статьи очень сильно помогают, разъясняя некоторые моменты и дополняя другие.

p.s.

Нашёл несколько опечаток в статье:

Мы укажем 0x01b, так как наша программа 32-ух битная.
Мы укажем 0x10b, так как наша программа 32-ух битная.

Так нас интересует только элемент с данным индексом, мы заполним
Так как нас интересует только элемент с данным индексом, мы заполним

Как мы и обсуловились, размер каждой секции
Как мы и обусловились, размер каждой секции

Настало очередь секций,
Настала очередь секций,
 

slern

New member
15.04.2024
2
0
BIT
33
Просто оставлю мысль для тех, кто только начинает вникать в окололоулевел тему. Наберитесь терпения и будьте последовательны. За три дня полюбил это место!
 
Мы в соцсетях:

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