• Открыта запись на вторую часть курса по анонимности и безопасности в сети интернет "Paranoid II" от команды codeby. Анонимные роутеры, Подъём, настройка и администрирование Tor-ноды, Работа с железом ПК, Удаление аппаратных закладок, Минимизация рисков, Авторские разработки и многое другое. Подробнее ...

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

PingVinich

PingVinich

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

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

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

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

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

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

Программа будет 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-редактор и создадим новый файл:

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


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

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

DOS-заголовок

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

Теперь приступаем к заполнению 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.

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

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

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

0x137 - 0x58 + 0x1 = 0xE0

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

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

Вот мы и закончили заполнение 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). Ознакомиться со значением характеристики можно здесь: .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

PingVinich

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

Вложения

konstata3

konstata3

Member
24.02.2017
10
8
Вообще хз зачем нужен такой мазохизм, но читать интересно:)
 
  • Нравится
Реакции: PingVinich
Debug

Debug

Grey Team
07.07.2017
157
360
Я надеюсь эти знание мне пригодятся)) Спасибо вам большое
 
  • Нравится
Реакции: PingVinich
Tihon49

Tihon49

Well-known member
06.01.2018
203
107
Капец! Очень круто!!! Если честно, мало что понял (в технической части) )))) но написано так, что при желании можно во всём разобраться (чём сейчас и занимаюсь).
 
  • Нравится
Реакции: PingVinich
Fox007

Fox007

New member
07.12.2018
2
0
Жалко что таким годным контентом интересуются так мало людей. Статья написано очень хорошо, все понятно и разьяснено, нету лишней воды. Четко! респект
 
Aleks Binakril

Aleks Binakril

Happy New Year
04.12.2019
25
4
гораздо информативнее чем у тех же индусов которые писали хелп для визуал студио
 
Мы в соцсетях: