пятница, 29 июля 2011 г.

Как работают обфускаторы. Переименование и Шифрование



Это первая статья из цикла “Как работают обфускаторы”. Основной целью этой и последующих статей прежде всего является систематизация и описание возможностей современных обфускаторов. Я попытаюсь не только описать что в них реализовано, но и объяснить неискушенному читателю, зачем оно там и как оно все работает. Я приложу все усилия для того, что бы не загружать читателя трудновоспринимаемым текстом, сложносформулированными оборотами и выражениями. Для этого материал будет преподнесен в максимально простой и доступной форме. Однако в некоторых случаях без использования сложных терминов и выражений однозначно не обойтись. Я постараюсь сделать это настолько аккуратно, что бы на качество материала это не повлияло никоим образом.
 Мне ближе всего платформа .NET, поэтому при подготовке материала я рассматривал обфускаторы, предназначенные для защиты .NET-кода. Ввиду этого в обзоре не будут учтены особенности, которые имеют место в обфускаторах предназначенных других областей, например для защиты кода скриптовых языков, таких как JScript. Хотя вряд ли там присутствуют существенные отличия – основные возможности обфускаторов из разных областей мало чем отличаются друг от друга. Поэтому статья может оказаться полезной даже если вы абсолютно не знакомы с .NET.
И еще один важный момент, на который я хочу обратить внимание, прежде чем мы пойдем дальше. Ни в этой статье, ни в последующих, насколько это будет возможно я постараюсь не упоминать явно или косвенно, какую то конкретную реализацию обфускатора, дабы не сделать из статьи рекламу того или иного продукта. Возможно в отдельных случаях это было бы и полезно – на живом примере объяснить или рассказать, как все то, о чем будет идти речь, устроено и работает. Однако я уверен, что качество материала от отсутствия подобных примеров если и пострадает, то незначительно. Если вам будет интересно узнать о существующих реализациях обфускаторов, вы наверняка сможете сами найти подобную информацию сами используя поисковую систему, или же обратившись к моей публикации, где был проведен небольшой обзор, какие обфускаторы существуют и что они умеют. Обзор уже не свежий, и сегодня наверняка появилось много других решений, которые в момент написания обзора были мною упущены из вида, поэтому отнеситесь к обзору только как к отправной точке и не более того.

Введение

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

Переименование

Самой базовой возможностью обфускторов является переименование составных частей программы. Это, пожалуй, самое первое что приходит на ум, при упоминании термина обфускация. И, возможно, самое важное. Другой термин, которым называется процесс переименования - Name mangling. Основной задачей процесса является приведения названия переменных, классов и их составляющих к форме, которая как минимум не позволила бы понять их предназначение, а максимум – лишила бы возможность визуально проследить использование данного объекта в коде.
Предназанчение переменной, как правило проще всего определить по ее названию – стало быть преназначение переменной itemCount говорит само за себя – эта переменная предназначена для хранения количества некоторых элементов, а вот предназначение aabc – не настолько понятно. Стало быть в самом простейшем случае изменение имени itemCount на aabc уже уменьшает восприятие кода, хотя проследить использование переменной и выяснить для чего она используется не так уж и сложно.
Однако, если взять пять различных переменных, например itemCount, firstItem, lastItem, currentItem, items,  и переименовать их скажем вот так: IIli, lilI, IiIl, iilI, Illl – то задача не кажется такой простой – названия переменных стали трудно различимы между собой и теперь нужно хорошенько напрячь зрение и внимание, что бы проследить использование каждой из переменных.
В действенности приведенного преобразования вы можете убедится изучив листинги,  приведенные ниже. В первом листинге показана реализация некоторго метода до преобразований, во втором - тот же метод, но после модификации.
Листинг 1.
        private List<int> SampleMethod(int firstItem, int lastItem, List<int> items)
        {
            var filteredItems = new List<int>();
            var itemsCount = items.Count;
            for (int i = 0; i < itemsCount; i++)
            {
                if (items[i] < firstItem || items[i] > lastItem)
                    continue;
                filteredItems.Add(items[i]);
            }
            return filteredItems;
        }

Листинг 2.
        private List<int> SampleMethod(int IIli, int lilI, List<int> IiIl)
        {
            var iilI = new List<int>();
            var Illl = IiIl.Count;
            for (int IlIl = 0; IlIl < Illl; IlIl++)
            {
                if (IiIl[IlIl] < IIli || IiIl[IlIl] > lilI)
                    continue;
                iilI.Add(IiIl[IlIl]);
            }
            return iilI;
        }

Можно пойти еще дальше, и в качестве имени переменной использовать неотображаемые символы. В таком случае вместо таких символов пользователь в зависимости от используемого инструмента будет либо видеть череду квадратов, либо вовсе ничего, пустоту на месте использования переменной. Согласитесь, выражение вида “ . += ;” или “□□.□□+=□□□;” любого поставит в тупик.
Еще одним приемом запутывания кода является приведения имени разных методов с различным набором аргументов к одному имени. Например имеется два метода: AddItem(item) и GetItemByIndex(index). Параметр первого метода –– это некоторый класс, параметр второго – целое число. Поскольку типы агрументов различные – оба метода могут вполне законно иметь одно и то же имя. И теперь после переименования методов и аргументов можно получить примерно следующее: II(Il) и II(lI). Попробуйте догадайтесть что первый метод добавляет элемент, а второй возвращает по индексу. Если пойти и здесь немного дальше и тем же именем которым были названы методы, назвать переменные, классы и типы защищаемой программы (ну разумеется только в тех случаях где это допустимо) то задача усложниться до неузнаваемости. Обратите внимание на листинг 3 и все станет понятно, о чем идет речь.
Листинг 3.
        private void a(a b)
        {
            while (b.a())
            {
                a = b.a(true);
                a.a();
                a(a);
            }
        }
Данный паттерн часто называют Overload Induction.
Существующие обфускаторы используют все эти приемы. Вместе с этим во многих из них существует возможность выбора набора символов которые будут применятся при переименовании или задания паттерна имени для переименования. 
Обфускатор может осуществлять переименование либо всех классов и переменных приложения, либо только тех, которые не являются публичными, либо только тех элементов программы, которые указал пользователь перез запуском обфускации в процессе конфигурации процесса. При переименовании публичного метода можно нажить проблему, так как другие сборки проекта, использующие этот метод, никогда не смогут получить доступ к нему, поскольку они могут обратиться к методу только используя старое имя. Такая ситуация может быть просто и эффективно решена большинством обфускаторов, но о решении я расскажу в последующих статьях цикла, когда будет идти разговор о обработке зависимостей.
Некоторые обфускаторы помимо переименования переменных и классов могут осуществлять также переименования элементов разметки XAML/BAML. Сама разметка находится в ресурсах защищаемого приложения и она также может пролить свет на понимание злоумыленником того, как работает программа или хотя бы как устроены составные части программы, относящиеся к пользовательскому интерфейсу. В целом, при переименовани элементов разметки вполне подойдут приемы описанные выше, за тем исключением что для новых имен элементов вряд ли подойдут неотображаемые символы.
В завершение необходимо добавить, что многие обфускаторы по окончанию обфускации при необходимости генерируют отчет, в котором содержится информация о переименованных элементах программы. Такой отчет может оказаться полезным как и для пользователя, если ему требуется осуществить отладку или исследовать обфусцированную программу, так и для самого обфускатора, поскольку некоторые из них умеют осуществлять деобфускацию. То есть осуществлять преобразования, обратные обфускации. Для таких целей им необходим сгенерированный ранее отчет, так как иначе обфускатор не будет иметь ни малейшего представления о первоначальном имени элемента.

Шифрование

После того как все необходимые элементы программы переименованы, задача исследования программы усложняется, однако по прежнему назвать ее сложно нельзя. Причиной тому, является тот факт, что все текстовые строки программы по прежнему присутствуют в коде в своем первозданном виде. Они являются замечательными помощниками в идентификации фрагментов программы, которые интересуют злоумышленников. Очевидно, что если где-то в коде встретилась строчка “Enter your password”, будте уверены –  где-то рядом есть переменная, в которой этот самый введенный пользователем пароль и находится. Определив эту переменную, задача по обходу системы защиты, для которой этот пароль предназначен уже не выглядит такой сложной.
Для того что бы сделать жизнь злоумышленнику несколько сложнее, очевидно, что с текстовыми строками нужно что-то делать. Разные обфускаторы используют различные приемы и механизмы для защиты текстовых данных, однако среди всех этих приемов можно выделить общий принцип. Если кратко, то суть защиты сводится к замене текстовой строки в коде, на некоторый метод, который в качестве параметра получает предварительно зашифрованную исходную строку, дешифрует ее и в качестве результата возвращает исходную строку. То есть, если изначально было выражение вида:
someString = “Some text”;
То после всех преобразований обфускатора получим что то вроде:
Il = ll.lIl(“□□□□□□□□□”)
Где Il это переименованая someString, а ll.lIl – вызов метода дешифрации. В данном случае мы имеем дело со статическим методом lIl некоторго вспомогательного класса ll. В качестве параметра метод получает предварительно зашифрованную строку, которая в процессе дешифрации станет эквивалентной исходной.  В самом простейшем случае данный метод может одновременно использоваться как для шифрации строки, так и для ее дешифрации. То есть если методу передать незашифрованную строку – он ее зашифрует и вернет зашифрованный эквивалент. Если передать методу зашифрованную строку, то на выходе получим исходную незашифрованный текст. Ниже на листинге 4 представлен один из возможных вариантов реализации такого метода.
Листинг 4.
public static string Encrypt(string str)
{
    var length = str.Length;
    var array = new char[length];
    for (int i = 0; i < array.Length; i++)
    {
        var c = str[i];
        var b = (byte)(c ^ length - i);
        var b2 = (byte)((c >> 8) ^ i);
        array[i] = (char)(b2 << 8 | b);
    }
    return string.Intern(new string(array));
}


Если для шифрации использовать подобный метод, то тогда сам процесс шифрования текстовых строк в процессе обфускации сводится к последовательности действий описанной ниже.
  1. Сгенерировать и добавить в код защищаемой программы метод дешифрации.
  2. Найти в коде инструкцию загрузки текстовой строки в стек.
  3. Зашифровать строку.
  4. Заменить в коде инструкцию загрузки строки на вызов метода дешифрации (пп.1), передав ему в качестве параметра зашифрованную строку которую мы получили в пп.2.
Эти действия необходимо выполнить для каждой строки используемой в коде. Вуаля! Не так уж все и сложно.
Однако есть один момент который потребует небольшой доработки, для того что бы наши изменения не нарушили работу всей программы. Проблема заключается в том, что выполнив преобразования, описанные выше, мы изменили суммарный объем кода каждого метода. Поясню. Инструкция загрузки текстовой строки занимает 5 байт: 1 байт – это сама инструкция и 4 байта – это адресс строки которую нужно загрузить. После преобразований у нас получится уже 10 байт: 1 байт – инструкция загрузки зашифрованной строки в стек, 4 байта – адрес этой строки, далее 1 байт интрукция вызова метода и наконец еще 4 байта, это собственно адрес этого метода.
Если в методе, где осуществляется сокрытие текстовых строк, присутствуют инструкции условных или безусловных переходов, то метод не будет работать так, как он работал ранее. Дело в том что адрес переходов указывается в форме смещения относительно начала метода, и после наших изменений, это значение будет указано неверно. Рассмотрим ситуацию на примере. На листинге 5 показана последовательность пседво-инструкций, которая была в программе изначально. Первая колонка это смещение инструкции относительно начала метода, вторая колонка – сама инструкция. Для простоты адрес смещения я указал в виде десятичного числа.
Листинг 5.
0000 instruction_1 param
0005 jump 0015
0010 load_string “some string”
0015 instruction_2 param

После того, как мы произведем изменения необходимые для шифрования строки, получим последовательность, представленную на листинге 6.
Листинг 6.
0000 instruction_1 param
0005 jump 0015
0010 load_string [cryped_string]
0015 call decrypt_method
0020 instruction_2 param

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

Заключение

Сегодня мы рассмотрели возможные приемы переименования элементов программы, которые используются в современных обфускаторах и получили общую картину того, как осуществляется шифрование тестовых строк и ресурсов в программе. Оба процесса являются неотъемлимой чертой любого обфукатора и предназначены для затруднения визуального восприятия кода. В следующей статье я расскажу о том, какие бывают паттерны изменения управляющей логики программы (Control flow modification). Эти паттерны подразумевают изменение следования инструкций, и предназначены для усложнения логического восприятия кода, делая процесс реверс-инжиниринга обфусцированной программы еще более сложным.