Неядерный реактор. Взламываем протектор .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 (в том числе нестандартных), но и быстрый способ патча подобных приложений без полного реверса и пересборки программы.