27
Формат исполняемых файлов Portable Executables (PE) существует уже очень долго, даже в архитектуру Windows 3.11 была заложена возможность запуска подобных файлов при установленном Win32s. Формат файла достаточно прост, но, тем не менее, в нем есть множество подводных камней, из-за которых некоторые программы умирают, а некоторые программисты пользуются ими в своих целях. Здесь кратко описывалось перемещение точки входа в программу для защиты от антивируса на свободное место в файле. Теперь мы рассмотрим подробно, как появилось это место, и что еще можно придумать с исполняемым файлом.
Для данного примера нам потребуются следующие программы:
- HEX-редактор (в моем случае FlexHex)
- Компилятор Borland C++ Builder 6.0
Все эти средства общедоступны в Интернете, желательно скачать самые последние версии.
Начнем с огромного количества скучной, но важной теории.
Формат EXE-файла, показанный в таблице, отражает его достаточно сильную структурированность:
|
Смещение |
Описание |
|
00h |
DOS 2 Header Заголовок DOS (форматированная часть), оставлен для совместимости. |
|
1Ch |
4 байта для выравнивания форматированной области заголовка с 1Ch до 20h. |
|
20h |
OEM Identifier & OEM Info Информация о программе, обычно не используется. |
|
3Ch |
Offset to PE Header Смещение реального PE заголовка в файле, DWord |
|
min 40h |
Программы-заглушки, на это поле указывает ReloOfs заголовка DOS 2 Header, соответственно его значение должно быть >=40h. |
|
min 40h + XXh |
Тело DOS программы, которая чаще всего говорит о невозможности запуска (”This program cannot be run in DOS mode“). 40h-нижняя граница данного поля. |
|
XXh |
PE Header Заголовок PE файла, который нас и интересует |
|
XXh |
Object Table Таблица объектов, содержит описаний секций файла. |
|
XXh |
Image Pages (import, export, fixup, resource, debug, …) Собственно основная часть, содержащая секции, служебные таблицы и т.д…. |
Для запуска файл должен быть в формате PE, минимально необходимо для этого, чтобы он был: во-первых EXE (байты по смещению 0h равны 5A4Dh – “MZ“), во-вторых, слово по смещению 18h должно быть >=40h, тогда и только тогда поле смещения PE Header по адресу 3Ch имеет смысл.
Для нахождения заголовка PE в файле воспользуемся полем Offset to PE Header, находящемуся по смещению 3Ch от начала файла.
Слова DWord и Word, обозначающие тип данных, имеют размеры 4 и 2 байта соответственно.
Формат PE-заголовка представлен в следующей таблице:
|
Смещение |
Размер |
Название |
Описание |
|
00h |
DWord |
Signature Bytes |
Сигнатура того, что этот файл является PE - должна быть 4550h – “PE”, два последних байта зарезервированы и должны быть равны 0h. |
|
04h |
Word |
CPU Type |
Поле указывает на тип процессора, под которым желательно запускать данную программу, обычно равно 14Ch – “i386″ |
|
06h |
Word |
Num of Objects |
Поле указывает на количество элементов таблицы Object Table |
|
08h |
DWord |
Time/Date Stamp |
Поле даты и времени создания/модификации файла при сборке |
|
0Ch |
DWord |
Pointer to COFF table |
Указатель, определяющий местонахождение отладочной COFF таблицы в файлах |
|
10h |
DWord |
COFF table size |
Количество символов в COFF таблице |
|
14h |
Word |
NT Header Size |
Размер заголовка PE файла, начиная с поля Magic, таким образом, полный размер заголовка PE файла составляет NT Header Size + 18h (смещение поля Magic) |
|
16h |
Word |
Flags |
Флаги, указывающие на предназначение программы, с битовыми значениями:
|
|
18h |
Word |
Magic |
Поле указывает на предназначение программы, обычно равно 010Bh. |
|
1Ah |
Byte |
Link Major |
Старший номер версии использовавшегося при создании линкера |
|
1Bh |
Byte |
Link Minor |
Младший номер версии использовавшегося при создании линкера |
|
1Ch |
DWord |
Size of Code |
Размер программного кода в файле, используется для фактического отведения памяти под загружаемую программу |
|
20h |
DWord |
Size of Init Data |
Размер секции инициализированных данных |
|
24h |
DWord |
Size of UnInit Data |
Размер секции неинициализированных данных |
|
28h |
DWord |
Entry point RVA |
Адрес относительно ImageBase, по которому передается управление при запуске программы или адрес инициализации/завершения библиотеки, т.н. точка входа |
|
2Ch |
DWord |
Base of Code |
RVA секции, которая содержит программный код |
|
30h |
DWord |
Base of Data |
RVA секции, которая содержит данные |
|
34h |
DWord |
Image Base |
Виртуальный начальный адрес загрузки программы (ее первого байта). |
|
38h |
DWord |
Object align |
Выравнивание программных секций, должен быть степенью 2 между 512 и 256М включительно |
|
3Ch |
DWord |
File align |
Выравнивание секций в программном файле, указывает на границу, по которую секции дополняются 0 при размещении в файле. Должен быть степенью 2 в диапазоне от 512 до 64К включительно. |
|
40h |
Word |
OS Major |
Старший номер версии ОС, необходимый для запуска программы. |
|
42h |
Word |
OS Minor |
Младший номер версии ОС |
|
44h |
Word |
USER Major |
Пользовательский номер версии, задается пользователем при сборке программы |
|
46h |
Word |
USER Minor |
Аналогично, младший номер |
|
48h |
Word |
SubSys Major |
Старший номер версии подсистемы |
|
4Ah |
Word |
SubSys Minor |
Аналогично, младший номер |
|
4Ch |
DWord |
Reserved |
Зарезервировано |
|
50h |
DWord |
Image Size |
Виртуальный размер в байтах всего загружаемого образа, вместе с заголовками, кратен Object align |
|
54h |
DWord |
Header Size |
Общий размер всех заголовков: DOS Stub + PE Header + Object Table |
|
58h |
DWord |
File CheckSum |
Контрольная сумма всего файла, как правило ее никто не контролирует. |
|
5Ch |
Word |
SubSystem |
Подсистема, необходимая для запуска файла (GUI-0002h, консоль-0003h, …) |
|
5Eh |
Word |
DLL Flags |
Указывает на специальные потребности при загрузке, устарел и не используется. |
|
60h |
DWord |
Stack Reserve Size |
Память, резервируемая для стека приложения. |
|
64h |
DWord |
Stack Commit Size |
Память, отводимая в стеке немедленно после загрузки. |
|
68h |
DWord |
Heap Reserve Size |
Максимально возможный размер локальной кучи (heap) |
|
6Ch |
DWord |
Heap Comit Size |
Размер отводимой при загрузке кучи |
|
70h |
DWord |
Loader Flags |
Не используется, связано с поддержкой отладки |
|
74h |
DWord |
Num of RVA and Sizes |
Указывает размер массива VA/Size, который следует ниже, зарезервирован и равен 10h |
|
78h |
DWord |
Export Table RVA |
RVA адрес таблицы экспорта |
|
7Ch |
DWord |
Export Data Size |
Размер таблицы экспорта |
|
80h |
DWord |
Import Table RVA |
RVA адрес таблицы импорта |
|
84h |
DWord |
Import Data Size |
Размер таблицы импорта |
|
88h |
DWord |
Resource Table RVA |
RVA адрес таблицы ресурсов |
|
8Ch |
DWord |
Resource Data Size |
Размер таблицы ресурсов |
|
90h |
DWord |
Exception Table RVA |
RVA адрес таблицы исключений |
|
94h |
DWord |
Exception Data Size |
Размер таблицы исключений |
|
98h |
DWord |
Security Table RVA |
RVA адрес таблицы безопасности |
|
9Ch |
DWord |
Security Data Size |
Размер таблицы безопасности |
|
A0h |
DWord |
Fix Up’s Table RVA |
RVA адрес таблицы настроек |
|
A4h |
DWord |
Fix Up’s Data Size |
Размер таблицы настроек |
|
A8h |
DWord |
Debug Table RVA |
RVA адрес таблицы отладочной информации |
|
ACh |
DWord |
Debug Data Size |
Размер таблицы отладочной информации |
|
B0h |
DWord |
Image Description RVA |
RVA адрес строки описания модуля |
|
B4h |
DWord |
Description Data Size |
Размер строки описания модуля |
|
B8h |
DWord |
Machine Specific RVA |
RVA адрес таблицы значений, специфичных для микропроцессора |
|
BCh |
DWord |
Machnine Data Size |
Размер таблицы значений, специфичных для микропроцессора |
|
C0h |
DWord |
TLS RVA |
RVA указатель на локальную область данных цепочек |
|
C4h |
DWord |
TLS Data Size |
Размер области данных цепочек |
|
C8h |
DWord |
Load Config RVA |
? |
|
CCh |
DWord |
Load Config Data Size |
? |
|
D0h |
08h |
Reserved |
Зарезервировано |
|
D8h |
DWord |
IAT RVA |
Указывает на таблицу адресов импорта в файле (помимо структуры импорта) |
|
DCh |
DWord |
IAT Data Size |
Размер поля IAT |
|
E0h |
08h |
Reserved |
Зарезервировано |
|
E8h |
08h |
Reserved |
Зарезервировано |
|
F0h |
08h |
Reserved |
Зарезервировано |
При этом VA - виртуальный адрес, который уже базирован на смещении Image Base, RVA - относительный адрес, ссылающийся на Image Base. RVA в PE Header, имеющий нулевое значение, указывает на то, что соответствующее поле не используется.
Сразу за заголовком в файле располагается таблица объектов. Число входов в таблице объектов (секций) определяется полем Num of Objects заголовка PE Header. Последовательность секций кода и данных в памяти выбирается линкером. Каждая секция (объект) располагает именем, которое никого ни к чему не обязывает, имя может быть произвольным, но вообще-то смысл содержания секции и ее наименования как правило совпадают.
Структура таблицы объектов показана в следующей таблице:
|
Смещение |
Размер |
Название |
Описание |
|
00h |
08h |
Object Name | Имя объекта, остаток заполнен нулями |
|
08h |
DWord |
Virtual Size |
Виртуальный размер секции, именно столько памяти будет отведено под секцию. Если Virtual Size превышает Physical Size, то разница заполняется нулями, так определяются секции неинициализированных данных (Physical Size = 0) |
|
0Ch |
DWord |
Section RVA | Размещение секции в памяти, её виртуальный адрес относительно Image Base. |
|
10h |
DWord |
Physical Size | Размер секции (ее инициализированной части) в файле, должно быть меньше или равно Virtual Size. |
|
14h |
DWord |
Physical Offset | Физическое смещение относительно начала EXE файла. |
|
18h |
0Ch |
Reserved | Зарезервировано |
|
28h |
DWord |
Object Flags | Битовые флаги секции со значениями:
|
Примеры стандартных названий секций Object Name:
- .text - исполняемый код Microsoft
- CODE - исполняемый код Borland
- .data - секция данных Microsoft
- DATA - секция данных Borland
- .bss - неинициализированные данные
- .CRT - инициализированные данные Borland C/C++
- .rsrc - ресурсы
- .idata - секция импорта
- .edata - секция экспорта
- .reloc - таблица настроек
- .tls - данные, на базе которых Windows запускает цепочки
- .rdata - отладочная информация
При этом следует отметить, что практически все упаковщики и крипторы исполняемых файлов создают свои секции, у многих из них также стандартные имена, например UPX0, .decode, pec1 и т.д.
Пример заголовка программы “Калькулятор” с указанием всех элементов представлен на рисунке. Здесь можно скачать архив с полной BMP-версией [23kb].

На данном фрагменте легко прослеживаются попытки Microsoft все выровнять, выстроить и структурировать. Вследствие этого существует достаточно много способов заражения (в данном контексте – записи своего кода в код существующей программы без потери ее работоспособности) файлов формата PE, основные три из них:
- внедрение в пустое пространство;
- расширение последней секции;
- добавление новой секции;
Рассмотрим подробно способ внедрения в пустое пространство
В некоторых других статьях этот метод называется внедрением в заголовок, хотя это не совсем так. Классическим примером использования является вирус Win95.CiH (Чернобыль).
Откуда же берется это пустое пространство?
Участок, помеченный на рисунке красным цветом, находится между последним элементом таблицы объектов и смещением первой секции. А появился он в результате выравнивания, за которое отвечает поле File Align по смещению 3Ch в заголовке.
Чтобы получить “координаты” и размер указанной области необходимо знать:
a- Размер PE-заголовка (обычно F8h, но лучше NT Header Size+18h, значение берется из PE Header)
b- Смещение PE-заголовка (поле Offset To PE header по смещению 3Ch от начала файла, содержит DWord)
c- Размер таблицы объектов (значение поля Num of Objects по смещению 06h из PE Header умножаем на размер объекта, т.е. на 40)
d- Физическое смещение первой секции (значение Physical Offset по смещению 14h из таблицы объектов для первого элемента).
Таким образом, началом пустой области у нас будет место в файле с физическим смещением a+b+c, а концом – место в файле со смещением d, соответственно размер области равен d-(a+b+c)
Попробуем получить это значение для какого-нибудь файла (сразу оговорюсь, с “Калькулятором” это не пройдет из-за оптимизации расположения импорта).
Следует сразу оговориться, что размер этой области не резиновый, поэтому кусок внедряемого кода очень ограничен. Приступим к программированию
Создаем новый проект в Builder, пишем несколько строчек кода, компилируем и берем полученный EXE файл на исследование. Можно взять любой другой не сжатый и не оптимизированный EXE файл. Builder можно не закрывать, программировать еще будем
После открытия файла считываем из него значение смещения PE заголовка (PEHeaderOffset), это DWord по смещению 3Ch.

На рисунке показано это значение в Hex-редакторе, оно равно 0200h (вспоминаем про порядок расположения байтов).

Так и есть, по адресу 0200h наблюдаем начало PE заголовка. Создаем для него структуру:

На рисунке отмечены необходимые нам поля: синее – количество элементов в таблице объектов; красное – размер заголовка от поля Magic, зеленое – RVA точки входа в программу.

Выяснив количество элементов (NumObjects) можно рассчитать размер таблицы объектов. В данном случае по смещению 06h от начала заголовка у нас находится число 8, таким образом размер таблицы составляет 8*40=320 байт.
К этому моменту мы уже нашли 3 необходимые нам переменные:
a = F8h (получаем как поле NTheaderSize+18h, у нас NTheaderSize=E0h)
b = 200h (получили из PEheaderOffset)
c = 140h (рассчитали из количества объектов NumObjects*40)
Осталось найти последний элемент, а именно физический адрес смещения первой секции в файле. Для этого мы используем структуру элемента таблицы объектов, а конкретно ее поле PhysicalOffset.

Считываем значения в созданную структуру, получаем, физическое смещение первой секции равное 0600h.

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

В данном случае мы считали PE заголовок, нашли количество объектов и создали динамический массив структур элементов таблицы объектов. Также мы нашли начало таблицы объектов на основании того, что она следует сразу за заголовком.
Считываем все элементы таблицы объектов в созданный массив структур и находим физическое смещение первой секции. После этого вычисляем начало пустого пространства как сумму размера всех заголовков и размера таблицы объектов. Концом пустого пространства является начало 1-й секции, разница между ними и будет размером.
В данном случае началом у нас является сумма Смещение заголовка PE (200h) + размер заголовка (F8h)+ размер таблицы объектов (140h)= 438h
Конец пустой области = 600h
Разница: 1C8h
Проверим, так ли это. Открываем файл в Hex-редакторе и идем к смещению 438h.

Так и есть, на этом месте заканчивается таблица объектов описанием последней секции .reloc и начинается пустое место, заполненное нулями вплоть до адреса 600h. Таким образом, мы получили 456 байт неиспользуемого пространства в файле, которое можно занять под свои нужды.
А дальше можно попробовать несколько вариантов: поменять точку входа (обозначена зеленым цветом на иллюстрации PE заголовка) на этот кусок, как мы уже делали в этом примере, или сделать еще хитрее – по адресу точки входа поставить jmp на эту область, не меняя точки входа. Но это уже дело фантазии. В заключение хочется сказать бессмертную фразу: не пишите вирусы и трояны, они не делают мир лучше!
Статья основана на труде Sars/HiTech, взятого с http://wasm.ru, а также описании Hard Wisdom’а, которое выложено в библиотеке. Его я настоятельно рекомендую скачать и изучить, потому что последующие примеры будут также основаны на нем.

В таблице объектов ошибка: смещение поля Object Flags должно быть равно 18h+0Ch=24h, соответственно размер структуры равен 24h+4h=28h. В источнике, откуда взята эта инфа, та же ошибка
Считаем:
ObjectName смещение 0, размер 8 байт
VirtualSize смещение 8, размер 4 байта
SectionRVA смещение 12, размер 4 байта
PhysicalSize смещение 16, размер 4 байта
PhysicalOffset смещение 20, размер 4 байта
Reserved смещение 24, размер 12 байт
ObjectFlags смещение 36, размер 4 байта.
Смещение 36d = 24h, всё правильно