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

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

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

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

Вот три кита, на которых держатся защитные механизмы: машинный код, шифровка и 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 в исполнении, иначе пользователи снесут защиту к чертям, мотивировав это «производственной необходимостью». Они будут правы (правда, так они не освобождаются от регистрации). Некоторые даже подадут в суд за умышленное ограничение функциональности, не отраженное на упаковке. Перейдем к практике...

Лучше всего привязываться к жесткому диску. Как показывает практика, он меняется реже всего. Чтобы прочесть серийный номер, совершенно не обязательно писать собственный драйвер, достаточно воспользоваться библиотекой ASPI (но она не установлена по умолчанию) или SPTI (есть только в NT/XP и требует прав администратора). Есть и вот такой трюк. Вызываешь GetVolumeInformation и смотришь на размер тома в байтах. Не слишком уникальная информация, но на «другой» машине с вероятностью, близкой к единице, она будет иной. Плюс такая проверка реализуется очень просто, работает стабильно на всем семействе Windows-подобных систем (в том числе эмуляторы) и не требует никаких прав.

Компиляция on demand

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

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

Как затруднить распаковку

Откомпилированная программа обычно подвергается упаковке. Цель здесь совсем не в уменьшении размера, а в затруднении анализа. Упаковщик набивает программу антиотладочными приемами и прочей бодягой, которая затрудняет пошаговую трассировку или даже делает ее невозможной. На самом деле хакер и не собирается ничего трассировать, а только снимает с работающего приложения дамп и дизассемблирует его (реконструировать exe-файл для этого необязательно). Надежно противостоять снятию дампа на прикладном уровне невозможно, а спускаться на уровень драйверов как-то не хочется. Некоторые защиты искажают PE-заголовок, гробят таблицу импорта и используют другие грязные трюки, с помощью которых затрудняют дампинг, но не предотвращают его в принципе.

глобальные инициализированные переменные

Предлагаю альтернативный путь — не препятствовать снятию дампа, а сделать полученный образ бесполезным, для чего достаточно использовать глобальные инициализированные переменные, «перебивая» их новыми значениями.

Листинг

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

char *p = 0; // глобальная переменная 1

DWORD my_icon = MY_ICON_ID; // глобальная переменная 2

if (!p) p = (char*) malloc(MEM_SIZE);

my_icon = (DWORD) LoadIcon (hInstance, my_icon);

Задумайся, что произойдет, если сбросить дамп с работающей программы. В переменной p окажется указатель на когда-то выделенный блок памяти, условие (!p) обломится и новая память не будет (!) выделена, а при обращении по старому указателю произойдет исключение. Другими словами, хакер уже не сможет изготовить исполняемый файл из дампа! Как минимум, придется восстановить значения всех глобальных переменных — геморрой :(. Ладно, изготовить исполняемый файл из дампа нельзя, но, может, дизассемблировать его? А вот и нельзя...

После выполнения функции LoadIcon переменная my_icon будет содержать не идентификатор иконки, а ее обработчик. Хакер не сможет установить, что это за иконка (строка, битмап или другой ресурс), и ему придется обращаться к отладчику, противостоять которому намного проще, чем дизассемблеру. Кстати, такой прием экономит память и широко используется во многих программах. Например, в стандартном «Блокноте» — попробуй снять с него дамп и обломайся :).

стартовый код

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

Листинг

типичный представитель стартового кода (в случае с Microsoft Visual C++ MFC)

.text:00402A82 push ebp
.text:00402A83 mov ebp, esp
.text:00402A85 push 0FFFFFFFFh
.text:00402A87 push offset unk_403748
.text:00402A8C push offset loc_402C06
.text:00402A91 mov eax, large fs:0
.text:00402A97 push eax
.text:00402A98 mov large fs:0, esp
.text:00402A9F sub esp, 68h
...
.text:00402BAA call ds:GetModuleHandleA
.text:00402BB0 push eax
.text:00402BB1 call _WinMain@16 ; WinMain(x,x,x,x)

точка останова на GetModuleHandleA

Вызов API-функции GetModuleHandleA сразу же бросается в глаза. Если хакер установит сюда точку останова, отладчик/дампер «всплывет» в start-up-коде еще до передачи управления WinMain (также можно поставить точки останова на GetVesion/GetVersionEx, GetCommandLine, GetStartupInfo и т.д.). Если точка останова программная, распаковщик может обнаружить ее по наличию ССh в начале API-функции и, с некоторой долей риска, снять ее. Если второй байт функции равен 8Bh, то перед нами, очевидно, предстает стандартный пролог, первый (оригинальный) байт которого равен 55h. Получаешь права на запись через VirtualAlloc, меняешь CCh на 8Bh и продолжаешь распаковку в обычном режиме. Пусть хакер крякнет! Правда, в последующих версиях Windows пролог API-функций может быть модифицирован, и тогда этот трюк не сработает.

отладочные регистры

Аппаратную точку останова можно обнаружить чтением регистров Drx. Команда mov eax,DrX на прикладном уровне приводит к исключению, кроме того, отладчик (теоретически) может отслеживать обращение к отладочным регистрам, чтобы маскировать свое присутствие, — x86 процессоры предоставляют все необходимое для этого. Но если распаковщик прочитает свой контекст, он сможет дотянуться и до Drx, причем не только на чтение, но и на запись! Получается, что можно не только обнаружить точки останова, но и обезвредить их. Весь вопрос в том, как получить контекст. Чтение SDK выявляет API-функцию GetThreadContext, которая как раз для этого и предназначена, однако пользоваться ей нельзя, иначе хакер установит сюда точку останова и защита проиграет войну.

Нужно действовать так. Регистрируешь из распаковщика собственный обработчик SEH, возбуждаешь исключение (делишь на ноль, обращаешься по недействительному указателю) и получаешь контекст в одном из аргументов структурного обработчика. Остается установить точку останова на fs:0, где и хранится указатель на SEH-обработчик (но до этого додумается не каждый хакер).

структурные исключения

Кстати о fs:0. Первое, что делает стартовый код, — это регистрация собственного SEH-обработчика, поэтому установка точки останова на fs:0 позволяет хакеру всплыть сразу же после завершения распаковки, следовательно, распаковщик должен обращаться к этой ячейке как можно чаще. Десятки или даже тысячи раз, причем следует класть туда не что угодно, а именно ESP, иначе хакер установит условную точку останова (soft-ice это позволяет) и легко обойдет защиту.

поиск по сигнатуре

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

контроль $pc

Еще один трюк. Большинство распаковщиков располагаются в стороне после распакованного кода и при передаче управления на оригинальную точку входа прыгают куда-то далеко. Хакер может использовать этот факт как сигнал о том, что распаковка уже завершена. Конечно, установить аппаратную точку останова на это условие уже не удастся, и придется прибегнуть к пошаговой трассировке (ей легко противостоять), но ради подобного случая хакер может написать и трейсер нулевого кольца. Несложно. Сложно определить, когда же заканчивается распаковка. Контроль на $pc (в терминологии x86 — «eip») — единственный универсальный способ, который позволяет сделать это без особых измен и, чтобы обломать хакера, распаковщик должен как бы «размазывать» себя вдоль программы. Тогда он победит!

Боремся с отладчиком

Распространенный миф гласит, что штурмовать отладчик уровня soft-ice можно только из ядра, а надежно обуть его с приказного уровня невозможно. Неправда. В частности, эффективная отладка форт-программ (как и другого p-кода) под soft-ice невозможна. Отладчик просто вращается внутри форт-машины, наматывая мили на кардан, хакер матерится, выкуривает одну сигарету за другой, но не может предложить ничего конструктивного, кроме как написать декомпилятор, на что требуется время, которого никогда нет.

антиотладка

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

Следует помнить и о существовании такой штуки, как IceExt — специальной примочки для soft-ice, которая скрывает его от многих защитных механизмов. Всегда проверяй все применяемые идеи на вшивость, и не только под soft-ice+IceExt, но и в альтернативных отладчиках типа OllyDbg. Совершенно недопустимо отказываться от работы в присутствии пассивного отладчика. Аргумент «soft-ice держат на компьютере только хакеры» идет лесом на хутор. Туда же посылаются и разработчики. Необходимо противодействовать лишь активной отладке. Нейтрализации точек останова обычно бывает вполне достаточно. Не нужно мешать отладчику, главное — сделать этот процесс максимально неэффективным.

вероятностное поведение программы

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

взлом по следам, взлом без следов

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

полезные советы россыпью

Проверяй серийный номер (пароль) не с первого байта (лучше всего с пятого), этим защита обломает начинающего хакера, который установил точку останова на начало. Также не проверяй последние символы серийного номера. И не проверяй середину! Проверяй не более половины символов вразброс. Смешно, конечно, но очень выручает. Никогда не считывай серийный номер (пароль, имя пользователя) из строки редактирования целиком! Хакер тут же найдет его в памяти и расставит точки останова, в которые угодит защитный механизм. Считывай только по одному вводимому символу за раз (через WM_CHAR или DDE) и тут же шифруй их (если скалывать считанные символы в локальный буфер, то получится тот же самый WM_GETTEXT, только реализованный своими руками).

Боремся с дизассемблером

Оптимальный дизассемблер — IDA PRO. Действительно очень мощный дизассемблер, его не так-то просто взять на испуг. С классическими защитными приемами (типа прыжка в середину команды) он справляется даже не замечая, что тут было что-то защитное. Если на пути хакера встретится стена зашифрованного/упакованного кода, он напишет короткий (длинный) скрипт и расшифрует все, что нужно, даже не выходя из дизассемблера. Для p-кода будет написан отдельный плагин типа «докомпилятор».

Самым мощным антихакерским средством был и остается косвенный вызов функций по указателю, передаваемому в качестве аргумента. Хакер натыкается на что-то типа call eax и матерится в бессильной злобе, пытаясь определить, что содержится в eax на данный момент. Если материнская функция вызывается обычным способом, взломщику достаточно просто перейти по перекрестной ссылке, заботливо сгенерированной IDA, и подсмотреть, что передается функции. Однако если указатель инициализируется далеко от места вызова, хакеру придется раскрутить всю цепочку вызовов. Если же функция, вызывающая косвенную функцию, сама вызывается косвенным путем, то вообще наступают кранты! Остается запускать отладчик, устанавливать точку останова на call eax и смотреть ее значение вживую, однако точкам останова легко противостоять. К тому же в различные моменты времени eax может указывать на разные функции, тогда простое подглядывание eax ничего не даст. Дизассемблер ослепнет и оглохнет!

Листинг

пример программы, вызывающей функции по указателю

sub_sub_demo(int a, void *p, void *d)
{
// printf(«sub_sub_demo\n»);
if (--a) return ((int(*)(int, void*, void*))p)(a, p, d);
return 0;
}

sub_demo(int a, void *p, void *d)
{
// printf(«sub_demo\n»);
if (--a) return ((int(*)(int, void*, void*))d)(a, p, d);
return 0;
}

demo(int a, void *p, void *d)
{
// printf(«demo\n»);
((int(*)(int, void*, void *))p)(a, p, d);
}

main()
{
demo(0x69,sub_demo, sub_sub_demo);
}

В исходном тесте все понятно. Функция main вызывает функцию demo, передавая ей указатели на sub_demo и sub_demo, которые поочередно вызывают друг друга, каждый раз уменьшая счетчик на единицу. Короче, мы имеем цикл. Но какой! Ты оцени его дизассемблерный код.

Листинг

дизассемблерный листинг, демонстрирующий мощь косвенного вызова функций

.text:00401000 loc_401000: ; DATA XREF: _maino
.text:00401000 mov ecx, [esp+4]
.text:00401004 dec ecx
.text:00401005 jz short loc_401018
.text:00401007 mov eax, [esp+0Ch]
.text:0040100B push eax
.text:0040100C mov eax, [esp+0Ch]
.text:00401010 push eax
.text:00401011 push ecx
.text:00401012 call eax
.text:00401014 add esp, 0Ch
.text:00401017 retn
.text:00401020
.text:00401020 loc_401020: ; DATA XREF: _main+5o
.text:00401020 mov ecx, [esp+4]
.text:00401024 dec ecx
.text:00401025 jz short loc_401038
.text:00401027 mov eax, [esp+0Ch]
.text:0040102B mov edx, [esp+8]
.text:0040102F push eax
.text:00401030 push edx
.text:00401031 push ecx
.text:00401032 call eax
.text:00401034 add esp, 0Ch
.text:00401037 retn
.text:00401040
.text:00401060 _main proc near ; CODE XREF: start+AF p
.text:00401060 push offset loc_401000
.text:00401065 push offset loc_401020
.text:0040106A push 69h
.text:0040106C call sub_401040
.text:00401071 add esp, 0Ch
.text:00401074 retn
.text:00401074 _main endp

IDA Pro не смог распознать функции, хотя восстановил перекрестные ссылки на main — они еще ни о чем не говорят! Функции sub_demo и sub_sub_demo вызываются совсем не оттуда. Возьмем «функцию» loc_401000. Она принимает указатель в качестве аргумента и тут же вызывает его. А что это за указатель? Да хвост его знает! Перекрестной ссылки на материнскую функцию же нет. Единственный способ установить истину — проанализировать всю цепочку вызовов с самого начала программы, с функции main, но слишком трудоемко, так что даже не обсуждается (особенно если ломать реальную программу). Даже в таком случае приходится постоянно следить за указателями, которые пляшут как кони, причем, даже если развяжешь себе пупок, не сможешь понять, что находится в них в каждый конкретный момент...

Защита будет значительно усилена, если реализовать модель Маркова — функцию, возвращающую указатель на функцию. Такой прием программирования не слишком популярен, так как непривычен и лишен языковой поддержки. Язык С вообще не позволяет объявлять функции, возвращающие указатели на функции, поскольку такие определения рекурсивны и программисту приходится возиться с постоянным преобразованием типов, что не слишком украшает программу и является потенциальным рассадником ошибок. Тем не менее на автоматическое дизассемблирование моделей Маркова не способен ни один дизассемблер, в том числе IDA PRO.

Боремся с мониторами

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

э либо, посылая специальные Window-сообщения, удалять из списка «свои» обращения. Естественно, если хакер переименует окно (делается через FindWindow/SetWindowText), защита обломается по полной программе. Так что такой прием слишком ненадежен.

А что надежно? Хранить флаг регистрации вместе с настройками в одном ключе реестра в двоичном виде — вот тут его мониторинг ничего не даст. Хакер видит, что из такой ветви читается куча байтов, но ему неведомо, какой из них и за что отвечает. Выяснить можно только отладкой/дизассемблированием защищенной программы, сопротивляться чему несложно.

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