Однажды один умный чувак (Кристофер Домас) читал статью другого умного чувака (Стивена Долана) про удивительную особенность архитектуры x86. Стивен ругал её за избыточность и утверждал, что набор инструкций можно сократить до одной лишь mov, потому что она Тюринг-полная. Если бы Стивен не был таким умным, в его словах можно было бы усомниться, но у Кристофера загорелись глаза: проработав двадцать лет с x86, он не слышал ни о чём подобном, и ему страшно захотелось написать компилятор, который бы переводил весь код в наборы одних лишь mov-инструкций. Так родились M/o/Vfuscator и M/o/Vfuscator2, наглядно иллюстрирующие ненормальное программирование.
Идея
Небольшое отступление про инструкцию mov: это самая простая инструкция в ассемблере, перемещающая значение из памяти в регистры или из регистров в память. Как может штука, перекладывающая байты из одного места в другое, оказаться Тьюринг-полной? Ну, если вам правда интересно, почитайте оригинальную статью Стивена с доказательством. Если не очень, перейдём сразу к выводу:
Удаление из будущих итераций архитектуры x86 всех инструкций, кроме mov, может обеспечить множество плюсов: формат инструкций будет значительно упрощен, дорогой блок декодирования станет намного дешевле в выполнении, а кремний, используемый в настоящее время для сложных функциональных блоков, можно было бы использовать для еще большего увеличения кэша. Как только кто-нибудь реализует компилятор.
Собственно, на последних словах Кристофер и загорается этой идеей и попутно соображает, что через подобную компиляцию можно нехило так обфусцировать код — сам чёрт ногу сломит в этих бесконечных mov'ax! Для сравнения, обычный, «читаемый» ассемблер:
и та же самая программа на мувах:
Представьте, какое немыслимое количество операций нужно совершить чтобы отреверсить такой код? Крис сам занимается реверс-инжинирингом, он понимает что это полное безумие — и поэтому кайфует ещё больше от своего проекта. В первую очередь, он выписывает основные принципы Стивена, на которых будет держаться компилятор:
mov может сравнивать значения
Допустим, вы хотите сравнить x и y, для этого вам понадобится следующий код:
mov [x], 0
mov [y], 1
mov R, [x]
Если x == y, то в третьей строчке, где считывается значение по адресу x, окажется не ноль, а перезаписавшая его единица.
Если x != y, то считается ноль, так как единица лежит по другому адресу.
Код выполняется без ветвлений
Согласно идее Стивена, правильно написанный блок кода может либо что-то делать, либо не делать, в зависимости (только!) от исходного состояния системы. То есть ветвление отсутствует как класс, если абсолютно все инструкции исполняются последовательно.
Ограничения
Для выполнения требуется одна инструкция jmp start (в конце списка mov'ов) для перевода программы в начало; для остановки нужен заведомо нерабочий адрес памяти.
Дальше Крис добавляет свои требования:
- Использовать примитивные операции машины Тьюринга как основу для высокоуровневой логики
- Работать надо с реальными данными, не с абстрактными символами (эксперимент Стивена всё-таки академичен, далёк от реального мира)
- Должны быть реализованы основные операции: условные ветвления, арифметика, логика, циклы и так далее
Подробно о реализации некоторых вещей можно послушать в его докладе по ссылке (таймкод 9:06), а мы сразу перепрыгнем к состоянию «оно реализовано и работает», чтобы не пересказывать оригинал.
Реализация
Первая версия компилятора была написана для брейнфака, для ощущения максимального абсурда и тщетности жизни реверсера, но, конечно, она осталась ужасно далека от реальных примеров и задач. Поэтому Крис спустя пару лет ВНЕЗАПНО выпустил M/o/Vfuscator2, рабочий mov-компилятор для С. Впечатляющий апгрейд, не правда ли?
Заявлена относительно легкая адаптация компилятора под другие платформы и языки, но всё же создавался он именно для x86, и с ним связан ворох особенностей и ограничений:
- Для дробных чисел используется самописный эмулятор плавающей точки, из-за размера поставляется в трёх версиях:
softfloat32.o
для float,softfloat64.o
для float и double, и softfloatfull для полной поддержки стандарта IEEE - Так как арифметика строится на таблицах поиска, таблицы символов могут занимать огромное количество места, и их, возможно, придётся обрезать флагом
-s
- Компилятор работает строго на C89 из-за использования LCC в качестве фронтенда. Нельзя использовать bool, for (int ...), и другие фишки C99
- Код с нестрогой типизацией или небезопасными конвертациями, скорее всего, не скомпилируется — тоже из-за LCC
- Функция, использующая внешние библиотеки, без прототипа лишает компилятор информации о необходимости и моменте подключения этих библиотек, что почти гарантированно повесит приложение
- Вызовы внешних функций (printf и т.д.) через указатели функций еще не реализованы
- Для подключения библиотек, скомпилированных не на mov, могут потребоваться другие инструкции. Полностью избавиться от них можно, перекомпилировав в mov все ресурсы
Заключение
Несмотря на всю крутизну проекта и Кристофера, нужно понимать, что такая обфускация скорее игрушка, чем реальный рабочий инструмент. И всё же, учитывая возможность прикрутить другие фронтенды и архитектуры открывает для M/o/Vfuscator больше возможностей, чем мог бы получить другой безумный ассемблерный проект.
Информацию по установке и использованию можно найти на гитхабе.
На правах рекламы
Эпично! Недорогие серверы на базе новейших процессоров AMD EPYC для размещения проектов любой сложности, от корпоративных сетей и игровых проектов до лендингов и VPN.
Присоединяйтесь к нашему чату в Telegram.
nerudo
mov al,byte ptr[eax+edx*N] это настолько прекрасно, что испытываешь чувство схожее с катарсисом.
JerleShannara
Классика жанра в том, что когда такое видишь в дизассемблере первое, что приходит в голову — «опять данные дизассемблирую» или «промахнулся мимо начала инструкции»
VioletGiraffe
А можно теперь пояснить для тех, кто не испытал катарсис, но тоже хочет? :) Я понимаю, что эта инструкция загружает в al один байт, лежащий по адресу, значение которого равно
eax+edx*N
(так ведь?), но ЧТО это значит?А вообще, это действительно интересно, я видел, что компиляторы применяют инструкцию
lea
вместо арифметики, когда нужно что-то умножить и сложить, но не знал, что один-единственныйmov
тоже умеет вычислять такую арифметику.LynXzp
Для тех кто не разбирается в ассемблере скажите, вот этот плюс и знак множества это же целые операторы, результат вычисляется на стадии компиляции?
nerudo
Да вы не парьтесь, в этом ассемблере уже почти никто не разбирается.
PS Берется текущее содержимое 32-разрядных регистров EDX и EAX, над ними проводятся соответсвующие арифметические манипуляции. Получившийся результат используется как адрес памяти по которому нужно взять значение и положить в регистр AL. Но не все 32 бита, а только 8. Потому что AL — это младшие 8 разрядов регистра EAX. Вроде ничего не перепутал.
Естественно это все происходит на этапе выполнения с использованием тех значений, которые оказались в регистрах на данный конкретный момент.
artiom_n
Компиляция — не вполне правильно.
В большинстве диалектов ассемблера для x86..64 есть макроподстановка и непосредственная трансляция в машинный код (плюс, связывание, конечно).
А это — вариант базово-индексной адресации.
Т.е., это не constexpr в C++ и не вычисление транслятором, а компоновка из данных значений нужной команды: транслятор не вычисляет ничего.