Есть множество различных способов защиты от реверса приложений .NET. Среди них — компрессия, шифрование и протекторы типа Agile.Net и Enigma. Мы уже рассказывали о взломе протектора Enigma. Сегодня поговорим о взломе защиты протектора .NET Reactor.
Еще по теме: Обход защиты протектора Obsidium
Протектор .NET Reactor
Предположим, что у нас есть программа с онлайн‑проверкой лицензии при запуске. Анализ приложения с помощью DIE говорит о платформе .NET. После загрузки приложения в отладчик dnSpy, мы видим две вещи: хорошую и плохую.
Статья написана в образовательных целях. Мы не призываем к взлому программ, а пытаемся показать и привлечь внимание разработчиков, к тому, насколько уязвимы популярные инструменты для защиты приложений.
Плохая заключается в том, что приложение надежно обфусцировано, большая часть методов переименована в комбинацию бессмысленных символов, а главное, вместо их кода везде пустые заглушки.
Взлом приложения с защитой .NET Reactor
Глупая идея, но давайте пробуем сделать дамп приложения — это часто позволяет восстановить скрытый код методов. К сожалению, в нашем случае — это не сработало: дампы модулей работоспособны, но не особо отличаются от оригинальных. Обфускация осталась, тела методов по прежнему пустые.
Вернемся в отладчик dnSpy и попробуем трассировать работающее приложение. А теперь хорошая новость: в программе нет антиотладчика и она прекрасно запускается и трассируется, причем при трассировке «пустых» методов во вкладке Call Stack мы видим, что счетчик команд перемещается по невидимому коду и проваливается в вызовы.
Побродив вслепую по коду, мы видим еще одну хорошую вещь: не все методы переименованы, некоторые названия очень даже осмысленны, и можем даже нащупать процесс проверки валидности (на скрине выше — isValid). Тело данного метода скрыто, однако название и индекс известны, и это вери гуд.
Теперь пробуем попробовать деобфускацию по стандартной схеме: в начале подсунем предложение в de4dot. Увы, но в нашем примере данный метод не сработал и de4dot не деобфусцирует. Более ранние версии сразу вываливаются с ошибкой:
1 2 3 4 5 6 7 8 9 10 11 |
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() ... |
Версии новее формулируют ошибку лаконичнее:
1 2 3 4 5 6 7 8 9 |
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. Та же ошибка и нам опять предлагают поискать версию поновее.
Открываем нашу злополучное приложение в отладчике 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 выглядит вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
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; } |
А после деобфускации сворачивается в коротенькое однострочное выражение:
1 2 3 4 5 |
public bool get_IsValid() { return (this.IsActivated || this.IsGenericLicense) && (!this.IsEvaluation || this.DaysLeft > 0); } |
То есть мы фактически добились своей цели — открыли код и даже деобфусцировали его, но, к сожалению, столь близкое счастье снова ускользает от нас. Деобфусцированный модуль напрочь неработоспособен: при запуске программы ошибки сыплются в самых неожиданных местах, и никакие комбинации ключей NetReactorSlayer не решают эту проблему.
При ближайшем рассмотрении мы понимаем и ее суть: несмотря на всю свою полезность, NetReactorSlayer — не волшебная кнопка, а развивающийся проект, к сожалению далекий от совершенства. В некоторых методах названия потеряны, код так и не открыт, и деобфускация оставляет желать лучшего.
Если у вас много времени и терпения, можно вдумчиво и кропотливо пофиксить каждый проблемный метод, но мы, как обычно, попробуем найти более короткий путь. По счастью, у нас есть исходный код NetReactorSlayer, попробуем его проанализировать. Снова не буду вдаваться в подробности, желающие могут открыть проект и подробно в нем разобраться. Вместо этого я заострю внимание на определенных моментах.
Расшифровкой кода в проекте занимается модуль NecroBit.cs, а конкретно метод Execute. Этот метод считывает из обфусцированного модуля блок зашифрованных данных и в два приема расшифровывает его. После строки:
1 |
XorEncrypt(methodsData, GetXorKey(decryptorMethod)) |
массив methodsData содержит расшифрованный код методов обфусцированного модуля. Давайте посмотрим, что NetReactorSlayer проделывает с этим массивом дальше, и выясним примерный формат хранения данных в нем. Цикл чтения и анализа всех расшифрованных методов выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 выглядит так:
1 2 3 4 5 6 7 8 9 |
/* 0x00158568 2B09 */ IL_0000: br.s IL_000B /* 0x0015856A 28FFFFFFFF */ IL_0002: call /* 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 ... |
Чтобы любая лицензия стала валидной, нам достаточно поменять в этом коде два первых байта на следующие:
1 2 |
/* 0x00158568 17 */ IL_0000: ldc.i4.0 /* 0x00158569 2A */ IL_0001: ret |
В исходном (нерасшифрованном) модуле по этому RVA значения двух байтов соответственно равны 9E F4. По счастью, метод шифрования данных — обычный XOR по ключу.
Считаем новые значения этих двух байтов:
1 2 |
9E XOR 2B XOR 17 = A2 F4 XOR 09 XOR 2A = D7 |
Меняем эти два байта на новые значения и на всякий случай проверяем правильность замены, еще раз натравив на исправленный модуль NetReactorSlayer.
Теперь и вправду декодированное тело метода содержит только return true, что и подтверждает запуск программы — лицензия подходит! Таким образом, мы получили не только полезный расширяемый и совершенствуемый инструмент для реверса приложений, защищенных .NET Reactor (в том числе нестандартных), но и быстрый способ патча подобных приложений без полного реверса и пересборки программы.
Полезные ссылки:
- Обход защиты StarForce
- Лучшие инструменты для реверс-инжиниринга
- Обзор фреймворка для реверс-инжиниринга Ghidra