Самомодифицирующиеся программы воспринимаются как нечто магическое, но при этом они весьма просты, и чтобы это продемонстрировать, я напишу такую программу под 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)


  1. zvszvs
    24.12.2021 16:47
    +2

    Стоит отметить, что под Win64 используется рандомизатор загрузки образа, т.е. при каждом запуске программы стартовый адрес может (и будет) не одинаковым в том числе и для программ написанных под Win32. В следствии возникает проблема при записи в код констант, означающих адреса (например, адрес переменной) в виде константы.
    Решение - указать в свойствах секции кода не только бит "разрешение записи", но и бит "грузить только по указанной базе".


    1. Angeld
      24.12.2021 18:24
      +5

      по умолчанию exe собирается с фиксированой базой, для загрузки по прозвольному адресу необходим специальный сегмент .reloc, который содержит список адресов в коде для настроки под нужную базу

      ну и никто не мешает определить адрес по которому ты находишься, как это упаковщики и вирусы делают


      1. zvszvs
        24.12.2021 21:19
        +1

        Насколько я знаю, по умолчанию (честно, не знаю как в NASM, не пользовался) как раз адрес не фиксированный. Только если сами установите.
        Таблица relocation работает только если вы указываете адрес переменной, используя ее имя. Если вы используете константу (т.е посмотрели адрес и просто указали число, как в моем комментарии и написано), то ни в какие relocation это не попадет, а значит при загрузке по другой базе будет большая проблема.
        Узнать адрес "где ты находишься" можно и, в данном случае нужно. Именно на то, что проблема есть, и ее надо решать, я и пытался указать.
        Это не спор с кем-то, а дополнение текста статьи для тех, кто захочет воспользоваться модификацией кода в runtime.


        1. Angeld
          24.12.2021 23:00
          +1

          так релокейшн не для ручных констант, а для тех которые компилятор в код вставляет, о том что твою константу тоже надо релоцировать компилятор не знает

          он генерится компиляторами для обычного кода чтобы ОС могла загрузить приложение по любому адресу

          и по умолчанию все exe идут без релокейшн с одинаковым адресом 0x40000... , а вот длл как раз с ней, потому что программа одна в своем адресном пространстве, а длл может быть сколько угодно. и их там надо как-то разместить чтоб не пересекались

          но линкеру можно указать как другой базовый адрес, так и включить выключить генерецию релоков


          1. zvszvs
            25.12.2021 14:44

            так релокейшн не для ручных констант, а для тех которые компилятор в код вставляет, о том что твою константу тоже надо релоцировать компилятор не знает

            Хорошо, что вы со мной согласны.

            он генерится компиляторами для обычного кода чтобы ОС могла загрузить приложение по любому адресу

            Вы реально решили мне объяснить это? Спасибо. :)

            и по умолчанию все exe идут без релокейшн с одинаковым адресом 0x40000...

            Нет. Relocation по умолчанию генерируется для всех EXE. 0x40000 - лишь "желаемый" адрес загрузки. Похоже лонгрид напрашивается.
            Множество уязвимостей в серверах (типа Apache) эксплуатируются, исходя из того, что грузится он по одному адресу и дома у хакера и на сервере. Дома отлаживается эксплойт, потом однократно "передается" на сервер и там работает. Поэтому (и не только) в Win64 добавили рандомизатор загрузки. Ему плевать куда вы хотите грузиться - он выбирает случайно один из 256 выравненных адресов и туда грузит EXE. Каждый раз - разный. Очевидно, что без таблицы relocation он этого сделать не сможет. Поэтому они есть во всех EXE, если вы ее сами не "отрубите". Для "чувствительных" к адресу загрузки приложений и есть бит "грузить по желаемому адресу, иначе вообще не грузить".


            1. Angeld
              25.12.2021 18:12

              так каким образом win64 загрузит 32 битный exe в большинстве которых нет секции reloc?

              там же куча адресов в коде константами расчитанными на фиксированный адрес

              в новых компиляторах reloc может и генериться по умолчанию для exe, а вот в огромном количестве старых exe этого нет


              1. zvszvs
                25.12.2021 18:34
                -1

                В большинстве именно, что есть 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 заголовка.


                1. Angeld
                  25.12.2021 19:52
                  +2

                  видел только один exe с релокейшн

                  и тот был с защитой, которая использовала эти данные
                  обычные же exe все без релоков. в 64 бит может уже не так, в 32 было именно так, и довольно странно для компилятора в dll создавать отдельную секцию для релоков, а для ехе ее прятать где-то

                  посмотрел сейчас несколько старых exe

                  у всех relocations stripped

                  видимо у вас специфика работы, для защиты exe обычно требуют с relocaitions.


                  1. zvszvs
                    25.12.2021 20:57
                    +1

                    Вот прямо сейчас посмотрел EXE в Windows (в том числе в SysWOW64). Все с таблицей relocation. В "Program Files (x86)" из всех произвольно выбранных - у всех таблица есть. А там полно известных утилит известных фирм. А как у вас с файлами в этих каталогах?
                    Можете назвать какое-нибудь известное ПО (чтобы я мог посмотреть), в котором нет этой таблицы?
                    Если вы программист, вы какой компилятор используете?
                    И вы не первый раз не внимательно читаете, что я пишу. Я сказал лишь, что таблица relocation может быть и не в секции ".reloc", т.к. загрузчику все равно в секции она или нет. Где вы увидели, что "ее компиляторы куда-то прячут в EXE"? Они создают ее ровно так же как и в DLL. А вот различные инструменты могут и перемещать ее.
                    И еще. Упаковщики EXE (уверен, знаете что это такое) могут не иметь этой таблицы - их задача сжать образ. Но в сжатом образе эта таблица все равно есть.
                    Для защиты relocation как раз не нужна - она раскрывает детали работы программы.


                    1. Angeld
                      25.12.2021 22:55

                      известные по которые сейчас используются сделаны свежими компиляторами

                      а так возми любою игру до 2000 года

                      позже не особо показательно будет, там большинство из-под защиты

                      и защите вполне нужна информация о деталях работы программы

                      все продвинутые защиты сейчас имеют функцию перевода части кода программы в свою виртуальную машину


                      1. zvszvs
                        26.12.2021 14:27

                        Мне 53 года, я хорошо знаю формат исполняемых файлов со времен DOS (вообще CPM). В EXE файлах DOS и Win16, в которых используется хотя бы одна глобальная переменная, вообще не возможно без таблицы relocation (хотя формат заголовка там, конечно не как в Win32+).
                        Среди игр под Win32 до 2000 года некоторая часть была без таблицы, т.к. ценился размер файла и лишний килобайт экономили.
                        Только странно сейчас говорить о том, что было 20 лет назад. Только как история, возможно.
                        Могу лишь повторить, что наличие таблицы relocation наоборот дает лишнюю информацию взломщику. Приведите хоть один пример зачем нужна таблица relocation для защиты.
                        Для "перевода" (серьезно?) своего кода в собственную VM используются собственные механизмы. Посмотрите, например DENUVO, как работал StarFoce.


                      1. firehacker
                        26.12.2021 15:15
                        +2

                        Среди игр под Win32 до 2000 года некоторая часть была без таблицы, т.к. ценился размер файла и лишний килобайт экономили.

                        Нет, не «так как ценился размер файла», а так как у Microsoft-овского линкера поведением по-умолчанию было подразумевать ключ /FIXED (означающий «сгенерировать неперемещаемый образ»), если генерируется EXE-файл, а не DLL.


                        Такая тактика даже закреплена в официальной MS-овской спецификации на форматы COFF и PE:
                        image


                        А вообще, читайте большой коммент. Ваш спор (zvszvs и Angeld) со стороны выглядит странно: вроде бы оба что-то знают на тему PE/COFF, но у обоих в голове каша — даже не только не уровне знаний, сколько на уровне подхода к самому спору.


                      1. zvszvs
                        26.12.2021 15:43

                        Не понял что за "большой комент" (ссылка верная?)
                        Интересно чем это противоречит тому, что я написал?
                        Начиная с появления 32-х битного защищенного режима (386+) и реализации его в Win32 (начиная с WinNT и затем в Win2000+) появилась возможность использовать виртуальное личное 4Гб адресное про-во. Поэтому считалось, что каждый EXE сможет грузиться по тому адресу по которому желает. Как и @Angeldзаметил, в DLL она все равно была нужна, т.к. они делил общее пространство. Так зачем по вашему ее исключал из образа линкер у EXE?
                        И вы за все линкеры сразу говорите? Я лично знаю (помню) несколько компиляторов Си, которые вполне себе таблицу relocation оставляли.
                        И я бы воздержался от подобного "у обоих в голове каша" (лично мне вот видится наоборот). Хотя бы прочтите с чего именно начался диалог (мой первый коммент).


                      1. Angeld
                        26.12.2021 15:57

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

                        Со старфорс я знаком, у меня были инструменты и документация по защите

                        В мире закрытого то что было 20 лет назад имеет как раз очень большое значение, для многих программ ожидать свежего exe не приходится.


                      1. zvszvs
                        26.12.2021 16:18

                        Хорошо. Я не буду с вами спорить.


                  1. qw1
                    25.12.2021 21:27

                    видел только один exe с релокейшн
                    Все популярные компиляторы — Visual C++, Delphi,… делают секцию .reloc

                    Так что наоборот, чтобы найти EXE без релоков, надо очень поискать.


                    1. Angeld
                      25.12.2021 22:51
                      +2

                      свежие делают

                      года до 2005 не делали по умолчанию, хотя и можно было включить


                      1. qw1
                        26.12.2021 09:46
                        +2

                        Хм. У меня есть в коллекции разные компиляторы, готовые к использованию. Проверил: MSVC6 из VS98 — не делает. MSVC7.1 из VS2000 — не делает. Delphi 7 (2002) — делает.


    1. firehacker
      26.12.2021 15:05
      +5

      Решение — указать в свойствах секции кода не только бит "разрешение записи", но и бит "грузить только по указанной базе".

      Нет такого бита в «свойствах секции». Это бит 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» (или любой другой из выше упомянутых) — это не один единственный инструмент из палаты мер и весов. Это целый зоопарк версий одного и того же инструмента. И одна версия может вести себя так, а другая версия уже иначе.


      Так о чём вы спорите, господа?
      image


      Ваш спор имел бы какой-то смысл, если бы вы конкретно говорили: «линкер от 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 существует два разных типов сущностей, чуть-чуть близких друг к другу по назначению, но всё-таки сильно разных — и обе называются релоками/релокациями.


      • Есть концепция релоков в рамках COFF-файлов (объектных файлов, поступающих на вход линкеру).
      • Есть концепция релоков в рамках PE-файлов (исполняемых файлов, выходящих как продукт работы линкера).

      И это не одно и то же. Это два разных типа сущностей: за ними стоят разные структуры данных, разные назначения и отличающиеся константы. Использование одинакового или почти одинакового названия способно вызывать у людей путаницу; и когда читаешь подобные диспуты, со стороны даже не понятно, то ли спорящие сами путают понятия, то ли пытаются запутать оппонента.


      Первое — это relocations в COFF-файлах (объектных файлах, OBJ-файлах). Этот тип информации не имеет никакого отношения к загрузке исполняемых файлов по нестандартному адресу. Как и PE-файл (который являет собой химеру из MZ- и COFF форматов), COFF-файл имеет внутри себя таблицу секций и сами секции. Это почти такие же секции, как в испоняемом PE-файла (как минимум таблица секций в COFF имеет абсолютно тот же формат/структуру записей — IMAGE_SECTION_HEADER), но это как правило большое количество довольно мелки секций, которые на этапе линковке будут перегруппированы, часть секций будет отброшено, а оставшиеся одноимённые секции будут склеены.


      Так вот у каждой секции COFF-файла есть собственная таблица релокаций (на неё и её размер указывают поля PointerToRelocations и NumberOfRelocations структуры IMAGE_SECTION_HEADER). Записями/элементами этой таблицы являются тройки вида {адрес_в_рамках_секции; тип_релокации; индекс_элемента_таблицы_символов} — и эта таблица является ни чем иным, как таблицей ссылок на внешние сущности/символы, с той лишь оговоркой, что ссылки вполне себе могут быть и не на внешние символы. Это ключевая структура для осуществления процесса линковки: именно пользуясь таблицей релокаций (точнее таблицами релокаций — у каждой секции COFF-файла она собственная) и таблицей символов линкер занимается тем, что во всех местах, где в коде/данных результирующего файла должна быть ссылка (указатель, адрес, относительное смещение) на какую-то сущность из другого COFF-файла (или даже из того же самого), он правильным образом проставляет эти указатели/адреса/относительные смещения.


      В этом ключе каждая «релокация» описывается структурой IMAGE_RELOCATION размером 10 байт (не кратно 4, да):


      //
      // Relocation format.
      //
      
      typedef struct _IMAGE_RELOCATION {
          union {
              DWORD   VirtualAddress;
              DWORD   RelocCount;             // Set to the real count when IMAGE_SCN_LNK_NRELOC_OVFL is set
          };
          DWORD   SymbolTableIndex;
          WORD    Type;
      } IMAGE_RELOCATION;
      typedef IMAGE_RELOCATION UNALIGNED *PIMAGE_RELOCATION;

      Первое поле содержит смещение места (в рамках COFF-секции) от начала COFF-секции. Второе поле содержит ссылку (в виде индекса) на элемент таблицы COFF-символов, описывающий сущность, на которую в данном месте секции после окончания линковки должна оказаться корректная ссылка того или иного вида. Третье поле содержит тип релокаций — по скольку «ссылки», вшитые в код или данные, могут быть разного типа. На ум приходит, что как минимум это могут быть абсолютные и относительные адреса (то есть уже два типа ссылок), в реальности же форматом COFF предусмотрено гораздо боле вариантов, причём для каждой процессорной архитектуры (I386, MIPS, ALPHA, PowerPC, ARM, IA64) набор типов свой.


      Показать типы COFF-релокаций
      
      //
      // I386 relocation types.
      //
      #define IMAGE_REL_I386_ABSOLUTE         0x0000  // Reference is absolute, no relocation is necessary
      #define IMAGE_REL_I386_DIR16            0x0001  // Direct 16-bit reference to the symbols virtual address
      #define IMAGE_REL_I386_REL16            0x0002  // PC-relative 16-bit reference to the symbols virtual address
      #define IMAGE_REL_I386_DIR32            0x0006  // Direct 32-bit reference to the symbols virtual address
      #define IMAGE_REL_I386_DIR32NB          0x0007  // Direct 32-bit reference to the symbols virtual address, base not included
      #define IMAGE_REL_I386_SEG12            0x0009  // Direct 16-bit reference to the segment-selector bits of a 32-bit virtual address
      #define IMAGE_REL_I386_SECTION          0x000A
      #define IMAGE_REL_I386_SECREL           0x000B
      #define IMAGE_REL_I386_REL32            0x0014  // PC-relative 32-bit reference to the symbols virtual address
      
      //
      // MIPS relocation types.
      //
      
      #define IMAGE_REL_MIPS_ABSOLUTE         0x0000  // Reference is absolute, no relocation is necessary
      #define IMAGE_REL_MIPS_REFHALF          0x0001
      #define IMAGE_REL_MIPS_REFWORD          0x0002
      #define IMAGE_REL_MIPS_JMPADDR          0x0003
      #define IMAGE_REL_MIPS_REFHI            0x0004
      #define IMAGE_REL_MIPS_REFLO            0x0005
      #define IMAGE_REL_MIPS_GPREL            0x0006
      #define IMAGE_REL_MIPS_LITERAL          0x0007
      #define IMAGE_REL_MIPS_SECTION          0x000A
      #define IMAGE_REL_MIPS_SECREL           0x000B
      #define IMAGE_REL_MIPS_SECRELLO         0x000C  // Low 16-bit section relative referemce (used for >32k TLS)
      #define IMAGE_REL_MIPS_SECRELHI         0x000D  // High 16-bit section relative reference (used for >32k TLS)
      #define IMAGE_REL_MIPS_JMPADDR16        0x0010
      #define IMAGE_REL_MIPS_REFWORDNB        0x0022
      #define IMAGE_REL_MIPS_PAIR             0x0025
      
      //
      // Alpha Relocation types.
      //
      
      #define IMAGE_REL_ALPHA_ABSOLUTE        0x0000
      #define IMAGE_REL_ALPHA_REFLONG         0x0001
      #define IMAGE_REL_ALPHA_REFQUAD         0x0002
      #define IMAGE_REL_ALPHA_GPREL32         0x0003
      #define IMAGE_REL_ALPHA_LITERAL         0x0004
      #define IMAGE_REL_ALPHA_LITUSE          0x0005
      #define IMAGE_REL_ALPHA_GPDISP          0x0006
      #define IMAGE_REL_ALPHA_BRADDR          0x0007
      #define IMAGE_REL_ALPHA_HINT            0x0008
      #define IMAGE_REL_ALPHA_INLINE_REFLONG  0x0009
      #define IMAGE_REL_ALPHA_REFHI           0x000A
      #define IMAGE_REL_ALPHA_REFLO           0x000B
      #define IMAGE_REL_ALPHA_PAIR            0x000C
      #define IMAGE_REL_ALPHA_MATCH           0x000D
      #define IMAGE_REL_ALPHA_SECTION         0x000E
      #define IMAGE_REL_ALPHA_SECREL          0x000F
      #define IMAGE_REL_ALPHA_REFLONGNB       0x0010
      #define IMAGE_REL_ALPHA_SECRELLO        0x0011  // Low 16-bit section relative reference
      #define IMAGE_REL_ALPHA_SECRELHI        0x0012  // High 16-bit section relative reference
      #define IMAGE_REL_ALPHA_REFQ3           0x0013  // High 16 bits of 48 bit reference
      #define IMAGE_REL_ALPHA_REFQ2           0x0014  // Middle 16 bits of 48 bit reference
      #define IMAGE_REL_ALPHA_REFQ1           0x0015  // Low 16 bits of 48 bit reference
      #define IMAGE_REL_ALPHA_GPRELLO         0x0016  // Low 16-bit GP relative reference
      #define IMAGE_REL_ALPHA_GPRELHI         0x0017  // High 16-bit GP relative reference
      
      //
      // IBM PowerPC relocation types.
      //
      
      #define IMAGE_REL_PPC_ABSOLUTE          0x0000  // NOP
      #define IMAGE_REL_PPC_ADDR64            0x0001  // 64-bit address
      #define IMAGE_REL_PPC_ADDR32            0x0002  // 32-bit address
      #define IMAGE_REL_PPC_ADDR24            0x0003  // 26-bit address, shifted left 2 (branch absolute)
      #define IMAGE_REL_PPC_ADDR16            0x0004  // 16-bit address
      #define IMAGE_REL_PPC_ADDR14            0x0005  // 16-bit address, shifted left 2 (load doubleword)
      #define IMAGE_REL_PPC_REL24             0x0006  // 26-bit PC-relative offset, shifted left 2 (branch relative)
      #define IMAGE_REL_PPC_REL14             0x0007  // 16-bit PC-relative offset, shifted left 2 (br cond relative)
      #define IMAGE_REL_PPC_TOCREL16          0x0008  // 16-bit offset from TOC base
      #define IMAGE_REL_PPC_TOCREL14          0x0009  // 16-bit offset from TOC base, shifted left 2 (load doubleword)
      
      #define IMAGE_REL_PPC_ADDR32NB          0x000A  // 32-bit addr w/o image base
      #define IMAGE_REL_PPC_SECREL            0x000B  // va of containing section (as in an image sectionhdr)
      #define IMAGE_REL_PPC_SECTION           0x000C  // sectionheader number
      #define IMAGE_REL_PPC_IFGLUE            0x000D  // substitute TOC restore instruction iff symbol is glue code
      #define IMAGE_REL_PPC_IMGLUE            0x000E  // symbol is glue code; virtual address is TOC restore instruction
      #define IMAGE_REL_PPC_SECREL16          0x000F  // va of containing section (limited to 16 bits)
      #define IMAGE_REL_PPC_REFHI             0x0010
      #define IMAGE_REL_PPC_REFLO             0x0011
      #define IMAGE_REL_PPC_PAIR              0x0012
      #define IMAGE_REL_PPC_SECRELLO          0x0013  // Low 16-bit section relative reference (used for >32k TLS)
      #define IMAGE_REL_PPC_SECRELHI          0x0014  // High 16-bit section relative reference (used for >32k TLS)
      #define IMAGE_REL_PPC_GPREL             0x0015
      
      #define IMAGE_REL_PPC_TYPEMASK          0x00FF  // mask to isolate above values in IMAGE_RELOCATION.Type
      
      // Flag bits in IMAGE_RELOCATION.TYPE
      
      #define IMAGE_REL_PPC_NEG               0x0100  // subtract reloc value rather than adding it
      #define IMAGE_REL_PPC_BRTAKEN           0x0200  // fix branch prediction bit to predict branch taken
      #define IMAGE_REL_PPC_BRNTAKEN          0x0400  // fix branch prediction bit to predict branch not taken
      #define IMAGE_REL_PPC_TOCDEFN           0x0800  // toc slot defined in file (or, data in toc)
      
      //
      // Hitachi SH3 relocation types.
      //
      #define IMAGE_REL_SH3_ABSOLUTE          0x0000  // No relocation
      #define IMAGE_REL_SH3_DIRECT16          0x0001  // 16 bit direct
      #define IMAGE_REL_SH3_DIRECT32          0x0002  // 32 bit direct
      #define IMAGE_REL_SH3_DIRECT8           0x0003  // 8 bit direct, -128..255
      #define IMAGE_REL_SH3_DIRECT8_WORD      0x0004  // 8 bit direct .W (0 ext.)
      #define IMAGE_REL_SH3_DIRECT8_LONG      0x0005  // 8 bit direct .L (0 ext.)
      #define IMAGE_REL_SH3_DIRECT4           0x0006  // 4 bit direct (0 ext.)
      #define IMAGE_REL_SH3_DIRECT4_WORD      0x0007  // 4 bit direct .W (0 ext.)
      #define IMAGE_REL_SH3_DIRECT4_LONG      0x0008  // 4 bit direct .L (0 ext.)
      #define IMAGE_REL_SH3_PCREL8_WORD       0x0009  // 8 bit PC relative .W
      #define IMAGE_REL_SH3_PCREL8_LONG       0x000A  // 8 bit PC relative .L
      #define IMAGE_REL_SH3_PCREL12_WORD      0x000B  // 12 LSB PC relative .W
      #define IMAGE_REL_SH3_STARTOF_SECTION   0x000C  // Start of EXE section
      #define IMAGE_REL_SH3_SIZEOF_SECTION    0x000D  // Size of EXE section
      #define IMAGE_REL_SH3_SECTION           0x000E  // Section table index
      #define IMAGE_REL_SH3_SECREL            0x000F  // Offset within section
      #define IMAGE_REL_SH3_DIRECT32_NB       0x0010  // 32 bit direct not based
      
      #define IMAGE_REL_ARM_ABSOLUTE          0x0000  // No relocation required
      #define IMAGE_REL_ARM_ADDR32            0x0001  // 32 bit address
      #define IMAGE_REL_ARM_ADDR32NB          0x0002  // 32 bit address w/o image base
      #define IMAGE_REL_ARM_BRANCH24          0x0003  // 24 bit offset << 2 & sign ext.
      #define IMAGE_REL_ARM_BRANCH11          0x0004  // Thumb: 2 11 bit offsets
      #define IMAGE_REL_ARM_SECTION           0x000E  // Section table index
      #define IMAGE_REL_ARM_SECREL            0x000F  // Offset within section
      
      //
      // IA64 relocation types.
      //
      
      #define IMAGE_REL_IA64_ABSOLUTE         0x0000
      #define IMAGE_REL_IA64_IMM14            0x0001
      #define IMAGE_REL_IA64_IMM22            0x0002
      #define IMAGE_REL_IA64_IMM64            0x0003
      #define IMAGE_REL_IA64_DIR32            0x0004
      #define IMAGE_REL_IA64_DIR64            0x0005
      #define IMAGE_REL_IA64_PCREL21B         0x0006
      #define IMAGE_REL_IA64_PCREL21M         0x0007
      #define IMAGE_REL_IA64_PCREL21F         0x0008
      #define IMAGE_REL_IA64_GPREL22          0x0009
      #define IMAGE_REL_IA64_LTOFF22          0x000A
      #define IMAGE_REL_IA64_SECTION          0x000B
      #define IMAGE_REL_IA64_SECREL22         0x000C
      #define IMAGE_REL_IA64_SECREL64I        0x000D
      #define IMAGE_REL_IA64_SECREL32         0x000E
      #define IMAGE_REL_IA64_LTOFF64          0x000F
      #define IMAGE_REL_IA64_DIR32NB          0x0010
      #define IMAGE_REL_IA64_RESERVED_11      0x0011
      #define IMAGE_REL_IA64_RESERVED_12      0x0012
      #define IMAGE_REL_IA64_RESERVED_13      0x0013
      #define IMAGE_REL_IA64_RESERVED_14      0x0014
      #define IMAGE_REL_IA64_RESERVED_15      0x0015
      #define IMAGE_REL_IA64_RESERVED_16      0x0016
      #define IMAGE_REL_IA64_ADDEND           0x001F

      Этот тип релоков формируется компилятором и формируется ВСЕГДА. Поэтому постановка вопросов «формирует ли компилятор релоки» не такой уж невинный, и откупиться фразой «ой, да ладно, мы под компилятором имели в виду совокупность из компилятора и линкера вообще и линкер в частности» не получится.


      Этот тип информации о релокациях используется при линковке (он является абсолютно ключевым для процесса линковки), но дальше процесса линковки он не идёт. В сформированном 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) набор констант:


      Показать набор типов PE-релокаций
      //
      // Based relocation types.
      //
      
      #define IMAGE_REL_BASED_ABSOLUTE              0
      #define IMAGE_REL_BASED_HIGH                  1
      #define IMAGE_REL_BASED_LOW                   2
      #define IMAGE_REL_BASED_HIGHLOW               3
      #define IMAGE_REL_BASED_HIGHADJ               4
      #define IMAGE_REL_BASED_MIPS_JMPADDR          5
      #define IMAGE_REL_BASED_SECTION               6
      #define IMAGE_REL_BASED_REL32                 7
      
      #define IMAGE_REL_BASED_MIPS_JMPADDR16        9
      #define IMAGE_REL_BASED_IA64_IMM64            9
      #define IMAGE_REL_BASED_DIR64                 10
      #define IMAGE_REL_BASED_HIGH3ADJ              11

      И наконец третий тип информации, связанный с релокациями второго типа, который тоже формируется линкером и тоже на основе релокаций первого типа из 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-ов использует тот же набор констант для поля «тип».


      1. Angeld
        26.12.2021 15:50

        изначально речь шла о том что win64 рандомизирует адрес загрузки любого приложения

        на что было указано, что существует много exe не содержащий информации для этого

        те релокции в объектный файлах отношения к вопросу не имеют, речь шла только о exe файле.


        1. firehacker
          26.12.2021 16:14

          те релокции в объектный файлах отношения к вопросу не имеют, речь шла только о exe файле.

          Не имеют, но если о них не написать, придёт кто-нибудь умный и к моей придирке «компилятор релокации не генрирует, их генерирует линкер» сам придирётся фразой «нет правда, компилятор релоки тоже же генерирует» (но это уже совсем другая история другие релоки).


          В целом, в этом диспуте, я больше на вашей стороне: я думаю, если на моём компьютере просканировать все EXE-файлы, то единственным, который будет иметь релокации, окажется OllyDbg, который стал перемещаемым не в угоду ASLR, а по совсем другой причине.


          1. qw1
            26.12.2021 22:01

            Вы сидите на WinXP? Последние 10-15 лет дефолтным поведением для компиляторов стало оставлять релокации в EXE-файлах. У себя на диске с релокациями я наблюдаю все EXE от Microsoft (Windows, VS, Office), от Google (Android SDK), Valve (Steam).


            1. firehacker
              27.12.2021 01:05

              ASLR появилась в Windows Vista, а это 2007 год. До введения ASLR делать подобное поведение дефолтным не имело никакого смысла, а значит дефолтным оно могло стать не более, чем 13 лет назад. Этому предшествовало как минимум 17 лет существования Microsoft-овского линкера с совершенно другим дефолтным поведением.


              И, если уж быть точным в формулировках, поведение «если /FIXED:NO не указан явно, а мы делаем EXE — подразумевать /FIXED и не делать релокаций» как было дефолтным в линкере, так и осталось до сих пор.


              Другое, что добавили ключ /DYNAMICBASE, одним из побочных эффектов применения которого (но не единственным) является «считать, что указан /FIXED:NO».


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


              1. qw1
                27.12.2021 08:42
                +1

                Я лишь удивился тому, что все EXE-файлы (кроме OllyDbg) на вашей системе старше 13 лет. Как так получилось?


      1. firehacker
        26.12.2021 15:56

        Поправлю некоторые опечатки, а то пост уже не отредактировать:


        а сторона, инициировавшая загрузку PE-образа, не потребовала сделать загрузку по нестандартной базе — PE-образ (хоть EXE, хоть DLL) будет загружен, даже если он перемещаемый.

        даже если он не перемещаемый.


        Microsoft-овский линкер умееть делать и так так.

        умеет делать и так, и так.


        Но если вы собираетесь свой PE-файл запускать под Win32s (это 32-битная подсистема для 16-битных Windows 3.1/3.11).

        Но если вы собираетесь свой PE-файл запускать под Win32s (это 32-битная подсистема для 16-битных Windows 3.1/3.11), образ обязательно должен быть перемещаемым и иметь релоки.


      1. zvszvs
        26.12.2021 16:16

        Спасибо еще раз за приведение общеизвестной информации. Хотелось бы понять по какому именно пункту вы считаете, что я ошибаюсь кроме следующего?
        Когда я писал: " Решение — указать в свойствах секции кода не только бит "разрешение записи", но и бит "грузить только по указанной базе"." я действительно описался - он не в свойствах секции (что очевидно). Это никак не меняет сути. Такой бит есть, лишь в другом месте. Делать из этого вывод " оба не в состоянии разделить компилятор и линкер ", мне кажется немного преждевременным. Вы не учитель в школе?
        Мне кажется неправильным писать слишком объемные комментарии, приводя уже давно хорошо задокументированную информацию. Я не про вас, про себя. Поэтому, когда я пытаюсь выразить лишь суть, мой комментарий может выглядеть "неполным", а для кого-то "неверным". Я не против. Если кто-то считает, что я "вообще ничего не понимаю", я тоже не против. Если кто-то спрашивает - я уточняю.
        И на счет спора - прочитайте мой первый коментарий. К нему еще вопросы у вас есть?


        1. firehacker
          26.12.2021 17:52
          -1

          Делать из этого вывод " оба не в состоянии разделить компилятор и линкер ", мне кажется немного преждевременным.

          Вывод о путанице между компилятором и линкером делается не из этого, а уже из другого: из того факта, что во всех ваших (в смысле у вас двоих, а не лично Ваших) постах ни разу не фигурировало слово «линкер», а везде фигурирова «компилятор». Он тоже генерирует некие релоки, но совершенно не те.


          Наличие или отсутствие в исполняемом файле информации, необходимой для перемещаемости образа, зависит от ключей командной строки, скормленных именно линкеру, а в случае, если соответствующие ключи не указаны — зависит от того, что за линкер используется и какая его версия. Одним этим выделенным жирным предложением в споре можно было бы поставить жирную точку. Без указания того, про какой линкер и про какую версию идёт речь, продолжать спорить не имело смысла. Но вы оба продолжили, причём спорили даже не о линкерах, а о компиляторах, которые тут вообще не причём.


          Потом вы пишите:


          Насколько я знаю, по умолчанию как раз адрес не фиксированный.

          и дополняете, что вам 53 и вы собаку съели на знании тонкостей формирования исполняемых файлов. Ну так тогда бы вы знали, что Microsoft-овский линкер большую часть истории своего существования по умолчанию предполагал именно /FIXED для EXE-файлов. Хотя, почему — может вы все эти годы с совершенно другим линкером имели дело. Но тогда с каким? То есть то ли вы такой опытный, но не знаете про дефолтность /FIXED для EXE и /FIXED:NO для DLL в MS-овском линкере, то ли вы просто работали с каким-то другим линкером, знаете его, возможно, довольно хорошо, но как будто бы в вашей картине мира других линкеров нет.


          Дальше идёт:


          Насколько я знаю, по умолчанию (честно, не знаю как в NASM, не пользовался) как раз адрес не фиксированный.

          По умолчанию где? Вы можете знать или не знать NASM, но NASM — не компоновщик. И выставлять флаг relocs stripped или же не выставлять его, а генерировать таблицу base-релокаций — это вообще не компетенция NASM-а. Как и MASM, NASM на входе принимает ассемблерный исходник, а на выходе выплёвывает объектный файл. Не исполняемый файл, а объектный. В котором нет места таблице релокаций (в том смысле, в каком она есть в PE) и нет места флагу IMAGE_FILE_RELOCS_STRIPPED.


          И на счет спора — прочитайте мой первый коментарий. К нему еще вопросы у вас есть?
          Если уж копнуть и посмотреть билд-скрипт, который использует автор статьи, там вообще используется nasm+ld, то есть автор собирает бинарник под линукс. А вы ему пишите о проблемах загрузки такого исполняемого файла под Win64.

          Вот эти все ваши мелкие оговорки: то флаг не в таблцие секций (хотя он в PE-заголовке), то компилятор вместо линкера (в споре о том, должен ли линкер делать EXE-файлы перемещаемыми), то вы от NASM-а ждёте один из двух вариантов активности (и не знаете, какой там по умолчанию), которые свойствены линкерам, тогда как в NASM-е нет одного из двух вариантов, так как он не вбирает в себя функции линкера — всё это в совокупности создаёт впечатление, что вы плаваете в теме, не знаете, где проходит граница раздела ответственности между компиляторами и линкером.


          Опять таки, решать проблему с ASLR вы предлагаете установкой флага IMAGE_FILE_RELOCS_STRIPPED, хотя логично было бы ассемберный код подправить так, чтобы вшитые в нём адресные константы попали в таблицу релокаций, либо же код самомодификации написать так, чтобы он определял реальную базу, вычислял дельту релокации на основании разницы между фактической базой и предпочитаемой базой и приплюсовывал эту дельту релокации всюду, где он в генерируемый машинный код вплетает адресные константы, заложенные в код на стадии написания ассемблерного исходника.


          1. zvszvs
            26.12.2021 18:26

            Ну, то есть, я понял с кем имею дело. Напомню, я спросил " прочитайте мой первый коментарий. К нему еще вопросы у вас есть?" Не вижу ни одного вопроса.
            Вы - чистый "grammar nazi" в отношении документации по компиляторам. "Не смей писать отсебятину - пиши строго по стандарту!" - вот ваш дивиз. Ок.
            К сожалению вы задели меня и придется быть таким же занудой, как вы. Откину вежливость и только факты.

            Вывод о путанице между компилятором и линкером делается не из этого, а уже из другого: из того факта, что во всех ваших (в смысле у вас двоих, а не лично Ваших) постах ни разу не фигурировало слово «линкер», а везде фигурирова «компилятор».

            Вранье. Откройте глаза и почитайте внимательно.

            Но вы оба продолжили, причём спорили...

            Мы не спорили, а дискутировали какого года EXE файлы имели таблицу relocation, а когда нет. Этот диалог, надеюсь пошел нам обоим во благо. Но нет, некоторые решили всех поставить на место. Спасибо. Жду от вас детальной статьи по поводу компиляторов и линкеров, где я обязательно найду пару опечаток и прокомментирую.

            что вам 53 и вы собаку съели...

            Вы просто провидец. )

            но как будто бы в вашей картине мира других линкеров нет.

            Они есть. Больше того, у меня есть свой собственный линкер и компилятор (о чудо, у меня это 2 в одном и называю я его компилятор). Я обязательно приведу "другие" (не мои) линкеры, когда вы напишите соответствующую статью на habr.

            ...но NASM — не компоновщик.

            Нормально. Человек видит, что я не знаю что такое NASM и обвиняет меня, что я не знаю, что он, оказывается, не компановщик. Не знаю что тут комментировать.

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

            Если вы реально читали мои комментарии, то там написано (во втором), что " Это не спор с кем-то, а дополнение текста статьи для тех, кто захочет воспользоваться модификацией кода в runtime." Ну ведь верно, как я не догадался, факториал только под линукс же считать можно...

            Вот эти все ваши мелкие оговорки ...

            Ну да, у вас ведь ни одной "оговорки" (я написал об одной опечатке, при чем не по сути, а по форме) нет. Вы же не писали потом коммент о ваших собственных оговорках. Браво! Отличная придирка. )

            Опять таки, решать проблему с ASLR вы предлагаете установкой флага IMAGE_FILE_RELOCS_STRIPPED, хотя логично было бы ассемберный код подправить так, чтобы вшитые в нём адресные константы попали в таблицу релокаций, либо же код самомодификации написать так, чтобы он определял реальную базу, вычислял дельту релокации на основании разницы между фактической базой и предпочитаемой базой и приплюсовывал эту дельту релокации всюду, где он в генерируемый машинный код вплетает адресные константы, заложенные в код на стадии написания ассемблерного исходника.

            Я написал самое простое решение. Когда началась дискуссия, я написал и остальные. Ровно то, что вы тут с пафосом излагаете (по мне так очевидные вещи). Вы этого не увидели? Да вы точно не читали всей дискуссии.

            Адью.


      1. mayorovp
        26.12.2021 22:29

        не «делают ли компиляторы EXE-файлы перемещаемыми или нет», а нужно говорить о линкере

        Один хрен, что компилятор, что линкер. Так-то они лишь в Си и С++ традиционно разделены, и то обычным делом является вызов линкера из компилятора. А в других языках отдельного компоновщика отродясь не было, либо он был достаточно хорошо спрятан.


        В любом случае, от замены линкера на компилятор и обратно ни одно из сообщений этой ветки смысла не теряет.


        1. firehacker
          27.12.2021 00:03
          -1

          Так-то они лишь в Си и С++ традиционно разделены

          Автор писал не на Си или 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 и закономерно получать противоречивые результаты — это всё было совершенно бессмысленным.


  1. alexxisr
    24.12.2021 17:46
    +2

    openbsd вобще запрещает ставить одному сегменту и запись и исполнение одновременно. Приходится специальные флаги монтирования фс выставлять для разрешения.


  1. electric13
    24.12.2021 20:02
    +5

    Нужна ли команда

    cmp ebx, 0

    Ведь чуть ранее sub уже устанавливает флаги.


    1. zvszvs
      24.12.2021 21:35

      Точно. Не нужна. Тем более, что в своей сути cmp работает точно как sub, только результат никуда не записывает, а, как раз, устанавливает флаги.


      1. 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 - и удивитесь, насколько медленнее стал работать цикл.


        1. 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;
          }


        1. zvszvs
          26.12.2021 14:34

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


  1. ARad
    24.12.2021 21:56
    +1

    Если для развлечения то еще понимаю. Такая самомодификация напрочь убивает возможность многопоточности.


    1. speshuric
      26.12.2021 15:31
      +2

      И тут кому-нибудь придёт в голову: "Но что если делать мьютексы и спинлоки путём самоисправления кода?". В этот момент должен явственно почувствоваться запах серы, а инженеры, проектирующие процессоры и взаимодействие кешей дьявольски хохочут.


  1. qw1
    24.12.2021 22:09
    +7

    Ожидал какой-нибудь красивый трюк, а в итоге программа с само-модификацией получилась длиннее и сложнее исходной. Тогда зачем всё это, модификация ради модификации?


    1. lorc
      26.12.2021 07:29
      +1

      Красивые трюки точно есть в гипервизоре Xen. Возможно в ядре линукса тоже.

      Там при первоначальной инициализации определяется конкретный тип процессора и код в fast path модифицируется под этот процессор. Чаще всего включаются фиксы для разных errata.


      1. qw1
        26.12.2021 09:58
        +2

        Но это скорее кодогенерация, чем самомодификация. Различие в том, что при кодогенерации код динамически создаётся один раз и много раз выполняется.


  1. mvv-rus
    24.12.2021 23:44
    +3

    Есть серьезное подозрение, что на оригинальном 8086 (а может, и на 8088, т.е. на IBM PC и IBM PC XT — тут надо уже смотреть листинг ассемблера, а мне лень) этот код работать не будет: у этих процессоров была очередь команд размером 6(8086)/4(8088) байт, которая никак не реагировала на изменение в памяти команд, уже выбранных в очередь.

    Кстати, один из методов борьбы с отладчиками тех времен был основан на этом принципе: защищавшаяся программа модифицировала свой код, который уже должен был быть в очереди. Если программа не находилась под пошаговой отладкой, то использовался оригинальный код, иначе эта модификация попадала в очередь команд и исполнялась. Ну, а дальше все зависело от фантазии программистов.


    1. Medeyko
      25.12.2021 20:12
      +2

      На 8086/8088 этот код явно работать не будет - там не было 32-битных регистров :). 80286 и выше сбрасывают очередь, так что на всех 32-битных процессора x86 это работать будет.

      Если же не обращать внимание на это, то mov bx, 0 занимает три байта. На 8088 в 16-битном аналоге ещё не будет загружено в очередь при модификации, должно бы работать.


  1. warus
    25.12.2021 10:19
    +2

    хе раньше такой способ само-кодирования дешифровки программ на ассемблере назывался метод бабы яги


  1. 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 программа и короче становится, и работает быстрее.

    Что касается самомодифицирующегося кода, то, по-моему, в приведённым примерах он мало оправдан. Хотя, разумеется, он бывает весьма красивым, но, как мне кажется, автору статьи это показать не удалось от слова совсем. Честно говоря, не очень понимаю, зачем так писать на ассебмлере.


    1. petropavel
      26.12.2021 14:38
      +1

      Он тут совсем не оправдан. По названию я предположил, что будет безусловный цикл с jmp, а потом его поменяют на ret. Тоже не фонтан, но в факториале сложно что-то новое придумать.

      Самомодифицирущийся код лучше показывать на примере какой-нибудь защиты, типа по паролю сама себя в памяти расшифровывает. Или смещения в jmp-ах менять. Выбрав нужную функциональность перед длинным циклом, чтоб избегать условных переходов. Тогда можно побенчмаркить и показать, что есть смысл (если он будет, branch prediction никто не отменял).


    1. zvszvs
      26.12.2021 14:42

      Выскажу предположение по поводу ebp. Возможно программа была написана на Си (или др.) и далее адаптировался ассемблерный листинг компилятора. Автор просто решил "не удалять" сохранение ebp по "только ему известным причинам". Ну или так больше похоже на "настоящую" функцию (автор показал, что знаком с прологом и эпилогом).
      Кстати, остальные лишние и не оптимальные конструкции тоже могут объясняться источником ассемблерного кода.


  1. Alexey_Alive
    27.12.2021 04:00
    -1

    От "mov eax, 0" аж больно стало. Почему не xor?!


    1. zvszvs
      27.12.2021 14:27

      Ну тут ответ вроде в тексте ясно прописан. MOV потому, что программа "на лету" меняет константу в инструкции. Для простого обнуления XOR, конечно оптимальнее и короче.