Гайд [.NET C#] Учимся создавать деобфускатор

ldloc.s <d0t.net> stloc.s <Reversed>
Пользователь
Статус
Оффлайн
Регистрация
21 Окт 2018
Сообщения
214
Реакции[?]
337
Поинты[?]
1K
Сразу: те, кому это не нужно, или кто только начинает изучать C#, пожалуйста, не нужно читать эту статью, она будет сложна для вас и бессмысленна.
Но если ради интереса, то можно)
Всем привет. В этой статье я хочу рассказать, как можно написать свой деобфускатор для .NET(на подобии de4dot, но до него еще очень и очень далеко?, кстати, не обновленный не сможет расшифровать строки). Моя задача – объяснить принцип создания деобфускации. В данном гайде я не буду полностью создавать на него деобфускатор(сегодня вы очень часто будете встречать это слово, извиняюсь за тавтологию). Это ооооочень просто, но касается только расшифровки строк.

Для примера, возьму обфускатор последней версии .NET Reactor 6.3.0.0(на эту версию еще нет полноценного деобфускатора и стандартный de4dot не работает, нужно обновлять:tonguewink:)

В данном гайде я напишу только расшифровку строк. Дальше пробуйте сами, кто заинтересовался. Может выложу еще что-то интересное.

Начнем, пожалуй. Для начала, создадим консольное приложение, и реализуем простейший драг-н-дроп:
C#:
if (args.Length == 0)
    {
        Console.WriteLine("Drag and drop file");
        Thread.Sleep(2000);
        Environment.Exit(1);
    }
string pathFile = args[0];
Далее загружаем сборку дважды, в 1-ом случае мне удобно работать с библиотекой DNLIB(3.3.2)
Почему? Потому что стандартная библиотека System.Reflection очень часто не видит обфусцированные классы-типы.
C#:
ModuleDef moduleDef = ModuleDefMD.Load(pathFile); // Для работы с библиотекой Dnlib, загружаем сборку и будем работать с ней в дальнейшем
Assembly assemblyDef = Assembly.LoadFile(pathFile); // Позволяет Invoke'ать методы и получать значения(часто используется для вызова точки входа, чтобы распаковать приложение)
MethodInfo methodInfo = (MethodInfo)assemblyDef.EntryPoint; // Вызов точки входа, но я не использую напрямую эту переменную, нужна только для быстрого доступа и передача аргументов в другие функции
StringDecrypt stringDecrypt = new StringDecrypt(ref moduleDef, ref methodInfo); // Наш класс-расшифровщик, который мы сейчас будем реализовывать
moduleDef.Write(Path.GetDirectoryName(args[0]) + "\\" + Path.GetFileNameWithoutExtension(args[0]) + "_strCleaned" + Path.GetExtension(args[0])); // Сохранение пропатченного файла
Немного теории:
.NET Reactor реализует обфускацию строк таким образом: генерируется алгоритм запутывания строк, для этого алгоритма генерируется аналогичный для РАСШИФРОВКИ, но в тело нашего метода, где используется строка, вместо нее подставляется некоторое значение(назовем его seed), и по нему расшифровывается строка. Для ясности приведу картинку:

Видно как раз метод расшифровки(он очень большой, поэтому разбирать я его даже не буду), и виден наш seed для каждой строки он естественно разный.
Теперь нам нужно узнать, где находится строка. Представим, что у нас нет исходной версии кода(ДО), и мы не знаем, где должна быть строка подставлена в чистом виде. Для этого я сделал такую сигнатуру: ищем инструкцию Ldc -> ЕСЛИ СЛЕДУЮЩАЯ ПОСЛЕ НЕЕ CALL-инструкция с операндом-методом с возвращающим значение String, и входящим параметром Int32 -> то на этом месте происходит подстановка строки.(всё, что я сейчас написал, проанализируйте с картинкой выше и сопоставьте все условия).
В MSIL-листинге: Опкоды слева : Операнды справа
Теперь приступим к реализации класса StringDecrypt

C#:
public class StringDecrypt
    {
        private ModuleDef moduleDef { get; }
        private MethodInfo methodInfo { get; }
        public StringDecrypt(ref ModuleDef moduleDef, ref MethodInfo methodInfo) // Конструктор, инициализируем поля
        {
            this.moduleDef = moduleDef;
            this.methodInfo = methodInfo;
            Decrypt();
        }

        void Decrypt() // Основной метод расшифровки
        {
            foreach (var typeDef in moduleDef.Types) // Проходим по каждому ТИПУ нашего модуля
            {
                foreach (var methodDef in typeDef.Methods) // Проходим по каждому МЕТОДУ нашего ТИПА
                {
                
                    if (methodDef.Body == null) continue; // Проверка на пустой метод(обычно, анти-тампер удаляет тело, но не каждый анти-тампер)
                    for (int i = 0; i < methodDef.Body.Instructions.Count(); i++) // Цикл-проход по нашим инструкциям МЕТОДА
                    {
                        if (methodDef.Body.Instructions[i].IsLdcI4() &&
                            methodDef.Body.Instructions[i + 1].OpCode.Name == "call")  // Вот сравнение ОпКода с семейством Ldc и последующей инструкции call
                        {
                            dynamic operand_call = methodDef.Body.Instructions[i + 1].Operand; // Почему dynamic? Потому что иногда возвращаются разные типы с одинаковой реализацией, поэтому будем получать эксепшены для статического типа(очень полезная вещь, но злоупотреблять ею не стоит)
                            if (operand_call.ReturnType.TypeName == "String" && operand_call.Parameters[0].Type.TypeName == "Int32") // Вот проверка нашего операнда на возвращаемый тип и тип аргумента
                            {
                                dynamic operand_ldc = methodDef.Body.Instructions[i].Operand; // Получаем seed
                                string decrypt_string = InvokeDecryptMethod(operand_ldc, operand_call.Module.Name, operand_call.DeclaringType2.Name, operand_call.Name.String); // ВЫЗЫВАЕМ МЕТОД РАСШИФРОВКИ, который возвращает нам уже расшифрованную строку
                                methodDef.Body.Instructions[i].OpCode = OpCodes.Nop; // Далее меняем IL-код. Ненужный нам уже Ldc опкод меняем на nop
                                methodDef.Body.Instructions[i + 1] = new Instruction(OpCodes.Ldstr, decrypt_string); // Следующую инструкцию, в которой вызывался бы метод расшифровки(call) заменяем на Ldstr с операндом нашей новой расшифрованной строки.
                                methodDef.Body.OptimizeBranches(); // Оптимизация всей условий, свитчей, "впаивание" наших IL-инструкций в тело метода.
                                methodDef.Body.SimplifyBranches(); // Упрощение инструкций(br.s -> br)
                            }
                        }
                    }
                }
            }
        }


        string InvokeDecryptMethod(int seed, string moduleName, string typeName, string methodName) // Сам метод-Invoke нашего метода-дешифратора
        {
            string decryptedString;
            var module = methodInfo.Module.Assembly.GetModule(moduleName);
            foreach (var typein module.GetTypes()) // Проход по типам в поисках нашего метода-дешифратора(усложненный вариант)
            {
                if (type.Name == typeName) // Если тип соответствует названием типу, который нам нужен, то проходим дальше
                {
                    foreach (var methodType in type.GetRuntimeMethods()) // Проход по методам найденного ранее НУЖНОГО нам типа
                    {
                        if (methodType.Name == methodName) // Если наш метод найден, то ...
                        {
                            decryptedString = (string)methodType.Invoke(null, new object[] { seed }); // .. просто вызываем его, первый параметр null - он вам не нужен, а вот вторым параметром нужно передать аргумент, в данном случае это наш seed
                            return decryptedString; // Возвращаем нашу расшифрованную строку
                        }
                    }
                }
            }
            return "NULL"; // Заглушка
        }
    }
Давайте испытаем. Просто drag'n'dropаем наш обфусцированный файл на наше приложение, и видим:


Если вы думаете, что дальше - легче, просьба покинуть помещение) Нет, дальше еще хуже.
Данный расшифровщик работает ТОЛЬКО с настройкой обфускатора: "String obfuscation", если будут настройки миксоваться - у вас ничего не получится, т.к. будут использоваться методы обфускации "покруче".
Исходник:
Пожалуйста, авторизуйтесь для просмотра ссылки.

Всем спасибо! Если у вас есть аргументированная критика, предложения, вопросы, рад буду всё выслушать.
 
Последнее редактирование:
Забаненный
Статус
Оффлайн
Регистрация
30 Дек 2019
Сообщения
52
Реакции[?]
9
Поинты[?]
0
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
А для чего, если есть готовые решения? А так годно!
 
Сверху Снизу