Сегодня я задумался о том, как обфускаторы скрывают код методов от утилит деобфускации вроде NET.Reflector. Как ни странно, но я не нашел в интернете никакой полезной информации по этому вопросу (возможно, плохо искал) и поэтому пришлось провести маленькое исследования самостоятельно. Под катом краткая заметка о результатах. В примере кода будет снова использоваться Mono.Cecil и генерация кода, так что не забудьте прочитать мою первую статью.
Начнем с теории. Каждый код MSIL инструкции может быть представлен одним или двумя байтами. Например, инстукция nop представляется как 0x00. Таким образом мы имеем 65536 различных вариантов. На текущий момент некоторая часть этого пространства занята валидными кодами MSIL инструкций, однако большая часть свободна. Переход JIT компилятора на инструкцию с некорректным кодом приведет к аварийному завершению приложения. NET.Reflector, натыкаясь на некорректную инструкцию прекращает разбор кода метода и выводит сообщение «Invalid method body», что нам и требуется.
Таким образом, наша цель — вставить в метод некорректную инструкцию, но так, чтобы переход на нее не мог произойти ни при каких условиях. Для этого можно использовать безусловный переход:
Reflector не будет анализировать достижимость кода и при анализе плохой инструкции споткнется и дальше метод анализировать не станет. Приложение же будет работать нормально, так как перехода на плохую инструкцию никогда не произойдет. Дело несколько осложняется тем, что Mono.Cecil не даст нам так просто вставить плохую инструкцию — все валидные коды представлены в виде перечисления и добавить свой стандартными средствами нельзя. Конечно, всегда можно поменять исходники Mono.Cecil, благо система с открытым кодом, но мне хотелось, чтобы работало на стандартной сборке. После получаса анализа исходников Mono.Cecil я нашел, как вставить некорректную инструкцию 0x0024 так, чтобы Mono.Cecil пропустил ее и не выдал исключения. Посмотрим на код:
Рассмотрим результат работы этого метода на некотором методе нашей сборки, который мы хотим защитить от просмотра
Метод до обработки:
![image](http://habrastorage.org/r/w1560/storage/habraeffect/6f/5b/6f5b21103d61c21f9eddca902db90f94.png)
![image](http://habrastorage.org/r/w1560/storage/habraeffect/86/b4/86b486dc3d1012b75f8d2df438d0fbb4.png)
Метод в рефлекторе после обработки:
![image](http://habrastorage.org/r/w1560/storage/habraeffect/09/af/09af00ba10fbce1091e9f52f5e3806a3.png)
![image](http://habrastorage.org/r/w1560/storage/habraeffect/2e/af/2eafc186eb241dc474914c97fcf91f1c.png)
При этом после обработки приложение запускается без проблем. Требуемый результат достигнут.
Напоследок замечу, что такая обфускация очень легко обходится (потребуются изменения сборки), однако способна защитить код от просмотра не очень сведущими разработчиками. Также, на мой взгляд, сама информация, приведенная в заметке весьма интересна.
Начнем с теории. Каждый код MSIL инструкции может быть представлен одним или двумя байтами. Например, инстукция nop представляется как 0x00. Таким образом мы имеем 65536 различных вариантов. На текущий момент некоторая часть этого пространства занята валидными кодами MSIL инструкций, однако большая часть свободна. Переход JIT компилятора на инструкцию с некорректным кодом приведет к аварийному завершению приложения. NET.Reflector, натыкаясь на некорректную инструкцию прекращает разбор кода метода и выводит сообщение «Invalid method body», что нам и требуется.
Таким образом, наша цель — вставить в метод некорректную инструкцию, но так, чтобы переход на нее не мог произойти ни при каких условиях. Для этого можно использовать безусловный переход:
goto MethodCode;
// здесь некорректная инструкция
MethodCode:
// основной код метода
Reflector не будет анализировать достижимость кода и при анализе плохой инструкции споткнется и дальше метод анализировать не станет. Приложение же будет работать нормально, так как перехода на плохую инструкцию никогда не произойдет. Дело несколько осложняется тем, что Mono.Cecil не даст нам так просто вставить плохую инструкцию — все валидные коды представлены в виде перечисления и добавить свой стандартными средствами нельзя. Конечно, всегда можно поменять исходники Mono.Cecil, благо система с открытым кодом, но мне хотелось, чтобы работало на стандартной сборке. После получаса анализа исходников Mono.Cecil я нашел, как вставить некорректную инструкцию 0x0024 так, чтобы Mono.Cecil пропустил ее и не выдал исключения. Посмотрим на код:
static void ProtectMethod(string path, string methodName)
{
var assembly = AssemblyDefinition.ReadAssembly(path);
foreach (var typeDef in assembly.MainModule.Types)
{
foreach (var method in typeDef.Methods)
{
if (method.Name == methodName)
{
var ilProc = method.Body.GetILProcessor();
// здесь получаем internal конструктор для класса OpCode
var constructor = typeof(OpCode).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(int), typeof(int) }, null);
// в Mono.Cecil инструкции создаются оригинальным способом - в конструктор передается два 4х-битных(int) числа, из которых операциями побитового сдвига получается 8 байт, а каждый байт отвечает за определенный параметр MSIL иструкции. Соответственно, такими же операциями побитового сдвига мы превращаем 8 байт в 2 числа. Каждый байт отвечает за определенную характеристику OpCode, но нам важны только первые два. Остальные тоже имеют некоторое значение для нашей задачи, так как если задать их абы как, Mono.Cecil может не допустить такую инструкцию и выкинет Exception, но я не буду останавливаться на подробностях.
int x =
0xff << 0 | //это первый байт IL инстуркции
0x24 << 8 | //это второй байт IL инструкции
0x00 << 16 |
(byte) FlowControl.Next << 24;
// дальнейшее не имеет отношения к нашей цели, однако необходимо для того, чтобы Mono.Cecil корректно обработал нашу инстукцию
int y = (byte) OpCodeType.Primitive << 0 |
(byte) OperandType.InlineNone << 8 |
(byte) StackBehaviour.Pop0 << 16 |
(byte) StackBehaviour.Push0 << 24;
var badOpCode = (OpCode) constructor.Invoke(new object[] {x, y});
// создаем плохую инструкцию
Instruction badInstruction = Instruction.Create(badOpCode);
Instruction firstInstruction = ilProc.Body.Instructions[0];
// вставляем инструкцию безусловного перехода на реальный код метода
ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Br_S, firstInstruction));
// вставляем плохую инструкцию перед началом кода метода
ilProc.InsertBefore(firstInstruction, badInstruction);
}
}
}
assembly.Write(path);
}
Рассмотрим результат работы этого метода на некотором методе нашей сборки, который мы хотим защитить от просмотра
Метод до обработки:
![image](http://habrastorage.org/storage/habraeffect/6f/5b/6f5b21103d61c21f9eddca902db90f94.png)
![image](http://habrastorage.org/storage/habraeffect/86/b4/86b486dc3d1012b75f8d2df438d0fbb4.png)
Метод в рефлекторе после обработки:
![image](http://habrastorage.org/storage/habraeffect/09/af/09af00ba10fbce1091e9f52f5e3806a3.png)
![image](http://habrastorage.org/storage/habraeffect/2e/af/2eafc186eb241dc474914c97fcf91f1c.png)
При этом после обработки приложение запускается без проблем. Требуемый результат достигнут.
Напоследок замечу, что такая обфускация очень легко обходится (потребуются изменения сборки), однако способна защитить код от просмотра не очень сведущими разработчиками. Также, на мой взгляд, сама информация, приведенная в заметке весьма интересна.