В этой статье мы раз­берем, как устро­ен кас­томный IL-обфуска­тор, скры­вающий вызовы через динами­чес­ки соб­ранные делега­ты и запутан­ный control flow. Мы прой­дем весь путь от обна­руже­ния «лег­ковес­ных» методов до ревер­са логики QLM, что­бы понять, как обфуска­тор мас­киру­ет реаль­ные вызовы и как это обой­ти.

warning

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

Се­год­ня я про­дол­жу рас­сказ про обфуска­торы IL-кода, их осо­бен­ности, прин­ципы работы и борь­бы с ними. Воз­можно, ты пом­нишь дру­гие мои статьи на эту тему, пос­вящен­ные таким обфуска­торам, как dnGuard или .NET Reactor. В этой статье мы поп­робу­ем при­менить получен­ные навыки для иссле­дова­ния при­ложе­ния, защищен­ного нес­тандар­тным кас­томным обфуска­тором, а заод­но при­обре­тем новые, разоб­рав нес­коль­ко при­емов сок­рытия кода, которые в нем исполь­зованы.

Что­бы сочетать полез­ное с еще более полез­ным, в качес­тве объ­екта иссле­дова­ния выберем широко извес­тную в узких кру­гах сис­тему защиты и лицен­зирова­ния Quick License Manager (QLM) от 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 — токен, соот­ветс­тву­ющий методу, вызыва­емо­му через делегат.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии