Показаны сообщения с ярлыком obfuscation. Показать все сообщения
Показаны сообщения с ярлыком obfuscation. Показать все сообщения

пятница, 7 октября 2011 г.

Orange Heap - Еще один бесплатный обфускатор для .NET


В последнее время я активно занимался изучением принципов обфускации и существующих приемов модификации кода, направленных на затруднение процесса реверс-инжиниринга .NET программ. Отчасти, результатом всего этого процесса исследования стал цилк статей “Как работают обфускаторы” (часть 1 (Переименование и шифрование), часть 2 (Изменения внутренней логики), часть 3 (Дополнительные возможности)). Данным циклом я занимался несколько последних месяцев. Если вы интересуетесь вопросами обфускации, крайне рекомендую его к прочтению.  Статьи легко читаются и содержат много полезной и наглядной информации.
Однако одним только циклом дело не закончилось. Для того что бы прочувствовать все нюансы, которые возникают с процессе создания средства обфускации я пошел немного дальше и решил написать свой собственный .NET обфускатор. Результат не заставил себя долго ждать и уже в начале сентября я выпустил первый публичный релиз, спустя четыре с половиной месяца после начала работы над проектом. А через две недели выпустил небольшое обновление с исправлениями ошибок, которые были найдены и исправлены после релиза.
На самом деле на реализацию конечного продукта было потрачено с апреля по сегодняшний день всего лишь чуть более 160 часов. Поскольку у меня есть основная работа, времени на собственные проекты не так то и много. В связи с этим реализация обфускатора растянулась на несколько месяцев. Уверен, если бы я занмался проектом фул-тайм, мне бы с лихвой хватило трех-четырех недель на доведения проекта с нуля до того состояния которое есть сегодня.
Посмотреть на результат моих трудов и даже воспользоваться ими можно зайдя на сайт поддержки продукта. Ссылка на сайт: http://orangeheap.blogspot.com.
А теперь немного о том, как получилось то, что получилось, и, собственно, что же все таки получилось?
***
В основу работы обфускатора Orange Heap положен фреймворк CCI (Common Compiler Infrastructure – http://ccimetadata.codeplex.com) С помощью него осуществляется доступ к метаданным программы и производятся необходимые изменения. Для таких целей, помимо CCI существует еще как минимум один доступный инструмент: Mono.Cecil. Однако мой выбор пал именно на CCI. Я не знаю лучше ли он чем Mono.Cecil и если лучше то чем. В тот момент, когда я только начинал заниматься проектом, для меня оба фреймворка былы чем-то неизвестным и в процессе выбора я руководствовался принципом «пальцем в небо». Единственное что я знал, так это то, что на базе Mono.Cecil уже существуют решения, в том числе и коммерческие, которые позволяют осуществлять обфускацию. Фреймворк CCI был менее исследован, и отзывов о его использовании было гораздо меньше. Но, наверное, тот факт, что CCI разрабатывался Microsoft Research подтолкнул меня к выбору именно этого фреймворка.
Ну, хватит лирики, лучше продолжим дальше разговор об Orange Heap. Продукт состоит из двух частей: консольное приложение и Windows-приложение.
С помощью Windows-приложения, можно осуществлять конфигурацию процесса обфускации. Данный процесс конфигурации позволяет указать такие опции как: нужно ли переименовывать публичные элемента и неймспейсы, нужно ли зашифровывать строки, нужно ли склеивать защищаемые сборки в одну монолитную сборку.
Помимо этого можно указать каким образом должно осуществляться переименование элементов. На сегодняшний день существует две политики переименования: нечитаемые символы (Normal policy) (в дизассемблере или рефлекторе такие имена будут отображаться в виде квадратиков) или имена вида a1, a2,.. a14,.. a445 (Developer policy). Наличие двух политик переименования связано лишь с тем, что в процессе обфускации (не только с помощью Orange Heap но и с любым другим обфускатором, в том числе и коммерческим) программа может сломаться и перестать работать так, как она работала до этого. Если такая ситуация произошла, значит, очевидно, нужно как-то изменить процесс обфускации (например запретить что-то переименовывать или шифровать)  и запустить его заново. Но в случае если все переменные, типы и методы отображаются в виде квардатиков – задача становится не очень тривиальной. В этом случае имеет смысл задавать имена в виде a1, a2, … Такие имена проще анализировать в процессе поломки программы средствами отладки. После того как причина поломки была устранена, можно вернуть «квадратики», обфусцировать программу и отдать ее на съедение врагам.
Помимо рассмотренных опций с помощью Windows-приложения можно также указать какие элементы должны быть переименованы, а какие должны остаться нетронутыми, то же самое касается и шифрования строк – для каждого метода, типа и сборки в отдельности можно указать, где нужно шифровать строки, а где не нужно. После того как процесс конфигурирования завершен Windows-приложение может запустить процесс обфускации на основании полученной конфигурации. При желании эту самую конфигурацию можно сохранить в конфигурационный файл. В этом случае ей можно будет воспользоваться в другой раз для запуска процесса обфускации без необходимости конфигурировать процесс еще раз.
Консольное приложение не имеет возможности конфигурировать процесс обфускации и может только использовать конфигурационный файл полученный в процессе конфигурации с помощью Windows-приложения. Для эстетов конечно, есть и тернистый путь – можно самому вручную создать или отредактировать конфигурационный файл, благо – это обыкновенный xml-файл, а  в документации Orange Heap присутствует описание его формата. Однако, мне кажется, что Windows-приложение позволяет достаточно гибко осуществлять настройку процесса обфускации и вручную править конфигурауционный файл имеет смысл только если очень чешутся руки.
В процессе обфускации Orange Heap формирует файл-отчет – в котором содержится информация о старом и новом имени переименованного элемента. В данный момент эта инфорамция из файла будет полезна при анализе и исправлении возникающих проблем в обфусцированной программе. Однако я не исключаю того, что в будущем (и только в том случае если у меня дойдут руки до реализации подобной функциональности) данный файл может быть полезен при осуществлении де-обфускации.
В данный момент Orange Heap поддерживает обфускацию WinForm-приложений. По крайней мере в процессе предрелизного тестирования я успешно и беспрепятственно смог обфусцировать несколько достаточно больших WinForm-приложений. Если какие-то вопросы с обфускаций WinForm существуют - я буду очень рад о них узнать. Пока же никто мне подобной информации не предоставил.
WPF/Silverlight – пока terra incognita для Orange Heap. Если хотите проверить - даже не пытайтесь. Все равно ничего не получится. Но я не думаю что это большая проблема. Ведь по правилам хорошего тона UI и логика должны быть разделены. Поэтому можно просто XAML поместить в одну сборку – логику в другую. В этом случае сборку с логикой можно безболезнено обфусцировать, защитив тем самым вашу интеллектуальную собственность. Собственно сам Orange Heap так обфусцирован с помощью самого себя (Windows-приложение базируется на WPF).
Хорошая новость заключается в том, что несмотря на то, что подобную ситуацию можно решить описанным выше способом, я все же планирую добавить поддержку WPF/Silverlight. Реализация этой функциональности только лишь дело времени.
Вот пожалуй и все об Orange Heap. Надеюсь данная статья вызвала интерес и вы обязательно посмотрите как он работает на деле. Orange Heap – абсолютно бесплатный, поэтому не медлите, заходите на сайт поддержки, скачивайте и пользуйтесь. И, разумеется, пишите письма. Ведь только с помощью ваших отзывов я смогу узнать о наличии проблем и попытаться их исправить, а также сделать продукт более удобным и функциональным.

среда, 28 сентября 2011 г.

Как работают обфускаторы. Дополнительные возможности


Введение

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

Обработка зависимостей

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

Завершение


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

четверг, 25 августа 2011 г.

Как работают обфускаторы. Изменения внутренней логики


Введение


В прошлой статье я рассказывал о самых базовых возможностях инструментов обфускации: переименовании типов и их составных частей и шифровании строк и ресурсов программы. Такие приемы могут быть эффективными и сами по себе в простейших случаях, однако они не способны защитить секретные алгоритмы и важные части программы, поскольку весь порядок следования инструкций в коде остался такой же как и был ранее. Да, если обыкновенный неподготовленый зевака откроет обфусцированную программу рефлектором или дизассемблером, то он ничего не сможет разобрать и почти наверняка слегка растроившись прекратит свои попытки понять, как там все внутри устроено. Но если же это более подготовленый человек, которому уж очень нужно разобраться с вашим кодом и он готов потратить на это свое время, то ему не составить труда проследить порядок выполнения инструкций вашей программы.
Сегодня мы пойдем дальше и постараемся разобраться с более сложными приемами, которые своей основной целью ставят изменение логики программы. После подбных преобразований разобраться с вашим кодом будет подсилу разве что матерым проффесионалам. Да и им уже придется изрядно попотеть. К слову, защитить свою программу на 100% абсолютно ото всех – это увы, задача нереализуемая, так как еще не существует такого решения, ни для .NET, ни для неуправляемого кода, которое позволило бы полностю обезопасить прогрммные продукты от взлома и несанкционированного доступа к коду. В наше время ломается все, другой вопрос – какой ценой.

Обфускация данных


Обфускация данных подразумевает изменения структуры переменных используемых в программе а также способов их использования.
Первым вариантом обфускации данных может служить изменение видимости переменной. Так, локальная переменная используемая в коде, может быть преобразована в глобальную переменную, либо поле класса, содержащего данный метод, либо вовсе в публичное поле отдельного вспомогательного класса, который может быть сгенерирован в процессе обфускации программы. В таком случае, для того что бы проследить использование данной переменной в коде, необходимо будет потратить больше времени на определение места объявления этой переменной. Если же переменная будет представлена в виде публичного поля отдельного класса, то в процессе реверс-инжиниринга необходимо будет дополнительно осуществлять проверку где оно еще используется в пределах всей программы.
Более продвинутым методом обфускации является изменение структуры переменных. Этот метод подразумевает к примеру преобразование двумерного массива к одномерному где это допустимо и наоборот. Такие преобразования усложняют восприятие кода и затрудняют реверс инжиниринг. Также можно осуществить разбивку скалярной переменной на две или более. Допустим и обратный процесс – склеивание нескольких переменных в одну. Для большей ясности можно рассмотреть пример разбивки переменной на несколько. На листинге 1 представлен некоторый метод, который в цикле выводит значения от 1 до 10.
Листинг 1.
            char[] array = ...;
            int i = 1;
            int k = 10;
            while (i < k)
            {
              Console.WriteLine(array[i]);
              i ++;
            }

Для того что бы затруднить восприятие логики программы, достаточно перменную i разделить на две переменные i и j, каждая из которых будет инкрементироваться в своем цикле. После подобных преобразований получим код, представленный на листинге 2.
Листинг 2.
            char[] array = ...;
            int i = 0;
            int k = 5;

            while(i < 2)
            {
                int j = 1;
                while(j <= k)
                {
                    Console.WriteLine(array[i * k + j]);
                    j++;
                }
                i++;
            }

В случае использования переменных в качестве счетчика для цикла можно использовать так называемое кодирование данных, которое добавляет дополнительные инструкции для вычисления значения счетчика. Можно рассмотреть данный метод обфускации на примере. В качестве исходного кода возьмем фрагмент представленный на листинге 1, на листинге 3 – код, полученный в результате преобразований.
Листинг 3.
            char[] array = ...;
            int i=11;
            int k = 83;
            while (i < k)
            {
                Console.WriteLine(array[(i - 3) / 8]);
              i += 8;
            }

Обфускация кода

Помимо обфускации данных, существует обфускация кода или обфускация следования управляющих инструкций (control flow obfuscation). Данный вид преобразований предназначен для усложнения восприятия кода защищаемого метода. Как правило все преобразования данного типа подразумевают добавление либо инструкций переходов, предназначенных на запутывание порядка выполнения инструкций, либо всевозможных вспомогательных методов и типов, которые получают управление в процессе выполнения программы. Рассмотрим наиболее распространенные преобразования.
Добавление запутывающих ветвлений программы. Принцип данных преобразований основан на том, что в оригинальный код добавляются инструкции переходов. Рассмотрим ситуацию на примере. После осуществления преобразований кода из листинга 1 получим код, представленный на Листинге 4.
Листинг 4.
            char[] array = ...;
            int i = 1;
            int k = 10;
            while (i < k)
            {
                int j = 10 - i;

                if(j > k)
                {
                    Console.WriteLine("error");
                    i = 1;
                }
                else
                {
                    Console.WriteLine(array[k - j]);
                    i++;  
                }
            }

Если внимательно посмотреть на этот фрагмент, то можно обратить внимание, что выражение if(j > k) всегда будет ложно и фрагмент кода, относящийся к этому условию, никогда не выполнится. Такие блоки кода часто называют мертвым кодом (Dead code). Основное их предназначение заключается в отвлечении внимания, человека, занимающегося реверс-инжинирингом. Другая ветвь ничем не отличается от исходной из листинг 1, за тем исключением, что вместо i используется k – j. Если посмотреть внимательно на код, то несложно прийти к выводу, что это выражение всегда будет эквивалентно i. Итак, после преобразований был получен код, имеющий на первый взгляд мало общего с исходным кодом, однако выполняющего в точности ту же самую работу.

Добавление классов-оберток для сокрытия вызовов методов стандартных типов. Даже после преобразований выполненых ранее, код по прежему остается читаемым. Причиной этому является использование метода Console.WriteLine, который переименовать как-то нету никакой возможности, так как это стандартный класс. Для того, что бы запутать код еще больше, можно создать прокси-класс, который иначе называется Dynamic Proxy. Он будет инкапсулировать в себе вызовы метода Console.WriteLine. Листинг 5 содержит реализацию класса.
Листинг 5.
    internal class llI
    {
        public static void Il(string s)
        {
            Console.WriteLine(s);
        }

        public static void Il(char c)
        {
            Console.WriteLine(c);
        }
    }

Теперь можно везде, где необходимо вызов метода Console.WriteLine заменить на вызов метода прокси llI.Il. В результате фрагмент из листинга 4, превратиться в фрагмент представленный на листинге 6.
Листинг 6.
            char[] array = ar;
            int i = 1;
            int k = 10;
            while (i < k)
            {
                int j = 10 - i;

                if(j > k)
                {
                    llI.Il("0xFF");
                    i = 1;
                }
                else
                {
                    llI.Il(array[k - j]);
                    i++;  
                }
            }

Подобные прокси отлично скрывают «настоящие» вызовы. Они могут использоваться не только со стандартными типами, но и с пользовательскими, или даже с другими прокси-объектами, создавая иерархии прокси-вызовов. Используя в комбинации приемы прокси-объектов и ветвлений можно добится невероятных результатов обфускации кода. Код полученный в результате будет очень сложен для восприятия, особенно учитывая тот факт, что названия всех методов и классов также будут в преобразованы к нечитаемому виду. Главное в этом деле не переусердствовать и вовремя остановится, так как необходимо помнить что с введением каждого нового прокси-объекта, защищаемая нами программа теряет в производительности, так как увеличивается количество избыточных вызовов и используемая глубина стека выполнения программы.

Добавление новых типов для сокрытия данных используемых в программе. Данный тип преобразований сочетает в себе обфускацию и кода и данных. Его предназначение – запутать некоторый код, который по входным данным получает в результате новые данные, которые необходимы для дальнейшего выполнения прграммы. Для ясности, рассмотрим пример, который изображен на листинге 7.
Листинг 7.
            switch(c)
            {
                case 0x00FF0000:
                    Console.WriteLine("Red Color");
                    break;
                case 0x0000FF00:
                    Console.WriteLine("Green Color");
                    break;
                case 0x000000FF:
                    Console.WriteLine("Blue Color");
                    break;
                case 0x00FFFFFF:
                    Console.WriteLine("White Color");
                    break;
            }

Этот код по числовому значению c определяет текстовое описание цвета и печатает его на консоль. Суть рассматриваемого преобразования заключается в создании нового типа, основной задачей которого будет для каждого значения входных данных ( в нашем случае это переменная c) получить соответствующие выходные данные (в нашем случае – текстовое название цвета). Реализация нового типа представлена на Листинге 8.
Листинг 8.
    internal class IIl
    {
        private static readonly uint[] _I = new uint[] { 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF};
        private static readonly string[] _l = new[] { "Red", "Green", "Blue", "White" };

        public static string ll(uint c)
        {
            var i = Array.IndexOf(_I, c);
            return i >= 0 ? _l[i] : string.Empty;
        }
    }

Теперь, используя данный клас можно переписать код, как показано на Листинге 9.
Листинге 9.
            var I = IIl.ll(c);

            if(!string.IsNullOrEmpty(I))
                llI.Il(I + " Color");

Обращаю внимание, что в данном примере мы воспользовались Dynamic Proxy из листинга 5 для сокрытия вызова метода Console.WriteLine. В качестве дальнейшего запутывания кода можно добавить аналогичный Dynamic Proxy для вызова метода string.IsNullOrEmpty, а также осуществить переименование всех переменных и шифтрацию строк. После этого уж наверняка будет не очевидно что на самом деле он реализует функцуиональность кода из листинга 7.

Способы отпимизации

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

Заключение

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