Исследование защиты 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.К нашему счастью, для Питона существует парочка декомпиляторов байт-кода. Чтобы не утруждать Вас поисками, дам ссылки на те, которые мне пригодились:
Easy Python Decompiler (EPD) — оболочка, в которой зашиты два декомпилятора (Uncompyle2 и Decompyle++); Форк 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 я решил не трогать, т.к., осмотрев его, можно увидеть, что там лежат библиотеки, которые можно установить отдельно для нашего Питона. Так и сделаем:
pip install docutils py2pdf — его положим в папку external; Imaging-1.1.7 — запустить и установить. Из папки external можно удалить; 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 c in ('-', ' ', '\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) if 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) Какой-то control вызывает функцию validate, передавая ему lichash (request code), имя операционной системы, для которой делается ключ, версию программы, и преобразованный код активации. Почему я остановил на этом месте внимание? Дело в том, что этот control — это 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@
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, и дополняют нулями, если нужно
Значит, чтобы получить правильный код активации нам теперь можно поступить следующим образом: Дать выполниться функции преобразования convert_reqest_key; На месте выполнения strcmp высмотреть содержимое out_key; Убрать лишние нули в начале out_key; Преобразовать out_key обратно в BASE30; Дописать в начало получившейся строки убранные три символа (AXX); По-желанию навтыкать дефисов через каждые пять символов. Не буду мудрствовать лукаво, а втисну print прямо в python-код программы: print («AXX» + textutils.BaseConvert («FCBCFEFD2FF684FA6A4F», textutils.BASE16, textutils.BASE30)) На выходе получил ключик: wingide — 2015/05/24 04:03:47 — AXX3Q6BQHKQ773D24P58
Введя его в поле ввода ключа активации, получил заветное:
ИТОГИ Как видите, процесс взлома не столько сложный, сколько интересный получился! Исследовать свои же исходники в скомпилированном их варианте… это, конечно, забавно.Не знаю, зачем авторы приложили к своей программе ее исходники (хоть и в большинстве своем, в виде байт-кода). Но, думаю, вы понимаете, что так делать не стоит!
Всем спасибо.