Решение .NET crackme от KilLo

Nov. 11, 2023

Решение 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

Начинаем исследование

Crackme main window Приложение состоит из единственного окна, в котором нас просят ввести имя пользователя и лицензионный ключ. При вводе данных (конечно же невалидных) мы получаем сообщение “Invalid license key”. Invalid license key Загрузим образец в dotPeek и посмотрим на внутренности. 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-блоке. По коду мы видим, что:

  1. Имя пользователя ввести необходимо, но оно не участвует в процедурах проверки
  2. У класса 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>());

Из приведенного листинга можно сделать следующие выводы:

  1. Для генерации ключа используется фиксированный алфавит (A-Z 0-9)
  2. “Угадать” ключ теоретически можно, но нужно каким-то образом повлиять на генератор случайных чисел.
  3. Из п.1 следует, что написать keygen к данному crackme - задача весьма нетривиальная.

Какие варианты решения остаются?

Инвазивные методы “лечения”

Инвазивные (предполагающие хирургическое вмешательство) методы решения данной crackme следующие:

  1. Модификация строки if (string.Equals(MainWindow.LicenseKey, MainWindow.InputKey)) в методе проверки ключа таким образом, чтобы проверка для невалидного ключа (вероятность ввода которого намного превышает вероятность угадать валидный ключ) всегда давала true.
  2. Модификация алфавита таким образом, чтобы ключ состоял из повторений одного и того же символа.
  3. Передача в конструктор экземпляра класса Random некоторого seed, которое бы зафиксировало ГСЧ.

Я проверил первые два способа, и расскажу о них ниже.

1-byte patch

Это самый быстрый метод, в котором мы немного изменим инструкции IL-кода таким образом, чтобы в условии перед string.Equals появилось отрицание, или иначе говоря, чтобы любой невалидный ключ приводил к выполнению метода CorrectKey().

Для этого нам понадобится:

  1. IL Disassembler
  2. CFF Explorer

IL to bytes

Сначала нам нужно понять, какие фактические байты, и по какому смещению в исполняемом файле отвечают за инструкции ветвления. Для этого воспользуемя утилитой ILDasm из поставки VisualStudio. Загружаем наш образец в ILDasm и выбираем Файл - Дамп. В открывшемся окне выбираем опции (под опцией “Вывести IL-код”):

  1. Фактические байты
  2. Номера строк ildasm Нажимаем “ОК”, вводим имя результирующего файла, и открываем его любым текстовым редактором. Далее простым поиском находим метод CheckButton_Click. Он будет выглядеть так:
.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. Запустим утилиту, и загрузим в нее наш файл. CFF explorer Далее, нам нужно найти начало метода CheckButton_Click и затем от него найти нужный байт. CFF Explorer Для поиска начала метода вернемся к листингу из ildasm. В самом заголовке метода указывается, что Метод начинается по RVA 0x2498. RVA (relative virtual address) - это относительный виртуальный адрес, который используется в операционных системах Windows для адресации участков памяти. В CFF Explorer мы переходим в Address converter (слева) и в поле RVA вводим значение 2498, после чего нас перекинет на начало байт-кода метода. CFF Explorer Далее, ищем байты из листинга:

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 с клавиатуры, сохраняем файл, запускаем, вводим ключ, и видим, что все прошло успешно. patch ok patch ok

Feel like a Bolshevik

Хотите почуствовать себя большевиком, сократив алфавит? Тогда поехали! Здесь нам снова потребуется CFF Explorer, но на этот раз мы, загрузив файл, пойдем в меню .NET Directory - Meta Data Streams - US. Здесь уже глазами находим комбинацию ABCDE… и правим тут же не отходя от кассы. После этого проверяем результат. Видим, что все прошло успешно, наши рандомно набранные буквы A очень хорошо совпали с A, введенными с клавиатуры. string fix patch ok patch ok

Выводы

Способы, использованные мной для решения этой задачи, достаточно примитивны, гораздо более интересным мне представляется вариант с фиксацией ГСЧ или подменой тела метода RandomString, но, об этом в следующий раз.