Самомодифицирующийся код — Википедия

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

По времени проведения модификации метод делится на:

  • Модификация при инициализации — проводится один раз перед запуском изменяемого кода
  • Модификация на лету (on-the-fly) — изменение состояния программы во время исполнения

В обоих случаях изменение проходит непосредственно в машинном коде, когда новые инструкции перезаписывают старые (напр. условный переход JZ, JNZ, JE, JNE и т.п. заменяются на безусловный переход JMP или NOP). В наборе инструкций IBM/360 и Z/Architecture имеется инструкция EXECUTE (EX), которая перезаписывает целевую инструкцию (записанную во втором байте команды EX) самыми младшими 8 битами регистра 1. На указанных архитектурах с её помощью реализуется стандартный, законный метод временного изменения инструкций.

Назначение

[править | править код]

Основные применения самомодифицирующегося кода:

  • В критичных к безопасности местах для усложнения исследования кода (полиморфные вирусы, некоторые типы защиты от копирования, упаковщики и т. д.).
  • В критичных к скорости местах для ускорения работы. Так, например, во время исполнения можно уменьшить длину критического пути исполнения. Вместо установки и последующей многократной проверкой флагов с условными переходами можно всего лишь изменить адрес и тип перехода в машинном коде. Многие порты движка Doom устанавливали прямо в машинном коде ширину экрана, это ускоряло отрисовку столбца[1].
  • Иногда используется для включения/отключения во время исполнения некоторой функциональности для тестирования или отладки. Так, в ОС Linux и Solaris при использовании отладочных инструментов Kprobes и DTrace в некоторые места кода ядра или программ вставляются последовательности инструкций nop. При включении инструмента некоторые из этих последовательностей заменяются на безусловный переход на процедуру отладки. Использование СМК позволяет расставить значительное количество точек, в которых возможна отладка, слабо при этом влияя на скорость исполнения с отключенной отладкой.
  • В ядре Linux и, возможно, других ОС, используются для отключения частей ядра, не нужных в данном окружении. При загрузке Linux определяет, исполняется ли он на SMP или на однопроцессорной машине. Во втором случае часть примитивов синхронизации удаляется из кода ядра.
  • Чтобы машинный код можно было набрать в текстовом поле — см. EICAR. Функция выхода из программы (int 20) имеет вид CD 20 и текстом надёжно не набирается — для этого её поместили в тестовый «вирус» в зашифрованном виде и расшифровали на месте.

Применимость к процессорам с гарвардской архитектурой

[править | править код]

В гарвардской архитектуре память для кода и память для данных разделены. Соответственно, в них сильно усложняется работа самомодифицирующегося кода. Хотя архитектура x86 определена как фон-неймановская (с единой памятью кода и данных), большинство современных процессоров имеет раздельные области кэша для кода и для данных. При этом кэш кода не поддерживает запись, и при изменении закэшированного участка памяти может потребоваться либо аппаратно проведенный частичный или полный сброс кэша кода (x86), либо явная инструкция процессору на сброс кэша кода (SPARC). Из-за этого только что измененный код может исполняться медленнее либо потребовать дополнительных команд для правильной работы. Также изменение кода сбрасывает конвейер процессора.[2]

Также, некоторые идеи гарвардской архитектуры реализуются в ОС (например, Data Execution Prevention в ОС Windows, W^X в OpenBSD) и в процессорах (для x86 — бит NX и подобные). В этих реализациях отдельные фрагменты памяти могут быть помечены как неисполняемые (то есть данные) или как исполняемые, но немодифицируемые (то есть код без права на изменение). Использование самомодифицирующегося кода в таких программных окружениях усложняется, так как его приходится располагать либо в незащищенной области памяти (иногда такой областью является стэк), либо явно отключать защиту для подлежащего изменению кода.

Использование

[править | править код]
  • JIT (Just in time — компиляция)
  • Динамическая трансляция
  • Динамическая рекомпиляция — при которой двоичный транслятор следит за частотой исполнения региона, и, если регион выполняется часто, проводится рекомпиляция этого региона с изменением его кода во время исполнения. В наиболее совершенных двоичных трансляторах может иметься до 4-5 последовательных уровней оптимизации региона.

Интерпретируемые языки

[править | править код]

Языки Perl, PHP и Python позволяют программе создавать новый код во время выполнения и выполнять его, используя функцию eval, но не позволяют самомодифицироваться существующему коду (interactive python shell):

>>> x = 1 >>> eval('x + 1') 2 >>> eval('x') 1 

Иллюзия модификации (при том, что никакой машинный код в действительности не изменяется) достигается путём изменения указателя функции, как в этом JavaScript-примере:

var f = function (x) {return x + 1}; alert(f(0)); //1  f = new Function('x', 'return x + 2'); // assign a new definition to f alert(f(0)); //2 

Примечания

[править | править код]
  1. См., например, исходный код Doom Legacy, функция ASM_PatchRowBytes.
  2. Касперски, абзац с "Процессоры семейства Pentium .."