Исследование защиты ArtMoney. Часть первая

в 20:00, , рубрики: artmoney, Delphi, interactive delphi reconstructor, reverse engineering, кейгеннинг, реверс-инжиниринг, халявщикам, метки: , , , ,

Приветствую! Сам ArtMoney был закейгенен мной давным-давно. Я не первый раз уже пробую начать писать статью о том, как происходил кейгенинг этой программы, но, всегда где-то стопорился. На этот раз, я решил доделать все до конца! Плюс, эту статью можно считать продолжением цикла статей о крякинге для новичков.

Итак, в этой статье вы узнаете, как я писал кейген к ArtMoney (здесь будет описана версия 7.45.1).

Этап первый: Анализ исполняемого файла

Я установил себе английскую версию программы, чтобы IDA и прочие утилиты нормально искали текст.

Первым делом, нужно выяснить, на чем написана/чем упакована AM. Откроем ее (файлик am745.exe) в моем любимом ExeInfo PE:

Исследование защиты ArtMoney. Часть первая - 1

Нам говорят, что это Aspack v2.24 — 2.34. Что ж, вроде бы не сложный упаковщик. Я сниму его первым автоматическим распаковщиком (ибо статья не о распаковке).

Этап второй: Снова анализы

Снова смотрим в ExeInfo PE, и видим, что программа написана на Borland Delphi. Прекрасно! Воспользуемся супер-программой для анализа Delphi-программ: IDR (Interactive Delphi Reconstructor). Кстати, также у неё есть открытые исходники. Версию IDE там и определим.

Скачаем все доступные базы, и положим в каталог с IDR. Затаскиваем в реконструктор нашего распакованного подопытного, ждем окончания анализа.

Весь процесс исследования я буду производить в IDA Pro (плюс Hex Rays), поэтому давайте сгенерим IDC-скрипт, который скажет ей обо всех именах форм, методов, классов и т.п. Жмем Tools -> IDC Generator в меню IDR. Ждем, пока создается скрипт.

Далее, откроем IDA, и тоже затолкаем в нее нашу программу. Дождемся окончания анализа. Затем применим сгенеренный IDC-скрипт: в IDA Pro жмем File -> Script File..., выбираем скрипт. Ждем применения.

Для того, чтобы IDA могла нормально декомпилить Delphi код, ей необходимо сказать, что мы имеем дело с Delphi компилятором, и __fastcall вызовами. К сожалению, сгенерированный IDC, как и сама IDA об этом ничего не говорят/не знают.

Переходим в настройки компилятора IDA: Options -> Compiler...:

Исследование защиты ArtMoney. Часть первая - 2

Далее жмём переанализировать программу: Options->General -> Analysis -> Reanalyze program и OK.

Ещё не всё...:) IDC-скрипт почему-то не разметил границы библиотечных функций. Из-за чего процесс дизассемблирования и декомпиляции не становится проще. Придётся размечать, и указывать типы (клавиша Y). Берём call на известную функцию и смотрим на её границы. Если функция начинается не там, куда ведёт call, исправляем. Переходим на инструкцию, что находится выше начала функции (чаще всего, это retn), и жмём там E (указать адрес конца функции). Так-то лучше.

Теперь нужно указать тип функции. Возьмём для примера @LStrClr. Заботливый IDR указал в комментарии, что данная функция принимает один аргумент — адрес строки, значит обозначим прототип функции как (помним, что у Delphi конвенция вызовов fastcall):

void __fastcall LStrClr(char*)

Надеюсь, понятно почему именно так. Ну а void потому, что procedure.

И так повторяем с многими и многими функциями… При указании прототипов пользуемся такими несложными правилами:

  • Аргументы передаются через eax, edx, ecx, стэк (слева направо). Это поможет, если декомпилятор натыкается на "positive sp value";
  • У всяких StrCatN функций, если указан ArgCnt: Integer, то это vararg функции, и прототип будет содержать .... В декомпиляторе нужно становиться на каждом месте вызова таких функций, и жать +/- на Numpad-клавиатуре, добавляя/удаляя аргументы. Количество можно посмотреть в дизазм-листинге. Пример прототипа: "void LStrCatN(char *, char *, ...)";
  • Если функция, судя по Delphi-прототипу, возвращает указатель на строку, то, чаще всего, это неявный выходной аргумент в прототипе, и он должен быть добавлен как последний аргумент тоже.

Ну, теперь можно приступать к поиску процедуры регистрации…

Этап три: Где ты моя, ненаглядная, где?

Переходим в IDR к формам, и, (скажу сразу, открываем форму Form28), переключаем на визуальный просмотр. Видим окошко регистрации. Прокрутим окно и найдем три контура кнопок. Левая из них — кнопка ОК, применение регистрации:

Исследование защиты ArtMoney. Часть первая - 3

Этап четыре: Анализ OnClick(). Поверхность

Жмем ПКМ по ней и переходим в обработчик OKClick. В IDA перейдем на адрес начала этой процедуры (кнопка G).
Первым делом укажем, что это bp-based функция, а то локальные переменные не распознаются. Жмём Alt+P (или ПКМ по функции, Edit function...), и ставим галку BP-based frame.

Попробуем декомпилировать… Если всё прошло успешно, увидим ужасный псевдокод декомпилера, иначе — фиксим прототипы:

Исследование защиты ArtMoney. Часть первая - 4

Жмём Y на каждом вызове библиотечной/самописной функции, и следим, чтобы прототипы были с __fastcall, и аргументы были правильно заданы.

Первый цикл, судя по IDR — это проверка ключа на принадлежность символов алфавиту, и склейка их в глобальную переменную. Обзовём её g_LicKey.
Далее идёт вызов неизвестной пока функции, и, судя по всему, это собственно сама функция проверки ключа…

Этап пять: Проверка ключа (вид сбоку)

Данная функция принимает два аргумента: первый — это out параметр, он будет содержать какой-то код ошибки, а второй — буква ('A' — в английской версии, либо 'B' — в русской).

Декомпилируем…

Код довольно большой, согласен, но, если подходить грамотно, не спеша, и не шарахаясь от обилия кода, можно успешно разобрать, что же у нас тут происходит.

Очень часто вы можете встретить подобный код, который выдал декомпилятор (имена переменных, понятно, могут отличаться):

  v2 = g_LicKey;
  if ( g_LicKey )
    v2 = (char *)*((_DWORD *)g_LicKey - 1);

Здесь стоит сказать, что у Delphi свой особый строковый тип, и в виде структуры его можно описать следующим образом:

d_str   struc ; (sizeof=0x8, mappedto_243, variable size)
_top            dd ?
length          dd ?
string          db 0 dup(?)             ; string(C)
delphi_string   ends

Первый дворд всегда равен 0xFFFFFFFF, второй — это длина строки, и далее идёт сама строка. Поэтому, подобные конструкции кода лишь получают длину строки.

Ещё одно важное замечание: Индексы в строках у Delphi из-за этого идут с 1, поэтому вы часто будете встречать в декомпиляторе/дизассемблере вычитание 1 из индекса (для соответствия реальному расположению символов в памяти).

Далее идёт проверка длины: >= 70 и <= 500.

Далее видим проверку первого символа ключа на равенство второму аргументу функции, т.е. 'A', либо 'B', и установку флага в зависимости от символа. Я обозвал этот флаг как rus_ver.

Согласитесь, выглядит громоздко:

LStrFromChar((char *)&v193, (char *)(unsigned __int8)g_LicKey[2]);
gvar_006F58C0 = Pos(v193, *(char **)_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ1234567890KLMNOPQRSTUVWXYZ_);
gvar_006F58C8 = *(_BYTE *)(*(_DWORD *)_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ1234567890KLMNOPQRSTUVWXYZ_
                             + (unsigned __int8)gvar_006F58C0
                             - 3);
gvar_006F58C9 = *(_BYTE *)(*(_DWORD *)_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ1234567890KLMNOPQRSTUVWXYZ_
                             + (unsigned __int8)gvar_006F58C0
                             - 4);
LStrFromChar((char *)&v192, (char *)(unsigned __int8)g_LicKey[1]);
v242 = Pos(v192, *(char **)_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ1234567890KLMNOPQRSTUVWXYZ_);

А вот так куда лучше:

LStrFromChar((char *)&lic_key, g_LicKey[2]);
g_PosChar2 = Pos(lic_key, str_EngAlpha);
g_AlphaChar1 = str_EngAlpha[g_PosChar2 - 3];
g_AlphaChar2 = str_EngAlpha[g_PosChar2 - 4];
LStrFromChar((char *)&key_char_1, g_LicKey[1]);
key_char1_pos = Pos(key_char_1, str_EngAlpha);

Далее — проверка key_char1_pos на равенство 12.

Теперь происходит суммирование символов ключа, кроме последних двух, и, пока сумма больше 0xFF, деление на 2, и инкремент на 1:

idx = key_len_minus_2 - 2;
if ( key_len_minus_2 - 2 > 0 )
{
  key_idx = 1;
  do
  {
    key_sum += (unsigned __int8)g_LicKey[key_idx++ - 1];
    --idx;
  }
  while ( idx );
}
for ( ; key_sum > 0xFF; key_sum = (key_sum >> 1) + 1 )
  ;

Преобразовываем полученную сумму в hex-строку, и, затем, в lowercase.
Теперь получаем два последних символа ключа, передаём каждый из них в какую-то функцию, и на выходе получаем по преобразованному символу. Давайте разберём эту функцию…

Этап шесть: Преобразование символа

В общем виде, функция преобразования символа выглядит вот так:

LStrFromChar((char *)&inChar_2, inChar);
v3 = Pos(inChar_2, str_EngAlpha);
if ( v3 > 0 )
{
  if ( g_PosChar2 > 0xAu )
  {
    for ( i = 1; i < g_PosChar2 - 5; i += 2 )
    {
      if ( i == v3 )
      {
        v3 = i + 1;
      }
      else if ( v3 == i + 1 )
      {
        v3 = i;
      }
    }
  }
  for ( j = g_PosChar2 + 1; j < 60; j += 2 )
  {
    if ( j == v3 )
    {
      v3 = j + 1;
    }
    else if ( v3 == j + 1 )
    {
      v3 = j;
    }
  }
  v6 = v3 - g_PosChar2 + ((char)(v3 - g_PosChar2) < 0 ? 0x3E : 0);
  if ( flag_1 )
    v2 = str_RusAlpha1[v6 - 1];
  else
    v2 = str_EngAlpha1[v6 - 1];
}
return v2;

Вроде выглядит просто. Скажу сразу, нам придётся её инвертировать. Назовём её DecodeChar.

В итоге, два последних символа ключа преобразовываются этой функцией, и сравниваются с ранее полученной hex-суммой всех остальных символов ключа.

ToRevert (данным словом я буду отмечать важные моменты в реверсе функции проверки ключа): В самом конце, когда ключ будет готов, считаем сумму его символов, преобразовываем в хекс (0xXX), затем, каждый ниббл — инвертированным DecodeChar, и доклеиваем к ключу.

Видим далее, что третий символ ключа преобразовывается, переводится из хекса в int, и проверяются биты. Так это всё и обзовём:

v202 = meffi_DecodeChar(g_LicKey[3], 0);
PStrCpy(&v132, str_Hex);
v134 = v202;
v133 = 1;
PStrNCat(&v132, &v133, 2);
LStrFromString((char *)&v129, &v132);
key_char_3 = StrToInt(v129);
k3_flag1 = (key_char_3 & 1) != 0;
k3_flag2 = (key_char_3 & 2) != 0;
k3_flag3 = (key_char_3 & 4) != 0;
k3_flag4 = (key_char_3 & 8) != 0;

Пока назначение битов мы не знаем, поэтому даём им хоть какие-то осмысленные названия.

Снова функция, которую мы не знаем, и в неё передаётся один из флагов:

key_idx = 5;
sub_66408C(&key_idx, k3_flag1, &v128);

Последний параметр, судя по всему, выходная строка, т.к. передаётся дальше в Trim() и используется далее.
Сразу дадим этой функции соответствующий прототип:

void __fastcall sub_66408C(int idx, bool flag, char *output)

Этап семь: Чтение строки из ключа

Да, именно этим данная функция и занимается. Что показывает отладка, и беглый обзор кода. Но обо всём по-порядку.

while ( g_LicKey[*(_DWORD *)idx_1 - 1] != g_AlphaChar1 && LStrLen(g_LicKey) >= *(_DWORD *)idx_1 )

Сразу отметим, что первый параметр используется как указатель на dword (int), поэтому меняем ему тип на int *Delphi это var-аргументами зовётся).

После всех преобразований типов, и корректировки аргументов, наша функция приобретает вид:

while ( g_LicKey[*idx_1 - 1] != g_AlphaChar1 && LStrLen(g_LicKey) >= *idx_1 )
{
  c1 = g_LicKey[*idx_1 - 1];
  if ( c1 == g_AlphaChar2 )
  {
    LStrFromChar((char *)&c1_str, c1);
    c1_str_ = c1_str;
    LStrFromChar((char *)&c2_str, g_LicKey[*idx_1]);
    c2_str_ = c2_str;
    LStrFromChar((char *)&c3_str, g_LicKey[*idx_1 + 1]);
    LStrCatN(c3_str, c2_str_, c1_str_, gvar_0070DF4C);
    PStrCpy(&str_hex, str_Hex_0);
    cc[1] = meffi_DecodeChar(g_LicKey[*idx_1], 0);
    cc[0] = 1;
    PStrNCat(&str_hex, cc, 2);
    PStrCpy(&hexVal, &str_hex);
    cc[1] = meffi_DecodeChar(g_LicKey[*idx_1 + 1], 0);
    cc[0] = 1;
    PStrNCat(&hexVal, cc, 3);
    LStrFromString((char *)&hexVal_1, &hexVal);
    value = ValLong(hexVal_1, &outCode);
    LStrFromChar((char *)&value_1, value);
    LStrCat((char *)&output_2, value_1);
    *idx_1 += 2;
  }
  else
  {
    LStrFromChar((char *)&keyChar, g_LicKey[*idx_1 - 1]);
    LStrCat((char *)&gvar_0070DF4C, keyChar);
    c = meffi_DecodeChar(g_LicKey[*idx_1 - 1], flag_1);
    LStrFromChar((char *)&c_str, c);
    LStrCat((char *)&output_2, c_str);
  }
   ++*idx_1;
}
++*idx_1;

Видим, что происходит чтение символов ключа, до тех пор, пока не встретится g_AlphaChar1. Если символ не равен g_AlphaChar2, доклеиваем его в глобальную переменную, а преобразованный с помощью DecodeChar() символ доклеиваем в выходной буфер.
Если же нам попался g_AlphaChar2 символ, читаем следующие за ним два символа, преобразовываем их, переводим в число, и приклеиваем к выходному буферу. Эти же два символа в непреобразованном виде доклеиваем вместе с g_AlphaChar2 к глобальной переменной. Назовём её g_stringFromKey1.
Судя по всему, эту функцию можно назвать DecodeString.

ToRevert: Строку, которую мы захотим закодировать в ключ, придётся преобразовывать с помощью функции, обратной для DecodeString().

P.S. На этом первую часть статьи про кейгенинг ArtMoney я пожалуй закончу. Во второй части мы продолжим декомпиляцию кода проверки ключа, столкнувшись с новыми трудностями, и дурацким на вид кодом. Но, разве нас это остановит?

Автор: DrMefistO

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js