http://habrahabr.ru/post/258613/
Доброго здравия! Не удивлюсь, что Вы раньше даже не слышали об этой программе. Как и я, до того дня, когда мне пригодился
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(c)
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_keyint __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, и дополняют нулями, если нужно
Значит, чтобы получить правильный код активации нам теперь можно поступить следующим образом:
- Дать выполниться функции преобразования 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
Введя его в поле ввода ключа активации, получил заветное:
ИТОГИ
Как видите, процесс взлома не столько сложный, сколько интересный получился! Исследовать свои же исходники в скомпилированном их варианте… это, конечно, забавно.
Не знаю, зачем авторы приложили к своей программе ее исходники (хоть и в большинстве своем, в виде байт-кода). Но, думаю, вы понимаете, что так делать не стоит!
Всем спасибо.