Сегодня я задумался о том, как обфускаторы скрывают код методов от утилит деобфускации вроде NET.Reflector. Как ни странно, но я не нашел в интернете никакой полезной информации по этому вопросу (возможно, плохо искал) и поэтому пришлось провести маленькое исследования самостоятельно. Под катом краткая заметка о результатах. В примере кода будет снова использоваться Mono.Cecil и генерация кода, так что не забудьте прочитать статью 1, статью 2, статью 3.
Начнем с теории. Каждый код MSIL инструкции в памяти представляет собой 1 или 2 байта. Например, инстукция nop представляется как 0x00. Таким образом мы имеем 256*256 различных вариантов. На текущий момент некоторая часть этого пространства занята валидными кодами 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});
Read more: gotDotNet
Начнем с теории. Каждый код MSIL инструкции в памяти представляет собой 1 или 2 байта. Например, инстукция nop представляется как 0x00. Таким образом мы имеем 256*256 различных вариантов. На текущий момент некоторая часть этого пространства занята валидными кодами 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});
Read more: gotDotNet
1 comments:
Всем привет, Это просто бесподобное сообщение ;)
Post a Comment