Морфим, не отходя от кассы: Мутация кода во время компиляцииОднажды, после написания программы, которую я хотел сделать платной, я задумался о вопросе ее защиты. Писать навесной протектор желания не было, да и времени тоже. Возможно ли сделать что-то средствами компилятора FASM, ведь у него мощнейший макроязык? В этой статье я решил описать, что вышло из моих экспериментов. Здесь на простых примерах будут описаны методы полиморфизма, пермутации, метаморфинга и обфускации бинарного кода. Итак, мне кажется, я добился отличных результатов, ведь у меня с блеском получилось реализовать: полиморфизм (генерация мусорного кода), метаморфинг (замена инструкций аналогами), пермутацию (случайное перемешивание блоков кода с сохранением функционала и логики работы), обфускацию (метод запутывания логики кода, противодействие анализу), контроль целостности кода (для защиты от изменения, patch'ей), шифрование кода и данных. Рандомизация кода служит для защиты от автоматических распаковщиков, анализаторов, патчей, обфускация — для запутывания исследователя; при достаточной обфускации анализ программы может затянуться на долгие месяцы... Хватит болтовни, приступим! 0XBA11EE PRNGПервоначально следует написать генератор псевдослучайных чисел — сердце любого движка рандомизации кода. Генератор я взял простой, наподобие ANSI C, для моих целей его вполне хватало.
Работает он исправно, но, так как инициализирующее значение постоянно, каждый раз, при каждой компиляции будет выдана одна и та же последовательность чисел. После недолгих раздумий и чтения официального форума, я нашел значение, которым можно завести генератор — это timestamp, UNIX-время. Получить его можно вот таким образом: Генерировать случайное число, к примеру, в диапазоне 0 - 0xDEAD, теперь можно так:
0XBADC0DE или генерация мусораДля начала, попробуем написать макрос для генерации простой инструкции — int. Состоит int из двух байт — опкода 0xCD и номера прерывания, который и будет случаен. Получаем номер прерывания: Далее пишем следующую незаурядную конструкцию:
Пока все просто. Оформив эти 4 строки в отдельный макрос gen_int и вызвав несколько раз, убеждаемся с помощью отладчика или дизассемблера, что код действительно случайный: rept 7 { gen_int }. И вот что получилось:
Метод rept fasm'а выполняет код указанное количество раз. По-моему, начало более чем хорошее, нас ждет много интересного. Давай теперь рассмотрим генерацию инструкции lea; здесь я хочу осветить несколько аспектов. Сперва нужно завести константы, соответствующие регистрам:
Чтобы не нарушить работу кода, следует учитывать занятые регистры. Заведем переменные, хранящие их:
Их может быть сколько угодно — зависит от логики работы программы, логики работы генератора и строения блока кода. Ниже представлен макрос, генерирующий случайный регистр, не учитывая занятые. Использовать будем только как источник.
В принципе, можно включить в варианты и Esp Ebp регистры, но мне захотелось так. Теперь макрос, генерирующий случайный незанятый регистр:
Регистры Esp и Ebp не трогаем, дабы не сорвать стековый фрейм. Это первое, что я хотел осветить. Чтобы код был похож на произведенный нормальным компилятором (дабы не показывать сразу исследователю, что его водят за нос), следует немного ограничивать фантазию. Приведу пример на инструкции lea, которая, как известно, используется для получения\вычисления адреса. Принимающий регистр будет случайным, а как быть со вторым операндом? Возьмем значение в диапазоне Entry Point - (Entry Point + размер секции кода), ну или, для простоты, возьмем значение 0x1000. Для большего соответствия с нормальным кодом следует брать адреса из секции данных. Макрос, генерирующий инструкцию lea по правилам, описанным ранее:
Константу ENTRY_POINT объявляем заранее:
Или, что предпочтительнее: ENTRY_POINT = $$. Итог работы макроса, вызванного несколько раз:
Как видишь, код случаен, и не бросается в глаза необычностью. Теперь не мешало бы объединить написанные макросы в один и построить код так, чтобы его было легко изменять или добавлять в него новые методы генерации инструкций, но для начала напишем еще один макрос для генерации FPUинструкций:
Проверим:
Отлично! Теперь группируем, создаем макрос gen_trash, принимающий параметром количество генерируемых инструкций. Улучшить этот макрос можно, сделав параметром не количество инструкций, а максимальный размер в байтах. Еще лучшим ходом будет параметр, являющийся пределом случайному количеству инструкций/размеру в байтах. Реализуем первый, упрощенный, но немного уступающий другим вариант:
Теперь для генерации 10 случайных инструкций указываем в коде: gen_trash 10. Следует расширить этот макрос, что не составит труда. Добавляй как можно больше инструкций\вариантов: ветвления; статистику повторения инструкций; порядок следования (куча FPU-инструкций вперемешку с обычным кодом — это подозрительно, ты не находишь? Или десяток инструкций lea, идущих подряд? А бесконтрольный генератор вполне может творить такое). Идей в процессе должно возникать великое множество — пробуй все, что придет в голову, не ограничивай себя. Теперь пара слов об использовании макроса gen_trash. Сделаем простой расшифровщик, разбавленный мусором:
XOR_KEY, между прочим, тоже следует сделать случайным.
При большом количестве мусора и при достойном его качестве не так просто будет разобраться, что же в коде происходит, и как отделить его от мусора. Улучшить генератор можно, добавив работу с локальными\ глобальными переменными, различные переходы, ветвления, процедуры, различные варианты инструкций, сложные инструкции вида lea eax,[ ecx*4+100 ]... Но — главное!.. Самое главное — не забывай, что код должен быть схожим с генерируемым нормальным компилятором и одновременно хитрым, запутанным. Изучи частоту повторений инструкций в распространенных или входящих в состав операционной системы программ, а затем примени эту статистику в своем генераторе. 0XACED1A антиотладкаНи одна защита кода просто не представляется без антиотладочных трюков. Добавим и мы, но будем хитрее. Сделаем вставку случайного антиотладочного трюка в случайном месте, то есть просто добавим к макросу gen_trash, и трюк будет генерироваться наравне с инструкциями. Простой пример — если отладчик обнаружен, выполняется переход на случайный адрес в пределах секции кода.
Также трюки следует разбавлять мусором. Добавляй больше антиотладки — больше сюрпризов исследователю. 0XACE или рандомизация api-вызововПомимо бинарного мусора, код следует сделать высокоуровневым. Вполне послужит для этого Windows API. Функции могут не нести смысла, а могут быть и неотъемлемой частью программы. Простой пример вставки случайного API-вызова:
Не стоит забывать, что API-функции не сохраняют регистры Eax, Ecx и Edx. Сохраняй значения этих регистров, если в них содержатся и используются важные значения. Вставим вызов этого макроса в gen_trash. Подключи фантазию; вызовы функций не обязательно должны быть одиночными, высокоуровневый мусор должен взаимодействовать с бинарным — не подкопаешься. Неплохо будет эмулировать некоторые функции, то есть реализовать их код у себя. Вызов или использование своего кода являются вариантами, пример:
0XA11A5, или метаморфингМетаморфинг я реализовал как замену инструкций своими функциональными аналогами. FASM позволяет переопределять инструкции макросами, что очень удобно. Возьмем, к примеру, инструкцию mov reg32_1, reg32_2. Какие могут быть аналоги? Первое, что приходит в голову (вообще их можно придумать великое множество):
Примени фантазию, не следуй шаблонам, и за небольшой промежуток времени можно будет написать достаточное количество аналогов для всех инструкций. Напишем макрос, переопределяющий инструкцию mov. Обязательно проверяем, что аргументы являются регистрами, так как у нас есть замена только этого варианта:
Проверяем:
Итог:
Замечательно, не правда ли? Добавив как можно больше инструкций и вариантов замены, можно добиться замечательных результатов. 0XAB1E, или пермутацияЗдесь все тоже предельно просто и дает мощный результат. Нам нужно изменить расположение некоторых блоков кода без изменения функциональности и без повреждения кода. Для начала за блоки возьмем процедуры, далее эти блоки следует максимально уменьшить. Над способом случайного изменения блоков кода я недолго думал, возможно, есть более изящное решение — подумай. Суть такова: каждую процедуру оборачиваем в макрос, создаем для нее переменную — флаг, сигнализирующий об использовании, дабы не вставлять процедуры несколько раз. Например (пермутируем три процедуры, скелет), код главной структуры теперича выглядит так:
Проверив этот код, убеждаемся, что процедуры выставляются как надо, случайно, код не портится. 0XDEFACED, или обфускация: динамическое вычисление адресовОдин из способов противодействия дизассемблерам и обману анализаторов — динамическое вычисление адресов переходов или адресов переменных. Пример, как можно вычислять адрес:
Итог работы макросов:
Без трассировки и не узнаешь, куда ведет переход, следовательно, статический анализ обламывается. Здесь также стоит учитывать занятые\свободные регистры в генераторе мусора, так как постоянное использование Esp ставит клеймо на способе, да и само по себе накладно. Еще одной неплохой уловкой является вставка переходов на данные, но переходы эти никогда не выполняются (или выполняются только при наличии отладчика). Это сбивает с толку анализаторы, и они пытаются дизассемблировать данные. Пример макроса:
В итоге адрес data_addr будет анализироваться как код. 0XA55 — зашифровка кода\данныхЗамечательными функциями макроязыка FASM, отличающими его от других макроассемблеров, являются load и store. Использовать их можно для шифрования кода или данных. Простой пример, для шифрования используется xor:
Очень полезный макрос, я его использовал для зашифровки строковых данных. Пример использования:
0XABA51A, или контроль целостности кодаВычислив на стадии компиляции контрольные суммы участков кода, можно защититься от модификации, пересчитывая и проверяя при выполнении эти суммы. Также подобным образом можно детектировать трассировку посредством вставки в код прерывания int3, как делают многие отладчики. Макрос, вычисляющий crc32 сумму блока кода:
Хочу заметить, что операции вида if(original_hash != current_hash) Error() абсолютно бесполезны! Хотя используются повсеместно, даже в крутых протекторах. А вот нечто подобное:
Совсем другое дело. Двух зайцев сразу: обфускация — динамическое вычисление адреса перехода, и контроль целостности кода, то есть, если код был каким-либо образом изменен, будет выполнен переход кот знает куда. 0XACCEDE, или обман анализаторовАнализаторы исполняемых файлов вроде PEiD используют сигнатурный поиск, в базе находятся цепочки байт, которые встречаются в популярных протекторах\упаковщиках. Для того, чтобы сбить с толку взломщиков своей программы, я создал макрос, добавляющий в Entry Point программы случайную сигнатуру. Воспользовавшись вышеупомянутым анализатором или его аналогом и получив ложный результат, взломщик попытается распаковать программу либо автоматическим распаковщиком, либо вручную, следуя описанию. И, конечно же, ничего не получится, кроме тяжелого ступора.
0XAD105. ЗаключениеГрамотное использование и комбинирование описанных мною техник позволяет сделать серьезную защиту. Это и очень удобно: написав, отладив программу, с минимальными правками исходного кода превращаем ее в неприступный бастион. После того, как я написал свой набор макросов, протестировал и применил их к своей программе, мне пришла в голову еще одна замечательная идея. Данный метод я еще и автоматизировал следующим способом: поместил на сервер исходный код программы и компилятор FASM, при запросе пользователем trial-версии программы она автоматически компилируется; таким образом получается, что каждому пользователю выдается уникальная версия программы. Универсальные взломщики (патчеры, crack'и и т.п.) просто бессильны — придется ломать каждую копию отдельно. А это ведь непросто, учитывая, что весь код изменен, а не как у навесных протекторов, только «сверху». Мне, как разработчику, остается только чаще обновлять исходники и совершенно не волноваться о том, что мою программу могут взломать. Так что, open your eyes, open your mind! |