Предупреждение


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

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


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-блоке глобального if'а. По коду мы видим, что:


  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 и п.2 следует, что написать keygen к данному crackme — задача весьма нетривиальная.

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


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


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


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

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


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, и посмотрим на его 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. Запустим утилиту, и загрузим в нее наш файл.


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


Комментарии (3)


  1. gdt
    24.11.2023 11:30
    +4

    https://github.com/dnSpyEx/dnSpy вот такое не пробовали использовать?


  1. EzYCaT
    24.11.2023 11:30
    +2

    1. "Угадать" ключ нельзя. Теоретически, без манипуляций с состоянием загруженного приложения сгенерированный ключ не извлечь.

    С Shift запустите, как написано в ридми - сгенерированный ключ сдампится в файл key.txt.


    1. Dmitry89 Автор
      24.11.2023 11:30

      Это против правил)