Эта статья продолжает повествование о методах циклической разработки.
Автор: Суфиан Тахири (Soufiane Tahiri)
Перед изучением материала этой статьи настоятельно рекомендую вам ознакомиться с предыдущими статьями. Я не буду заново объяснять некоторые методы и повторно описывать утилиты. Будем считать, что бы знакомы с основами и изучили все, о чем я говорил в предыдущих четырех статьях этого цикла:
Краткий обзор
Эта статья продолжает повествование о методах циклической разработки. Здесь мы более углубленно рассмотрим ассемблерный язык IL, используемый для исследования необфусцированных (на данный момент) NET-сборок и модулей (Управляемые NET-приложения называются сборками, а управляемые исполняемые файлы NET называются модулями; управляемое NET-приложение может быть сборкой с одним или несколькими модулями).
В предыдущей статье было рассказано о том, что дизассемблирование и реассемблирование называется циклической разработкой. В нашем случае циклическая разработка касается управляемых исполняемых файлов и подразделяется на два важных шага:
Однако простое дизассемблирование и реассемблирование не так интересно, если вы не изменяете исходный код 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 предоставляет свойства для управления компонентами компьютера. Иерархия наследования выглядит так:
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-код можно разделить на несколько блоков:
Теперь, когда мы знаем логику работы метода, нам нужно использовать некоторые методы циклической разработки для изменения логики работы нашего 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 перед сравнением серийных номеров.
Теперь серийный номер находится в стеке, но не в виде строки, а виде объекта, и мы не можем вывести его в окне сообщения. Перед этим нам необходимо извлечь строковое значение из объекта серийного номера; в .NET Frameworks есть метод RuntimeHelpers.GetObjectValue, который возвращает упакованную копию объекта. При вызове метода нужно указывать полную сигнатуру:
После выполнения последнего метода в стеке содержится две строки: “The serial number is:” и серийный номер. Теперь нам нужно соединить обе строки вместе.
Для этой цели в Microsoft MSDN существует метод String.Concat (object,…)(для вызова необходимо также указывать полную сигнатуру):
Мы почти закончили. Осталось изменить метку перехода в инструкции brfalse.s.
Вместо перехода к метке IL_0032 Crack Me будет переходить к метке 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».
Ссылки: