• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Анализируем хитрую обфускацию в Quick License Manager

stihl

bot
Moderator
Регистрация
09.02.2012
Сообщения
1,440
Розыгрыши
0
Реакции
792
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
В этой статье мы разберем, как устроен кастомный IL-обфускатор, скрывающий вызовы через динамически собранные делегаты и запутанный control flow. Мы пройдем весь путь от обнаружения «легковесных» методов до реверса логики QLM, чтобы понять, как обфускатор маскирует реальные вызовы и как это обойти.

warning​



Чтобы сочетать полезное с еще более полезным, в качестве объекта исследования выберем широко известную в узких кругах систему защиты и лицензирования Для просмотра ссылки Войди или Зарегистрируйся от Soraco Technologies. Эта система, как и подавляющее большинство других, предоставляет разработчикам возможности как онлайн, так и офлайн‑активации своих продуктов. Офлайн‑активация проходит по стандартной схеме: менеджер лицензий выкидывает диалоговое окошко, содержащее информацию о компьютере (Computer Identifier), которую надо отослать продавцам программы.

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

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

Давай посмотрим, как это можно реализовать. В каталоге менеджера лицензий бросается в глаза наличие библиотеки QlmLicenseLib.dll, поэтому начинать анализ будем сразу с нее. Detect It Easy не видит в этом файле никаких протекторов и обфускаторов, это чистый дотнет, что весьма странно для лицензирующей библиотеки.

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

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

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

Для просмотра ссылки Войди или Зарегистрируйся
Видишь, ни один из методов ValidateLicense?? напрямую не вызывается из другого, вызовы идут через странные шлюзы вида QlmLicenseLib.dll!?????.\uEB3D(). Ткнув в любой из подобных методов, мы обнаруживаем класс‑переходник, в котором, что характерно, тоже нет прямой ссылки на вызываемый метод. Судя по всему, мы поняли причину разнообразия безымянных методов на скриншоте с подписью «Обфускация» — похоже на то, что обфускатор на каждый метод генерирует шлюз следующего вида:

Код:
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;
}

Количество и типы параметров метода \uEB3D могут варьироваться, но первый параметр obj имеет класс, содержащий вызываемый метод. Далее нужно ткнуть в \uF0D1.\uEB3D(obj, text);. Посмотреть, что там внутри (и провалиться туда при отладке), не получится по простой причине: \uEB3D — это делегат, объявленный в самом низу класса и вызываемый через 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, и у нас возникает два вопроса: как именно инициализируется этот «указатель» и почему «по его адресу» нельзя провалиться при отладке?

Первое и самое очевидное предположение — он инициализируется при конструировании класса. Собственно, в каждом из многочисленных классов‑переходников и нет других методов, кроме вызывающего \uEB3D и конструктора. Причем конструктор практически всегда вызывает один и тот же метод \uE063.\uE056(int,int,int) с тремя разными волшебными константами.

Перейдем в класс \uE063 и поставим точку останова на \uE056. Конструктор вызывается сразу при вызове \uEB3D, и мы попробуем реверсировать логику его работы, продравшись сквозь обфускацию 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);

    }

Как и предполагалось, конструктор действительно инициализирует делегат \uF0D1.\uEB3D, причем весьма хитрым образом. Попробуем разобрать, что делает этот код. Для начала определимся с обозначением идентификаторов:
  • moduleHandle_0 — условное обозначение хендла на текущий модуль, содержащий выполняемые методы и их классы. Он возвращается из typeof(\uE063).Assembly.ManifestModule.ModuleHandle, в нашем случае это QlmLicenseLib;
  • decodedTypeToken — токен, соответствующий типу делегата \uF0D1.\uEB3D (в каждом классе он разный, в нашем примере он internal sealed class \uF0D1 : MulticastDelegate);
  • decodedFieldToken — токен, соответствующий полю делегата \uF0D1.\uEB3D;
  • decodedMethodToken — токен, соответствующий методу, вызываемому через делегат.
Три этих значения получаются путем долгих пошаговых математических манипуляций с аргументами конструктора int int_0, int int_1 и int int_2. Все это делается для того, чтобы максимально усложнить работу реверсера при отслеживании вызываемого через делегат метода. Они нам нужны, чтобы с их помощью сначала получить дескрипторы среды выполнения для типа поля и метода (через ResolveTypeHandle, ResolveFieldHandle и ResolveMethodHandle соответственно).

В дальнейшем эти данные используются следующим образом: дело в том, что не просто ссылка на вызываемый метод присваивается делегату, а для пущей вредности создается еще одна дополнительная прокладка в виде динамического метода, на лету конструируемого из IL-кода. Для создания этого метода нам необходима информация о вызываемом через него методе methodInfo (получаем через GetFieldFromHandle из дескриптора) и информация о типе порождающего класса typeFromHandle (в нашем примере \uF0D1, тоже получаем через GetTypeFromHandle из дескриптора). Снова не буду заострять внимание на процессе создания динамического метода, при желании ты можешь самостоятельно подробно ознакомиться с ним, например, из Для просмотра ссылки Войди или Зарегистрируйся Для просмотра ссылки Войди или Зарегистрируйся. В итоге мы имеем «легковесный» (lightweight) метод вида

Код:
ldarg_0 // Класс вызываемого метода
ldarg_1 // 1-й параметр метода
ldarg_2 // 2-й параметр метода
...
callvirt <токен вызываемого метода>
ret

Ссылка на этот метод присваивается делегату \uEB3D, информацию о котором fieldInfo мы получаем из дескриптора через GetFieldFromHandle. Подобный заднепроходный алгоритм необходим, чтобы стопорить любой отладчик на барьере из «легковесного» кода в процессе выполнения. Есть в этом еще кое‑что, и мы увидим это в ближайшее время.

Итак, когда мы разобрались, как все работает, самое время приступить к продумыванию стратегии дальнейшего реверса. Если у тебя нет проблем со свободным временем и ты хочешь полностью деобфусцировать эту библиотеку для удобства дальнейшего исследования, то можно пошагово свернуть все control flow в модуле, посчитать все динамически вычисляемые константы токенов вызываемых методов, после чего подменить все токены в вызовах вида QlmLicenseLib.dll!?????.\uEB3D() расчетными. Выглядит как великолепный план, надежный, будто швейцарские часы, но есть нюансы. С одной стороны, построение такого деобфускатора действительно сильно облегчит дальнейший реверс и попутно прокачает твои скиллы. Но если вдуматься, это будет деобфускатор одной‑единственной программы: ведь наверняка в следующей версии принцип обфускации поменяется и все придется повторять по новой.

Оставляю за тобой право идти по этому непростому пути, мы же продолжим исследование обфусцированого кода. В свете открывшейся информации стратегия отладки процедуры валидации активационного ключа выглядит следующим образом. Ставим точку останова на метод \uE063.\uE056(int,int,int), (а еще лучше — на вызов ResolveMethodHandle, когда значение токена вызываемого метода уже посчитано). При достижении этой точки останова мы посмотрим полученное значение и станем искать по нему нужный метод, а потом поставим точку уже на него. Несколько занудно, но достаточно долгое время это реально работает, пока в определенный момент не ломается.

Суть бага в следующем: при очередном шаге на входе в ResolveMethodHandle мы видим значение токена 0x06001FA2 — этому значению соответствует метод \IsLicenseNet\LicenseEngineBase\Decode. Однако в указанном методе код отсутствует напрочь, более того, все методы класса \IsLicenseNet\LicenseEngineBase представляют собой пустые заглушки следующего вида.

Для просмотра ссылки Войди или Зарегистрируйся
Это уже само по себе подозрительно, поскольку упомянутый класс никак не может быть пустым, так как он и занимается раскодированием строки лицензии. Ставим точку останова на Decode и убеждаемся, что управление туда не передается вообще, весь класс просто маскировочный. Еще раз последовательно перепроверяем все действия — ошибки нигде нет, вот фрагмент вызывающего кода:

Код:
private bool \uE000(LicenseEngineBase \uE000, string \uE001, string \uE002, bool \uE003, bool \uE004, out string \uE005)
{
    bool flag = true;
    while (true)
    {
        int num = AddOrganizationCompletedEventArgs.\uE000(85);
        while (true)
        {
            num ^= 140;
            if (-74 <= -126)
            {
...
        bool arg_24B_0 = \uE000\uE22F.\uEB3D(\uE000, \uE001, this.encryptionKey);
        \uE000\uE22A.\uEB3D(this, \uE05F.\uE04C(57974));
        if (!arg_24B_0)
        {
...

Вроде никакой хитрой перезагрузки нет, у метода на входе (LicenseEngineBase, string, string), на выходе bool, и при компиляции «легковесный код» тоже получается верный:

Код:
ldarg_0
ldarg_1
ldarg_2
callvirt <0x06001FA2> // По всем признакам \IsLicenseNet\LicenseEngineBase\Decode
ret

Откуда же тогда подобные чудеса? Ставим точку останова на \uE000\uE22F.\uEB3D(obj, text, text2), а затем внимательно смотрим на передаваемые туда параметры. И обнаруживаем маленькую неточность: несмотря на то что тип передаваемого в метод первого параметра \uE000 заявлен, как и должно быть, \IsLicenseNet\LicenseEngineBase, реальный же тип переданного значения obj слегка другой — \IsLicenseNet\LicenseEngine. Этот класс, хоть и схож по названию и является дочерним от \IsLicenseNet\LicenseEngineBase, кардинально отличается от последнего тем, что у него все методы очень даже не пустые.

Для просмотра ссылки Войди или Зарегистрируйся
Как ты уже, вероятно, догадался, управление при вызове передается именно в \IsLicenseNet\LicenseEngine\Decode, если поставить там точку останова. Как получилось, что LicenseEngineBase превратился в LicenseEngine при передаче параметров? Все очень просто: поднявшись на несколько вызовов вверх, мы обнаруживаем, что эта переменная изначально и инициировалась как LicenseEngine:

Код:
...
    LicenseEngine licenseEngine = \uE000\uE228.\uEB3D();
    if (licenseEngine != null)
    {
        \uE000\uE229.\uEB3D(licenseEngine, this);
        flag = \uE000\uE227.\uEB3D(this, licenseEngine, licenseKey, computerID, skipWrites, skipValidation, ref errMsg);
        if (flag)
        {
            this.engineVersion = LicenceManager.LATEST_ENGINE_VERSION;
        }
    }
...

А потом была замаскирована многократной передачей параметров, через которую пролезла, так как в ней был заявлен родительский класс LicenseEngineBase.

И все равно кажется настоящей магией, каким образом вызов callvirt <\IsLicenseNet\LicenseEngineBase\Decode> с его родным токеном вызывает метод совершенно другого класса с совершенно другим токеном?

А никакой магии нет, если вспомнить, чем команда call отличается от callvirt. Если бы в динамическом «легковесном коде» стоял call, то управление бы однозначно передавалось на метод, токен которого приведен в ее параметре. А callvirt специально имеет первым параметром класс, в таблице виртуальных методов которого (VMT) он ищет метод, соответствующий методу своего токена в иерархии наследования, и для дочерних методов такое соответствие имеется. Подробно с примерами кода ты можешь почитать про описанный процесс Для просмотра ссылки Войди или Зарегистрируйся, но сразу предупреждаю о возможных проблемах, если ты будешь использовать приведенный в ней код.

Конструкция return dm.CreateDelegate<Action<GreeterA>>(); не компилируется (ошибка CS0308 Неуниверсальный метод "DynamicMethod.CreateDelegate(Type)" нельзя использовать с аргументами типа). Возможно, что это и не вина автора, а просто особенности версии фреймворка, но у меня лично скомпилировалась конструкция return (Action < GreeterA >) dm.CreateDelegate(typeof(Action<GreeterA>)); вместо приведенной выше строки. Но уж явно ошибочно автор пытается прямо заменить инструкцию callvirt инструкцией call — при вызове sayHelloCall(a); по понятной причине (лишний параметр при вызове call) выскакивает исключение:

Код:
System.Security.VerificationException
  HResult=0x8013150D

Сообщение = Операция может вызвать нестабильность при выполнении.
Источник = <Не удается определить источник исключения>
Трассировка стека:
<Не удается определить трассировку стека исключения>
В общем, с учетом разобранных выше моментов, дальнейший реверс библиотеки лицензирования представляет собой стандартную операцию, и я предоставляю тебе довести ее до конца самому. Буду рад, если описанные в статье методы обфускации отложатся у тебя в памяти и ты будешь готов к встрече с ними при разборе других дотнетовских обфускаторов (а они встречаются там с завидной регулярностью).
 
Activity
So far there's no one here