V-zlom.ru » Статьи » Защита программ от взлома

Защита программ от взлома

КРИС КАСПЕРСКИ АКА МЫЩЪХ

Прим редактора: Реальное название статьи - "защита игр от взлома", но игра - это тоже программа, а значит и защищают их аналогично

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

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

Арсенал защиты

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

Аппаратная защита — совсем другой разговор. При желании в микрочип можно перенести программу даже целиком, и тогда никто не сможет скопировать (если, конечно, выбрать правильный чип). Однако процесс разработки и отладки усложняется в десятки и даже сотни раз, «железное обеспечение» получается крайне негибким, неудобным в тиражировании и распространении, не говоря уже о невозможности исправить обнаруженные ошибки. Даже если чип имеет перепрошиваемое ПЗУ, разрешение на запись равносильно разрешению на чтение, так как самое ценное в железе — это прошивка. Скопировать железо намного проще, чем выдрать из защищенного чипа прошивку, если же она будет свободно распространяться как обновление... Можно, конечно, распространять не всю прошивку, а только измененные модули или даже их части. Что сделает хакер? Он просто создаст слегка модифицированный модуль, считывающий содержимое ПЗУ и выводящий его наружу контрабандным путем.

Защиты, о которых будем говорить, не требуют никакого дополнительного железа, они системно независимы, работают на прикладном уровне и не требуют прав администратора. Никаких драйверов! Никакого ассемблера! Возможно ли эффективно противостоять опытным хакерам в таких условиях? Возможно! Большинство защит ломаются из-за досадных ошибок и мелких конструктивных просчетов, допущенных разработчиком, который никогда не заглядывал в дизассемблерный листинг и не пытался взломать свое творение.

Тайники машинного кода

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

Текстовые строки

ASCII- и UNICODE-строки несут в себе очень богатую информацию: и текстовые сообщения, выводимые на экран при регистрации/окончании trial-срока/неправильном вводе пароля, и ветви реестра, и имена ключевых файлов, иногда и сами серийные номера/пароли. С паролями все ясно. Хранить их в открытом виде нельзя, нужно хэшировать их. А что плохого в текстовых сообщениях? Обнаружив их в теле программы, хакер по перекрестным ссылкам очень быстро найдет тот код, выводящий их, — вот что плохо. То же самое относится и к именам ключевых файлов с ветвями реестра.

 

Листинг

текстовая строка «wrong s/n» с перекрестной ссылкой, ведущей к процедуре sub_401000

.text:00401016 call sub_401000
.text:0040101B add esp, 4
.text:0040101E test eax, eax
.text:00401020 jz short loc_40102F
.text:00401022 push offset aWrongSN ; «wrong s/n\n» ; указатель на строку
.text:00401027 call _printf
...
.data:00406030 aWrongSN db 'wrong s/n',0Ah,0 ; DATA XREF: 00401022h^o

Чтобы затруднить анализ, необходимо либо зашифровать все строки, расшифровывая только перед непосредственным употреблением (если расшифровать сразу же после запуска программы, хакер просто снимет дамп и увидит их в виде прямого текста), либо поместить их в ресурсы и грузить через LoadString — это программируется легче, но в то же время легче ломается. Хакеру достаточно открыть файл в любом редакторе ресурсов, найти нужную строку, запомнить ее идентификатор, запустить отладчик и установить условную точку останова. Далее — дождаться вызова LoadString с нужным uID, определить адрес буфера lpBuffer, установить на него точку останова и... если lpBuffer выводится не сразу, а передается через цепочку промежуточных буферов, хакер матерится, но ничего не делает, потому что не может сделать ничего. Аппаратных точек останова всего четыре, и если защита использует не последний буфер в цепочке, то отследить момент реального обращения к строке становится невозможно (на самом деле возможно: с помощью секретного хакерского оружия NO_ACCESS на страницу, только об этом не все знают).

В дизассемблере же отследить перемещение строки по локальным буферам практические невозможно (во всяком случае, автоматически). Глобальные буфера (в которые компилятор пихает константные строки, в том числе зашифрованные) отслеживаются сразу, так что LoadString по некоторым позициям очень даже рулит!

Символьная информация

отладочная информация

По умолчанию компилятор генерирует файл без отладочной информации, и она попадет туда только в исключительном случае, но все-таки попадет. Такая участь настигает не только начинающих программистов, не знающих, чем отличается Debug от Release, но и «маститые» фирмы, выпускающие достойные программные продукты. Допустим, у нас имеется глобальная программа IsRegistered, тогда смысл пары машинных команд будет ясен и без комментариев.

Листинг

дизассемблерный листинг исполняемого файла с отладочной информацией

.text:00405664 cmp _IsRegistered, 0
.text:0040566B jz short loc_40567A

Никогда не оставляй отладочную информацию в откомпилированной программе!

динамические библиотеки

Имена неэкспортируемых функций уничтожаются компилятором, экспортируемые же по умолчанию остаются «как есть». С++-компиляторы в дополнение «замангляют» имена, дописывая к ним «зашифрованный» прототип функции, но IDA PRO с легкостью возвращает их в исходный вид. Если защитный модуль реализуется в виде динамической библиотеки (очень часто случается именно так), наличие символьных имен (причем с готовыми прототипами) значительно упрощает анализ. Например, OO Software (создатель одноименного дефрагментатора) любит таскать за собой библиотеку oorwiz.dll (очевидно, расшифровывается как «OO Registration Wizard»), экспортирующую всего три функции, но зато какие...

 

Листинг

библиотека oorwiz.dll от OO Software экспортирует функции, говорящие сами за себя

3 0 00001FD0 RegWiz_InitLicMgr
1 1 000019D0 RegWiz_InitReadOnly
2 2 00001D00 RegWiz_InitTrial

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

rtti

Динамические классы, тесно связанные с механизмом RTTI (Runtime Type Identification) и активно используемые компиляторами DELPHI/Borland С++ Builder, сохраняют в откомпилированном файле не только свою структуру, но и символьные имена! Если брать как пример результат работы утилиты DEDE, реконструировавшей структуру классов программы Etlin HTTP Proxy Server, сразу в глаза бросится класс TfrmRegister, который соответствует форме fRegister и обрабатывает нажатие кнопки «OK» процедурой bOKClick, расположенной по адресу 48D2DCh. Благодаря динамическим классам сердце защитного механизма было локализовано всего за несколько секунд!

Не используй RTTI в защитных механизмах или, по крайней мере, не давай формам и обработчикам осмысленные имена!

Обфускация

Код, генерируемый компилятором, очень громоздок, и разобраться в нем крайне непросто, но возможно. Чтобы помешать злоумышленникам, некоторые протекторы используют «запутывание», или обфускацию (англ. obfuscation). В простейшем случае автоматический кодогенератор, свинченный с полиморфного движка, внедряет в код огромное количество незначащих команд типа NOP, XCHG EAX, EBX/XHG EBX, EAX, нашпиговывая ими программу как рождественскую утку/гуся. Более совершенные генераторы используют разветвленную систему условных переходов, математические операции и присвоения, результат которых никак не используется, и другие полиморфные технологии.

Листинг

фрагмент программы, защищенной протектором armadillo

.00434000: 60 pushad
.00434001: E800000000 call .000434006 —----— (1)
.00434006: 5D pop ebp
.00434007: 50 push eax
.00434008: 51 push ecx
.00434009: EB0F jmps .00043401A —----— (2)
.0043400B: B9EB0FB8EB mov ecx,0EBB80FEB ;«e?0e»
.00434010: 07 pop es
.00434011: B9EB0F90EB mov ecx,0EB900FEB ;«e?0e»
.00434016: 08FD or ch,bh
.00434018: EB0B jmps .000434025 —----— (3)
.0043401A: F2 repne
.0043401B: EBF5 jmps .000434012 —----— (4)
.0043401D: EBF6 jmps .000434015 —----— (5)
.0043401F: F2 repne
.00434020: EB08 jmps .00043402A —----— (6)
.00434022: FD std
.00434023: EBE9 jmps .00043400E —----— (7)
.00434025: F3 repe
.00434026: EBE4 jmps .00043400C —----— (8)
.00434028: FC cld
.00434029: E959585051 jmp 051533887
.0043402E: EB0F jmps .00043403F —----— (9)

Можно сгенерировать хоть миллион команд — это легко. Проанализировать их намного сложнее, если вообще возможно. Назначающие команды и заведомо никогда не исполняющиеся условные переходы типа XOR EAX, EAX/JNZ trg сможет отсеять и компьютер (достаточно написать простенький плагин к дизассемблеру IDA PRO). Освободиться от ненужных вычислений значительно сложнее. Как минимум, необходимо загнать все команды на граф, отображающий зависимости по данным, и убрать замыкающиеся ветви. Некоторые хакерские команды уже решили эту задачу (например, группа Володи с wasm'а), однако готовых инструментов в публичном доступе что-то не наблюдается, значит, юные взломщики, наткнувшись на обфускаторный код, скорее обломаются, чем взломают его. С точки зрения разработчика программы очень хорошо!

Высаживаться на разработку собственного обфускатора совершенно необязательно, есть готовые — как коммерческие, так и бесплатные. Например, .NET Obfuscator —http://blogs.msdn.com/obfuscator/default.aspx. Забавно, но большинство обфускаторов не используют обфускацию для защиты самих себя от взлома! А все потому, что в программах, критичных к производительности (к ним, например, относятся трехмерные игры), обфускация вызывает значительные тормоза и запутывать можно только редко вызываемые модули, например код защитного механизма. Однако здесь возникает угроза: хакер просто «выломает» защитный механизм из программы не анализируя его устройство. Как правило, для этого достаточно проанализировать код материнской процедуры (которая не подвергалась обфускации) и удалить вызов «запутанной» защитной функции, подсунув «правильный» код возврата, который ожидает вызывающая функция. Чтобы помешать подобным действиям хакера, защитная процедура, кроме проверки аутентичности копии программы, должна делать что-то полезное, такое, без чего программа не сможет работать. Но и в этом случае шансы хакера на взлом остаются высокими: шпионаж за API-функциями и реестром дает богатую пищу для размышлений, которая часто избавляет от необходимости анализировать машинный код.

Обфускация — не панацея. Слепое использование готовых обфускаторов лишь увеличивает объем защищаемой программы и ухудшает производительность, но далеко не всегда затрудняет взлом!

Шифровка

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

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

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

Широкому внедрению динамической шифровки препятствуют следующие проблемы. Во-первых, сложность разработки и трудоемкость отладки. Во-вторых, производительность, точнее, полное отсутствие таковой (что очень критично в играх). В-третьих, возможность снять дамп руками самого расшифровщика, которому последовательно передаются адреса всех зашифрованных регионов. Есть другой способ, при котором легким битхаком его тело исправляется таким образом, чтобы он только расшифровывал, но ничего не зашифровывал. Конечно, способы сопротивления существуют. Например, используют перекрывающие шифроблоки, которые могут быть расшифрованы только по очереди, но не все сразу, или многослойную шифровку типа «луковицы», при которой один шифровщик плюс немного полезного кода вложен в другой, а тот в третий и т.д. Шифровщики как бы перемешаны с кодом, и «отодрать» чистый дамп невозможно. Очень надежно, но реализуется очень и очень сложно.

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

 

p-код

Термин p-код восходит к интерпретатору Visual Basic'а, но в хакерских кругах означает нечто большее и подразумевает любой интерпретируемый код произвольной виртуальной машины (не путать с VMWare). Непосредственное дизассемблирование в этом случае становится невозможно, и приходится прибегать к декомпиляции, для чего необходимо проанализировать алгоритм работы интерпретатора, написать (и отладить!) декомпилятор, что требует пива и времени. Правда, разработка (и отладка) интерпретатора тоже не обходится даром. Плюс разработка языка тянет за собой кучу вспомогательного инструментария (в первую очередь понадобится отладчик). Иначе как на нем программировать?..

Все это выливается в солидный проект, который может быть использован всего один раз, в одной-единственной программе, причем желательно слегка изменять ядро интерпретатора после выхода нескольких версий, чтобы написанный хакером декомпилятор перестал работать. Что поделаешь, защита требует жертв и больших вложений. Плюс производительность интерпретируемого кода плетется в самом хвосте, отставая от динамической расшифровки и обфускации, но... Может быть, есть возможность реализовать на p-коде только защитные модули? Нет! Тогда их отломают! Не глядя! На p-коде должна быть реализована вся программа целиком, в том числе защитный механизм, тогда без декомпилятора его будет не хакнуть. Но это все теория. На практике полностью загнать программу в p-код не удается из-за производительности и критичные к быстродействию функции пишутся на языке высокого уровня или даже ассемблере. Зато вызываются они уже из p-кода, в котором сосредоточена основная логика по типу «если нельзя, то все-таки».

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

Чаще всего разработчики используют Pascal- или Basic-подобные языки — не самый лучший выбор в плане защищенности. Программа на p-коде при этом представляет собой последовательность ключевых слов (операторов языка) с аргументами и декомпилируется очень просто. На другом конце шкалы сложности находится Машина Тьюринга, Сети Петри, Стрелка Пирса и прочие примитивные виртуальные машины, которые реализуют фундаментальные логические операции, в результате чего даже такая конструкция, как «IF THEN ELSE», распадается на сотни микрокоманд! Прежде чем хакер проанализирует их, солнце успеет погаснуть, или… Может, все-таки не погаснет? Существует множество продвинутых способов наглядной визуализации таких алгоритмов, к тому же мы опять забываем о производительности... Реализовать crackme на базе Машины Тьюринга еще можно, но коммерческое приложение — едва ли.

Хорошая идея — приложить к этому делу Форт. Его преимущества: простота реализации Форт-машины, компактность p-кода, довольно высокая производительность, сложность декомпиляции и ни-на-что-непохожесть. Форт стоит особняком от всех языков, совсем не стремясь соответствовать человеческим привычкам, «здравому смыслу» и «логике». Разобраться в его идеологии с нуля непросто. Хакеру придется нарыть кучу литературы и запастись терпением. Даже если ему не наскучит, разработчик защиты получит хорошую фору по времени...

Листинг

дизассемблерный листинг Форт-машины (из программы see.exe, поставляемой вместе с Interrupt List'ом)

seg000:1F29 loc_1F29: ; CODE XREF: start+39B9vj
seg000:1F29 call sub_1F04
seg000:1F2C mov word_A5C, bx
seg000:1F30 mov si, dx
seg000:1F32 mov bp, [bx+38h]
seg000:1F35 mov sp, [bx+36h]
seg000:1F38 lodsw
seg000:1F39 mov bx, ax
seg000:1F3B jmp word ptr [bx]

 

Другая хорошая идея — использовать готовые интерпретаторы малораспространенных языков (Пролог, Хаскель, Оберон), под которые еще не написаны декомпиляторы. Многие из них разработаны в недрах исследовательских институтов и поставляются в исходных текстах, что упрощает модификацию ядра интерпретатора и позволяет легко адаптировать чужой язык к своим целям.

Круче всего — нарыть в Сети эмулятор малоизвестной ЭВМ, под которую есть компилятор с языка высокого уровня (С, ФОРТАН), и... Хакеру придется очень худо, придется осваивать неизвестный ему машинный язык, искать дизассемблер (или, скорее, писать собственный), прилагая титанические усилия для взлома. Разработчик тем временем будет пить пиво и кодить в привычной для него среде. Самое замечательное — то, что в следующей версии программы можно использовать эмулятор от «другой» ЭВМ, перекомпилировав весь код без адаптации и каких-либо изменений. Точнее, совсем без изменений, конечно, не получится, но трудоемкость переноса не сравнить с тем объемом работы, который предстоит проделать взломщику!

Секреты привязки

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

Практически нереально создать защиту, которая безошибочно распознавалась бы любым приводом (включая что-то сильно раздолбанное или слишком нестандартное), но в то же время не копировалась бы ни одним известным копировщиком. Лучший результат дает привязка к геометрии спиральной дорожки (защиты типа CD-COPS, Star-Force) — ее невозможно скопировать, но легко проэмулировать или… подобрать диск с похожими характеристиками. Для предотвращения эмуляции разработчикам Star-Force пришлось очень глубоко зарыться в систему, в результате чего получился полный глюкодром, а установка очередного пакета обновлений на Windows требует параллельной установки соответствующего обновления для Star-Force. Так что лучше похоронить эту тему раз и навсегда и даже не пытаться возвращаться к ней. Иначе столкнешься с таким количеством проблем, осилить которое сможет только крупная компания, да и то…

Серийные номера

Самое простое — защитить программу серийным номером, который элементарно программируется и не вызывает никаких конфликтов ни с чем. Конечно, это не помешает Пете скопировать понравившуюся ему программу у Васи или даже выложить серийный номер в Сеть напоказ всем желающим. Соответствующий способ борьбы — использовать черный список «засвеченных» серийных номеров, проверяя их на соответствие при установке обновлений. Если защищенная программа предполагает взаимодействие с удаленным сервером, процедура проверки валидности серийных номеров значительно упрощается и нелегальный пользователь просто не получит доступ к серверу (очень актуально для сетевых игр!).

Несетевые программы также могут периодически ломиться в Сеть, передавая свой серийный номер. Если этот номер не подтверждается, регистрация считается недействительной и работа программы прекращается. При этом возникают следующие проблемы: поддержка выделенного и постоянно работающего сервера стоит денег, к тому же такой сервер сам по себе представляет весьма нехилую мишень для атаки. Что если хакеры завесят его или, еще хуже, проникнут внутрь и украдут легальные номера? Значит, необходимо содержать толкового администратора и разрабатывать протокол проверки серийных номеров с учетом возможного перехвата, например использовать несимметричную криптографию — тогда украденная база не позволит восстановить ни один легальный серийный номер. Впрочем, что-то я все о мелочах да о мелочах.

 

Как быть с персональными брандмауэрами, популярность которых постоянно растет? Пользователь не выпустит программу в Сеть! Хотя любой брандмауэр легко обойти установкой собственного драйвера, так поступать нельзя. Журналисты тут же поднимут шумиху и обольют программиста грязью. Требовать обязательного наличия интернета нельзя. До сих пор он есть не у всех (если все-таки требовать, необходимо как минимум уметь работать через прокси). Правда, можно прибегнуть к одному очень хитрому маневру: через функцию InternetGetConnectedState определить, доступен ли в настоящее время интернет, и если сетевое соединение действительно присутствует, потребовать от пользователя открыть брандмауэр (или что там у него есть). Популярная программа Macro Express поступает именно так, но и этот путь не свободен от проблем. Если выход осуществляется через локальную сеть, функция InternetGetConnectedState не сможет сказать, имеется ли выход оттуда в интернет. С удаленным доступом по модему намного проще, однако не стоит забывать, что пользователь может подключаться не только к провайдеру, но и к своему приятелю, у которого стоит импровизированный «сервер», не предоставляющий доступа в интернет... Однако InternetGetConnectedState этого не объяснишь! Наконец, системную библиотеку WININET.DLL, которая экспортирует функцию InternetGetConnectedState, очень просто хакнуть в контексте памяти ломаемого приложения, и тогда защите настанут кранты.

Криптография и все, что связано с ней

Вместо серийных номеров некоторые разработчики предпочитают использовать криптографические ключи и прочие системы цифровой подписи типа сертификатов, усиленно продвигаемые на рынок компанией Microsoft и прочими гигантами. Все чушь собачья! Цифровая подпись хорошо работает только в связке «клиент-сервер», но на локальной машине она бессмысленна. Да, сгенерировать «левый» ключ в этом случае невозможно, но если как следует пошурудить hiew'ом в исполняемом файле, никакие ключи уже не понадобятся. Так что цифровая подпись должна использоваться совместно с шифровкой кода и проверкой целостности!

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

Оборудование — физическое и виртуальное

Привязка к аппаратуре — это грязный и пакостный трюк, отравляющий жизнь легальным пользователям и все-таки не спасающий от взлома, поскольку любая привязка обнаруживается опытным хакером элементарно. Либо эмулируется, либо вырезается. Виртуальные машины типа VM Ware представляют собой большую проблему, на варезных серверах уже появились программы с примечанием «Запускать под VM Ware». Достаточно зарегистрировать программу один раз, и дальше можно тиражировать ее сколько угодно.

Распознать виртуальные машины довольно легко, они несут на своем горбу довольно специфичный набор оборудования. Однако уже появились патчи, которые скрывают присутствие VM Ware, и, что хуже всего, многие легальные пользователи предпочитают запускать второстепенные утилиты из-под виртуальных машин, чтобы не засирать основную систему. Защита не может (не имеет ни морального, ни юридического права!) отказывать VM Ware в исполнении, иначе пользователи снесут защиту к чертям, мотивировав это «производственной необходимостью». Они будут правы (правда, так они не освобождаются от регистрации). Некоторые даже подадут в суд за умышленное ограничение функциональности, не отраженное на упаковке. Перейдем к практике...

<span style="font-family: tahoma, arial, verdana, sans-serif, 'Lucida Sans'

  • Автор: kennen
  • Комментарии: 0
  • Просмотры: 1387
0

Добавить комментарий

Вы не авторизованы и вам запрещено писать комментарии. Для расширенных возможностей зарегистрируйтесь!