Неядерный реактор. Взламываем протектор .NET Reactor

Неядерный реактор. Взламываем протектор .NET Reactor

Life-Hack [Жизнь-Взлом]/Хакинг

#Обучение

Для защиты при­ложе­ний .NET от отладки и ревер­са сущес­тву­ет мно­жес­тво спо­собов (шиф­рование, ком­прес­сия и дру­гие), а так­же спе­циаль­ных про­тек­торов, таких как, нап­ример, Agile.Net и Enigma. О взло­ме мно­гих из них мы уже пи­сали. Сегод­ня я рас­ска­жу, как побороть еще один популяр­ный про­тек­тор и обфуска­тор — .NET Reactor.

Итак, пред­ста­вим себе такую гипоте­тичес­кую задачу: у нас име­ется некое при­ложе­ние с онлайн‑про­вер­кой лицен­зии при заг­рузке. Его ана­лиз при помощи DIE детек­тит наличие плат­формы .NET. Заг­рузив прог­рамму в отладчик dnSpy, обна­ружи­ваем сра­зу две вещи: пло­хую и хорошую. Нач­ну с пло­хой: при­ложе­ние жес­токо обфусци­рова­но, часть методов пере­име­нова­на в бес­смыс­ленный набор сим­волов, а глав­ное, вмес­то их кода пов­сюду пус­тые заг­лушки.

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

Воз­вра­щаем­ся в отладчик dnSpy и про­буем пот­расси­ровать работа­ющую прог­рамму. А вот и хорошая новость: в при­ложе­нии нет анти­отладчи­ка, при­ложе­ние прек­расно запус­кает­ся и трас­сиру­ется, при­чем при трас­сиров­ке «пус­тых» методов во вклад­ке Call Stack вид­но, что счет­чик команд переме­щает­ся по невиди­мому коду и про­вали­вает­ся в вызовы. Погуляв всле­пую по коду, мы обна­ружи­ваем еще одну хорошую новость: не все методы пере­име­нова­ны, некото­рые наз­вания впол­не осмыслен­ны, и мож­но даже нащупать про­цеду­ру про­вер­ки валид­ности (на скрин­шоте выше — isValid). Тело дан­ного метода скры­то, но наз­вание и индекс извес­тны, и это уже что‑то.

Поп­робу­ем подой­ти к деоб­фуска­ции по стан­дар­тной схе­ме: для начала нат­равли­ваем на при­ложе­ние de4dot. К сожале­нию, в нашем слу­чае этот метод не работа­ет, de4dot ничего не деоб­фусци­рует. Более ста­рые вер­сии сра­зу валят­ся с ошиб­кой:

de4dot v3.1.41592.3405 Copyright (C) 2011-2015 de4dot@gmail.com

Latest version and source code: https://github.com/0xd4d/de4dot

Detected .NET Reactor 4.8

Не­обра­ботан­ное исклю­чение: System.Security.Cryptography.CryptographicException: Недопус­тимая дли­на дан­ных для дешиф­рования. в System.Security.Cryptography.RijndaelManagedTransform.TransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount)

 в de4dot.code.deobfuscators.DeobUtils.AesDecrypt(Byte[] data, Byte[] key, Byte[] iv) в D:\a\de4dot-cex\de4dot-cex\de4dot.code\deobfuscators\DeobUtils.cs:стро­ка 87 в de4dot.code.deobfuscators.dotNET_Reactor.v4.EncryptedResource.DecrypterV1.Decrypt(EmbeddedResource resource) в D:\a\de4dot-cex\de4dot-cex\de4dot.code\deobfuscators\dotNET_Reactor\v4\EncryptedResource.cs:стро­ка 225

 в de4dot.code.deobfuscators.dotNET_Reactor.v4.EncryptedResource.Decrypt()

...

Вер­сии пос­вежее фор­мулиру­ют ошиб­ку лаконич­нее:

Latest version and source code: http://www.de4dot.com/

21 deobfuscator modules loaded!

Detected .NET Reactor 4.8

ERROR:

ERROR:

ERROR:

ERROR: Hmmmm... something didn’t work. Try the latest version.

Ну теперь мы хотя бы зна­ем, с чем име­ем дело, — это .NET Reactor вер­сии пред­положи­тель­но 4.8. Вер­сия доволь­но ста­рая, одна­ко с ней не справ­ляет­ся даже спе­циаль­но обу­чен­ный под .NET Reactor de4dot. Ошиб­ка та же, и нам сно­ва пред­лага­ют поис­кать вер­сию пос­вежее.

Труд­ности нас не оста­нав­лива­ют: в кон­це кон­цов, мы уже научи­лись раз­бирать более кру­тые обфуска­торы типа Agile бук­валь­но изнутри на самом низ­ком уров­не. Заг­ружа­ем нашу зло­получ­ную прог­рамму в отладчик x32dbg и вспо­мина­ем все то, о чем я писал в пре­дыду­щей статье. Дабы не тра­тить вре­мя на пов­торение, опус­каю длин­ное опи­сание теоре­тичес­кой час­ти про­цес­са. Вкрат­це: заг­ружа­ем биб­лиоте­ку cljit.dll, отла­доч­ные сим­волы к ней и ста­вим точ­ку оста­нова на вход JIT-ком­пилято­ра CILJit::compileMethod.

Ука­зан­ный спо­соб работа­ет, то есть при каж­дом вызове ком­пилято­ра в поле ILCode струк­туры CORINFO_METHOD_INFO мы видим рас­шифро­ван­ный IL-код каж­дого метода. В прин­ципе, мож­но ана­лизи­ровать код и даже пат­чить на лету, но это дол­го и уто­митель­но, вдо­бавок нас ждет еще одна лож­ка дег­тя. Напом­ню, что в пре­дыду­щей статье я опи­сывал слег­ка жуль­ничес­кий спо­соб опре­делить индекс ком­пилиро­ван­ной про­цеду­ры. Суть его сос­тоит в том, что хендл ftn (пер­вое двой­ное сло­во в струк­туре CORINFO_METHOD_INFO), если его исполь­зовать как ука­затель, ука­зыва­ет на оди­нар­ное сло­во — индекс метода в .NET метада­те EXE-модуля.

Так вот, этот хал­турный спо­соб работа­ет не всег­да, в чем мы с огор­чени­ем и убеж­даем­ся. Зная индекс метода isValid 25250 (0x62A2), дела­ем усло­вием оста­нов­ки на брейк‑пой­нте CILJit::compileMethod выраже­ние word:[[[esp+0xc]]]==0x62A2, но точ­ка оста­нова не сра­баты­вает, хотя опре­делен­ные этим спо­собом индексы на дру­гих методах похожи на пра­виль­ные. Что‑то пош­ло не так, надо искать более кор­рек­тный спо­соб иден­тифика­ции метода. Ины­ми сло­вами, дви­жение по это­му пути, конеч­но, пер­спек­тивно, но тер­нисто, да и сам путь готовит нам мас­су подоб­ных сюр­при­зов. По счастью, для нашего слу­чая есть и более прос­тые спо­собы решения задачи, ибо умные люди, как обыч­но, все при­дума­ли за нас.

А при­дума­ли они про­ект под наз­вани­ем NetReactorSlayer. Так же как и упо­мяну­тый выше мод de4dot, он заточен под рас­шифров­ку и деоб­фуска­цию .NET Reactor, но в отли­чие от пре­дыду­щего он не валит­ся с ошиб­кой, а впол­не себе успешно соз­дает деоб­фусци­рован­ный модуль, в котором методы уже не скры­ты от дизас­сем­бли­рова­ния в dnSpy.

При­чем деоб­фуска­цией мож­но управлять: у прог­раммы есть клю­чи коман­дной стро­ки. К при­меру, при исполь­зовании клю­ча --no-deob (Don’t deobfuscate methods) мы получа­ем исходный обфусци­рован­ный байт‑код метода в том виде, в котором он хра­нит­ся в фай­ле. При отсутс­твии же это­го клю­ча по умол­чанию NetReactorSlayer пыта­ется отфиль­тро­вать код от паразит­ных инс­трук­ций в исходный вид до обфуска­ции. Нап­ример, обфусци­рован­ный код иско­мого метода isValid выг­лядит вот так:

public bool get_IsValid()

{

 while (false)

 {

 object arg_0A_0 = null[0];

 }

 int arg_66_0 = 0;

 while (true)

 {

 switch (arg_66_0)

 {

 case 0:

 if (!this.IsActivated)

 {

 arg_66_0 = 4;

 if (!false)

 {

 continue;

 }

 }

 break;

 case 1:

 case 4:

 goto IL_49;

 case 5:

 goto IL_93;

 }

 IL_32:

 if (!this.IsEvaluation)

 {

 return true;

 }

 arg_66_0 = 5;

 if (false)

 {

 goto IL_49;

 }

 continue;

 IL_83:

 goto IL_32;

 IL_49:

 if (this.IsGenericLicense)

 {

 goto IL_83;

 }

 return false;

 }

 IL_93:

 return this.DaysLeft > 0;

}

А пос­ле деоб­фуска­ции сво­рачи­вает­ся в коротень­кое однос­троч­ное выраже­ние:

public bool get_IsValid()

{

 return (this.IsActivated || this.IsGenericLicense) && (!this.IsEvaluation || this.DaysLeft > 0);

}

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

Ес­ли у тебя мно­го вре­мени и тер­пения, мож­но вдум­чиво и кро­пот­ливо пофик­сить каж­дый проб­лемный метод, но мы, как обыч­но, поп­робу­ем най­ти более корот­кий путь. По счастью, у нас есть исходный код NetReactorSlayer, поп­робу­ем его про­ана­лизи­ровать. Сно­ва не буду вда­вать­ся в под­робнос­ти, жела­ющие могут открыть про­ект и под­робно в нем разоб­рать­ся. Вмес­то это­го я заос­трю вни­мание на опре­делен­ных момен­тах. Рас­шифров­кой кода в про­екте занима­ется модуль NecroBit.cs, а кон­крет­но метод Execute. Этот метод счи­тыва­ет из обфусци­рован­ного модуля блок зашиф­рован­ных дан­ных и в два при­ема рас­шифро­выва­ет его. Пос­ле стро­ки

XorEncrypt(methodsData, GetXorKey(decryptorMethod));

мас­сив methodsData содер­жит рас­шифро­ван­ный код методов обфусци­рован­ного модуля. Давай пос­мотрим, что NetReactorSlayer про­делы­вает с этим мас­сивом даль­ше, и выяс­ним при­мер­ный фор­мат хра­нения дан­ных в нем. Цикл чте­ния и ана­лиза всех рас­шифро­ван­ных методов выг­лядит так:

while ((ulong)methodsDataReader.Position < (ulong)((long)(methodsData.Length - 1)))

{

 ...

 int size2 = methodsDataReader.ReadInt32(); // Размер IL-кода метода

 byte[] methodData = methodsDataReader.ReadBytes(size2); // IL-код метода

 if (!rvaToIndex.TryGetValue(rva3, out int methodIndex)) // methodIndex — индекс метода

 {

 Logger.Warn("Couldn't find method with RVA: " + rva3);

 }

 else

 {

 uint methodToken = (uint)(100663297 + methodIndex); Токен метода

Вот сра­зу за этим мес­том мы уже зна­ем сме­щение до рас­шифро­ван­ного IL-кода метода нуж­ного нам индекса и сам рас­шифро­ван­ный код. В нашем слу­чае IL-код метода isValid выг­лядит так:

/* 0x00158568 2B09 */ IL_0000: br.s IL_000B

/* 0x0015856A 28FFFFFFFF */ IL_0002: call <null>

/* 0x0015856F 14 */ IL_0007: ldnull

/* 0x00158570 16 */ IL_0008: ldc.i4.0

/* 0x00158571 9A */ IL_0009: ldelem.ref

/* 0x00158572 26 */ IL_000A: pop

/* 0x00158573 16 */ IL_000B: ldc.i4.0

/* 0x00158574 2DF9 */ IL_000C: brtrue.s IL_0007

...

Что­бы любая лицен­зия ста­ла валид­ной, нам дос­таточ­но поменять в этом коде два пер­вых бай­та на сле­дующие:

/* 0x00158568 17 */ IL_0000: ldc.i4.0

/* 0x00158569 2A */ IL_0001: ret

В исходном (нерас­шифро­ван­ном) модуле по это­му RVA зна­чения двух бай­тов соот­ветс­твен­но рав­ны 9E F4. По счастью, метод шиф­рования дан­ных — обыч­ный XOR по клю­чу. Поэто­му, что­бы поменять их на нуж­ные, вспо­мина­ем дру­гую мою статью про патч инстал­лятора, где мы про­делы­вали ана­логич­ную опе­рацию.

Счи­таем новые зна­чения этих двух бай­тов:

9E XOR 2B XOR 17 = A2

F4 XOR 09 XOR 2A = D7

Ме­няем эти два бай­та на новые зна­чения и на вся­кий слу­чай про­веря­ем пра­виль­ность замены, еще раз нат­равив на исправ­ленный модуль NetReactorSlayer.

Бин­го! Теперь и вправ­ду декоди­рован­ное тело метода содер­жит толь­ко return true, что и под­твержда­ет запуск прог­раммы — лицен­зия под­ходит! Таким обра­зом, мы получи­ли не толь­ко полез­ный рас­ширя­емый и совер­шенс­тву­емый инс­тру­мент для ревер­са при­ложе­ний, защищен­ных .NET Reactor (в том чис­ле нес­тандар­тных), но и быс­трый спо­соб пат­ча подоб­ных при­ложе­ний без пол­ного ревер­са и перес­борки прог­раммы.

Источник


Report Page