Дослідження захисту Wing IDE



Доброго здоров'я! Не здивуюся, що Ви раніше навіть не чули про цю програму. Як і я, до того дня, коли мені знадобився Python Debugger. Так, знаю, є pdb, але його функціонал і те, як він представлений, мені зовсім не сподобалося. Після нетривалих пошуків я натрапив на цей чудовий продукт. Тут є все, що може знадобитися в налагодженні ваших Python додатків (скажу відразу: ця мова я не вивчав, тому, якщо якісь неточності спливуть, прохання не лаятися).

Застереження: повторюючи дії з статті, ви дієте на свій страх і ризик!


Отже, ми починаємо...

Пацієнт, відразу скажу, незвичайний. По-перше: він поставляється з вихідними кодами (!!!), нехай і в байт-код; по-друге, як це іноді буває… загалом, побачите.

Першим ділом, качаємо програму (Wing IDE Professional v 5.1.4). Встановлюємо, оглядаємо папку. Головний виконуваний файл знаходиться за адресою ./bin/wing.exe. Запустимо його. Лається на відсутність Python, тому і встановимо його. Потрібен версії 2 (на даний момент це версія 2.7.9). Знову запускаємо програму. На цей раз пропонує встановити патчі, і перезавантажитися. Так і зробимо.

Тепер вилазить віконце із запитом ліцензії (т. к. у нас про-версія). Введемо якусь дурницю:



Отримуємо таку відповідь:



Що забавно: програма нам сама каже довжину ключа (20, не враховуючи дефісів), і символи, з яких він повинен починатися. В принципі, з цього вже можна і почати досліджувати захист — знайдемо цю сходинку у файлах програми.
Далі — цікавіше. Результат пошуку знайшовся у файлі ./bin/2.7/src.zip!

Так-так. Все дійсно так: програма йде з вихідними кодами. У них-то нам і доведеться копатися.

Етап два: риємося в исходниках

Включимо у Total Commander пошук по архівах, і знайдемо ту рядок знову. Рядок лежить у файлі: ./bin/2.7/src.zip/process/wingctl.pyo. PYO-файли представляють із себе бінарники з "оптимізованим" байт-кодом Python.

На наше щастя, для Пітона існує парочка декомпиляторов байт-коду. Щоб не обтяжувати Вас пошуками, дам посилання на ті, які мені знадобилися:

  1. Easy Python Decompiler (EPD — оболонка, в якій зашиті два декомпилятора (Uncompyle2 та Decompyle++);
  2. Форк Uncompyle2 — іноді розпаковує те, що не можуть розпакувати інші.
Отже, распакуем весь архів src.zip в каталог src (поруч вже є папка src, нехай туди розпаковується і все інше) і натравим на неї EPD:



Чекаємо закінчення процесу, і йдемо оглядати що вийшло. А вийшли на виході декомпилированные файли з закінченням _dis. Їх ми перейменуємо у .py. Все б добре, але, з'ясовується, що є також файли з закінченням _dis_failed, що говорить про те, що ці файли декомпілятор не подужав. На щастя, файл тільки один: edit/editor.pyo_dis_failed

Спробуємо на нього нацькувати Decompyle++… Та ж біда. Не дарма я дав посилання на запасний декомпілятор, оскільки саме він і зробив те, що не вдалося іншим. Тепер видалимо всі pyo/pyc файли з папки src .py*_dis перейменуємо у .py.

Далі повторимо все вищеописане для архіву opensource.zip, розпакувавши його в сусідню однойменну папку. Архів external.zip я вирішив не чіпати, т. к., оглянувши його, можна побачити, що там лежать бібліотеки, які можна встановити окремо для нашого Пітона. Так і зробимо:

  1. pip install docutils
  2. py2pdf — його покладемо в папку external;
  3. Imaging-1.1.7 — запустити і встановити. З папки external можна видалити;
  4. pygtk — те ж, що і з попереднім файлів.
Інші бібліотеки (pyscintilla2 та pysqlite) просто витягнемо з архіву external.zip і декомпіліруем, як і раніше.

Етапи три і чотири: власне вихідний код. Налагодження.

Поганявши по питоновским скриптам, я натрапив на файлик wing.py в корені папки з програмою. І перший же коментар нам підказує:
# Top level script for invoking Wing IDE. If --use-src is specified
# as an arg, then the files in WINGHOME/src, WINGHOME/external,
# WINGHOME/opensource will be used; otherwise, the files in the version
# specific bin directory will be used if it exists.


У двох словах: якщо скрипту дати параметр --use-src, то при запуску будуть використовуватися исходники з папок src, external, opensource кореневого каталогу з Wing IDE (а не зі скриптом).

Заглянувши в кореневу папку, я виявив ще одну папку src та .py-файли в ній. Підкинемо їх в нашу папку src, з перезаписом (тут все таки оригінали, а не декомпилированные файли).

Тепер всі три папки (зазначені трохи вище), скопіюємо в кореневий каталог програми. Спробуємо подебажить…

Запускаємо Wing IDE і відкриваємо в ній файл wing.py з каталогу bin. Далі в меню Debug -> Debug Environment... у полі параметрів вказуємо --use-src. Тепер будемо стартувати дебагер (клавіша F5). Якщо всі махінації з копированиями папок пройшли успішно, ми отримаємо другу копію запущеної Wing IDE. Чудово!

Далі: відкриємо в батьківському Wing IDE той файлик, в якому ми знайшли раніше рядок про погане license id (wingctl.py), і поставимо бряку до цього повідомлення:



У отлаживаемом Wing IDE зайдемо в меню Help -> Enter License... і введемо ключик згідно з правилами (пам'ятаєте?: 20 символів, при тому, перший з набору ['T', 'N', 'E', 'C', '1', '3', '6']):



Тиснемо Continue і потрапляємо на бабки бряку. Перша ж цікава функція: abstract.ValidateAndNormalizeLicenseID(id). Зайдемо в неї з F7. Там ще одна: __ValidateAndNormalize(id). Зайдемо і в неї.

Перша перевірка на валідність:
for c in code:
if in c('-', ' ', '\t'):
pass
elif c not in textutils.BASE30:
code2 += c
badchars.add©
else:
code2 += c

Бачимо, що від нас вимагають, щоб символи License ID належали набору textutils.BASE30:
BASE30 = '123456789ABCDEFGHJKLMNPQRTVWXY'

Начебто інших перевірок у __ValidateAndNormalize(id) немає. Виправляємо введений нами ідентифікатор і повторюємо знову. Перевірку на перший символ ми вже пройшли:
if len(id2) > 0 and id2[0] not in kLicenseUseCodes:
errs.append(_('Invalid first character: Should be one of %s') % str(kLicenseUseCodes))

А ось і другий символ:
if len(id2) > 1 and id2[1] != kLicenseProdCode:

kLicenseProdCodes = {config.kProd101: '1',
config.kProdPersonal: 'L',
config.kProdProfessional: 'N',
config.kProdEnterprise: 'E'}
kLicenseProdCode = kLicenseProdCodes[config.kProductCode]

Т. к. у нас Professional версія, то другий символ повинен бути N — виправляємо, і повертаємося. abstract.ValidateAndNormalizeLicenseID(id) пройшовся без помилок. Чудово. Упс:
if len(errs) == 0 and id[0] == 'T':
errs.append(_('You cannot enter a trial license id here'))

Фиксим (я вибрав E), і продовжуємо. Пробігшись очима нижче за кодом, нічого додатково до попередніх перевірок я не виявив, тому сміливо відпустив налагодження далі по F5. Нове вікно:



Вводимо випадковий текст, отримуємо повідомлення про помилку (знову 20 символів, починатися код активації повинен з AXX), знаходимо його в файлах, ставимо бряку:



Перша функція перевірки: abstract.ValidateAndNormalizeActivation(act). У ній знову перевірка на належність BASE30. Перевірка на префікс, яку ми вже пройшли:
if id2[:3] != kActivationPrefix:
errs.append(_("Invalid prefix: Should be '%s'") % kActivationPrefix)

Таке цікаве місце:
err, info = self.fLicMgr._ValidateLicenseDict(lic2, None)
якщо err == abstract.kLicenseOK:

Заходимо у self.fLicMgr._ValidateLicenseDict. Тут формується хеш від ліцензії:
lichash = CreateActivationRequest(lic)
act30 = lic['activation']
if lichash[2] not in 'X34':
hasher = sha.new()
hasher.update(lichash)
hasher.update(lic['license'])
digest = hasher.hexdigest().upper()
lichash = lichash[:3] + textutils.SHAToBase30(digest)
errs, lichash = ValidateAndNormalizeRequest(lichash)

Якщо подивитися на вміст lichash після виконання цього блоку, можна помітити, що текст її схожий на request code, який відображається в віконці введення коду активації, хоча кілька цифр і відрізняється. Гаразд, будемо думати, що тут мають місце бути якісь рандомные частині, що не впливають на активацію (що, до речі, далі підтвердиться!).

Далі з коду активації відрізають три перших символи, прибирають дефіси, перетворюють у BASE16 і доповнюють нулями, якщо потрібно:
act = act30.replace('-', ")[3:]
hexact = textutils.BaseConvert(act, textutils.BASE30, textutils.BASE16)
while len(hexact) < 20:
hexact = '0' + hexact

І ось воно, найцікавіше:
valid = control.validate(lichash, lic['os'], lic['version'][:lic['version'].find('.')], hexact)

Якийсь ctrl викликає функцію validate, передаючи йому lichash (request code), ім'я операційної системи, для якої робиться ключ, версію програми, і перетворений код активації. Чому я зупинив на цьому місці увагу? Справа в тому, що цей ctrl — це pyd-файл (у чому можна переконатися, додавши ім'я об'єкта у watch, і глянувши поле __file__), які представляють із себе звичайні DLL з одного експортованої функцією (не validate), яка дає Пітону інформацію про те, що вона вміє робити. Ну що ж, давайте подивимося на неї з боку декомпилятора Hex Rays

Етап п'ять: це вже не Python

Затащим у IDA Pro наш control (ctlutil.pyd) і подивимося на експортовану функцію initctlutil:
int initctlutil()
{
return Py_InitModule4(aCtlutil, &off_10003094, 0, 0, 1013);
}

off_10003094 представляє собою структуру, в якій вказані імена та адресу експортованих методів. Ось і наш validate:
.data:100030A4 dd offset aValidate ; "validate"
.data:100030A8 dd offset sub_10001410

З усього коду, який містить процедура sub_10001410 найцікавішим виглядає це:
if ( sub_10001020(v6, &v9) || strcmp(&v9, v7) )
{
result = PyInt_FromLong(0);
}

Зайдемо і у sub_10001020 теж. Цікаво було б не на око давати імена змінних, а подебажить і обізвати їх як слід. Так і зробимо. Налаштуємо відладчик IDA Pro:



Думаю, все зрозуміло з скріншота: ми вказали додаток, яке в підсумку буде довантажувати наш pyd-файл.

Тепер ставимо бряк на початок sub_10001020 і починаємо заглядати в змінні та вхідні параметри. Після нетривалого процесу налагодження приходимо до такого ось лістингу функції:
Код функції convert_reqest_key
int __usercall convert_reqest_key@<eax>(char *version@<eax>, const char *platform@<ecx>, const char *activation_key, char *out_key)
{
unsigned int len_1; // edi@1
const char *platform_; // esi@1
char *version_; // ebx@1
int ver_; // eax@2
signed int mul1; // ecx@3
signed int mul2; // esi@3
signed int mul3; // ebp@3
bool v11; // zf@15
const char *act_key_ptr; // eax@31
char v13; // dl@32
const char *act_key_ptr_1; // eax@35
unsigned int len_2; // ecx@35
char v16; // dl@36
const char *act_key_ptr_2; // eax@39
unsigned int len_3; // ecx@39
char v19; // dl@40
int P3_; // ebx@42
const char *act_key_ptr_3; // eax@45
unsigned int len_4; // ecx@45
char v23; // dl@46
unsigned int P4; // ebp@47
signed int mul4; // [sp+10h] [bp-18h]@0
unsigned int P3; // [sp+14h] [bp-14h]@1
unsigned int P2; // [sp+18h] [bp-10h]@1
unsigned int P1; // [sp+1Ch] [bp-Ch]@1

len_1 = 0;
platform_ = platform;
version_ = version;
P1 = 0;
P2 = 0;
P3 = 0;
if ( !strcmp(platform, aWindows) )
{
ver_ = (unsigned __int8)*version_;
if ( *version_ == '2' )
{
mul1 = 142;
mul2 = 43;
mul3 = 201;
mul4 = 38;
goto LABEL_31;
}
if ( (_BYTE)ver_ == '3' )
{
mul1 = 23;
mul2 = 163;
mul3 = 2;
mul4 = 115;
goto LABEL_31;
}
if ( (_BYTE)ver_ == '4' )
{
mul1 = 17;
mul2 = 87;
mul3 = 120;
mul4 = 34;
goto LABEL_31;
}
}
else if ( !strcmp(platform_, aMacosx) )
{
ver_ = (unsigned __int8)*version_;
if ( *version_ == '2' )
{
mul1 = 41;
mul2 = 207;
mul3 = 104;
mul4 = 77;
goto LABEL_31;
}
if ( (_BYTE)ver_ == '3' )
{
mul1 = 128;
mul2 = 178;
mul3 = 104;
mul4 = 95;
goto LABEL_31;
}
if ( (_BYTE)ver_ == '4' )
{
mul1 = 67;
mul2 = 167;
mul3 = 74;
mul4 = 13;
goto LABEL_31;
}
}
else
{
v11 = strcmp(platform_, aLinux) == 0;
LOBYTE(ver_) = *version_;
if ( v11 )
{
if ( (_BYTE)ver_ == '2' )
{
mul1 = 48;
mul2 = 104;
mul3 = 234;
mul4 = 247;
goto LABEL_31;
}
if ( (_BYTE)ver_ == '3' )
{
mul2 = 52;
mul1 = 254;
mul3 = 98;
mul4 = 235;
goto LABEL_31;
}
if ( (_BYTE)ver_ == '4' )
{
mul1 = 207;
mul2 = 45;
mul3 = 198;
mul4 = 189;
goto LABEL_31;
}
}
else
{
if ( (_BYTE)ver_ == '2' )
{
mul1 = 123;
mul2 = 202;
mul3 = 97;
mul4 = 211;
goto LABEL_31;
}
if ( (_BYTE)ver_ == '3' )
{
mul1 = 127;
mul2 = 45;
mul3 = 209;
mul4 = 198;
goto LABEL_31;
}
if ( (_BYTE)ver_ == '4' )
{
mul2 = 4;
mul1 = 240;
mul3 = 47;
mul4 = 98;
goto LABEL_31;
}
}
}
if ( (_BYTE)ver_ == '5' )
{
mul1 = 7;
mul2 = 123;
mul3 = 23;
mul4 = 87;
}
else
{
mul1 = 0;
mul2 = 0;
mul3 = 0;
}
LABEL_31:
act_key_ptr = activation_key;
do
v13 = *act_key_ptr++;
while ( v13 );
if ( act_key_ptr != activation_key + 1 )
{
do
P1 = (P1 * mul1 + activation_key[len_1++]) & 0xFFFFF;
while ( len_1 < strlen(activation_key) );
}
act_key_ptr_1 = activation_key;
len_2 = 0;
do
v16 = *act_key_ptr_1++;
while ( v16 );
if ( act_key_ptr_1 != activation_key + 1 )
{
do
P2 = (P2 * mul2 + activation_key[len_2++]) & 0xFFFFF;
while ( len_2 < strlen(activation_key) );
}
act_key_ptr_2 = activation_key;
len_3 = 0;
do
v19 = *act_key_ptr_2++;
while ( v19 );
if ( act_key_ptr_2 != activation_key + 1 )
{
P3_ = 0;
do
P3_ = (P3_ * mul3 + activation_key[len_3++]) & 0xFFFFF;
while ( len_3 < strlen(activation_key) );
P3 = P3_;
}
act_key_ptr_3 = activation_key;
len_4 = 0;
do
v23 = *act_key_ptr_3++;
while ( v23 );
P4 = 0;
if ( act_key_ptr_3 != activation_key + 1 )
{
do
P4 = (P4 * mul4 + activation_key[len_4++]) & 0xFFFFF;
while ( len_4 < strlen(activation_key) );
}
sprintf(out_key, a_5x_5x_5x_5x, P1, P2, P3, P4);
return 0;
}


А місце виклику цієї функції набуває наступний вигляд:
if ( convert_reqest_key(version, platform, request_key, out_key) || strcmp(out_key, act_key_hash) )
{
result = PyInt_FromLong(0);
}

З цього всього можна зробити висновок, що request code перетворюється за допомогою функції convert_reqest_key і потім порівнюється з тим перетвореним кодом активації. Пам'ятайте то перетворення?
Далі з коду активації відрізають три перших символи, прибирають дефіси, перетворюють у BASE16 і доповнюють нулями, якщо потрібно
Значить, щоб отримати правильний код активації нам тепер можна поступити наступним чином:
  1. Дати здійснитися функції перетворення convert_reqest_key;
  2. На місці виконання strcmp вивідати вміст out_key;
  3. Прибрати зайві нулі на початку out_key;
  4. Перетворити out_key назад BASE30;
  5. Дописати в початок вийшла рядка прибрані три символи (AXX);
  6. За бажанням навтыкать дефісів через кожні п'ять символів.
Не буду мудрувати лукаво, а втисну print прямо у python-код програми:
print("AXX" + textutils.BaseConvert("FCBCFEFD2FF684FA6A4F", textutils.BASE16, textutils.BASE30))

На виході отримав ключик:
wingide — 2015/05/24 04:03:47 — AXX3Q6BQHKQ773D24P58


Ввівши його в поле вводу ключа активації, отримав заповітне:



ПІДСУМКИ

Як бачите, процес злому не стільки складний, скільки цікавий вийшов! Досліджувати свої ж исходники в скомпільованому їх варіанті… це, звичайно, кумедно.

Не знаю, навіщо автори доклали до своєї програми її вихідні коди (хоч і здебільшого, у вигляді байт-коду). Але, думаю, ви розумієте, що так робити не варто!

Всім дякую.

Джерело: Хабрахабр

0 коментарів

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