Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Предупреждение
Crackme (как, впрочем, следует из названия) это программы, созданные специально для того, чтобы их взломать (понять алгоритмы, заложенные разработчиком, и используемые для проверки введенных пользователем паролей, ключей, серийных номеров). Подходы и методики, использованные в статье, ни в коем случае не должны применяться к программному обеспечению, производителями которого подобные действия не одобрены явно (например, в лицензионных соглашениях, договорах, и т.д.). Материал опубликован исключительно в образовательных целях, и не предназначен для получения нелегального доступа к возможностям программного обеспечения в обход механизмов защиты, установленных производителем.
Введение
Решение crackme это (время от времени) достаточно увлекательное занятие, позволяющее взглянуть на некоторые вещи под непривычным углом. В этой статье я расскажу о том, как можно патчить скомпилированные .NET-приложения не прибегая к перекомпиляции.
Автор crackme говорит, что ключ (понимание алгоритма генерации которого обычно, вместе с написанием генератора валидных ключей, и является решением) случайным образом генерируется при старте приложения, и наша цель заключается в том, чтобы получить пропатченую версию, принимающую любой ключ.
Welcome to KilLo's CrackMe!
The key is always randomly generated on startup, you can hold shift to dump the key to a TXT file, but that's kind of cheating...
This is a challenge program made for you to crack.
The goal is to make a "Cracked" version of this program that always allows access no matter the license key, or you can make a keygen if you know how.
Created by KilLo
youtube.com/KilLo445
Начинаем исследование
Приложение состоит из единственного окна, в котором нас просят ввести имя пользователя и лицензионный ключ. При вводе данных (конечно же не валидных) мы получаем сообщение "Invalid license key".
Загрузим образец в dotPeek и посмотрим на внутренности.
В Assembly explorer находим класс MainWindows, и по именам методов понимаем, что кнопка, отвечающая за проверку корректности введенного ключа называется CheckButton, а обработчик нажатия на кнопку — CheckButton_Click. Код этого метода приведен ниже.
MainWindow.InputUsername = this.UsernameInput.Text;
MainWindow.InputKey = this.KeyInput.Text;
if (MainWindow.InputUsername == null || MainWindow.InputUsername == "")
{
int num1 = (int) MessageBox.Show("Please enter a username.", "", MessageBoxButton.OK, MessageBoxImage.Hand);
}
else if (MainWindow.InputKey == null || MainWindow.InputKey == "")
{
int num2 = (int) MessageBox.Show("Please enter a license key.", "", MessageBoxButton.OK, MessageBoxImage.Hand);
}
else
{
if (MainWindow.InputUsername != null)
MainWindow.InputKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(MainWindow.InputKey));
if (string.Equals(MainWindow.LicenseKey, MainWindow.InputKey))
this.CorrectKey();
else
this.IncorrectKey();
}
Вся логика проверки корректности ключа находится в последнем else-блоке глобального if'а. По коду мы видим, что:
- Имя пользователя ввести необходимо, но оно не участвует в процедурах проверки;
- У класса MainWindow есть поле LicenseKey, с которым сравнивается наш введенный ключ InputKey.
Посмотрим на то, как генерируется LicenseKey. В конструкторе класса присутствуют следующие строки:
MainWindow.LicenseKey = MainWindow.RandomString(5) + "-" + MainWindow.RandomString(5) + "-" + MainWindow.RandomString(5) + "-" + MainWindow.RandomString(5) + "-" + MainWindow.RandomString(5);
MainWindow.LicenseKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(MainWindow.LicenseKey));
То есть, ключ составляется из пяти результатов работы метода RandomString, по 5 символов в блоке, и имеет вид AAAAA-AAAAA-AAAAA-AAAAA-AAAAA.
Метод RandomString имеет следующий вид:
public static string RandomString(int length) => new string(Enumerable.Repeat<string>("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", length).Select<string, char>((Func<string, char>) (s => s[MainWindow.random.Next(s.Length)])).ToArray<char>());
Из приведенного листинга можно сделать следующие выводы:
- Для генерации ключа используется фиксированный алфавит (A-Z 0-9).
- "Угадать" ключ нельзя. Теоретически, без манипуляций с состоянием загруженного приложения сгенерированный ключ не извлечь.
- Из п.1 и п.2 следует, что написать keygen к данному crackme — задача весьма нетривиальная.
Какие варианты решения остаются?
Инвазивные методы "лечения"
Инвазивные (предполагающие хирургическое вмешательство) методы решения crackme следующие:
- Модификация строки if (string.Equals(MainWindow.LicenseKey, MainWindow.InputKey)) в методе проверки ключа таким образом, чтобы проверка для не валидного ключа (вероятность ввода которого намного превышает вероятность угадать валидный ключ) всегда давала true.
- Модификация алфавита таким образом, чтобы ключ состоял из повторений одного и того же символа.
Я проверил эти два способа, и расскажу о них ниже.
1-byte patch
Это самый быстрый метод, в котором мы немного изменим инструкции IL-кода таким образом, чтобы в условии перед string.Equals появилось отрицание, или иначе говоря, чтобы любой не валидный ключ приводил к выполнению метода CorrectKey().
Для этого нам понадобится:
- IL Disassembler
- CFF Explorer
IL to bytes
Сначала нам нужно понять, какие фактические байты, и по какому смещению в исполняемом файле отвечают за инструкции ветвления. Для этого воспользуемся утилитой ILDasm из поставки VisualStudio. Загружаем наш образец в ILDasm и выбираем Файл — Дамп. В открывшемся окне выбираем опции (под опцией "Вывести IL-код"):
- Фактические байты
- Номера строк
Нажимаем "ОК", вводим имя результирующего файла, и открываем его любым текстовым редактором. Далее простым поиском находим метод CheckButton_Click, и посмотрим на его IL-код.
.method private hidebysig instance void
CheckButton_Click(object sender,
class [PresentationCore]System.Windows.RoutedEventArgs e) cil managed
// SIG: 20 02 01 1C 12 55
{
// Метод начинается по RVA 0x2498
// Размер кода: 185 (0xb9)
.maxstack 4
.locals init (string V_0)
IL_0000: /* 02 | */ ldarg.0
IL_0001: /* 7B | (04)000012 */ ldfld class [PresentationFramework]System.Windows.Controls.TextBox KilLo_s_CrackMe.MainWindow::UsernameInput
IL_0006: /* 6F | (0A)00003D */ callvirt instance string [PresentationFramework]System.Windows.Controls.TextBox::get_Text()
IL_000b: /* 80 | (04)00000E */ stsfld string KilLo_s_CrackMe.MainWindow::InputUsername
IL_0010: /* 02 | */ ldarg.0
IL_0011: /* 7B | (04)000013 */ ldfld class [PresentationFramework]System.Windows.Controls.TextBox KilLo_s_CrackMe.MainWindow::KeyInput
IL_0016: /* 6F | (0A)00003D */ callvirt instance string [PresentationFramework]System.Windows.Controls.TextBox::get_Text()
IL_001b: /* 80 | (04)00000D */ stsfld string KilLo_s_CrackMe.MainWindow::InputKey
IL_0020: /* 7E | (04)00000E */ ldsfld string KilLo_s_CrackMe.MainWindow::InputUsername
IL_0025: /* 2C | 11 */ brfalse.s IL_0038
IL_0027: /* 7E | (04)00000E */ ldsfld string KilLo_s_CrackMe.MainWindow::InputUsername
IL_002c: /* 72 | (70)0003C0 */ ldstr ""
IL_0031: /* 28 | (0A)00003E */ call bool [mscorlib]System.String::op_Equality(string,
string)
IL_0036: /* 2C | 14 */ brfalse.s IL_004c
IL_0038: /* 72 | (70)0003EC */ ldstr "Please enter a username."
IL_003d: /* 72 | (70)0003C0 */ ldstr ""
IL_0042: /* 16 | */ ldc.i4.0
IL_0043: /* 1F | 10 */ ldc.i4.s 16
IL_0045: /* 28 | (0A)000035 */ call valuetype [PresentationFramework]System.Windows.MessageBoxResult [PresentationFramework]System.Windows.MessageBox::Show(string,
string,
valuetype [PresentationFramework]System.Windows.MessageBoxButton,
valuetype [PresentationFramework]System.Windows.MessageBoxImage)
IL_004a: /* 26 | */ pop
IL_004b: /* 2A | */ ret
IL_004c: /* 7E | (04)00000D */ ldsfld string KilLo_s_CrackMe.MainWindow::InputKey
IL_0051: /* 2C | 11 */ brfalse.s IL_0064
IL_0053: /* 7E | (04)00000D */ ldsfld string KilLo_s_CrackMe.MainWindow::InputKey
IL_0058: /* 72 | (70)0003C0 */ ldstr ""
IL_005d: /* 28 | (0A)00003E */ call bool [mscorlib]System.String::op_Equality(string,
string)
IL_0062: /* 2C | 14 */ brfalse.s IL_0078
IL_0064: /* 72 | (70)00041E */ ldstr "Please enter a license key."
IL_0069: /* 72 | (70)0003C0 */ ldstr ""
IL_006e: /* 16 | */ ldc.i4.0
IL_006f: /* 1F | 10 */ ldc.i4.s 16
IL_0071: /* 28 | (0A)000035 */ call valuetype [PresentationFramework]System.Windows.MessageBoxResult [PresentationFramework]System.Windows.MessageBox::Show(string,
string,
valuetype [PresentationFramework]System.Windows.MessageBoxButton,
valuetype [PresentationFramework]System.Windows.MessageBoxImage)
IL_0076: /* 26 | */ pop
IL_0077: /* 2A | */ ret
IL_0078: /* 7E | (04)00000E */ ldsfld string KilLo_s_CrackMe.MainWindow::InputUsername
IL_007d: /* 2C | 1B */ brfalse.s IL_009a
IL_007f: /* 28 | (0A)000016 */ call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8()
IL_0084: /* 7E | (04)00000D */ ldsfld string KilLo_s_CrackMe.MainWindow::InputKey
IL_0089: /* 6F | (0A)000032 */ callvirt instance uint8[] [mscorlib]System.Text.Encoding::GetBytes(string)
IL_008e: /* 28 | (0A)000033 */ call string [mscorlib]System.Convert::ToBase64String(uint8[])
IL_0093: /* 0A | */ stloc.0
IL_0094: /* 06 | */ ldloc.0
IL_0095: /* 80 | (04)00000D */ stsfld string KilLo_s_CrackMe.MainWindow::InputKey
IL_009a: /* 7E | (04)00000A */ ldsfld string KilLo_s_CrackMe.MainWindow::LicenseKey
IL_009f: /* 7E | (04)00000D */ ldsfld string KilLo_s_CrackMe.MainWindow::InputKey
IL_00a4: /* 28 | (0A)00001B */ call bool [mscorlib]System.String::Equals(string,
string)
IL_00a9: /* 2C | 07 */ brfalse.s IL_00b2
IL_00ab: /* 02 | */ ldarg.0
IL_00ac: /* 28 | (06)000011 */ call instance void KilLo_s_CrackMe.MainWindow::CorrectKey()
IL_00b1: /* 2A | */ ret
IL_00b2: /* 02 | */ ldarg.0
IL_00b3: /* 28 | (06)000012 */ call instance void KilLo_s_CrackMe.MainWindow::IncorrectKey()
IL_00b8: /* 2A | */ ret
} // end of method MainWindow::CheckButton_Click
Здесь нас интересуют следующие строки, отвечающие на if-else ветвление в самом конце тела метода:
IL_00a4: /* 28 | (0A)00001B */ call bool [mscorlib]System.String::Equals(string,
string)
IL_00a9: /* 2C | 07 */ brfalse.s IL_00b2
Инструкция call вызывает метод string.Equals, а brfalse.s, в случае если результат логической операции равен false перекидывает нас на 7 байт исполняемого кода вперед (на IncorrectKey).
В MSDN можно найти, что для инструкции brfalse.s есть противоположная инструкция brtrue.s с опкодом 2D. То есть фактически для изменения поведения нам нужно найти в exe-файле байт 2C и поменять его на 2D.
CFF Explorer
В поиске и замене байта опкода нам поможет утилита под названием CFF Explorer. Запустим утилиту, и загрузим в нее наш файл.
Далее, нам нужно найти начало метода CheckButton_Click и затем от него найти нужный байт.
Для поиска начала метода вернемся к листингу из ildasm. В самом заголовке метода указывается, что Метод начинается по RVA 0x2498. RVA (relative virtual address) — это относительный виртуальный адрес, который используется в операционных системах Windows для адресации участков памяти. В CFF Explorer мы переходим в Address converter (слева) и в поле RVA вводим значение 2498, после чего нас перекинет на начало байт-кода метода.
Далее, ищем байты из листинга:
IL_00a9: /* 2C | 07 */ brfalse.s IL_00b2
IL_00ab: /* 02 | */ ldarg.0
IL_00ac: /* 28 | (06)000011 */ call instance void KilLo_s_CrackMe.MainWindow::CorrectKey()
Нас интересует последовательность 2C 07 02 28. Она расположена по смещению 740. Теперь, остается сделать одно — исправить 2C на 2D. В этом же hex-редакторе исправляем байт, введя букву d с клавиатуры, сохраняем файл, запускаем, вводим ключ, и видим, что все прошло успешно.
Feel like a Bolshevik
Хотите почувствовать себя большевиком, сократив алфавит? Тогда поехали! Здесь нам снова потребуется CFF Explorer, но на этот раз мы, загрузив файл, пойдем в меню .NET Directory — Meta Data Streams — US. Здесь уже глазами находим комбинацию ABCDE... и правим тут же не отходя от кассы. После этого проверяем результат. Видим, что все прошло успешно, наши рандомно набранные буквы A очень хорошо совпали с A, введенными с клавиатуры.
Выводы
Способы, использованные мной для решения этой задачи, достаточно примитивны, гораздо более интересным мне представляется вариант с фиксацией ГСЧ или подменой тела метода RandomString, но, об этом в следующий раз.