Как стать автором
Поиск
Написать публикацию
Обновить

Исследование защиты Wing IDE

Время на прочтение9 мин
Количество просмотров31K


Доброго здравия! Не удивлюсь, что Вы раньше даже не слышали об этой программе. Как и я, до того дня, когда мне пригодился 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 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_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


Введя его в поле ввода ключа активации, получил заветное:



ИТОГИ


Как видите, процесс взлома не столько сложный, сколько интересный получился! Исследовать свои же исходники в скомпилированном их варианте… это, конечно, забавно.

Не знаю, зачем авторы приложили к своей программе ее исходники (хоть и в большинстве своем, в виде байт-кода). Но, думаю, вы понимаете, что так делать не стоит!

Всем спасибо.
Теги:
Хабы:
Всего голосов 43: ↑35 и ↓8+27
Комментарии52

Публикации

Ближайшие события