Введение
В предыдущих двух статьях я рассказывал о том, как работают обфускаторы. Были рассмотрены методы и приемы, которые используются ими для переименования различных символов программы и для шифрования строк и ресурсов. Далее я привел простейшие примеры нелинейного преобразования кода, которые предназначены для изменения порядка следования инструкций программы. Такие преобразования делали более сложным восприятие кода и практически невозможным преобразование его к исходнму варианту. Собственно все обфускаторы как правило и отличаются качеством подобных преобразований. Чем лучше и качественнее данные преобразования осуществляются, тем выше качество защиты обфусцированного кода и соответственно больше ценится тот или иной обфускатор.
Вместе с тем, нынешние средства обфускации имеют в своем арсенале и ряд других возможностей, которые не связаны непосредственно с такими преобразованиями, а предназначены прежде всего для повышения удобства работы с инструментом и предоставления пользователю дополнительных вспомогательных функций. Как раз о таких возможностях и пойдет речь в данной статье.
Обработка зависимостей
В самом простейшем случае приложение, которое требуется защитить состоит из одного файла – единственного исполняемого exe-модуля. В данном случае при обфускации, как правило, не возникает никаких вопросов – от обфусцированного модуля никто больше не зависит, соответственно все символы (типы, поля, методы, интерфейсы, и т.д.) могут быть беспрепятственно переименованы, в том числе и те которые, имеют публичную область видимости, то есть помеченные как public.
В случае если защищаемый продукт состоит из нескольких составляющих, скажем, главного исполняемого модуля и двух-трех подключаемых dll-библиотек, ситуация немного меняется. Дело в том, что безболезненно переименовать все символы без дополнительных действий не получится. И если с приватными (private) или внутренними (internal) символами, которые используются только внутри данной сборки проблем по прежнему нету – их можно переименовывать без оглядки на всю остальную систему, то с публичными элементами такой подход неприемлем. Связано это с тем, что одна сборка, которая использует некоторые типы другой, ничего не знает об актуальном именовании типов импортируемой сборки. При связывании на этапе компиляции все имена импортируемых типов сохраняются в специальную таблицу. Позже, в процессе выполнения программы, все обращения к импортируемым типам будут осуществлятся с использованием этой таблицы. А поскольку в сборке, где эти типы объявлены, в процессе обфускации имена этих типов были переименованы, то получить информацию о типе по страму имени будет невзоможно, и в итоге мы получим исключение, информирующее о том, что запрашиваемый тип не найден.
Различные обфускаторы решают подобную проблему различными способами.
Самый простой способ – превентивный. Это значит обфускаторы либо не позволяют изменять имена публичных символов, либо в ультимативной форме делать это крайне не рекомендуют.
Следующий способ – также предполагает решение в лоб. Суть его заключается в том, что прежде чем осуществлять переименование типов, все сборки приложения скливаются в одну. То есть вместо одного исполняемого модуля и нескольких подключаемых, получим один исполняемый модуль, который содержит все типы. После этого процесс обфускации можно проводить точно также, как это было в самом простейшем случае, когда имеется только один файл. Такой способ часто называют склеиванием зависимостей (Dependencies Merging).
Третий способ (и последний в этом обзоре) - самый корректный. Он предполагает обновление таблицы имен импортируемых символов. В данном случае после того, как все типы подключаемого модуля были переименованы, осуществляется обновление таблицы импорта исполняемого модуля, в процессе которого старые имена импортируемых символов заменяются на новые. Такой процесс преобразований получил название Inter-Assembly или Cross Assembly обфускации. Подобный вид обработки зависимостей обладает рядом преимуществ, по сравнению с предыдущими. Так, он позволяет осуществлять более глубокую обфускацию, и вместе с тем не нарушать исходную структру приложения. Это особенно актуально когда к примеру приложение состоит не из одного исполняемого файла а из двух или трех, которые совместно используют подключаемые модули. В таком случае склеивание зависимостей невозможно и стало быть рассматриваемый вид обработки зависимостей - единственный способ обфусцировать публичные символы подключаемого модуля.
Защита от реверс-инжиниринга и модификаций
Защита от дизассемблирования
Несмотря на то, что в процессе обфускации структура исполняемого кода программы претерпела серьезные изменения, полученный в результате код все равно является уязвимым для несанкционированного изучения и реверс-инжиниринга. Для этого даже не нужно каких-то специализированных инструментов и знаний. Достаточно открыть исполняемый файл с помощью так называемого рефлектора или дизассемблера. Конечно же, былой очевидности исполняемого кода уже нету, однако если очень нужно исследовать некоторый алгоритм – то это не составит большого труда, Несколько часов анализа кода и цель достигнута. Для более подготовленного злоумышленника достаточно будет десятка минут.
Что бы обезопасить себя от подобных ситуаций, необходимо сделать невозможным загрузку анализируемого исполняемого модуля подобными средствами. Существуют различные варианты реализации подобного защитного механизма.
Одним из простых вариантов является вставка в тело каждого метода дополнительного фрагмента кода, который содержит некоторую некорректную или несуществующую инструкцию. При загрузке метода, у которого добавлен такой фрагмент, дизассемблер не сможет распознать эту инструкцию и соответственно не сможет загрузить тело метода для анализа злоумышленником. А для того, что бы данная инструкция не влияла на процесс выполнения программы ее следует оградить инструкциями безусловных переходов таким образом, что бы она никогда не смогла получить управление.
Более продвинутый способ может выглядеть следующим образом. Код метода шифруется с помощью некоторого алгоритма шифрования и результат шифрации помещается либо в ресурсы, либо вовсе в отдельный файл. Исходное тело метода замещается кодом некоторого загрузчика, который знает где находится зашифрованный фрагмент кода и способ как его расшифровать. При вызове такого метода загрузчик получает управление, он дешифрует реальный код, и выполняет его. Таким образом при дезассемблировании исследовать этот код будет абсолютно невозможно.
Защита от модификаций
Некоторые обфускаторы предоставляют функциональность, которая способна защитить приложение от модификаций. Такой механизм часто называют tamper protection или tamper defense. В этом случае программа будет корректно выполнятся только в том случае если они не была изменена злоумышленником. Если какие то изменения были – программа просто перестанет работать, либо будет выводить некоторое сообщение о недопустимости использования модифицированной программы.
Для того что бы реализовать подобную функциоанльность используется подписывание (watermarking) исполняемых и подключаемых модулей. Алгоритм подписывания может быть любой. Это может быть и стандартное подписывание (signing assembly with a strong name) или же некоторый другой алгоритм, основанный на CRC или MD5. Суть такого алгоритма заключается в том, что перед выполнением программы осуществляется анализ существующего кода, в процессе которого на основании существующих инструкций кода формируется некоторое числовое значение или иными словами так называемый хэш, который впоследствии сравнивается с эталонным. Если модификаций не было, эти хеши будут идентичны, если же хеши отличаются – явный признак того, что код был модифицирован.
Кроме того подписывание, может использоваться не только для отслеживания модификации кода, но и для скрытой идентифиакции или маркировки конкретного экземпляра приложения. Подобная маркировка может помочь выявить факт несанкционированного использования приложения и определить его владельца, на который рарегистрирована покупка данного экземпляра приложения.
Способы конфигурации и пользовательский интерфейс
Немалое значение в популярности того или иного обфускатора играет удобство его использования, а также гибкость и интуитивность конфигурации процесса обфускации. Из всех вариантов конфигурирования можно выделить три основных подхода.
Первый подход предполагает наличие специального конфигурационного файла и возможности только ручного конфигурирования процесса обфускации. В этом случае придется изучать формат конфигурационного файла и вручную в текстовом редакторе осуществлять настройку процесса обфускации. Процесс достаточно сложен и утомителен, особенно в случае необходимости конфигурирования большой системы с огромным количеством различных правил.
Следующий подход основан на использовании Windows приложения для конфигурации процесса обфускации. В данном случае пользователю не нужно владеть знаниями о формате конифигурационного файла, ему достаточно указать, какие опции защиты должны быть активны и правильно настроить правила преобразований для защищаемых модулей, их типов и составных частей.
Третий вариант основан на декларативном конфигурировании защищаемого кода с помощью специальных аттрибутов. Суть данного подхода заключается в том, что на этапе разработки определенным модулям, типам и их составным частям присваиваются специальные аттрибуты, которую содержат информацию о том, как нужно обрабатывать данный элемент в процессе обфускации. Для того, что бы унифицировать процесс декларативного конфигурирования в фреймворке .NET уже существуют такие аттрибуты. К ним относятся System.Reflection.ObfuscateAssemblyAttribute и System.Reflection.ObfuscationAttribute. Первый предназначен для задания параметров конфигурации для заданной сборки, второй – для конкретного типа или его составных частей. Более подробную информацию об этих аттрибутах можно посмотреть в MSDN.
Идеальным обфускатором с точки зрения способов конифигурации процесса обфускации можно считать обфускатор, который позволяет осуществлять конфигурацию всеми тремя способами. Однако на практике такое встречается нечасто, и в большинстве случаев приходится довольствоваться меньшими возможностями.
Помимо гибкости конфигурирования процесса обфускации огромным плюсом будет наличие коммандной строки. Это позволит использовать обфускатор в автоматизированных сценариях, например при создании очередного билда или построении инсталяционного пакета.
Завершение
В данной статье мы рассмотрели существующие варианты обработки зависимостей, когда защищаемое приложение состоит из нескольких модулей, которые связаны между собой. Далее были рассмотрены наиболее распространенные варианты защиты приложения от дизассемблирования и модификаций. И наконец был дан беглый обзор возможных способов конфигурирования процесса обфускации и вариантов поставки обфускатора конечному пользвателю.
Разумеется, в статье я рассмотрел далеко не все, а лишь только самые базовые особенности, присущие большинству существующих обфускаторов. Для того что бы получить больше информации, обращайтесь к документации конкретных обфускаторов или к другим публикациям по данной тематике.
Комментариев нет:
Отправить комментарий