
Предупреждение
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, но, об этом в следующий раз.


