Реверс-инжиниринг NET-приложений. Часть пятая: Продвинутые методы циклической разработки

Реверс-инжиниринг NET-приложений. Часть пятая: Продвинутые методы циклической разработки

Эта статья продолжает повествование о методах циклической разработки.

Автор: Суфиан Тахири (Soufiane Tahiri)

Перед изучением материала этой статьи настоятельно рекомендую вам ознакомиться с предыдущими статьями. Я не буду заново объяснять некоторые методы и повторно описывать утилиты. Будем считать, что бы знакомы с основами и изучили все, о чем я говорил в предыдущих четырех статьях этого цикла:

Краткий обзор

Эта статья продолжает повествование о методах циклической разработки. Здесь мы более углубленно рассмотрим ассемблерный язык IL, используемый для исследования необфусцированных (на данный момент) NET-сборок и модулей (Управляемые NET-приложения называются сборками, а управляемые исполняемые файлы NET называются модулями; управляемое NET-приложение может быть сборкой с одним или несколькими модулями).

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

  1. Дизассемблирование PE-файла при помощи ILDASM в .il файл (который содержит исходный текст ILAsm) и управляемые и неуправляемые файлы ресурсов.
  2. Реассемблирование измененных файлов из предыдущего шага в новый PE-файл, используя компилятор ILASM.

Однако простое дизассемблирование и реассемблирование не так интересно, если вы не изменяете исходный код ILAsm перед созданием новой сборки. Циклическая разработка (как и большинство техник реверс-инжиниринга) – это не просто «взлом защиты приложения», однако в следующих параграфах мы будем затрагивать именно эту тему (которую начали рассматривать в предыдущей статье).

Последнее, что мы сделали - удалили всплывающее окно, однако осталась еще одна защита – проверка серийного номера. Эту защиту мы будем исследовать в следующих главах.

Анализ механизма проверки серийного номера

Вновь возвращаемся в ILDASM и смотрим структуру дерева:

Каждый узел представляет собой пространство имен, внутри которого находятся объекты классов. Разворачивая каждый такой объект, мы может видеть свойства и методы класса. В данный момент нас интересует класс «GenSerial», содержащий очень интересный метод:

IL-код метода CalculSerial:
.method public instance object CalculSerial() cil managed
{
// Code size 43 (0x2b)
.maxstack 4
.locals init (object V_0)
IL_0000: ldarg.0
IL_0001: call class CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyComputer CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyProject::get_Computer()
IL_0006: callvirt instance class [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ComputerInfo [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ServerComputer::get_Info()
IL_000b: callvirt instance string [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ComputerInfo::get_OSVersion()
IL_0010: ldstr “.”
IL_0015: ldstr “”
IL_001a: callvirt instance string [mscorlib]System.String::Replace(string,string)
IL_001f: stfld stringCrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::SerialNumber
IL_0024: ldarg.0
IL_0025: ldfld stringCrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::SerialNumber
IL_002a: ret
} // end of method GenSerial::CalculSerial

Метод CalculSerial чуть более сложен и нуждается в некоторых пояснениях:

.method public instance object CalculSerial() cil managed: Как было сказано в предыдущей статье, это элемент метаданных MethodDef. Единственное отличие в том, что тип возвращаемого значения - object.

.maxstack: директива, определяющая максимальное число элементов, которое может находиться в стеке вычислений во время выполнения метода.

.locals init (object V_0): поскольку в любом языке программирования переменные очень важны, в ILAsm существует свой особый способ их объявления; .locals init (object V_0) объявляет локальную переменную V_0 типа object. Служебное слово init заставляет JIT-компилятор обнулить все локальные переменные перед началом выполнения метода.

ldarg.0 инструкция, которая загружает в стек аргумент 0. У методов экземпляра класса (подобно нашему) присутствует ссылка «this» (используемая в языках высокого уровня) на экземпляр, передаваемый первым аргументом в сигнатуре метода. Таким образом, в данном случае ldarg.0 загружает в стек указатель на экземпляр.

call class CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyComputer

CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyProject::get_Computer(), MyComputer наследуется от базового класса, который определен в пространстве имен Microsoft.VisualBasic. Call вызывает статический метод get_Computer(), который принадлежит классу MyProject пространства имен «My», и возвращает экземпляр CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyComputer.

Свойство «get_Computer» (подобно геттерам и сеттерам в большинстве языков высокого уровня) будет иметь доступ к экземпляру объекта Microsoft.VisualBasic.Devices.Computer (подробное объяснение деталей увеличит эту статью в несколько раз).

callvirt instance class [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ComputerInfo [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ServerComputer::get_Info() вызывает виртуальный метод/свойство (геттер) get_Info(), а служебное слово instance означает, что мы используем методы экземпляра (вместо статических). В Microsoft MSDN указано, что класс ComputerInfo предоставляет информацию о памяти компьютера, загруженных сборках, имени и операционной системе. Класс ServerComputer предоставляет свойства для управления компонентами компьютера. Иерархия наследования выглядит так:

  • System.Object
    • Microsoft.VisualBasic.Devices.ServerComputer
      • Microsoft.VisualBasic.Devices.Computer

callvirt instance string [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ComputerInfo::get_OSVersion() вызывает виртуальный метод get_OSVersion() класса ComputerInfo, а затем результирующая строка помещается в стек вычислений (как можно догадаться из имени метода, метод возвращает версию операционной системы).

Инструкции ldstr “.” и ldstr “” создают объекты строк «.» и «» (пустая строка), а затем помещают ссылки на эти объекты в стек вычислений.

Call instance string [mscorlib]System.String::Replace(string, string): инструкция вызывает функцию Replace(string,string) из библиотеки классов NET Frameworks. Параметры / аргументы для метода берутся из стека вычислений («.» и «»), а затем результат помещается обратно стек. В данном случае берется версия компьютера, удаляются все точки, и результат помещается в стек.

stfld string CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::SerialNumber: инструкция устанавливает результат работы функции Replace(string, string) в поле SerialNumber, объявленное ранее как private (что эквивалентно private-переменным в языках высокого уровня).

ldarg.0 загружает ссылку объекта (эквивалент this в языках высокого уровня) и далее, используя ldfld string CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::SerialNumber, получает ссылку на экземпляр из стека и загружает значение поля SerialNumber в стек.

Инструкция ret, как вы догадались, выполняет возврат из метода. Поскольку наш метод возвращает тип object, в стеке вычислений вызываемого метода есть одно значение типа object.

На данный момент нам известно, что поле SerialNumber содержит версию операционной системы без точек. Мы легко можем узнать полную версию ОС (в моем случае 6.1.7601.65536). Удалив все точки, я получаю правильный серийный номер 61760165536.

Все это, конечно, интересно, но теперь давайте займемся более серьезными вещами, поскольку до этого мы не изменили ни строчки кода и не использовали ILASM! Наша задача – изменить Crack Me так, чтобы вместо сообщения об ошибке он показывал правильный серийный номер.

Вернемся в ILDASM и развернем вторую ветку:

Из полученного списка методов класса Form1 мы легко находим тот метод, который вызывается при нажатии на кнопку «Register»:

Надеюсь, что IL-код метода reg_Btn_Click вам понятен, и я не буду построчно объяснять алгоритм его работы:

.method private instance void reg_Btn_Click(object sender,class [mscorlib]System.EventArgs e) cil managed
{
// Code size 69 (0×45)
.maxstack 3 IL_0000: ldarg.0
IL_0001: callvirt instance class [System.Windows.Forms]System.Windows.Forms.TextBox CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::get_txt_Serial()
IL_0006: callvirt instance string [System.Windows.Forms]System.Windows.Forms.TextBox::get_Text()
IL_000b: ldarg.0
IL_000c: ldfld class CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::cGenSerial
IL_0011: callvirt instance object CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::CalculSerial()
IL_0016: ldc.i4.0
IL_0017: call bool [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::ConditionalCompareObjectEqual

(object, object, bool)
IL_001c: brfalse.s IL_0032 IL_001e: ldstr “Serial is correct!”
IL_0023: ldc.i4.s 64

IL_0025: ldstr “Congratulation”
IL_002a: call valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult
[Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object, valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle, object)
IL_002f: pop
IL_0030: br.s IL_0044 IL_0032: ldstr “Wrong serial number.”
IL_0037: ldc.i4.s 16
IL_0039: ldstr “Error”
IL_003e: call valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object, valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle, object)
IL_0043: pop
IL_0044: ret
} // end of method Form1::reg_Btn_Click

Если вы ознакомитесь с предыдущими статьями этого цикла, то поймете алгоритм работы метода. Кстати, обратите внимание на System.EventArgs. Данная конструкция означает вызов метода по событию (в нашем случае - нажатие на кнопку формы). Кликнув на кнопку «Register», мы вызываем метод reg_Btn_Click(…).

Весь IL-код можно разделить на несколько блоков:

  1. Во-первых, Crack Me, используя метод get_text(), который считывает введенный нами текст (серийный номер).
  2. Затем вызывается экземпляр класса, который вычисляет правильный серийный номер при помощи метода CalculSerial().
  3. В данный момент в стеке вычислений находятся три элемента (как указано в директиве .maxstack 3). Первый элемент (объект) - серийный номер, введенный пользователем и считанный методом get_text(). Второй элемент (объект) – правильный серийный номер, вычисленный методом CalculSerial(). Третий элемент – 4-х байтовый ноль, загруженный инструкцией ldc.i4.0. Последний элемент необходим для корректного использования метода ConditionalCompareObjectEqual(…), который представляет перегруженный оператор равенства (=) Visual Basic и возвращает значение типа Boolean. Загрузка нуля в стек вычислений как параметра TextCompare означает, что сравнение не чувствительно к регистру (хотя в данном случае это не столь важно, поскольку серийный номер – набор цифр).
  4. Если сравнение прошло неудачно, это означает, что серийный номер введен неверно. В этом случае после инструкции brfalse.s происходит переход к строке IL_0032, где загружается ссылка на строку «Wrong serial number» и создается окно сообщения, после чего происходит очищение стека вычислений инструкцией pop. Если же сравнение прошло успешно, происходит все то же самое, но выводится строка «Serial correct!», очищается стек вычислений, и далее происходит безусловная передача управления (инструкция br.s IL_0044) инструкции ret.

Теперь, когда мы знаем логику работы метода, нам нужно использовать некоторые методы циклической разработки для изменения логики работы нашего Crack ME, чтобы он показывал правильный серийный номер вместо сообщения об ошибке. Попросту говоря, нам нужно изменить следующие участки кода:

А также:

Теперь нужно решить, какой код мы хотим внедрить в Crack Me. Очевидно, нам нужно вызвать функцию, которая вычисляет и возвращает правильный серийный номер, удалить сообщение «Wrong serial number» и показать правильный серийный номер.

Рассмотрим, как должен выглядеть стек вычислений после изменения кода:

0

“The serial number is:”

1

#SERIAL#

2

Message box style

3

Message box title

Первое, на что следует обратить внимание – в стеке находится четыре элемента, однако директива .maxstack разрешает JIT-компилятору зарезервировать только три места (для этого метода). Таким образом, первое, что мы сделаем – изменим значение директивы .maxstack:

1. .maxstack 4

Следующий шаг – удаление строки IL_0032: ldstr “Wrong serial number” и подготовка строки, которую мы хотим загрузить. Использование меток необязательно, однако нам нужно использовать как минимум одну, чтобы пометить строку, откуда мы начнем вносить изменения:

1. IL_Patch: ldstr “The serial number is: “

Далее мы должны вызвать экземпляр класса, содержащего функцию вычисления правильного серийного номера. В данном случае мы просто позаимствуем те инструкции, которые использует Crack Me перед сравнением серийных номеров.

  1. IL_Patch0: ldarg.0
  2. IL_Patch1: ldfld class CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::cGenSerial
  3. IL_Patch2: callvirt instance object CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::CalculSerial()

Теперь серийный номер находится в стеке, но не в виде строки, а виде объекта, и мы не можем вывести его в окне сообщения. Перед этим нам необходимо извлечь строковое значение из объекта серийного номера; в .NET Frameworks есть метод RuntimeHelpers.GetObjectValue, который возвращает упакованную копию объекта. При вызове метода нужно указывать полную сигнатуру:

  1. IL_Patch3: call object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)

После выполнения последнего метода в стеке содержится две строки: “The serial number is:” и серийный номер. Теперь нам нужно соединить обе строки вместе.

Для этой цели в Microsoft MSDN существует метод String.Concat (object,…)(для вызова необходимо также указывать полную сигнатуру):

  1. IL_Patch4: call string [mscorlib]System.String::Concat(object, object)

Мы почти закончили. Осталось изменить метку перехода в инструкции brfalse.s.

Вместо перехода к метке IL_0032 Crack Me будет переходить к метке IL_Patch:

  1. IL_001c: brfalse.s IL_Patch

Теперь все сделано! После всех изменений измененный код должен выглядеть так:

Сохраните .il файл, а затем выполните реассемблирование утилитой ILASM для тестирования изменений.

C:\Windows\Microsoft.NET\Framework\v4.0.30319>ilasm C:\Users\Soufiane\Desktop\round-t\crackme3.il -res=C:\Users\Soufiane\Desktop\round-t\crackme3.res

Если компиляция прошла успешно, появится следующее сообщение:

Resolving local member refs: 0 -> 0 defs, 0 refs, 0 unresolved
Writing PE file
Operation completed successfully

Результат работы Crack ME:

Мы можем также слегка изменить внешний вид окна сообщения (заголовок и стиль):

Еще одна доработка - и теперь Crack Me записывает правильный серийный номер в текстовое поле (вместо вывода окна с сообщением):

IL_Patch0:
ldarg.0
callvirt instance class [System.Windows.Forms]System.Windows.Forms.TextBox CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::get_txt_Serial()
ldarg.0
ldfld class CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::cGenSerial
callvirt instance object CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::CalculSerial()
call string [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Conversions::ToString(object)
callvirt instance void [System.Windows.Forms]System.Windows.Forms.TextBox::set_Text(string)
IL_0044:
ret

Результат:

После первого клика на кнопку «Register» Crack Me записывает серийный номер в текстовое поле, после второго клика выводит сообщение «Serial is correct».

Ссылки:

Большой брат следит за вами, но мы знаем, как остановить его

Подпишитесь на наш канал!