Доброго времени суток, друзья. В прошлой статье, мы изучили принципы виртуальной адресации и процесс загрузки исполняемого файла в память. Ну а эта статья (и следующая), создана для того, чтобы закрепить полученный в двух прошлых статьях материал. В этой статье нас ожидает программирование на машинных кодах, глубокая работа с памятью, сборка таблицы импорта вручную и т.д.. Сделаем мы это путём создания вручную собственного исполняемого .exe файла без использования средств сборки (компилятора и линкера), а используя только Hex-редактор. Для полноценного понимания нынешней статьи, прочитайте две предыдущих. Ну-с, приступим.
Также, при создании нам понадобится схема различных структуры, поэтому мы вооружимся следующей замечательной схемой ниже:
Программа будет 32-ух битной, для облегчения задачи.
Алгоритм программы будет выглядеть вот так:
На языке C данная программа будет выглядеть вот так:
Но нам придётся писать не на языке C или C++, а на машинном языке (да, на тех самых 0 и 1, но в HEX-представлении). К счастью, мы можем облегчить данную задачу с помощью языка ассемблера. Мы напишем нашу программу на языке ассемблера, а после этого превратим код на языке ассемблера в байтовое представление.
Теперь давайте определим количество секций. Нам нужны только 3 секции:
Теперь мы начнём попорядку заполнять нужные нам структуры. Начнём с заголовков, а после них начнём заполнять секции.
Вспомним какие существуют заголовки
Как мы знаем, каждый исполняемый файл начинается со структуры под названием DOS-заголовок:
Необходимыми полями для загрузки у DOS-заголовка являются, как мы знаем, e_magic и e_lfanew. Поле e_magic хранит в себе специальную сигнатуру MZ, а поле e_lfanew смещение до PE-заголовка. Давайте заполним только эти поля в DOS заголовке, а остальные забьём нулями. Так как мы пока не знаем смещения до PE заголовка, заполним это поле байтами AA (почему AA? Для того, чтобы это поле выделялось. Позже мы поменяем его на смещение до PE-заголовка).
Сразу после PE-сигнатуры идёт файловый заголовок. Приступим к его заполнению.
Вторым двухбайтовым полем является число секций (2 байта). Мы обусловились в начале на 3 поля. Поэтому запишем сюда число 3.
Далее идут 3 четырёхбайтовых поля: TimeDateStamp, PointerToSymbolTable и NumberOfSymbols. Так как эти поля не являются обязательными, мы забьём их нулями. В дальнейшем будем поступать так со всеми некритичными полями.
Теперь нам нужно указать размер дополнительного заголовка (2 байта). Мы пока его не знаем, поэтому заполним также байтами AA.
После размера дополнительного заголовка следуют характеристики файла. Укажем здесь 0x102, что будет означать что это 32-ух битный исполняемый файл.
Дополнительный заголовк
Мы заполним только поля Magic, AddressOfEntryPoint, ImageBase, SectionAlignment, FileAlignment, MajorSubsystemVersion, SizeOfImage, SizeOfHeaders, SubSystem, NumberOfRvaAndSizes и таблицу информации о каталогах. Остальные заполним нулями.
Поле Magic: тут всё просто. Это поле хранит битность программы. Вот список принимаемых значений:
Мы укажем 0x01b, так как наша программа 32-ух битная.
Поле AddressOfEntryPoint, как мы знаем, хранит относительный виртуальный адрес точки входа в программу. Так как мы пока его не знаем заполним это поле байтами AA.
Поле ImageBase хранит предпочитаемый адрес базовой выгрузки файла. Запишем сюда значение по-умолчанию, т.е. 0x00400000.
Далее у нас идёт поле SectionAlignment. Это поле содержит смещение начала заголовков программы в виртуальной памяти. Пока заполним байтами AA.
FileAlignment содержит смещение начала заголовков относительно начала файла. Его значение должно быть равно числу 2 в натуральной степени (от 512 до 65 536). По умолчанию это значение 512 или 0x200 в шестнадцатеричном виде. Так и запишем.
На данный момент наш файл выглядит так:
Двигаемся дальше.
В MajorSubsystemVersion мы поместим цифру 4, обозначающее Windows NT.
Поле SizeOfImage хранит размер образа в памяти. Его значение должно быть кратно значению SectionAlignment, поэтому также заполним байтами AA.
Поле SizeOfHeaders содержит размер всех заголовков. Оно должно быть кратно значению FileAlignment (или равно). Заполним байтами BB.
Поле Subsystem, содержит тип подсистемы. Доступны следующие значения:
Укажем 2, так как наша программа является графической программой Windows.
Поле NumberOfRvaAndSizes содержит число элементов в массиве DataDirectory. По умолчанию 16 или 0x10. Так и укажем.
На данный момент наш заголовок выглядит так:
Теперь приступаем к заполнению DataDirectory. DataDirectory - массив элементов VirtualAddress, Size. Так как NumberOfRvaAndSizes 0x10, т.е. 16, то и элементов в массиве будет столько же. Вспомним об индексах элементов:
Из всех этих индексов нас интересует только один - IMAGE_DIRECTORY_ENTRY_IMPORT. Элемент с этим индексом содержит информацию о таблице импорта. Так нас интересует только элемент с данным индексом, мы заполним только его, а остальные заполним NULL байтами (00).
Структура элемента хранящегося в этом массиве выглядит так (на языке C/C++):
Элемент VirtualAddress содержит адрес, куда будет выгружена секция, а Size - размер секции. Поле Size в данном случае можно не заполнять.
Давайте заполним массив DataDirectory. Так как мы не определились с адресом таблицы импорта, заполним его байтами AA.
Вот мы и заполнили дополнительный заголовок. Теперь мы можем узнать, какой у него размер (в байтах) и занести это значение в байтовый заголовок. Для того, чтобы узнать его размер, получим разность смещения первого байта заголовка и последнего и прибавим единицу.
0x137 - 0x58 + 0x1 = 0xE0
Получившееся значение и запишем в поле SizeOfOptionalHeader файлового заголовка
Вот мы и закончили заполнение PE заголовка. Из заголовков осталось заполнить только таблицу секций. К ней мы сейчас и перейдём.
Как мы узнали в прошлых статьях, таблица секций это массив элементов IMAGE_SECTION_HEADER. Вспомним строение элемента:
Существенными элементами являются лишь Name, VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData, Characteristics. Ну что же, вспомнили строение элемента, приступим к заполнению.
Как мы договорились ранее, у нас будет 3 секции:
Сначала заполним восьмибайтовое поле 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). Ознакомиться со значением характеристики можно здесь:
Остальные поля заполним нулями.
А пока, взглянем на структуру элемента данных о секции в файле:
Остальные элементы заполним по такому же принципу, запомнив, что VirtualAddress каждого элемента равен сумме VirtualAddress и VirtualSize прошлого элемента, а PointerToRawData равен сумме PointerToRawData и SizeOfRawData прошлого элемента. Как мы и обсуловились, размер каждой секции в виртуальной памяти будет равен 0x1000, а в файле 0x200.
Теперь, когда мы знаем виртуальный адрес (0x1000) загрузки секции кода (.text) в виртуальную память, мы можем изменить значения AddressOfEntryPoint, SectionAlignment, SizeOfImage, SizeOfHeaders, а также виртуальный адрес таблицы импорта.
AddressOfEntryPoint будет равен 0x1000, так как секция кода загрузится по этому адресу, а в секции кода будет находится наш код (логично!). (Поле AddressOfEntryPoint хранит адрес точки входа в программу).
SectionAlignment будет также равен 0x1000, так как секция кода является первой и она будет загружена по этому адресу (Поле SectionAlignment хранит адрес начала секций в виртуальной памяти).
Давайте договоримся, что SizeOfImage будет равен 0x4000, так как размер файла не будет превышать данного значения. (Почему? Потому что последняя секция файла будет находиться по смещению 0x600 и иметь размер 0x800) (Поле SizeOfImage хранит размер виртуального образа).
Поле SizeOfHeaders будет равен 0x200, так как секции в файле будут начинаться именно с этого значения, а как мы помним секции следуют сразу после заголовков (Поле SizeOfHeaders хранит размер всех заголовков (в байтах)).
Адрес таблицы импорта будет равен 0x400 (PointerToRawData таблицы .idata), так как таблица импорта загрузится именно по этому адресу.
Мы заполнили все заголовки! Настало очередь секций, но увы и ах, мы начнём заполнять их только в следующей статье.
Если у Вас есть какие-нибудь вопросы или поправки, я буду рад их увидеть в комментариях. Спасибо за то что прочитали эту статью до конца .
Следующая статья
Подготовка
Для начала, если у вас нету hex-редактора, то скачайте его. Я же, использую бесплатный и удобный hex-редактор, который носит название HxD Hex Editor. Скачать его можно по этой ссылке:
Ссылка скрыта от гостей
.Также, при создании нам понадобится схема различных структуры, поэтому мы вооружимся следующей замечательной схемой ниже:
Перед началом создания самого файла, давайте распланируем, что будет делать наш исполняемый модуль и какой у него будет приблизительный вид (формат). Я предлагаю создать небольшую программу, которая непрерывно будет выводить приветственное окно. Выглядеть оно будет вот так:
Алгоритм программы будет выглядеть вот так:
- Выводим окошко с сообщением
- Переходим к шагу 1
На языке C данная программа будет выглядеть вот так:
C:
int main()
{
while (true)
{
MessageBoxA(0, "Hello codeby.net!", "Codeby.net", 0);
}
}
Но нам придётся писать не на языке C или C++, а на машинном языке (да, на тех самых 0 и 1, но в HEX-представлении). К счастью, мы можем облегчить данную задачу с помощью языка ассемблера. Мы напишем нашу программу на языке ассемблера, а после этого превратим код на языке ассемблера в байтовое представление.
Теперь давайте определим количество секций. Нам нужны только 3 секции:
- .text - в этой секции будет храниться код.
- .idata - в этой секции будет храниться таблица импорта.
- .data - в этой секции будут храниться данные.
Начало
Для начала, откроем Hex-редактор и создадим новый файл:
Теперь мы начнём попорядку заполнять нужные нам структуры. Начнём с заголовков, а после них начнём заполнять секции.
Вспомним какие существуют заголовки
- DOS-заголовок
- DOS-заглушка
- PE-заголовок
- Файловый подзаголовок
- Дополнительный подзаголовок
- Таблица секций
DOS-заголовок
Как мы знаем, каждый исполняемый файл начинается со структуры под названием DOS-заголовок:
DOS заглушка
DOS заглушка не является обязательной частью PE-файла, поэтому мы можем просто проигнорировать и не заполнять данную структуру .
PE-сигнатура
Вот мы и подошли к началу PE-заголовка. Давайте запишем сюда сигнатуру PE файла ("PE\x00\x00") и изменим поле e_lfanew (смещение до PE-заголовка) в DOS-заголовке (cмещением до PE-заголовка будет число 0x40 (не забывайте записывать числа в обратном порядке!)).
Файловый заголовок
Первое поле файлового заголовка содержит архитектуру процессора. Мы сюда запишем значение 0x014c (опять же в обратном порядке), что означает что мы указали 32-ух битную архитектуру процессора. Вот список возможных значений:
Далее идут 3 четырёхбайтовых поля: TimeDateStamp, PointerToSymbolTable и NumberOfSymbols. Так как эти поля не являются обязательными, мы забьём их нулями. В дальнейшем будем поступать так со всеми некритичными полями.
Теперь нам нужно указать размер дополнительного заголовка (2 байта). Мы пока его не знаем, поэтому заполним также байтами AA.
После размера дополнительного заголовка следуют характеристики файла. Укажем здесь 0x102, что будет означать что это 32-ух битный исполняемый файл.
Вот мы и закончили заполнение файлового заголовка. Мы к нему ещё вернёмся, чтобы указать размер дополнительного заголовка, но позже. К концу заполнения заголовок должен выглядеть вот так:Дополнительный заголовк
Поле Magic: тут всё просто. Это поле хранит битность программы. Вот список принимаемых значений:
Поле AddressOfEntryPoint, как мы знаем, хранит относительный виртуальный адрес точки входа в программу. Так как мы пока его не знаем заполним это поле байтами AA.
Поле ImageBase хранит предпочитаемый адрес базовой выгрузки файла. Запишем сюда значение по-умолчанию, т.е. 0x00400000.
Далее у нас идёт поле SectionAlignment. Это поле содержит смещение начала заголовков программы в виртуальной памяти. Пока заполним байтами AA.
FileAlignment содержит смещение начала заголовков относительно начала файла. Его значение должно быть равно числу 2 в натуральной степени (от 512 до 65 536). По умолчанию это значение 512 или 0x200 в шестнадцатеричном виде. Так и запишем.
На данный момент наш файл выглядит так:
В MajorSubsystemVersion мы поместим цифру 4, обозначающее Windows NT.
Поле SizeOfImage хранит размер образа в памяти. Его значение должно быть кратно значению SectionAlignment, поэтому также заполним байтами AA.
Поле SizeOfHeaders содержит размер всех заголовков. Оно должно быть кратно значению FileAlignment (или равно). Заполним байтами BB.
Поле Subsystem, содержит тип подсистемы. Доступны следующие значения:
Укажем 2, так как наша программа является графической программой Windows.
Поле NumberOfRvaAndSizes содержит число элементов в массиве DataDirectory. По умолчанию 16 или 0x10. Так и укажем.
На данный момент наш заголовок выглядит так:
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.
0x137 - 0x58 + 0x1 = 0xE0
Получившееся значение и запишем в поле SizeOfOptionalHeader файлового заголовка
Таблица секций
Как мы узнали в прошлых статьях, таблица секций это массив элементов 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
Сначала заполним восьмибайтовое поле 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). Ознакомиться со значением характеристики можно здесь:
Ссылка скрыта от гостей
.Остальные поля заполним нулями.
А пока, взглянем на структуру элемента данных о секции в файле:
AddressOfEntryPoint будет равен 0x1000, так как секция кода загрузится по этому адресу, а в секции кода будет находится наш код (логично!). (Поле AddressOfEntryPoint хранит адрес точки входа в программу).
SectionAlignment будет также равен 0x1000, так как секция кода является первой и она будет загружена по этому адресу (Поле SectionAlignment хранит адрес начала секций в виртуальной памяти).
Давайте договоримся, что SizeOfImage будет равен 0x4000, так как размер файла не будет превышать данного значения. (Почему? Потому что последняя секция файла будет находиться по смещению 0x600 и иметь размер 0x800) (Поле SizeOfImage хранит размер виртуального образа).
Поле SizeOfHeaders будет равен 0x200, так как секции в файле будут начинаться именно с этого значения, а как мы помним секции следуют сразу после заголовков (Поле SizeOfHeaders хранит размер всех заголовков (в байтах)).
Адрес таблицы импорта будет равен 0x400 (PointerToRawData таблицы .idata), так как таблица импорта загрузится именно по этому адресу.
Если у Вас есть какие-нибудь вопросы или поправки, я буду рад их увидеть в комментариях. Спасибо за то что прочитали эту статью до конца .
Следующая статья
Последнее редактирование: