warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Сегодня я продолжу рассказ про обфускаторы IL-кода, их особенности, принципы работы и борьбы с ними. Возможно, ты помнишь другие мои статьи на эту тему, посвященные таким обфускаторам, как dnGuard или .NET Reactor. В этой статье мы попробуем применить полученные навыки для исследования приложения, защищенного нестандартным кастомным обфускатором, а заодно приобретем новые, разобрав несколько приемов сокрытия кода, которые в нем использованы.
Чтобы сочетать полезное с еще более полезным, в качестве объекта исследования выберем широко известную в узких кругах систему защиты и лицензирования Quick License Manager (QLM) от Soraco Technologies. Эта система, как и подавляющее большинство других, предоставляет разработчикам возможности как онлайн, так и офлайн‑активации своих продуктов. Офлайн‑активация проходит по стандартной схеме: менеджер лицензий выкидывает диалоговое окошко, содержащее информацию о компьютере (Computer Identifier), которую надо отослать продавцам программы.

Взамен они присылают два привязанных к Computer Identifier ключа — активационный (Activation Key) и компьютерный (Computer Key). Эти ключи надо ввести в соответствующие поля диалогового окна (причем заведомо неправильный Activation Key вызывает сообщение об ошибке непосредственно при вводе в поле), после чего нажать на кнопку Activate внизу формочки. Если все срослось, программа активируется, причем, что характерно, на нужный срок и с нужными опциями.
Этот факт наводит на мысль, что информация об опциях и сроке действия лицензии содержится прямо в активационном ключе, и это вызывает у каждого уважающего себя хакера стойкий соблазн реверсировать алгоритм его валидации, чтобы на его основе написать свой собственный генератор с максимально возможными плюшками внутри.
Давай посмотрим, как это можно реализовать. В каталоге менеджера лицензий бросается в глаза наличие библиотеки QlmLicenseLib., поэтому начинать анализ будем сразу с нее. Detect It Easy не видит в этом файле никаких протекторов и обфускаторов, это чистый дотнет, что весьма странно для лицензирующей библиотеки.

Однако, загрузив библиотеку в отладчик dnSpy, мы видим, что обфускация там, конечно же, имеется, хотя ее тип не детектируется de4dot и прочими стандартными дотнетовскими деобфускаторами.

Ну что ж, мы к этому готовы, обфускаторы видали и посерьезнее. Первое, что бросается в глаза, — стандартная обфускация имен и flattening control flow, присутствующие на каждом уважающем себя обфускаторе. Банальные вещи, на которых мы даже останавливаться не будем. По счастью, не все имена в этой библиотеке обфусцированы в непроизносимую кашу, основные классы и их методы имеют вполне читаемые и говорящие за себя имена.

ValidateLicense выглядит как именно то, что нам надо, даже имена параметров подходящие. Правда, количество одноименных методов несколько пугает, но мы трудностей не боимся и тщательно ставим на каждый точки останова. Результат не заставляет себя долго ждать — сразу при валидации ввода активационного ключа эти точки по очереди срабатывают. Обрати внимание на интересную особенность вложенных вызовов.

Видишь, ни один из методов ValidateLicense?? напрямую не вызывается из другого, вызовы идут через странные шлюзы вида QlmLicenseLib.. Ткнув в любой из подобных методов, мы обнаруживаем класс‑переходник, в котором, что характерно, тоже нет прямой ссылки на вызываемый метод. Судя по всему, мы поняли причину разнообразия безымянных методов на скриншоте с подписью «Обфускация» — похоже на то, что обфускатор на каждый метод генерирует шлюз следующего вида:
using System;internal sealed class \uF0D1 : MulticastDelegate{ public extern \uF0D1(object, IntPtr); public virtual extern void Invoke(object, string); public static void \uEB3D(object obj, string text) // <------------- Вызываемый метод { \uF0D1.\uEB3D(obj, text); } static \uF0D1() { \uE063.\uE056(890911107, 1662027739, 1215538209); } public static \uF0D1 \uEB3D;}Количество и типы параметров метода \ могут варьироваться, но первый параметр obj имеет класс, содержащий вызываемый метод. Далее нужно ткнуть в \. Посмотреть, что там внутри (и провалиться туда при отладке), не получится по простой причине: \ — это делегат, объявленный в самом низу класса и вызываемый через Invoke, это хорошо видно, если переключить просмотр кода в режим IL:
.method public static void \uEB3D ( object obj, string text ) cil managed { .maxstack 8 /* 0x00117E94 7EE7100004 */ IL_0000: ldsfld class \uF0D1 \uF0D1::\uEB3D /* 0x00117E99 02 */ IL_0005: ldarg.0 /* 0x00117E9A 03 */ IL_0006: ldarg.1 /* 0x00117E9B 281A380006 */ IL_0007: call instance void \uF0D1::Invoke(object, string) /* 0x00117EA0 2A */ IL_000C: ret }Если ты никогда раньше не слышал про делегаты, не беда — подтянуть матчасть можно, прочитав, например, вот эту статью. Мы же подробно останавливаться на делегатах не будем, так как про это достаточно много написано и без нас. В двух словах делегат — это ссылка на метод, в определенной степени аналог указателя ** в C, и у нас возникает два вопроса: как именно инициализируется этот «указатель» и почему «по его адресу» нельзя провалиться при отладке?
Первое и самое очевидное предположение — он инициализируется при конструировании класса. Собственно, в каждом из многочисленных классов‑переходников и нет других методов, кроме вызывающего \ и конструктора. Причем конструктор практически всегда вызывает один и тот же метод \ с тремя разными волшебными константами.
Перейдем в класс \ и поставим точку останова на \. Конструктор вызывается сразу при вызове \, и мы попробуем реверсировать логику его работы, продравшись сквозь обфускацию control flow.
Не буду утомлять тебя процессом пошагового сворачивания обфусцированной логики, ты и сам сможешь это проделать, а если лень самому, подключи к процессу какую‑нибудь подходящую нейросеть. В итоге после упрощения получается примерно следующий код:
public static void \uE056(int int_0, int int_1, int int_2) { ... Type typeFromHandle = Type.GetTypeFromHandle(moduleHandle_0.ResolveTypeHandle(decodedTypeToken)); FieldInfo fieldInfo = FieldInfo.GetFieldFromHandle(moduleHandle_0.ResolveFieldHandle(decodedFieldToken)); ... MethodInfo methodInfo = (MethodInfo)MethodBase.GetMethodFromHandle(moduleHandle_0.ResolveMethodHandle(decodedMethodToken)); Delegate value; ... ParameterInfo[] parameters = methodInfo.GetParameters(); int num3 = parameters.Length + 1; Type[] array = new Type[num3]; array[0] = typeof(object); for (int k = 1; k < num3; k++) { array[k] = parameters[k - 1].ParameterType; } DynamicMethod dynamicMethod = new DynamicMethod(string.Empty, methodInfo.ReturnType, array, typeFromHandle, skipVisibility: true); ILGenerator iLGenerator = dynamicMethod.GetILGenerator(); iLGenerator.Emit(OpCodes.Ldarg_0); if (num3 > 1) { iLGenerator.Emit(OpCodes.Ldarg_1); } if (num3 > 2) { iLGenerator.Emit(OpCodes.Ldarg_2); } if (num3 > 3) { iLGenerator.Emit(OpCodes.Ldarg_3); } if (num3 > 4) { for (int l = 4; l < num3; l++) { iLGenerator.Emit(OpCodes.Ldarg_S, l); } } iLGenerator.Emit(OpCodes.Callvirt , methodInfo); iLGenerator.Emit(OpCodes.Ret); value = dynamicMethod.CreateDelegate(typeFromHandle); fieldInfo.SetValue(null, value); }Как и предполагалось, конструктор действительно инициализирует делегат \, причем весьма хитрым образом. Попробуем разобрать, что делает этот код. Для начала определимся с обозначением идентификаторов:
-
moduleHandle_0— условное обозначение хендла на текущий модуль, содержащий выполняемые методы и их классы. Он возвращается изtypeof(\, в нашем случае этоuE063). Assembly. ManifestModule. ModuleHandle QlmLicenseLib; -
decodedTypeToken— токен, соответствующий типу делегата\(в каждом классе он разный, в нашем примере онuF0D1.\ uEB3D internal);sealed class \ uF0D1 : MulticastDelegate -
decodedFieldToken— токен, соответствующий полю делегата\;uF0D1.\ uEB3D -
decodedMethodToken— токен, соответствующий методу, вызываемому через делегат.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
