Защита .NET-приложений при помощи Sentinel LDK Envelope
Утилита Sentinel LDK Envelope, о которой пойдет речь в этой статье, предназначена для установки навесной защиты на исполняемые модули (EXE и DLL) для платформ Win32, Windows x64, .NET, а так же, на Java-приложения (JAR и WAR). Защита осуществляется путем «привязывания» кода приложения к ключу защиты Sentinel (новое поколение ключей HASP), причем, ключ может быть как аппаратным (HL), так и программным (SL). Обработанный таким образом исполняемый модуль, будет работать только в присутствии требуемого ключа со всеми необходимыми лицензиями. Помимо проверки наличия ключа, внедренный в приложение код защиты, так же, обеспечит активное противодействие отладке и затруднит реверс-инжиниринг приложения, включая статический анализ кода.Цель данной статьи — рассмотреть способы и особенности защиты .NET-приложений, причем, с упором на максимальную автоматизацию процесса установки защиты. Поэтому, далее мы будем рассматривать только тот функционал Envelope, который касается защиты именно .NET-приложений.
В целом, с точки зрения автоматизации процесса защиты приложения, наилучшим вариантом являлась бы реализация защиты на уровне исходного кода приложения с использованием функций Sentinel LDK API. При такой реализации разработчик самостоятельно решает, как будет защищен тот или иной класс или метод, и, при повторной сборке проекта, в случае внесения изменений в исходный код, вся ранее проделанная работа по реализации защиты автоматически учитывается естественным образом, не требуя переделки. Однако, особенности платформы .NET не позволяют использовать множество техник защиты, которые могут применяться в нативном коде, например, динамическое шифрование кода во время работы приложения, контроль целостности кода, восстановление кода с использованием помехозащищенного кодирования и т.д. Вернее, скажем так, реализовать все это можно, но это будет уже, скорее всего, не комфортная работа на C#, а системное программирование на более «мощных» языках, низкоуровневая работа с CIL-кодом, а так же, необходимость сопровождения и актуализации использованных приемов работы в случае каких-либо изменений внутри .NET, например, при выходе новой версии данной платформы.
С другой стороны, можно воспользоваться навесной защитой — это потребует гораздо меньше времени на реализацию, не предъявляя чрезмерно высоких требований к квалификации разработчика (знание других языков программирования, владение CIL, …), однако, здесь тоже не всё безоблачно, полной автоматизации процесса установки защиты достичь трудно. Поэтому, сейчас мы подробно рассмотрим использование навесной защиты при обычном применении, после чего подумаем, как все это максимально автоматизировать. Предположим, что мы создаем изначально незащищенное приложение, не используя Sentinel LDK API, после чего обработаем сборку при помощи Envelope. Технологический цикл создания защищенного приложения в данном случае будет выглядеть так:1) Построение незащищенной сборки в среде разработки, например в Visual Studio.2) Защита построенной сборки в процессе интерактивной работы с Envelope.Рассмотрим более подробно второй этап и разберемся, какие способы защиты может нам предоставить Envelope. Процесс защиты состоит из следующих операций:1. Загрузив первый раз Envelope, мы увидим пустой проект. Сразу же укажем, с какой серией ключей будет работать наша защищенная сборка (см. рис. 1):
Рис. 1 — Выбор кода серии ключей2. Установим общие параметры защиты, которые будут применяться ко всем входящим в проект сборкам, подлежащим защите (см. рис. 2): Рис. 2 — Установка общих параметров защиты Назначение параметров следующее: • String encryption — зашифрование всех данных, хранящихся в куче #US, содержащей в стандарте UNICODE все строки, которые разработчик определил в своем исходном коде. Вот так, к примеру, в .NET Reflector«е выглядит код с незашифрованными строками: А вот так — с зашифрованными: • Periodic Background checks — фоновый опрос ключа с заданным интервалом.• Apply compression — сжатие при размещении в сборке тел классов и методов, помеченных как защищенные. Реально на стойкость защиты не влияет, .NET Reflector, например, это сжатие в упор не замечает, независимо от параметров данной опции.• Obfuscate Symbols — обфускация данных, хранящихся в куче #Strings, содержащей символьную информацию, такую, как имена классов, методов, полей, и т.д. Возможна полная обфускация или частичная, когда не изменяются имена ресурсов.Класс Program до обфускации: После обфускации: 3. Включим одну или несколько готовых сборок в проект утилиты Envelope (см. рис. 3).
Рис. 3 — Добавление целевых файлов в проект 4. Для каждой входящей в проект сборки задаем пути (откуда брать исходный файл и куда класть защищенный), с какими ключами и как работать, какую лицензию использовать (см. рис. 4): Рис. 4 — Установка параметров сборки 5. При необходимости для каждой конкретной сборки можно установить свои параметры защиты, перекрывающие общие (см. рис. 5), и более тонко настроить работу защитных механизмов (см. рис. 6): Рис. 5 Рис. 6 Параметры на рис. 5 уже рассматривались в п.2. Из представляющих интерес параметров (с точки зрения защиты) на рис. 6 можно отметить следующие: • LOCKING_TYPE — определяет типы ключей, с которыми будет работать защита данной сборки. Возможные варианты: где: — HL — аппаратный ключ— SL-UserMode — программный ключ, умеющий работать без драйвера— SL-AdminMode — программный ключ, работающий совместно с драйвером— SL — SL-UserMode + SL-AdminMode• MIN_CODE_SIZE — минимальный размер CIL-кода метода в байтах, начиная с которого он будет защищаться Envelope в случае групповой операции, например, когда для защиты отмечается весь класс целиком.
6. Осталась самая трудоёмкая часть работы — определить классы и методы, подлежащие защите (рис. 7–1), и для каждого выбранного класса/метода задать параметры защитных механизмов (рис. 7–2):
Рис. 7 — Управление защитой методов и классов Чтобы выбрать для защиты класс или метод, просто поставьте галочку напротив его имени в окне 1. При этом в окне 2 отобразятся текущие параметры защиты для выбранного объекта. Назначение параметров защиты: • Feature ID — номер лицензии из ключа. Для разных классов/методов может различаться в случае их раздельного лицензирования.• Frequency — как часто будет проверяться ключ для защищенного класса/метода. Возможные варианты: где: — Once per program — один раз за все время работы приложения— Once per class instance — каждый раз при создании экземпляра класса— Every time — при каждом проходе управления через защищенный код
• Symbol obfuscation — обфускация символьной информации, может быть применена к методу, даже если он не отмечен, как подлежащий защите, т.к. не связана с ключом. Возможные варианты: где: — Use global definition — используется текущее значение параметра Obfuscate Symbols, см. п. 2 и п. 5.— Enabled — обфускация производится всегда, независимо от глобальных установок.— Disabled — обфускация никогда не выполняется, независимо от глобальных установок.
• Code obfuscation — control_flow-обфускация CIL-кода, может быть применена к методу, даже если он не отмечен, как подлежащий защите, т.к. не связана с ключом. Так выглядит в IDA фрагмент кода функции после обфускации:
А это — граф передачи управления в функции после обфускации:
Чётко видно, что обфускация заключается в том, что код буквально разрывается по одной инструкции, и все они перемешиваются, а чтобы сохранился прежний порядок их выполнения, после каждой инструкции дополнительно вставляется инструкция перехода (опкод br) на нужный адрес. Очевидно, что скорость работы функции, защищенной таким образом, может сильно упасть, а код — значительно увеличиться в размерах. Поэтому, применять данный метод защиты следует с осторожностью.
• Code encryption — зашифрование CIL-кода выбранного метода через ключ, с использованием алгоритма AES. При использовании этого способа защиты оригинальный CIL-код извлекается из тела метода, зашифровывается через ключ, и помещается в ресурсы сборки в зашифрованном виде. На его место вставляется короткий переходник, обеспечивающий загрузку, расшифрование, динамическую компиляцию и передачу управления на код защищенного метода. После окончания выполнения метода, его код удаляется из памяти. Вот так выглядит фрагмент тела защищенного метода в IDA: Инструкция call class [mscorlib]System.Reflection.Emit.DynamicMethod определяет и представляет динамический метод, который будет скомпилирован в памяти, выполнен и впоследствии удален. Инструкция callvirt instance object [mscorlib]System.Reflection.MethodBase: Invoke осуществляет передачу управления на метод в случае его успешной компиляции. Данный способ защиты практически не влияет на быстродействие защищаемого кода, обеспечивая вполне приличную стойкость.
7. После того, как все необходимые классы/методы помечены как предназначенные к защите, осталось только сохранить результаты работы в виде файла проекта и нажать кнопку Protect. В результате мы получим защищенную сборку. Конец работы.
Теперь вернемся к вопросу автоматизации процесса. Очевидно, что только что проделанная нами работа слабо автоматизирована. Да, мы сохранили всё, что сделали, в виде проекта, и, в следующий раз, когда потребуется защищать сборку, можно им воспользоваться, сразу загрузив в Envelope. Однако, весь технологический цикл создания защищенного приложения, по-прежнему состоит из двух слабо связанных этапов, которые выполняются в разных программах. Попробуем их связать, сделав защиту прозрачным для разработчика процессом. Для этого воспользуемся консольной версией Envelope, которая называется envelope.com (пусть вас не пугает расширение, это нормальный exe-файл, а не привет из древней ms-dos). Итак, при запуске с ключом –h нам сообщается, что:
Будем использовать утилиту с ключом –p на этапе «событие после построения» в Visual Studio. Можно сделать, например, так:
Вся работа с envelope.com вынесена в командный файл envelope.cmd, находящийся в каталоге проекта и содержащий следующие команды:
…\…\LDK\VendorTools\VendorSuite\envelope.com -n --protect %1move /Y %2.protect %2
Для того, чтобы все правильно работало, нужно указать корректный путь до каталога с утилитой envelope.com (можно использовать абсолютный путь, а не относительный, как здесь), и внести небольшое изменение в проект защиты в Envelope, изменив расширение выходного файла, для того, чтобы нормально отработала команда move:
Работу утилиты можно проверить по логу в окне вывода Visual Studio:
После окончания процесса создания сборки в среде Visual Studio, мы сразу получаем защищенное приложение без лишних телодвижений.Вроде бы, все получилось неплохо, но как быть, если в исходный код приложения вносятся значительные изменения? Например, если появляются новые классы/методы, подлежащие защите, удаляются старые или планируется изменить способ защиты существующих классов/методов? При традиционном подходе придется открывать проект защиты в Envelope и вносить все накопившиеся изменения, как ранее описывалось в п. 6, что печально, поскольку ставит крест на автоматизации процесса защиты при любых сколько-нибудь серьёзных изменениях в исходном коде приложения.Однако, есть возможность автоматизировать актуализацию таких изменений в исходном коде. Сделать это можно посредством использования пользовательских атрибутов в исходном коде приложения. Объектом защиты при помощи пользовательских атрибутов может быть метод, класс или вся сборка — в зависимости от того, где размещается тот или иной набор атрибутов. Список доступных атрибутов для защиты объекта полностью повторяет набор параметров защиты для классов/методов, описанный в п. 6. Вот список доступных атрибутов: • Protect — тип BOOL, возможные значения — TRUE/FALSE, указывает нужно ли защищать объект с использованием ключа.• FeatureId — тип int, возможные значения — [0;65535], указывает номер лицензии (Feature ID), которая будет использована при защите объекта. Принимается к исполнению, только если Protect == TRUE.• Encrypt — тип BOOL, возможные значения — TRUE/FALSE, указывает нужно ли зашифровывать CIL-код объекта. Принимается к исполнению, только если Protect == TRUE.• CodeObfuscation — тип BOOL, возможные значения — TRUE/FALSE, указывает нужно ли выполнять control_flow-обфускацию CIL-кода объекта. Принимается к исполнению независимо от значения Protect. Данный метод защиты приводит к снижению скорости работы защищенного кода.• Frequency — перечисляемый тип EnvelopeMethodProtectionFrequency, указывает, как часто будет проверяться лицензия для защищаемого объекта. Принимается к исполнению, только если Protect == TRUE. Возможные значения: — CheckOncePerApplicaton — однократная проверка в процессе работы приложения.— CheckOncePerInstance — однократная проверка для каждого экземпляра объекта.— CheckEveryTime — проверка при каждом проходе управления через код объекта.• SymbolObfuscation — перечисляемый тип EnvelopeSymbolObfuscation, указывает на метод обфускации символьной информации в защищаемом объекте. Принимается к исполнению независимо от значения Protect. Возможные значения: — ObfuscateSkip — полный запрет на обфускацию всей символьной информации.— ObfuscateForce — принудительное выполнение обфускации для всей символьной информации.— ObfuscateDefault — выполнять обфускацию для всей символьной информации, кроме public-имен, а так же, объектов с модификаторами virtual и protected.
Для того, чтобы Envelope мог получить атрибуты из исходного кода, необходимо при помощи директивы using включить в него пространства имен Aladdin.HASP.Envelope и Aladdin.HASP.EnvelopeRuntime. Естественно, перед этим необходимо добавить файлы Aladdin.HASP.Envelope.DLL и Aladdin.HASP.EnvelopeRuntime.DLL в ссылки проекта Visual Studio. Распространять эти файлы с защищенным приложением не нужно, они требуются только на этапе установки защиты на сборку. Ниже приведен пример защиты из исходного кода с помощью пользовательских атрибутов:
Вообще говоря, все изменения, связанные с защитой сборки можно условно разделить на две категории:1. Глобальные изменения, которые происходят довольно редко. Описаны в п. 1 — 5, управлять ими из исходного кода нельзя.2. Изменения в исходном коде, связанные с появлением или удалением классов/методов, подлежащих защите, происходят значительно чаще. Описаны в п. 6. Теперь можно полностью актуализировать такие изменения из исходного кода, не занимаясь правкой проекта защиты в Envelope.
И вот теперь, подытожив все вышеизложенное, можно сказать, что разработчик полностью управляет защитой из исходного кода, так же, как и в случае использования Sentinel LDK API. Загружать проект защиты в Envelope и исправлять её параметры придется только в случае каких-либо глобальных изменений, например, смена серии ключей, изменение имени сборки или установка другой величины интервала фонового опроса ключа, что происходит несравнимо реже.