Как стать автором
Поиск
Написать публикацию
Обновить

Комментарии 56

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

Статья понравилась, раньше было модно писать полиморфные вирусы.

НЛО прилетело и опубликовало эту надпись здесь

ASPack / ASProtect применял много прикольных техник - упаковка, полиморфизм (в памяти), интиотладочные циклы / проверки. С оптикой в квартире и винтами на террабайты как-то перестали заботиться о размере исполняемых файлов... а зря.

Спасибо за статью.

А как размер бинарника относится к способам защиты?

Чтобы программу взломать, надо дизассемблировать. А дизассемблировав упакованный бинарник вы получите белиберду. Чтобы его распаковать, надо знать как, а это не всегда тривиально.

Т.е. до текущего времени упаковка бинарников была не просто ещё одной защитой, но и вынужденной мерой из-за ограниченности памяти? А сейчас защитники перестали упаковывать, облегчив взломщикам задачу?

Что-то сомнительно - обычно используют всё, что возможно, чтобы затруднить взлом. Тут явно другая причина.

Упаковщики не помогали против ограниченности памяти, они точно так же самораспаковывали бинарник при выполнении программы.

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

Сейчас есть что-то наподобие упаковщика - это Денуво, но там скорее не упаковщик, а "запутывальщик". Ну еще, в корпоративном сегменте сейчас разные электронные ключи популярны, типа Гуарданта.

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

Такое делают только тогда, когда хотят что-то исследовать

Я лет 30 назад на асме для z80 писал самомодифицирующий код вынимания значений из ram по смещению – потому что такой код быстрее работал, чем обращение через индексный регистр. И выигрыш по тактам был существенный, чуть не кратный, насколько я помню.

То-есть из вполне практических соображений.

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

В микроконтроллере код обычно во флэше хранится...

Ну, фон-неймановской архитектуры тоже бывают, я бы даже сказал их больше. Понятно что всякие 8051 или там Пики – гарвардская схема, но остальное то, хоть чуть пожирнее, уже как минимум гибридное.

Какой-нибудь RP2040 например позволяет копировать из ROM в SRAM и исполнять оттуда, и куда шустрее работает.

Помню, как в 2007 (блин, больше 15 лет назад!) разбирался с процессом старта тогдашних популярных устройств на Windows Mobile и узнал про XIP...

НИИЭТ-овский К1921ВГ015 позволяет загружать отладчиком программу в ОЗУ и запускать оттуда. Отличное решение чтобы не дёргать лишний раз flash при отладке.

30 лет назад на асме почти все так делали.

Помню писал игру теннис на Правец 8а

Там у процессора были только аккумулятор и регистры X и Y. Остальное все в памяти, все переменные. У меня координаты мяча были в двух байтах и экран текстовый.

Пока мяч летит вперед выполняем команду inc [ptr] а чтобы лететь назад меняем эту команду на dec [ptr]. Как-то так.

Ну, 6502 вообще в смысле регистров зверь ) Я сталкивался на советском Агате-8. На 6502 я единственный раз в жизни абьюзал SP как регистр условно-общего назначения, как shadow для регистра X.

эх, насколько лучше были бы программы для х86, если бы убрали бесполезные инструкции с SP в качестве регистра общего назначения и сделали бы еще один регистр

Там же по сути zero page вместо регистрового файла )

Любопытно, мне такое не приходило в голову. Хотя я задумывался, зачем нужны команды TXS и TSX. 6502 неисчерпаем.

Мне бы нынешнему тоже не пришло. Я тогда был школотой и мне ещё никто не сказал, что указатель стека не для этого.

А как Вы изучали ассемблер? Не по Мореру?

Что вы. Ассемблер 6502 – я даже не знал тогда, что процессор так называется – я изучал по подшитым ротокопиям, которые совместно образовывали «Инструкцию по эксплуатации ЭВМ Агат».

Там было что-то в районе 10 томов, каждая страница оформлена по ЕСКД, и в целом инструкции были чудовищно низкой пробы. Вот по ним и разбирался, любопытную школоту разве такой ерундой остановишь )

Это в районе 1987 было. Может, 1988.

Круто. А я в 1988 или 1989 ездил Морера конспектировать в городскую детскую библиотеку.

Детской, поразительно…

У меня этот период случился года на 2–3 позже, я только конспектировал Нортона, Брябрина и пр, меня для этого записали в читальный зал местной научной библиотеки. Местная детская библиотека была, как бы помягче, от компьютеров бесконечно далека )

А z80 я уже изучал по Ларченко/Родионову, абсолютно великая книжка которую можно было купить даже в нашей провинции https://zxpress.ru/book.php?id=116

Во времена z80 это было вообще нормой. Например:

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

  • патчили адрес вызываемой функции в call вместо чтения адреса функции из данных (indirect call -> direct call)

  • патчили загрузку константы в регистр вместо того, чтобы загружать значение из переменной (т.е. по факту переменная была не отдельно, а как часть команды)

  • и тп и тд

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

В современных условиях такие хаки уже не актуальны. Код, как ниже заметили, в микрухах обычно во флеше хранится, даже если архитектура позволяет иначе (хотя и есть подход с копированием важных для скорости функций в zero-wait state RAM); SRAM маленький; выхлоп от таких "мелких" изменений особого смысла обычно не дает; а патчи делать довольно дорого в условиях пайплайновой суперскалярной архитектуры (а это даже мелочь типа Cortex-M), потому что надо очищать пайплайн после патча.

Сейчас кмк из подобных извращений SMC код бывает исчезающе редко, а вот JIT генерация - более-менее часто: VM, включая и простенькие интерпретаторы с "шитым" кодом, генерация каких-нибудь DSP ядер и подобные извраты. Именно из SMC я навскидку могу указать только на антиотладочные/антианализные хаки, и... ядро Linux (механизм альтернатив, ftracer/perf)

Спасибо, отличный коммент!

Берете любой форт (а лучше пишете сами), и получаете возможность поиграть с самомодификацией кода досыта.)

О дивный новый мир, в котором можно порадоваться статье, если первый дисклеймер говорит о том, что автор изучил материал, а не использовал "ии"!

Старый мир, увы. Исходная статья 2013 года

Но да, авторское обоснование "так это ж интересно изучить" радует

Единственный разумный сценарий применения самоизменяемых программа в реальном мире — это механизм маскировки зловредного ПО от антивирусов.

Вообще-то любой JIT-компилятор это самомодифицирующаяся программа. Она на лету создает в своей памяти новый исполняемый код и тут же его исполняет.

Но не модифицирует сам себя.

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

Единственный разумный сценарий применения самоизменяемых программа в реальном мире — это механизм маскировки зловредного ПО от антивирусов

Ещё защита программ (от пиратства, например) может на это опираться

Вот бы на Хабре вместо бесполезного переключателя «Светлая тема»/«Тёмная тема» был переключатель «Синтаксис Intel»/«Синтаксис AT&T».

Самомодифицирующийся код в JS на странице Хабра. ;)

В коде оригинального Doom есть несколько мест с самомодификацией. Насколько я помню, там в коде отрисовки что-то модифицировали, вроде каких-то значений, записывающихся прямо в код, ибо так быстрее было, чем их из регистра читать.

Также стоит отметить, что этот код был написан на языке ассемблера. Модифицировать выхлоп компилятора Си, как делает автор данной статьи, очень плохая затея, ибо этот выхлоп может быть различным для различных версий компилятора, а уж если ещё и оптимизация вмешается, то самомодификация и вовсе будет невозможна.

наверное не в Doom, а в Wolf 3D?

Техник полиморфизма - несколько. Автор рассмотрел одну из них - простая замена. Есть еще разбавление кода нейтральными командами, которые каждый раз меняются. В этих игрищах главное не забыть, что есть кэш команд процессора, и если вы меняете код, следующий далее по ветке исполнения, то есть вероятность, что в ОЗУ он изменится, но это не повлияет никак на исполнение кода процессором, потому что он исполняет то. что засосал в свой кэш команд. Помню, на этой особенности строилась одна из защит редактора "Слово и дело". Под отладчиком - ветка исполнения была одна - завершение программы (сбрасывался кэш из-за прерываний отладки), а без отладчика ветка исполнения была другая - запускался редактор (так как в кэше сохранялся немодифицированный код).

есть кэш команд процессора

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

Под отладчиком - ветка исполнения была одна

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

А как это согласуется с виртуальной памятью в современных ОС? там для загрузки бинарников используется mmap, секция с кодом mmap'ится как read-only, и если у системы мало памяти, она может выгрузить секцию из памяти и загрузить повторно из файла с бинарником при необходимости. А если секция с кодом стала изменяемой? файл с бинарником тогда остаётся как есть, а страница памяти кода с изменениями выгружается уже не в никуда, а в своп, как страница с данными?
А если файл и так находится в памяти (например, в tmpfs), тогда при обычном запуске он из этой же памяти и запускается, а при модификации кода создаётся копия? или как-то по-другому?

Read-only с возможностью изменить на copy-on-write.

В Лиспе самомодицикация кода не то, что допустимая практика, а вообще обычная и постоянно используемая. Более того, разработка и отладка программы на Лиспе как раз состоит в модификации кода в памяти, пока он не заработает, после чего образ сбрасывается на диск в виде исполнимого файла. Но и после загрузки этого файла программа вполне может самомодифицироваться, если логика ее работы этого требует. По этой причине для айфона нет нормального лиспа, например. Там самомодификация жестко запрещена.

Там всё таки не модификация бинарного кода функции, а генерация нового при изменении исходника, больше похоже на JIT.

В основном, да. Но можно еще модифицировать содержимое backquote-выражения, например. Как правило, это получается ненамеренно :) Наверняка есть возможность и напрямую в код залезть, но это уже сильно машинно-зависимо будет, конечно.

Модифицировать значения литералов, в том числе и квазицитат, в Лиспе и производных от него языках обычно запрещено (в стандарте CL сказано "you shouldn't"). Но Вы правы, что это самое неприятное место Лиспа, и проблемы тут в практической жизни могут возникать.

К примеру, компилятор Gambit и без Apple шизеет от такого.

В Фортране в своё время можно было значения целочисленных литеральных констант менять )

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

Ну да, именно MMU поломало эту шалость )

С точки зрения айфона это же не самомодификация, а просто изменение данных интерпретатора (поскольку код модифицируется в исходном представлении, т.е. в S-выражениях, а не в машкодах). Есть для айфона и Common Lisp, и Gambit Scheme. Я ими периодически пользуюсь.

Ну вот SBCL на айфон нельзя портировать по этой причине, например. Так-то интерпретатору ничего не мешает, конечно.

В расчете адреса символа 1 в команде addl ошибка - не 40055a, а 40054a

Забавно, ошибка в "опускаем стек на 4 байта" прожила 12 лет в оригинале и пережила перевод. Там ведь rsp уменьшают на 0x10, т.е. на 16, а не на 4.

@ru_vds, наверное runtime во фразе
> The first step of writing a self-mutating program is being able to change the code at runtime

лучше перевести как-то вроде "на лету" чем в "среде исполнения". Здесь имеется ввиду что программа запущена и в процессе выполенения меняет себя. Это вот и есть ран тайм. Но не среда исполнения.

В вики пишут "время выполнения" что звучит хорошо. А ваша среда - это execution environment скорее.

https://ru.wikipedia.org/wiki/Среда_выполнения

По умолчанию страницы текстового сегмента имеют разрешения на чтение и выполнение. Мы не можем выполнять в них запись.

"выполнение" и сразу же "выполнять запись" - лучше мб - мы не можем писать в них. Но это уже стилистика.

Для выполнения системного вызова в x86_64 мы должны подготовиться к нему, записав нужные значения в нужные регистры, а затем выполнив команду syscall

как-то не по-русски? мб "а затем выполнить .."?

В данном случае системный вызов execve сообщает ядру, что мы хотим запустить другой процесс

Неверно тут и в оригинале. execve не запускает другой процесс, а в том же процессе меняет текст программы на другой - считанный из файла.

смотрим `man execve`:
> execve() executes the program referred to by pathname. This causes the program that is cur‐ rently being run by the calling process to be replaced with a new program, with newly initial‐ ized stack, heap, and (initialized and uninitialized) data segments.
и

> many attributes of the calling process remain unchanged (in particular, its PID)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий