В этой статье будет рассказано про самый маленький файл формата PE - Portable Executable (меньше уже некуда). Программа размером всего в 153 байта показывает окно функцией MessageBoxA, после завершается. Успешно запускается в Windows XP! Насколько всё-таки гибким оказался этот формат файлов, да и загрузчик тоже не критично его обрабатывает, кроме того программа корректно запустится в DOS - на экран будет выведено следующее: "Program too big to fit in memory" - замечательно, тем более что от STUB ничего не осталось. Между MZ и PE всего два байта - конечно заголовки наложены - удалось даже оба правильно заполнить, а одно очень важное поле ну просто совпало по размеру и значению в обоих. Также удалось найти хорошее место для описания одной единственной секции, место нашлось тоже в заголовке PE, что было немного неожиданно. Адреса функций определяет сам загрузчик, в файле есть таблица импорта, такая маленькая. Сам код расположен в неиспользуемых и не критичных по содержанию полях заголовков и различных структур файла, там же имена функций и библиотек, причём последние записаны в сокращённом формате (есть и такой). И вообще всё наложено и урезано наверное уже предельно. Теперь всё подробнее. Предполагается, что читатель хоть немного знает Assembler.

Итак начало файла.

db "MZ"
db ".."
db "PE"
dw 0
dw 14Ch

В начале, конечно, MZ, потом следующее поле в его заголовке должно содержать размер последней страницы файла старого типа - там две точечки, далее PE завершающиеся двойным нулём - это сигнатура современных исполнимых файлов для Windows и она наложена в старом заголовке на длину файла в страницах и число элементов в таблице настройки адресов, в первом буквы, во втором ноль. Получилась строка с завершающим нулём - именно она и будет показана в окне. Далее идёт двухбайтное поле со значением 14Ch - это в новом заголовке означает тип процессора i386 и выше, а в старом заголовке это просто длина заголовка в параграфах, так что поле не критично для старого заголовка, но очень важно для нового.

MBOX:
dw 1
db "MessageBoxA",0
dw Firtst_Object-Optional_NT
dw 10Fh

Идём дальше. Следующее двухбайтное поле содержит 1, в старом заголовке там должно быть минимальное количество памяти в параграфах, которое нужно выделить после
загрузки модуля, а в новом заголовке там количество секций в файле - важное поле - у меня в файле одна секция, поэтому там единичка. Как видно дальше довольно нагло заполнены целых 12 байтов - строка
"MessageBoxA", она перекрыла целых 6 полей в старом заголовке: максимальное количество памяти в параграфах, которое нужно выделить после загруженного модуля, сегментное смещение для установки сегмента стека - это для того, чтобы при запуске старого типа файла правильно установить значение в регистр сегмента стека SS - так получилось, что это поле совпадает с фрагментом строки, как раз содержащей буковки "ss", далее стартовое значение для SP, контрольная сумма, точка входа IP, сегментное смещение для установки регистра
CS.

Это всё, что закрыла строка в старом заголовке для DOS, что же там для Windows в новом заголовке? А там время создания файла линкером - 4 байта и целых 8 зарезервированных байтов, точнее неиспользуемых, хотя в winnt.h написано, что это указатель на таблицу символов и количество символов, например в kernel32.dll в Windows XP там нулевое значение. Далее очень важное поле - размер опционального заголовка - это в новом формате, а в старом формате там смещение в файле первого элемента таблицы настройки адресов. Собственно это поле проверяет загрузчик (это смещение 18h), если там 40h, или больше (иногда я видел, что требовалось именно равенство) тогда двойное слово по смещению 3Ch содержит смещение начала PE заголовка, хотя на это уже давно не проверяется. У меня получилось так, что там оказалось ровно 40h - что немного меньше чем размер опционального заголовка, но загрузчик на это не обращает внимание. На самом деле 
поле ему нужно чтобы вычислить адрес массива структур, содержащих описания свойств секций (такой макрос даже есть в winnt.h IMAGE_FIRST_SECTION), он просто использует его в вычислениях, а единственная структура в этом массиве находится внутри самого опционального заголовка. Вот как! Далее 10Fh - флажки для нового формата и номер оверлея для старого, конечно флажки важнее. Заметим, что есть ещё метка MBOX, это уже описание импорта наложено на это место. Структура такая есть в winnt.h (IMAGE_IMPORT_BY_NAME), сначала там двухбайтное значение - всего лишь подсказка загрузчику для быстрого поиска в таблице имён, оно вовсе не должно быть правильным - это просто стартовый индекс для двоичного поиска, далее имя функции с завершающим нулём. Кстати получилось, что размер образа старого файла должен быть 2749840 байтов - поэтому и получим соответствующее сообщение от DOS.

Optional_NT:
dw 10Bh

Вот, наконец, начинается опциональный заголовок. И причём как раз тут закончился старый заголовок.
Значение 10Bh - это специальное значение, про него мало что известно, но загрузчик его проверяет и оно должно быть именно таким - это значение IMAGE_NT_OPTIONAL_HDR32_MAGIC, впрочем есть ещё варианты для этого поля, но они для других случаев. Далее целых 14 свободных байтов, там я разместил часть кода, здесь же точка входа.

EntryPoint:

xor bp,bp
push bp
db 0BBh
dd IMAGE_BASE
push bx
push bx
push bp
jmp short next_1
db 0

Это написано так, потому что файлик я компилировал как в .COM формат, на самом деле для Windows там будет следующее:

xor ebp,ebp
push ebp
mov ebx,IMAGE_BASE
push ebx
push ebx
push ebp
jmp short next_1
db 0

Ну, в конце один байтик оказался лишним. Это уже параметры для вызова MessageBoxA, помещаются в стек и совершается короткий переход в другое место, где продолжается код. Тут я перекрыл такие поля: версия линкера - 2 байта - вообще не критично, размер всего кода - 4 байтовое поле, размер инициализированных данных - 4 байтовое поле и размер не инициализированных данных - 4 байтовое поле - ничего из этого не проверяется загрузчиком.

dd RVA EntryPoint

next_1:
db 0FFh,15h
dd RVA ADDRESS_MBOX+IMAGE_BASE
retn
db 0

Далее следует точка входа, написано RVA - Relative Virtual Address - это терминология загрузчика, относительный виртуальный адрес, относительно базы (Image Base) загруженного модуля, в данном случае просто смещение от начала файла, так как у меня в этом файле смещение от начала совпадает с RVA. Потом снова свободное место - там продолжение кода программы. Целых 8 байтов, в одном источнике написано, что они зарезервированы, в другом что это базовый адрес кода и базовый адрес данных, в любом случае загрузчику всё равно что там, хотя на этот раз в kernel32.dll правильные значения. Итак код там такой:

next_1:

call dword ptr __imp__MessageBoxA@16
retn
db 0

И опять один байт лишний. Выполнится вызов MessageBoxA, после стоит одна инструкция retn - она считает со стека значение адреса и передаст по этому адресу управление. Управление попадёт примерно на такой код:

push eax
call ExitThread
int 3

Это находится где-то глубоко в kernel32.dll, видно что на этом исполнение закончено. Идём дальше.

dd IMAGE_BASE
dd 4
dd 4

Тут предпочитаемый адрес загрузки модуля - у этого файла нет релокаций, поэтому по этому адресу и загрузится (нет и конфликтов среди адресов библиотек), далее четырёхбайтное поле по смещению 3Ch именно оно содержит смещение начала PE заголовка, кроме того в самом заголовке это то на сколько нужно выравнивать секции в памяти, PE как раз по смещению 4, далее выравнивание секций в файле, тоже на 4 байта.

user32_dll:

db "user32",0,0
dd 4
dd 0
dd RVA The_End
dd RVA Section

Вот и имя библиотеки, оно записано без расширения - это сокращённый формат, расширение по умолчанию .dll, если оно другое, то должно быть указано, если его нет, тогда нужно в конце имени поставить точку. Подробности можно узнать в описании функции LoadLibraryEx. Тут строка закрывает такие поля: версия операционной системы и версия исполнимого файла - это последнее вообще заполняется программистом при линковке. Далее 4 - версия подсистемы NT далее странное поле и по всей видимости неиспользуемое - пусть будет нулевым, потом идёт размер всего образа файла в памяти и размер всех заголовков вместе с таблицей описания секций.

Firtst_Object:

dd 0
dw 2
dw 0
dd The_End-Section
dd RVA Section
dd The_End-Section
dd RVA Section
dd 0
dd 2
dd 0
dd 60300020h
dd RVA Import

Ну теперь пришло место описания первой и единственной секции в этом файле. Причём это описание удалось впихнуть в сам опциональный заголовок - это значит, что поля наложены, но тем не менее заполнены корректно. Первые 8 байт для описания секции - это имя секции - там может быть всё что угодно, загрузчику абсолютно всё равно, что там. Но зато в заголовке там находятся поля: контрольная сумма - не критично для данного случая - пусть будет нулём, подсистема - это конечно 2 (IMAGE_SUBSYSTEM_WINDOWS_GUI - Image runs in the Windows GUI subsystem) и флаги библиотеки - конечно там ноль.

Дальше в описании секции должно быть по порядку: виртуальный размер секции (сколько памяти будет занимать), виртуальный адрес начала секции (выровненный RVA начала секции), физический размер (сколько она в файле занимает), положение её в файле от начала (просто смещение). В данном файле простые смещения и относительные виртуальные адреса совпадают (что совсем не обязательно в общем случае), физический и виртуальный размеры у секции тоже можно указать одинаковые. Всё равно загрузчик выделяет память страницами по 1000h (4096) байт и предварительно заполняет её нулями - потом это будет важно.

А в заголовке на месте размеров и смещений должны быть по порядку: размер стека необходимый программе, обязательный размер стека, размер кучи необходимой программе, обязательный размер кучи. Загрузчик увидит столь малые значения и всё равно назначит как минимум по одной странице, хотя в данном случае много и не надо. Далее в описании секции идут 3 неиспользуемых поля, но в заголовке это: флаги загрузчика (тоже не используется - пусть там ноль), количество записей в массиве адресов-размеров (сам массив расположен ниже, сразу после этого поля, значение поля - 2 - в файле нужна таблица импорта, но указатель на неё идёт вторым в этом массиве, первый - это экспорт), относительный виртуальный адрес таблицы экспорта - там конечно ноль, это указывает на то, что таблицы экспорта нет. Следующее поле в описании секции должно содержать флаги секции - 60300020h - это значение попало в поле размера таблицы экспорта, но ведь её нет, так как указатель на таблицу экспорта нулевой, поэтому всё нормально и загрузчик об этом знает. Да, даже можно сказать точнее - загрузчику вообще всё равно (ну, почти, за редким исключением кое каких случаев), что записано в поле размера таблицы, ему важен адрес. И вот следующая запись в массиве - это импорт, указатель на таблицу импорта и её размер (об этом ниже).

Section:

Import:

dd 0

user32:

ADDRESS_MBOX:

dd RVA MBOX
dd 0
dd RVA user32_dll
db RVA user32

The_End:

Началась секция и как видно опять произошло наложение структур и уже идёт сама таблица импорта, а в её описании в массиве адресов-размеров в поле её размера попадёт нулевое значение, но загрузчику это совсем не важно, зато это позволило немного сжать файл.

Итак, таблица импорта состоит из массива структур - описателей импорта из одной библиотеки, и этот массив должен завершаться структурой с нулевым полем адреса имени библиотеки. В данном случае нужна одна запись для user32.dll и одна вырожденная запись, опять всё сжато. В записи всего 5 полей: флаги (всего только одно нулевое значение, или указатель ...) именно у меня там и ноль - это значит используем
terminating null import descriptor, поле времени и поле перенаправления - там расположен ещё один массив, относительный виртуальный адрес на имя библиотеки и на массив указателей на описатели функций из данной библиотеки. Поле времени не критично - это позволило разместить там массив указателей на описатели функций - громко звучит, всего одна запись и одна нулевая запись - она попадает как раз в поле перенаправления, а там у большинства файлов ноль. Всё как надо. Заметим, что последнее поле в структуре - относительный виртуальный адрес массива указателей на описатели функций будет вполне влезать в байт и старшие байты его будут нулевыми, и именно поэтому в самом файле оставлен только младший байт. В памяти ведь всё равно будет двойное слово, загрузчик выделит для всего образа 4096 байтов памяти, занулит их и закопирует туда байты из файла (а в данном случае образ в файле просто совпадёт с образом в памяти),
то есть можно считать, что после идут одни нули, вот этим и воспользуемся. Причём где-то ещё должна быть вырожденная запись описания библиотеки, а она будет в памяти после загрузки, там как раз нужное поле будет нулевым. Теперь можно получить это файлик, если добавить наверх два определения:

RVA EQU (-100h)+offset
IMAGE_BASE EQU 800000h

И всё это откомпилировать как код для простого старого файла типа .COM, переименовать в mzpe153.exe! Получим такой результат:

(mzpe153.exe)

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии