Дослідження захисту ArtMoney. Частина перша

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

Отже, в цій статті ви дізнаєтеся, як я писав кейген до ArtMoney (тут буде описана версія 7.45.1).

перший Етап: Аналіз виконуваного файлу
Я встановив собі англійську версію програми, щоб IDA і інші утиліти нормально шукали текст.

Насамперед, потрібно з'ясувати, на чому написана/ніж упакована AM. Відкриємо її (файлик am745.exe) в моєму улюбленому ExeInfo PE:



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

Етап другий: Знову аналізи
Знову дивимося в ExeInfo PE, і бачимо, що програма написана на Borland Delphi. Чудово! Скористаємося супер-програмою для аналізу Delphi-програм: IDR (Interactive Delphi Reconstructor. До речі, також у неї є відкриті вихідні коди. Версію IDE там і визначимо.

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

Весь процес дослідження я буду робити у IDA Pro (плюс Hex Rays), тому давайте сгенерим IDC-скрипт, який скаже їй про всіх іменах форм, методів, класів і т. п. Тиснемо ToolsIDC Generator у меню IDR. Чекаємо, поки створюється скрипт.

Далі, відкриємо IDA і теж затолкаем в неї нашу програму. Дочекаємося закінчення аналізу. Потім застосуємо сгенеренний IDC-скрипт: IDA Pro тиснемо FileScript File..., вибираємо скрипт. Чекаємо застосування.

Для того, щоб IDA могла нормально декомпилить Delphi код, їй необхідно сказати, що ми маємо справу з Delphi компілятором, і __fastcall викликами. На жаль, згенерований IDC, як і сама IDA про це нічого не говорять/не знають.

Переходимо в налаштування компілятора IDA: OptionsCompiler...:


Далі тиснемо переанализировать програму: OptionsGeneralAnalysisReanalyze 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), перемикаємо на візуальний перегляд. Бачимо віконце реєстрації. Прокрутимо вікно і знайдемо три контури кнопок. Ліва з них — кнопка OK, застосування реєстрації:


Етап чотири: Аналіз OnClick(). Поверхня
Тиснемо ПКМ по ній і переходимо в обробник OKClick. У IDA перейдемо на адресу початку цієї процедури (кнопка G).

Насамперед зазначимо, що це bp-based функція, а то локальні змінні не розпізнаються. Тиснемо Alt+P (або ПКМ по функції, Edit function...), і ставимо галку BP-based frame.

Спробуємо декомпілювати… Якщо все пройшло успішно, побачимо жахливий псевдокод декомпилера, інакше — фиксим прототипи:


Тиснемо 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©
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 я мабуть закінчу. У другій частині ми продовжимо декомпіляцію коду перевірки ключа, зіткнувшись з новими труднощами, і безглуздим на вигляд кодом. Але, хіба нас це зупинить?
P. P. S. Даєш більше цікавих статей по реверсу!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.