Всем привет. В этой статье я хочу рассказать, как можно написать свой деобфускатор для .NET(на подобии de4dot, но до него еще очень и очень далеко?, кстати, не обновленный не сможет расшифровать строки). Моя задача – объяснить принцип создания деобфускации. В данном гайде я не буду полностью создавать на него деобфускатор(сегодня вы очень часто будете встречать это слово, извиняюсь за тавтологию). Это ооооочень просто, но касается только расшифровки строк. Для примера, возьму обфускатор последней версии .NET Reactor 6.3.0.0(на эту версию еще нет полноценного деобфускатора и стандартный de4dot не работает, нужно обновлять) В данном гайде я напишу только расшифровку строк. Дальше пробуйте сами, кто заинтересовался. Может выложу еще что-то интересное. Начнем, пожалуй. Для начала, создадим консольное приложение, и реализуем простейший драг-н-****: if (args.Length == 0) { Console.WriteLine("Drag and drop file"); Thread.Sleep(2000); Environment.Exit(1); } string pathFile = args[0]; 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 очень часто не видит обфусцированные классы-типы. 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])); // Сохранение пропатченного файла 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 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"; // Заглушка } } 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", если будут настройки миксоваться - у вас ничего не получится, т.к. будут использоваться методы обфускации "покруче".