Самомодифицирующиеся программы воспринимаются как нечто магическое, но при этом они весьма просты, и чтобы это продемонстрировать, я напишу такую программу под x86 архитектуру в NASM.
Базовый факториал
Для начала нам понадобится обычная программа вычисления факториала.
factorial:
push ebp
mov ebx, eax
factorial_start:
sub ebx, 1
cmp ebx, 0
je factorial_end
mul ebx
jmp factorial_start
factorial_end:
pop ebp
ret
Здесь все довольно просто.
Самомодифицирующийся факториал
В алгоритме вычисления факториала есть два места, в которых изменение значения при выполнении имеет смысл: начальное значение и множитель.
Технические особенности
Во-первых, самомодифицирующиеся программы имеют свою специфику. По умолчанию nasm собирает программу без возможности ее дальнейшей самостоятельной модификации, потому что раздел
.text
из соображений безопасности отмечается как не перезаписываемый. Чтобы изменить флаги этого раздела для активации возможности записи потребуется задействовать objcopy
и кастомную программу.Мой скрипт для сборки этих программ лежит здесь.
Начальное значение
В исходном коде начальное число передается через регистр
eax
. Чтобы использовать для этого самомодифицирующийся код, первым делом потребуется, чтобы в начале функции присутствовала обнуляющая инструкция mov
для eax
. _start:
mov dword [factorial+2], 0x5
call factorial
factorial:
push ebp
mov eax, 0
Как видите, для передачи начального значения программа изменяет инструкцию
mov eax
. Значение 0
этой инструкции на 2 байта смещается от начала метода factorial
. Множитель
factorial_start:
; multiply
mov ebx, 0
mul ebx
Выше представлена заглушка, используемая для умножения. Далее нам нужна логика для установки
mov ebx, 0
, его декрементирования и выхода из цикла.Инициализация множителя
Для установки множителя берем
ebx
, где хранится его первое значение, и копируем это значение в mov eax, 0
в начало метода factorial_start
.factorial:
...
mov dword [factorial_start+1], ebx ; init decrementer
Декрементирование множителя
В стандартной программе логика будет такой:
- декрементировать множитель;
- если он окажется 0, выйти;
- перепрыгнуть назад.
В нашей самомодифицирующейся программе изменяется единственная деталь – декрементирование множителя.
Для этого необходимо получить его текущее значение, уменьшить это значение и скопировать обратно.
factorial_start:
...
; decrement
mov ebx, dword [factorial_start+1]
sub ebx, 1
mov dword [factorial_start+1], ebx
Результат
Совмещая все это, получаем:
extern printf
section .data
format: db "num: %d",10,0
section .text
global _start
_start:
mov dword [factorial+2], 0x5 ; start number
call factorial
; print result
push eax
push format
call printf
; exit
mov eax, 1
mov ebx, 0
int 80h
factorial:
push ebp
mov eax, 0
mov ebx, eax
sub ebx, 1
mov dword [factorial_start+1], ebx ; init decrementer
mov ebx, 0
factorial_start:
; multiply
mov ebx, 0
mul ebx
; decrement
mov ebx, dword [factorial_start+1]
sub ebx, 1
mov dword [factorial_start+1], ebx
; exit if at 0
; could exit at 1, but then it doesn't handle 0x2
cmp ebx, 0
je factorial_end
; loop back
jmp factorial_start
factorial_end:
pop ebp
ret
Заключение
Я нахожу самомодифицирующиеся программы довольно интересными – их код выглядит несколько иначе, немного беспорядочен и содержит пустые значения, но при этом продумывать для них логику очень увлекательно.
Применяются они в различных областях, в основном относящихся к обфускации – к примеру, при реализации защиты лицензий или вредоносного ПО. Я подумываю создать на этом принципе собственный упаковщик или, по меньшей мере, прикольный crackme.
Если вам интересно познакомиться с другими примерами самомодифицирующихся программ под x86, то милости прошу в мой репозиторий.
Комментарии (49)
alexxisr
24.12.2021 17:46+2openbsd вобще запрещает ставить одному сегменту и запись и исполнение одновременно. Приходится специальные флаги монтирования фс выставлять для разрешения.
electric13
24.12.2021 20:02+5Нужна ли команда
cmp ebx, 0
Ведь чуть ранее sub уже устанавливает флаги.
zvszvs
24.12.2021 21:35Точно. Не нужна. Тем более, что в своей сути cmp работает точно как sub, только результат никуда не записывает, а, как раз, устанавливает флаги.
Medeyko
25.12.2021 20:17-1Только есть одна тонкость - sub ebx, 1 / cmp ebx, 0 иногда работает быстрее, чем sub ebx, 1. Современные процессоры ориентируются на тупые конструкции. Поставьте вместо sub ecx, 1 / jnz loop_label логичное, созданное для этого loop loop_label - и удивитесь, насколько медленнее стал работать цикл.
qw1
25.12.2021 21:46Не поленился, проверил на intel Comet Lake.
Результат совпадает такт-в-такт, что есть лишний cmp, что нет егоСкрытый текст#include <windows.h> #include <stdio.h> int main() { __int64 tm1, tm2; QueryPerformanceCounter((LARGE_INTEGER*)&tm1); __asm { mov ebx, -1 l: sub ebx, 1 //cmp ebx, 0 jnz l } QueryPerformanceCounter((LARGE_INTEGER*)&tm2); printf("%I64d", tm2-tm1); return 0; }
zvszvs
26.12.2021 14:34Ну мое подтверждение касалось лишь отсутствия необходимости лишней инструкции. Думаю, что временные параметры могут зависеть от поколения процессоров. Кстати, не удивлюсь, если современные процессоры способны просто игнорировать вторую (ненужную) инструкцию, а это выигрывает время.
ARad
24.12.2021 21:56+1Если для развлечения то еще понимаю. Такая самомодификация напрочь убивает возможность многопоточности.
speshuric
26.12.2021 15:31+2И тут кому-нибудь придёт в голову: "Но что если делать мьютексы и спинлоки путём самоисправления кода?". В этот момент должен явственно почувствоваться запах серы, а инженеры, проектирующие процессоры и взаимодействие кешей дьявольски хохочут.
qw1
24.12.2021 22:09+7Ожидал какой-нибудь красивый трюк, а в итоге программа с само-модификацией получилась длиннее и сложнее исходной. Тогда зачем всё это, модификация ради модификации?
lorc
26.12.2021 07:29+1Красивые трюки точно есть в гипервизоре Xen. Возможно в ядре линукса тоже.
Там при первоначальной инициализации определяется конкретный тип процессора и код в fast path модифицируется под этот процессор. Чаще всего включаются фиксы для разных errata.
qw1
26.12.2021 09:58+2Но это скорее кодогенерация, чем самомодификация. Различие в том, что при кодогенерации код динамически создаётся один раз и много раз выполняется.
mvv-rus
24.12.2021 23:44+3Есть серьезное подозрение, что на оригинальном 8086 (а может, и на 8088, т.е. на IBM PC и IBM PC XT — тут надо уже смотреть листинг ассемблера, а мне лень) этот код работать не будет: у этих процессоров была очередь команд размером 6(8086)/4(8088) байт, которая никак не реагировала на изменение в памяти команд, уже выбранных в очередь.
Кстати, один из методов борьбы с отладчиками тех времен был основан на этом принципе: защищавшаяся программа модифицировала свой код, который уже должен был быть в очереди. Если программа не находилась под пошаговой отладкой, то использовался оригинальный код, иначе эта модификация попадала в очередь команд и исполнялась. Ну, а дальше все зависело от фантазии программистов.Medeyko
25.12.2021 20:12+2На 8086/8088 этот код явно работать не будет - там не было 32-битных регистров :). 80286 и выше сбрасывают очередь, так что на всех 32-битных процессора x86 это работать будет.
Если же не обращать внимание на это, то mov bx, 0 занимает три байта. На 8088 в 16-битном аналоге ещё не будет загружено в очередь при модификации, должно бы работать.
warus
25.12.2021 10:19+2хе раньше такой способ само-кодирования дешифровки программ на ассемблере назывался метод бабы яги
Medeyko
25.12.2021 19:43+4Я писал на ассеблере для разных архитектур, в том числе несколько лет писал для x86, но правда, довольно давно. Может быть, конечно, я отстал от жизни, но мне несколько дико видеть написанное.
Вот взять прямо первый пример. Зачем там сохранять и восстанавливать ebp, если он не изменяется? Чтобы руку не сбивать? Зачем делать лишний переход в цикле?
factorial2: mov ebx, eax jmp factorial2_start ; move eax, 1 или mov al, 1 factorial2_next: mul ebx factorial2_start: dec ebx jnz factorial2_next ret
Я бы сходу написал бы так, это без попыток оптимизации, просто напрашивается с ходу.
Я специально проверил, откомпилировал, провёл замеры времени - этот напрашивающийся вариант работает заметно быстрее предложенного автором. Ну и он короче в полтора раза.
Потом я бы заменил jmp factorial2_start на mov eaxl, 1 - убираем переход, который работает медленнее. (Если хочется минимизировать длину, то mov eaxl, 1 заменить на mov al, 1. Всё равно при параметре больше, чем 22h результат из-за переполнения будет нулевым.)
Или взять последний пример. Может быть в остальном есть какой-то тайный дидактический смысл, но вот в этой конструкции я никакого смысла предполжить не могу:
cmp ebx, 0 je factorial_end ; loop back jmp factorial_start factorial_end:
При замене je factorial_end / jmp factorial_start на jne factorial_start программа и короче становится, и работает быстрее.
Что касается самомодифицирующегося кода, то, по-моему, в приведённым примерах он мало оправдан. Хотя, разумеется, он бывает весьма красивым, но, как мне кажется, автору статьи это показать не удалось от слова совсем. Честно говоря, не очень понимаю, зачем так писать на ассебмлере.
petropavel
26.12.2021 14:38+1Он тут совсем не оправдан. По названию я предположил, что будет безусловный цикл с
jmp
, а потом его поменяют наret
. Тоже не фонтан, но в факториале сложно что-то новое придумать.Самомодифицирущийся код лучше показывать на примере какой-нибудь защиты, типа по паролю сама себя в памяти расшифровывает. Или смещения в jmp-ах менять. Выбрав нужную функциональность перед длинным циклом, чтоб избегать условных переходов. Тогда можно побенчмаркить и показать, что есть смысл (если он будет, branch prediction никто не отменял).
zvszvs
26.12.2021 14:42Выскажу предположение по поводу ebp. Возможно программа была написана на Си (или др.) и далее адаптировался ассемблерный листинг компилятора. Автор просто решил "не удалять" сохранение ebp по "только ему известным причинам". Ну или так больше похоже на "настоящую" функцию (автор показал, что знаком с прологом и эпилогом).
Кстати, остальные лишние и не оптимальные конструкции тоже могут объясняться источником ассемблерного кода.
Alexey_Alive
27.12.2021 04:00-1От "mov eax, 0" аж больно стало. Почему не xor?!
zvszvs
27.12.2021 14:27Ну тут ответ вроде в тексте ясно прописан. MOV потому, что программа "на лету" меняет константу в инструкции. Для простого обнуления XOR, конечно оптимальнее и короче.
zvszvs
Стоит отметить, что под Win64 используется рандомизатор загрузки образа, т.е. при каждом запуске программы стартовый адрес может (и будет) не одинаковым в том числе и для программ написанных под Win32. В следствии возникает проблема при записи в код констант, означающих адреса (например, адрес переменной) в виде константы.
Решение - указать в свойствах секции кода не только бит "разрешение записи", но и бит "грузить только по указанной базе".
Angeld
по умолчанию exe собирается с фиксированой базой, для загрузки по прозвольному адресу необходим специальный сегмент .reloc, который содержит список адресов в коде для настроки под нужную базу
ну и никто не мешает определить адрес по которому ты находишься, как это упаковщики и вирусы делают
zvszvs
Насколько я знаю, по умолчанию (честно, не знаю как в NASM, не пользовался) как раз адрес не фиксированный. Только если сами установите.
Таблица relocation работает только если вы указываете адрес переменной, используя ее имя. Если вы используете константу (т.е посмотрели адрес и просто указали число, как в моем комментарии и написано), то ни в какие relocation это не попадет, а значит при загрузке по другой базе будет большая проблема.
Узнать адрес "где ты находишься" можно и, в данном случае нужно. Именно на то, что проблема есть, и ее надо решать, я и пытался указать.
Это не спор с кем-то, а дополнение текста статьи для тех, кто захочет воспользоваться модификацией кода в runtime.
Angeld
так релокейшн не для ручных констант, а для тех которые компилятор в код вставляет, о том что твою константу тоже надо релоцировать компилятор не знает
он генерится компиляторами для обычного кода чтобы ОС могла загрузить приложение по любому адресу
и по умолчанию все exe идут без релокейшн с одинаковым адресом 0x40000... , а вот длл как раз с ней, потому что программа одна в своем адресном пространстве, а длл может быть сколько угодно. и их там надо как-то разместить чтоб не пересекались
но линкеру можно указать как другой базовый адрес, так и включить выключить генерецию релоков
zvszvs
Хорошо, что вы со мной согласны.
Вы реально решили мне объяснить это? Спасибо. :)
Нет. Relocation по умолчанию генерируется для всех EXE. 0x40000 - лишь "желаемый" адрес загрузки. Похоже лонгрид напрашивается.
Множество уязвимостей в серверах (типа Apache) эксплуатируются, исходя из того, что грузится он по одному адресу и дома у хакера и на сервере. Дома отлаживается эксплойт, потом однократно "передается" на сервер и там работает. Поэтому (и не только) в Win64 добавили рандомизатор загрузки. Ему плевать куда вы хотите грузиться - он выбирает случайно один из 256 выравненных адресов и туда грузит EXE. Каждый раз - разный. Очевидно, что без таблицы relocation он этого сделать не сможет. Поэтому они есть во всех EXE, если вы ее сами не "отрубите". Для "чувствительных" к адресу загрузки приложений и есть бит "грузить по желаемому адресу, иначе вообще не грузить".
Angeld
так каким образом win64 загрузит 32 битный exe в большинстве которых нет секции reloc?
там же куча адресов в коде константами расчитанными на фиксированный адрес
в новых компиляторах reloc может и генериться по умолчанию для exe, а вот в огромном количестве старых exe этого нет
zvszvs
В большинстве именно, что есть relocation. И нет, нет тех EXE где компилятор размещает "константы, расчитанные на фиксированный адрес". Такие вещи может сделать только сам программист. И это его головная боль как это решать. Об этом я изначально и писал.
Впрочем это выглядит как лекбез. Я лучше ссылку дам на общедоступный хэлп, например по опциям линкека в VS22: https://docs.microsoft.com/en-us/cpp/build/reference/dynamicbase-use-address-space-layout-randomization?view=msvc-170#remarks
Достаточно прочитать 10 строк в разделе Remarks.
Ответ на ваш вопрос очевиден - никак (т.е. придется грузить по тому адресу, который указан как желательный или вообще не грузить). Именно поэтому компиляторы производят таблицу relocation для всех EXE. Это было раньше, это есть сейчас. Это установилось как обязательный раздел еще в Win16, т.к. там программы разделяют общее адресное пространство, как DLL сейчас. Поэтому это не новшество никак.
Простите, когда вы пишите "в огромном количестве старых exe этого нет", вы реально это проверяли. Лично мой опыт (25+ лет занятия защитой прикладного ПО) показывает, что практически в 100% EXE есть таблица relocation.
Да, и таблица relocation не обязательно формируется в секцию ".reloc". Она может быть вообще вне секции. Доступ к ней загрузчик получает не так. Посмотрите формат PE заголовка.
Angeld
видел только один exe с релокейшн
и тот был с защитой, которая использовала эти данные
обычные же exe все без релоков. в 64 бит может уже не так, в 32 было именно так, и довольно странно для компилятора в dll создавать отдельную секцию для релоков, а для ехе ее прятать где-то
посмотрел сейчас несколько старых exe
у всех relocations stripped
видимо у вас специфика работы, для защиты exe обычно требуют с relocaitions.
zvszvs
Вот прямо сейчас посмотрел EXE в Windows (в том числе в SysWOW64). Все с таблицей relocation. В "Program Files (x86)" из всех произвольно выбранных - у всех таблица есть. А там полно известных утилит известных фирм. А как у вас с файлами в этих каталогах?
Можете назвать какое-нибудь известное ПО (чтобы я мог посмотреть), в котором нет этой таблицы?
Если вы программист, вы какой компилятор используете?
И вы не первый раз не внимательно читаете, что я пишу. Я сказал лишь, что таблица relocation может быть и не в секции ".reloc", т.к. загрузчику все равно в секции она или нет. Где вы увидели, что "ее компиляторы куда-то прячут в EXE"? Они создают ее ровно так же как и в DLL. А вот различные инструменты могут и перемещать ее.
И еще. Упаковщики EXE (уверен, знаете что это такое) могут не иметь этой таблицы - их задача сжать образ. Но в сжатом образе эта таблица все равно есть.
Для защиты relocation как раз не нужна - она раскрывает детали работы программы.
Angeld
известные по которые сейчас используются сделаны свежими компиляторами
а так возми любою игру до 2000 года
позже не особо показательно будет, там большинство из-под защиты
и защите вполне нужна информация о деталях работы программы
все продвинутые защиты сейчас имеют функцию перевода части кода программы в свою виртуальную машину
zvszvs
Мне 53 года, я хорошо знаю формат исполняемых файлов со времен DOS (вообще CPM). В EXE файлах DOS и Win16, в которых используется хотя бы одна глобальная переменная, вообще не возможно без таблицы relocation (хотя формат заголовка там, конечно не как в Win32+).
Среди игр под Win32 до 2000 года некоторая часть была без таблицы, т.к. ценился размер файла и лишний килобайт экономили.
Только странно сейчас говорить о том, что было 20 лет назад. Только как история, возможно.
Могу лишь повторить, что наличие таблицы relocation наоборот дает лишнюю информацию взломщику. Приведите хоть один пример зачем нужна таблица relocation для защиты.
Для "перевода" (серьезно?) своего кода в собственную VM используются собственные механизмы. Посмотрите, например DENUVO, как работал StarFoce.
firehacker
Нет, не «так как ценился размер файла», а так как у Microsoft-овского линкера поведением по-умолчанию было подразумевать ключ /FIXED (означающий «сгенерировать неперемещаемый образ»), если генерируется EXE-файл, а не DLL.
Такая тактика даже закреплена в официальной MS-овской спецификации на форматы COFF и PE:
А вообще, читайте большой коммент. Ваш спор (zvszvs и Angeld) со стороны выглядит странно: вроде бы оба что-то знают на тему PE/COFF, но у обоих в голове каша — даже не только не уровне знаний, сколько на уровне подхода к самому спору.
zvszvs
Не понял что за "большой комент" (ссылка верная?)
Интересно чем это противоречит тому, что я написал?
Начиная с появления 32-х битного защищенного режима (386+) и реализации его в Win32 (начиная с WinNT и затем в Win2000+) появилась возможность использовать виртуальное личное 4Гб адресное про-во. Поэтому считалось, что каждый EXE сможет грузиться по тому адресу по которому желает. Как и @Angeldзаметил, в DLL она все равно была нужна, т.к. они делил общее пространство. Так зачем по вашему ее исключал из образа линкер у EXE?
И вы за все линкеры сразу говорите? Я лично знаю (помню) несколько компиляторов Си, которые вполне себе таблицу relocation оставляли.
И я бы воздержался от подобного "у обоих в голове каша" (лично мне вот видится наоборот). Хотя бы прочтите с чего именно начался диалог (мой первый коммент).
Angeld
любые собственные механизмы должны понимать, что тут адрес и его надо корректировать соответствующим образом, что выполнить код в своей вм. существуют защиты которые на основе таблицы релоков затирают код в этих местах, и восстанавливают его во время выполнения.
Со старфорс я знаком, у меня были инструменты и документация по защите
В мире закрытого то что было 20 лет назад имеет как раз очень большое значение, для многих программ ожидать свежего exe не приходится.
zvszvs
Хорошо. Я не буду с вами спорить.
qw1
Так что наоборот, чтобы найти EXE без релоков, надо очень поискать.
Angeld
свежие делают
года до 2005 не делали по умолчанию, хотя и можно было включить
qw1
Хм. У меня есть в коллекции разные компиляторы, готовые к использованию. Проверил: MSVC6 из VS98 — не делает. MSVC7.1 из VS2000 — не делает. Delphi 7 (2002) — делает.
firehacker
Нет такого бита в «свойствах секции». Это бит relocs stripped поля Characteristics из NT-заголовка.
Дальнейшее ваше обсуждение с Angeld тоже расстраивает: оба не в состоянии разделить компилятор и линкер и спорите с изначально неправильным подходом к спору и к определению истины.
Ваш спор о том «по умолчанию имеют ли EXE фиксированную базу» лишён смысла в первую очередь потому, что он построен так, как будто существует какой-то общемировой догмат/стандарт/подход о способе формирования EXE, и в его рамках есть своё «по-умолчанию». А его нет.
Во-первых, будет PE-файл иметь возможность загружаться по произвольной базе или будет иметь фиксированную базу зависит не от компилятора, а от линкера. Линкер, а не компиятор формирует основообразующие структуры PE-файла. Линкер, а не компилятор, определяет, будет ли в поле Characteristics стоять флаг IMAGE_FILE_RELOCS_STRIPPED. Линкер, а не компилятор, формирует таблицу релокаций. Компилятор определяет только набор секций, и начинку и характеристики, а линкер склеивает начинку всех одноимённых секций всех входных объектных файлов, комбинирует характеристики секций, формирует окончательную таблицу секций, разрешает зависимости/проставляет адреса, формирует прочие структуры PE-файла и даёт на выходе готовый исполняемый файл. Поэтому нужно спорить не «имеют ли EXE-файлы релокации вообще в принципе» и не «делают ли компиляторы EXE-файлы перемещаемыми или нет», а нужно говорить о линкере.
Во-вторых, линкер — он не как бог в монотеических религиях. Нет единого истинного линкера со своим линкерским догматом, всё кроме которого считалось бы ересью. Очевидно, что различных линкеров существует очень большое число. Многие из них вообще не имеют никакого отношения к генерации PE-файлов и миру Windows соответственно: те же линкеры из *nix-мира, формирующие ELF-бинарники, или вообще линкеры, используемые при сборке прошивок для микроконтроллеров.
Но даже если взять из всего множества линкеров те, что заточены на формирование исполняемых бинарников под Windows в формате PE, то и таких линкеров много. Есть линкер от Microsoft, который идёт в составе Visual C++ и DDK/WDK/Platform SDK. Который на вход принимает объектные файлы формата COFF. Есть линкер от Borland, который шёл в составе Delphi и C++ Builder, который на вход принимает объектные файлы формата OMF, кстати говоря. Есть линкер ulink от Юрия Харона, который в своих статьях постоянно восхвалял Крис Касперски. Есть, надо полагать, ряд линкеров из никс-мира, способных при желании генерировать PE-бинарники из объектных файлов формата ELF — в рамках кросс-компиляции/кросс-сборки. Есть линкеры из продуктов менее известных, из какого-нибудь Lazarus-а, например. Есть, наконец, самодельные/экспериментальные линкеры.
И все эти линкеры ведут себя по разному. Более того: «линкер от Microsoft» (или любой другой из выше упомянутых) — это не один единственный инструмент из палаты мер и весов. Это целый зоопарк версий одного и того же инструмента. И одна версия может вести себя так, а другая версия уже иначе.
Так о чём вы спорите, господа?
Ваш спор имел бы какой-то смысл, если бы вы конкретно говорили: «линкер от Microsoft такой-то версии по-умолчанию делает EXE-файлы неперемещаемыми». Тогда был бы предмет спора, была бы правая и неправая сторона. Можно было бы поставить эксперимент, и можно было бы пойти и ткнуть кого-то носом в документацию. Или вообще, если говорить о способах получения EXE какими-то инструментами, вообще не предполагающими использование линкера (например каким-нибудь FASM-ом) — то так и нужно ставить вопрос «когда FASM генерирует EXE сразу из ассемблерного исходника, минуя стадию формирования объектных файлов и линковку, делает ли он образ перемещаемым по умолчанию».
Ещё раз: будет ли в PE-файле присутствовать таблица релокаций, или же её не будет, а зато будет установлен флаг IMAGE_FILE_RELOCS_STRIPPED — зависит от линкера. И в этом плане способности линкеров и тактика по умолчанию может отличаться от линкера к линкеру. Microsoft-овский линкер умееть делать и так так.
Ключ /FIXED заставить его не делать таблицу релокаций. Ключ /FIXED:NO заставит его сделать образ перемещаемым. Долгое время для Microsoft-овского линкера тактикой по умолчанию было подразумевание /FIXED для EXE, и /FIXED:NO для DLL/OCX/SYS и всего остального. Оно и сейчас позиционируется так же:
/FIXED:NO is the default setting for a DLL, and /FIXED is the default setting for any other project type.
В какой-то момент был добавлен ключик /DYNAMICBASE для ASLR, который подразумевает /FIXED:NO.
В третьих, вы может быть спорите не насчёт того, формируют ли инструменты сборки EXE-файл с или без таблицы релокаций, а спорите о том, требует ли Windows от EXE-файлов наличие перемещаемости? Опять же, вопрос не имеет смысла без уточнения версии ОС. Большинству версий ОС непосредственно на наличие перемещаемости наплевать: и не только для EXE, но и для DLL. Если участок виртуального адресного пространства процесса, в которое PE-образ хочет (в соответствии со своим полем ImageBase) быть спроецирован, свободен, а сторона, инициировавшая загрузку PE-образа, не потребовала сделать загрузку по нестандартной базе — PE-образ (хоть EXE, хоть DLL) будет загружен, даже если он перемещаемый. Если PE-образ не может быть загружен туда, куда он хочет быть загруженным, а должен быть загружен по непривычной для него базе — либо потому что «родному» базовому адресу он не может быть загружен, так как это участок виртуального АП чем-то занят, либо потому что сторона, инициировавшая загрузку, потребовала загрузить образ по указанному адресу (MapViewOfFileEx имеет такую опцию) — тогда, если он не перемещаемый, загрузка PE-файла не удастся (будь-то хоть EXE, хоть DLL).
Но если вы собираетесь свой PE-файл запускать под Win32s (это 32-битная подсистема для 16-битных Windows 3.1/3.11). Поэтому я не зря выше сказал, что нужно уточнять версию ОС. После появления технологии ASLR, очевидно, жёсткого требования, что все бинарники должны быть перемещаемыми, не появилось — это бы уничтожило обратную совместимость. Поэтому если ASLR включен, а PE-файл не предполагает перемещаемости, он будет загружен так, как если бы ASLR не действовало.
В-четвёртых, когда вы ниже говорите «компиляторы (не) генерирует релокации» и спорите на эту тему, это не просто маленькая оговорка, которую можно оправдать фразой «да ладно, все же прекрасно поняли, что мы имеем в виду линкеры, а не компиляторы». Это пускание рассуждений по ложному пути, потому что при желании к этим словам можно прицепиться.
Потому что на самом деле в рамках PE\COFF существует два разных типов сущностей, чуть-чуть близких друг к другу по назначению, но всё-таки сильно разных — и обе называются релоками/релокациями.
И это не одно и то же. Это два разных типа сущностей: за ними стоят разные структуры данных, разные назначения и отличающиеся константы. Использование одинакового или почти одинакового названия способно вызывать у людей путаницу; и когда читаешь подобные диспуты, со стороны даже не понятно, то ли спорящие сами путают понятия, то ли пытаются запутать оппонента.
Первое — это relocations в COFF-файлах (объектных файлах, OBJ-файлах). Этот тип информации не имеет никакого отношения к загрузке исполняемых файлов по нестандартному адресу. Как и PE-файл (который являет собой химеру из MZ- и COFF форматов), COFF-файл имеет внутри себя таблицу секций и сами секции. Это почти такие же секции, как в испоняемом PE-файла (как минимум таблица секций в COFF имеет абсолютно тот же формат/структуру записей — IMAGE_SECTION_HEADER), но это как правило большое количество довольно мелки секций, которые на этапе линковке будут перегруппированы, часть секций будет отброшено, а оставшиеся одноимённые секции будут склеены.
Так вот у каждой секции COFF-файла есть собственная таблица релокаций (на неё и её размер указывают поля PointerToRelocations и NumberOfRelocations структуры IMAGE_SECTION_HEADER). Записями/элементами этой таблицы являются тройки вида {адрес_в_рамках_секции; тип_релокации; индекс_элемента_таблицы_символов} — и эта таблица является ни чем иным, как таблицей ссылок на внешние сущности/символы, с той лишь оговоркой, что ссылки вполне себе могут быть и не на внешние символы. Это ключевая структура для осуществления процесса линковки: именно пользуясь таблицей релокаций (точнее таблицами релокаций — у каждой секции COFF-файла она собственная) и таблицей символов линкер занимается тем, что во всех местах, где в коде/данных результирующего файла должна быть ссылка (указатель, адрес, относительное смещение) на какую-то сущность из другого COFF-файла (или даже из того же самого), он правильным образом проставляет эти указатели/адреса/относительные смещения.
В этом ключе каждая «релокация» описывается структурой IMAGE_RELOCATION размером 10 байт (не кратно 4, да):
Первое поле содержит смещение места (в рамках COFF-секции) от начала COFF-секции. Второе поле содержит ссылку (в виде индекса) на элемент таблицы COFF-символов, описывающий сущность, на которую в данном месте секции после окончания линковки должна оказаться корректная ссылка того или иного вида. Третье поле содержит тип релокаций — по скольку «ссылки», вшитые в код или данные, могут быть разного типа. На ум приходит, что как минимум это могут быть абсолютные и относительные адреса (то есть уже два типа ссылок), в реальности же форматом COFF предусмотрено гораздо боле вариантов, причём для каждой процессорной архитектуры (I386, MIPS, ALPHA, PowerPC, ARM, IA64) набор типов свой.
Этот тип релоков формируется компилятором и формируется ВСЕГДА. Поэтому постановка вопросов «формирует ли компилятор релоки» не такой уж невинный, и откупиться фразой «ой, да ладно, мы под компилятором имели в виду совокупность из компилятора и линкера вообще и линкер в частности» не получится.
Этот тип информации о релокациях используется при линковке (он является абсолютно ключевым для процесса линковки), но дальше процесса линковки он не идёт. В сформированном PE-файле в таблице секций поля PointerToRelocations и NumberOfRelocations всегда будут занулены — в PE-файла нет ни места таблицам релокаций, ни смысла для их существования.
Тем не менее, релокации этого рода влияют на формирование релокаций второго года.
Второй тип информации, который тоже называется релокациями — это информация совершенно иного рода и формата. Если первый тип присутствует только в объектных файлах, и никогда не присутствует в исполняемых файлах, то второй тип, наоборот, может присутствовать в исполняемых файлах, но никогда не присутствует в объектных файлах.
Это по сути, перечень всех мест в готовом исполняемом файле, которые нужно подправить (adjust), если образ будет проецироваться по нестандартной базе.
Если в COFF-файлах таблиц релокаций много (у каждой секции своя собственная — на неё указывается IMAGE_SECTION_HEADER::PointerToRelocations ), то в PE-файле таблица релокаций, если и есть, то одна — и указывает на неё директория релокаций (OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]).
Если в COFF-файлах таблица релокаций была гомогенной таблицей 10-байтовых записей с тремя полями (троек: {смещение; индекс_символа; тип_релока}), то в PE-файлах таблица релокаций представляет собой блочный список записей вида {тип_релока; смещение}: каждый блок описывает область образа размером 4 Кб и содержит переменное число двухбайтовых (а не 10-байтовых, как в COFF) структур вида {тип_релока; смещение} — смещение в данном случае отсчитывается от начала 4К-страницы, описываемой данным блоком, а в качестве типов релокаций используется совершенно другой (по сравнению с релокациями в COFF) набор констант:
И наконец третий тип информации, связанный с релокациями второго типа, который тоже формируется линкером и тоже на основе релокаций первого типа из COFF-файлов: это таблцица FIXUP-ов, которая попадает в debug-директорию PE-файла или в отдельный DBG-файл, если отладочная информация из PE-файла извлекается+изымается. В то время как в PE-формате есть таблица директорий, и одна из директорий типа IMAGE_DIRECTORY_ENTRY_DEBUG описывает блок отладочной информации, внутри отладочной информации есть своя таблица директорий — таблица отладочных директорий. В этой таблице отладочных директорий может присутствовать директория IMAGE_DEBUG_TYPE_FIXUP, описывающая таблицу фиксапов.
Фиксапы — это тройки вида {тип; rva_ссылки; rva_места_куда_ссылаются}. Эта таблица перечисляет все места в готовом исполняемом бинарнике, где применена абсолютная адресация. Как не трудно догадаться, она сформирована на основе COFF-релокаций из COFF-файлов (но не только на её основе!), подобно тому, как таблица base relocations в PE-файле сформирована линкером на основе COFF-релокаций
Она похожа таблицу COFF-релокаций в том плане, что это тоже гомогенный набор структур с тремя полями. Она похожа на таблицу base-релокаций PE-файла тем, что как и таблица base-релокаций, fixup-таблица одна на весь образ (а таблица COFF-релокаций у каждой секции своя). В отличие от COFF-релокаций, хоть и тут и там мы имеем набор структур с тремя полями, в fixup-таблице нет никаких ссылок на таблицу символов (хотя таблица символов является частью отладочной информации и таблица fixup-ов тоже является частью отладочной информации). В отличие от таблицы base-релокаций в таблице fixup-ов перечислены не только места, где применена абсолютная адресация и которые требуют корректировке при загрузке не по предпочтительной базе, но и места, где применена RVA-адресация (а она применена в таблицах импорта/экспорта/ресурсов). Места с RVA-адресацией не перечислены в base-релокациях PE-файла: при загрузке по нестандартной базе такие места корректировать не нужны, но таблица fixup-ов в составе отладочной информации решает немного другую задачу: обеспечение хот-патч-абильности образа, судя по всему. В отличие от таблицы COFF-релокаций, в таблице fixup-ов не перечислены места, где используется относительная адресация (call/jmp/jcc-инструкции). Тем не менее, как и таблица COFF-релокаций, таблица fixup-ов использует тот же набор констант для поля «тип».
Angeld
изначально речь шла о том что win64 рандомизирует адрес загрузки любого приложения
на что было указано, что существует много exe не содержащий информации для этого
те релокции в объектный файлах отношения к вопросу не имеют, речь шла только о exe файле.
firehacker
Не имеют, но если о них не написать, придёт кто-нибудь умный и к моей придирке «компилятор релокации не генрирует, их генерирует линкер» сам придирётся фразой «нет правда, компилятор релоки тоже же генерирует» (но это уже совсем
другая историядругие релоки).В целом, в этом диспуте, я больше на вашей стороне: я думаю, если на моём компьютере просканировать все EXE-файлы, то единственным, который будет иметь релокации, окажется OllyDbg, который стал перемещаемым не в угоду ASLR, а по совсем другой причине.
qw1
Вы сидите на WinXP? Последние 10-15 лет дефолтным поведением для компиляторов стало оставлять релокации в EXE-файлах. У себя на диске с релокациями я наблюдаю все EXE от Microsoft (Windows, VS, Office), от Google (Android SDK), Valve (Steam).
firehacker
ASLR появилась в Windows Vista, а это 2007 год. До введения ASLR делать подобное поведение дефолтным не имело никакого смысла, а значит дефолтным оно могло стать не более, чем 13 лет назад. Этому предшествовало как минимум 17 лет существования Microsoft-овского линкера с совершенно другим дефолтным поведением.
И, если уж быть точным в формулировках, поведение «если /FIXED:NO не указан явно, а мы делаем EXE — подразумевать /FIXED и не делать релокаций» как было дефолтным в линкере, так и осталось до сих пор.
Другое, что добавили ключ /DYNAMICBASE, одним из побочных эффектов применения которого (но не единственным) является «считать, что указан /FIXED:NO».
То есть дефолтное поведение изменено не непосредственно, а как побочный эффект от другого нововведения: с точки зрения стороннего наблюдателя дефолтное поведение всей системы действительно изменилось, но только в случае, если линкер запускается вообще с минимальным набором ключей командной строки.
qw1
Я лишь удивился тому, что все EXE-файлы (кроме OllyDbg) на вашей системе старше 13 лет. Как так получилось?
firehacker
Поправлю некоторые опечатки, а то пост уже не отредактировать:
даже если он не перемещаемый.
умеет делать и так, и так.
Но если вы собираетесь свой PE-файл запускать под Win32s (это 32-битная подсистема для 16-битных Windows 3.1/3.11), образ обязательно должен быть перемещаемым и иметь релоки.
zvszvs
Спасибо еще раз за приведение общеизвестной информации. Хотелось бы понять по какому именно пункту вы считаете, что я ошибаюсь кроме следующего?
Когда я писал: " Решение — указать в свойствах секции кода не только бит "разрешение записи", но и бит "грузить только по указанной базе"." я действительно описался - он не в свойствах секции (что очевидно). Это никак не меняет сути. Такой бит есть, лишь в другом месте. Делать из этого вывод " оба не в состоянии разделить компилятор и линкер ", мне кажется немного преждевременным. Вы не учитель в школе?
Мне кажется неправильным писать слишком объемные комментарии, приводя уже давно хорошо задокументированную информацию. Я не про вас, про себя. Поэтому, когда я пытаюсь выразить лишь суть, мой комментарий может выглядеть "неполным", а для кого-то "неверным". Я не против. Если кто-то считает, что я "вообще ничего не понимаю", я тоже не против. Если кто-то спрашивает - я уточняю.
И на счет спора - прочитайте мой первый коментарий. К нему еще вопросы у вас есть?
firehacker
Вывод о путанице между компилятором и линкером делается не из этого, а уже из другого: из того факта, что во всех ваших (в смысле у вас двоих, а не лично Ваших) постах ни разу не фигурировало слово «линкер», а везде фигурирова «компилятор». Он тоже генерирует некие релоки, но совершенно не те.
Наличие или отсутствие в исполняемом файле информации, необходимой для перемещаемости образа, зависит от ключей командной строки, скормленных именно линкеру, а в случае, если соответствующие ключи не указаны — зависит от того, что за линкер используется и какая его версия. Одним этим выделенным жирным предложением в споре можно было бы поставить жирную точку. Без указания того, про какой линкер и про какую версию идёт речь, продолжать спорить не имело смысла. Но вы оба продолжили, причём спорили даже не о линкерах, а о компиляторах, которые тут вообще не причём.
Потом вы пишите:
и дополняете, что вам 53 и вы собаку съели на знании тонкостей формирования исполняемых файлов. Ну так тогда бы вы знали, что Microsoft-овский линкер большую часть истории своего существования по умолчанию предполагал именно /FIXED для EXE-файлов. Хотя, почему — может вы все эти годы с совершенно другим линкером имели дело. Но тогда с каким? То есть то ли вы такой опытный, но не знаете про дефолтность /FIXED для EXE и /FIXED:NO для DLL в MS-овском линкере, то ли вы просто работали с каким-то другим линкером, знаете его, возможно, довольно хорошо, но как будто бы в вашей картине мира других линкеров нет.
Дальше идёт:
По умолчанию где? Вы можете знать или не знать NASM, но NASM — не компоновщик. И выставлять флаг relocs stripped или же не выставлять его, а генерировать таблицу base-релокаций — это вообще не компетенция NASM-а. Как и MASM, NASM на входе принимает ассемблерный исходник, а на выходе выплёвывает объектный файл. Не исполняемый файл, а объектный. В котором нет места таблице релокаций (в том смысле, в каком она есть в PE) и нет места флагу IMAGE_FILE_RELOCS_STRIPPED.
Вот эти все ваши мелкие оговорки: то флаг не в таблцие секций (хотя он в PE-заголовке), то компилятор вместо линкера (в споре о том, должен ли линкер делать EXE-файлы перемещаемыми), то вы от NASM-а ждёте один из двух вариантов активности (и не знаете, какой там по умолчанию), которые свойствены линкерам, тогда как в NASM-е нет одного из двух вариантов, так как он не вбирает в себя функции линкера — всё это в совокупности создаёт впечатление, что вы плаваете в теме, не знаете, где проходит граница раздела ответственности между компиляторами и линкером.
Опять таки, решать проблему с ASLR вы предлагаете установкой флага IMAGE_FILE_RELOCS_STRIPPED, хотя логично было бы ассемберный код подправить так, чтобы вшитые в нём адресные константы попали в таблицу релокаций, либо же код самомодификации написать так, чтобы он определял реальную базу, вычислял дельту релокации на основании разницы между фактической базой и предпочитаемой базой и приплюсовывал эту дельту релокации всюду, где он в генерируемый машинный код вплетает адресные константы, заложенные в код на стадии написания ассемблерного исходника.
zvszvs
Ну, то есть, я понял с кем имею дело. Напомню, я спросил " прочитайте мой первый коментарий. К нему еще вопросы у вас есть?" Не вижу ни одного вопроса.
Вы - чистый "grammar nazi" в отношении документации по компиляторам. "Не смей писать отсебятину - пиши строго по стандарту!" - вот ваш дивиз. Ок.
К сожалению вы задели меня и придется быть таким же занудой, как вы. Откину вежливость и только факты.
Вранье. Откройте глаза и почитайте внимательно.
Мы не спорили, а дискутировали какого года EXE файлы имели таблицу relocation, а когда нет. Этот диалог, надеюсь пошел нам обоим во благо. Но нет, некоторые решили всех поставить на место. Спасибо. Жду от вас детальной статьи по поводу компиляторов и линкеров, где я обязательно найду пару опечаток и прокомментирую.
Вы просто провидец. )
Они есть. Больше того, у меня есть свой собственный линкер и компилятор (о чудо, у меня это 2 в одном и называю я его компилятор). Я обязательно приведу "другие" (не мои) линкеры, когда вы напишите соответствующую статью на habr.
Нормально. Человек видит, что я не знаю что такое NASM и обвиняет меня, что я не знаю, что он, оказывается, не компановщик. Не знаю что тут комментировать.
Если вы реально читали мои комментарии, то там написано (во втором), что " Это не спор с кем-то, а дополнение текста статьи для тех, кто захочет воспользоваться модификацией кода в runtime." Ну ведь верно, как я не догадался, факториал только под линукс же считать можно...
Ну да, у вас ведь ни одной "оговорки" (я написал об одной опечатке, при чем не по сути, а по форме) нет. Вы же не писали потом коммент о ваших собственных оговорках. Браво! Отличная придирка. )
Я написал самое простое решение. Когда началась дискуссия, я написал и остальные. Ровно то, что вы тут с пафосом излагаете (по мне так очевидные вещи). Вы этого не увидели? Да вы точно не читали всей дискуссии.
Адью.
mayorovp
Один хрен, что компилятор, что линкер. Так-то они лишь в Си и С++ традиционно разделены, и то обычным делом является вызов линкера из компилятора. А в других языках отдельного компоновщика отродясь не было, либо он был достаточно хорошо спрятан.
В любом случае, от замены линкера на компилятор и обратно ни одно из сообщений этой ветки смысла не теряет.
firehacker
Автор писал не на Си или C++, а на ассемблере, используя компилятор NASM, и внезапно, линкер не является частью NASM-а. Даже в Visual Basic, где компилятор реально встроен в IDE и не существует в виде отдельного исполняемого файла, линкер, тем не менее, не встроен в IDE, а является отдельным бинарником — отдельным инструментом.
Вы говорите по отношению к компоновщику фразу «в языках». Как будто бы линкер это как пятое колесо к какому-то из языков программирования. Линкер вообще не должен быть «в языке» или связан с языком. Это отдельный компонент в конвейере по производству исполняемых файлов.
Отстранённость линкера от компиляторов и языков программирования позволяет в рамках одного проекта написать часть исходного кода на Си, часть на Си++, часть на ассемблере, часть на паскале, часть на самодельном узко-специфичном языке программирования, а потом всё это слинковать в один исполняемый файл. При желании туда же может попасть фортран, кобол и прочая экзотика.
Исполняемые файлы без кода (resource-only DLL в Windows) тоже вполне себе обычное дело, и в рождении таких файлов линкер по прежнему нужен, но никакой компиятор и никакого ЯП участия уже не принимают. Для Windows приложений .res-файл утилитой CVTRES.EXE превращается в .obj-файл и подаётся на вход линкеру. Сам же .res файл может быть либо собран каким-то редактором ресурсов, либо компиятором resource script'ов RC.EXE из resource script-а — файла с текстовым описание дерева ресурсов. И без крайних частных случаев вроде resource-only DLL — в случае с обычными исполняемыми файлами, имеющими внутри себя ресурсы — ровна та же схема (resource script —> [RC.EXE] —> бинарный res-файл —> [CVTRES.EXE] —> объектный файл COFF-формата —> [линкер] —> исполняемый файл) остаётся актуальной.
Само по себе существование такой распределённой и гибкой (в стиле unix-way — делай одно маленькое дело, но делай его хорошо) концепции и стандартизированность объектных файлов (COFF у MS, OMF у Borland, ELF в мире юникс) это сродни существованию стандартов вроде ATX или PCI в железе).
Одни компании производят материнские платы, платы расширения, планки памяти, корпуса, накопители, соответствующие стандартам, но не собирают готовые ПК. Другие организации не производят электронику, но могут собрать компьютер из комплектующих от совершенно разных производителей.
С точки зрения любого технаря любая стандартизация и даруемая ей взаимозаменяемость есть благо. Но есть и другой подход в индустрии: делать полностью готовый компьютер-моноблок из своих нестандартных комплектующих полностью самим. Технари скалят зубы, зато маркетологи бьются в экстазе.
То, что вы из «новой волны» программистов, которые нонсенсом считают случаи, когда продукт может быть построен из исходников сразу на двух-трёх-четырёх ЯП, когда продукт собирается из makefile-а, билд скрипта, батника, а не из IDE со свистелками и блек-джеком, куда глубоко и крепко встроен компилятор, в котором хорошо запрятан линкер — примерно понятно, но ничего хорошего в этом не вижу.
Не теряет, потому что его изначально не было: «как оно будет» зависит ровно от ключей командной строки, а если их нет, от «модели» и версии линкера. Вспоминать про игры 2000-го, защиты типа Starforce, бежать проверять в разных IDE и закономерно получать противоречивые результаты — это всё было совершенно бессмысленным.