В данной статье мы представим себя в роли разработчика лицензированного ПО и рассмотрим способы защиты от взлома нашей программы пиратами.
Введение
Мы будем рассматривать методы защиты программного обеспечения, написанного на компилируемом языке, от реверс-инжиниринга, пиратства и внедрения стороннего вредоносного ПО. Изложенная информация актуальна, поскольку одной из целей любого проприетарного проекта является лицензировать и контролировать распространение данного программного обеспечения. Практически все программы, требующие наличие лицензии подвергаются попыткам взлома и многие из них на данное время имеют пиратски аналоги. Далее для простоты взломанную программу будем называть – крякнутой, а процесс взлома – кряком (англ. crack).
В данной статье мы представим себя в роли разработчика лицензионного программного обеспечения на С++ и будем анализировать какие методы защиты следует использовать, чтобы усложнить процесс кряка нашей программы. Следует понимать, что любая программа может быть в конечном итоге взломана, а абсолютной защиты не существует, целью является сделать реверс инжиниринг наиболее сложным и время, требуемое на взлом программы больше, чем время между обновлениями нашего вымышленного ПО.
Общие сведения
Пиратство программного обеспечения является одной из основных проблем для разработчиков. Для защиты ПО от незаконного распространения недостаточно лицензии, необходимо защитить программу от обратного инжиниринга. Существуют сотни способов защиты программы, однако все они также имеют свой обход. Предположим, что мы хотим написать проприетарную программу на компилируемом языке С++ под операционную систему Windows, мы её лицензируем и будем продавать копии. Рассмотрим, как же нам необходимо поступить, чтобы единственным способом получения нашего ПО – была покупка его лицензии у разработчика.
Разработанное на С++ ПО будет собрано в исполняемый файл, который и будет продаваться конечному пользователю. Во избежание его дальнейшего незаконного распространения в первую очередь нам необходимо встроить проверку лицензии в наш код: при запуске программы будет проверяться наличие лицензии у данного пользователя. Часть кода, которая занимается проверкой лицензии будем называть протектором. Оптимально чтобы протектор проверял наличие лицензии у пользователя через интернет. В таком случае если пользователь, который купил нашу программу передаст её другому, протектор не обнаружит лицензию и попросит приобрести ПО, либо просто завершит процесс.
Реверс инжиниринг
В случае, если мы проверяем наличие лицензии через интернет, подделать её практически невозможно, однако стоит понимать, что программа вместе с протектором находится на компьютере пользователя. Это значит, что для человека, владеющего отладчиком и умеющего редактировать ассемблерный код, не составит никакого труда просто удалить протектор из программы или сделать так, чтобы проверка лицензирования работала неправильно и всегда думала, что программа лицензионная. Анализ работы бинарного кода и внесение в него изменений без наличия исходников называется обратной инженерией (англ. reverse engineering). Декомпиляция ассемблерного кода обратно в код на С или С++ невозможна, однако возможно получить дизассемблер – текстовое представление машинного кода. Несмотря на то, что чтение такого кода достаточно сложно, для реверс-инженера этого вполне достаточно, чтобы найти протектор. Рассмотрим способы защититься от обратной инженерии.
Методы защиты от реверса
Существуют множество методов защиты от кряка. Я перечислю наиболее эффективные современные методы защиты, а далее в статье мы рассмотрим каждый из них подробнее:·
Создание зависимостей между протектором и основным кодом
Защита от отладки программы и изменения ассемблерного кода
Обфускация (мутация) нативного кода
Виртуализация нативного кода
Нативным кодом будем называть код, исполняемый напрямую на процессоре (т. е. то, что мы получаем после компиляции нашего кода под целевую архитектуру). Отладчик – в нашем случае программа для пошагового обхода и изучения работы ассемблерного кода, позволяющая увидеть значения всех переменный в процессе исполнения и последовательность выполнения кода.
Создание зависимостей
Как только мы добавили проверку лицензии в нашу программу, пирату достаточно было лишь удалить её из ассемблерного кода, и он получал версию программы, работающую без лицензии.
Необходимо написать ПО так, чтобы от наличия протектора зависела дальнейшая работа. Один из наиболее очевидных способов – это добавление зависимостей между протектором и функциональной частью. Например, инициализация важных значений и объектов, используемых далее. При этом значения будут зависеть от лицензионного ключа или будут получены через интернет при проверке лицензии.
Теперь реверс инженер не может просто так удалить протектор, так как это приведет к дальнейшей неправильной работе или крашу в программе. Но ПО всё еще плохо защищено, так как под отладчиком можно легко сломать проверку лицензирования в протекторе или, анализируя сам протектор, получить необходимые для программы значения и инициализировать их в обход защиты.
Защита от отладчика
Теперь нам необходимо защитить наш код от отладки. С этого момента кроме лицензирования и инициализации наш протектор будет ловить отладчик. Если протектор обнаружит следы отладчика, то он ломает стек фреймы программы или как-либо иначе делает восстановление работы программы невозможным.
Первое, что нам необходимо сделать, это запускать протектор в другом потоке. Регистр DR7 – debug control register, может сигнализировать о том, что приложение запущено под отладчиком. Также можно проверять значения регистров, используемых при отладке, таких как DR0 – DR3. Есть множество возможностей обойти эту защиту, но её реализация в протекторе не будет лишней.
Второй способ детектирования отладчика – подписаться на хуки ОС windows, которые будут вызывать переопределенную нами функцию при попытке прикрепить к процессу отладчик или изменить наш нативный код. Например, можем использовать хук DbgUiRemoteBreakin(), который вызовется, когда к процессу прикрепится отладчик, сообщив нам об этом. Данный метод не сработает, если реверс-инженер запустит наш процесс изначально под отладчиком. В таком случае во время вызова хука протектор еще не будет запущен, а значит и словить его не сможет. Для комплексной защиты используют большое количество различных хуков.
Третий способ – создания ситуаций, не влияющих на работу ПО, но вызывающие действия в отладчике. Например, мы можем выбросить исключение, сразу-же его обработать и замерить время между выбросом и обработкой. Исключение в отладчике может вызвать остановку, после чего, обработав исключение, по времени протектор поймет, что программа работает под отладчиком.
Четвертый способ – воспользоваться интерфейсом ОС. В windows API есть множество функций, позволяющих узнать о наличии отладчика, прикрепленного к программе. Можно использовать такие функции, как:
IsDebuggerPresent() – активен ли отладчик
CheckRemoteDebuggerPresent() – удаленный отладчик
NtQueryInformationProcess() – получает информацию о процессе
RtlQueryProcessHeapInformation() – получает флаги кучи процесса
RtlQueryProcessDebugInformation() – получает флаги отладчика процесса
NtQuerySystemInformation() – получает информацию из системы
Полученная информация позволит определить наличие отладчика в системе.
Обфускация нативного кода
Описанные выше методы очень сильно усложнят запуск кода под отладчиком. Реверс-инженеру придется обходить каждый из способов защиты по-отдельности, но спустя некоторое время пирату удастся запустить наше приложение под отладчиком, тогда наш код снова будет уязвим для кряка. Необходимо сам ассемблерный код защитить от анализа. Для этого подойдет обфускация. Обфускация — приведение кода программы к виду, сохраняющему её функциональность, но затрудняющему анализ, понимание алгоритмов работы и модификацию при декомпиляции. Обфусцировать можно как уже скомпилированную программу, так и исходный код, что достаточно актуально для скриптовых языков.
В нашем случае мы будем обфусцировать ассемблерный код. Такую обфускацию называют мутацией нативного кода. Для обфускации кода используют специальные программы – обфускаторы. Данный метод защиты создает лишние инструкции, запутывает, добавляет ложные зависимости в коде, тем самым делая код сложнее для анализа и реверса, однако это замедляет программу. Чтобы не сильно влиять на работу ПО мы будем обфусцировать только наш протектор. Поскольку он связан со работой ПО, реверс-инженеру придется разбираться в работе мутированного кода протектора. Мутация одного лишь протектора незначительно повлияет на производительность всей программы, но значительно усилит безопасность. Для большей безопасности можно обфусцировать уже мутированный код протектора повторно.
Несмотря на все, даже такой код подвержен анализу опытных ревёрсеров. Перейдем к еще более мощному механизму защиты кода
Виртуализация нативного кода
Более надежной альтернативой мутации является виртуализация. При виртуализации мы исполняем наш код не напрямую на процессоре, а имеем уникальную виртуальную машину, исполняющую защищенный участок кода. В нашем проекте мы воспользуемся данным методом защиты вместо обычной обфускации. Для этого нам придется купить программное обеспечение VMProtect или Themida. Принцип работы его следующий: наш нативный код протектора транслируется в байткод и для него создается уникальная виртуальная машина. Пользователь получает виртуальную машину с байткодом внутри. При запуске программы пользователем, протектор запускается через виртуальную машину. Данный способ защиты снижает производительность в тысячи раз, но поскольку протектор – это лишь малая часть ПО, общее снижение производительности все еще незначительно.
Заключение
В статье мы рассмотрели, как защитить программное обеспечение от пиратов. Применение всех этих методов защиты очень значительно увеличило время и ресурсы, необходимые на кряк нашей программы. Не существует метода абсолютной защиты, любую защиту можно обойти. В таком случае главная цель нашей защиты - сделать так, что для взлома программы понадобиться больше времени, чем для выхода обновления. В таком случае программа может считаться хорошо защищённой.