Исследование защиты программы Xilisoft Video Cutter
Скриншот программы Xilisoft Video Cutter
Компания Xilisoft на протяжении многих лет занимает лидирующие позиции по производству разнообразных мультимедийных "ужа-в-ежа конвертеров". Достаточно лишь посмотреть список и наименования их продуктов. Остальные их программы, не относящиеся к конвертерам, все равно так или иначе связаны с обработкой мультимедийных файлов, например, Xilisoft Video Cutter. Главное, что за каждое свое поделие они хотят отдельную денежку. Это при том, что все действия можно выполнить с помощью гораздо более мощных и бесплатных инструментов. Ну да ладно, считать чужие деньги неприлично, лучше сделать так, чтобы ничего считать не пришлось.
Забираем с сайта дистрибутив, устанавливаем, запускаем, смотрим. К слову, просто так дистрибутив с сайта не скачать, ссылка на загрузку с офсайта ведет на какую-то софтопомойку с кучей рекламы и адвары, с которой тоже фиг скачаешь. Так что пользуйтесь моей прямой ссылкой. Похоже, что доходов от продаж не хватает, вот они и решили получить немного дополнительного профита с инсталляции адвары. В папке с установленной программой обнаруживается куча динамических библиотек с характерным префиксом "Qt". Я уже не первый год пересекаюсь с продуктами Xilisoft. Раньше им вполне хватало обычных виндовых компиляторов, но в какой-то момент они решили притянуть пользователей Mac, поэтому перешли на использование кроссплатформенного Qt. Запускаем программу и пробуем зарегистрировать ее каким-нибудь левым серийником.
Сообщение о неправильной регистрации
Вот, уже хорошо. У нас есть текст сообщения о неправильной регистрации. Поищем по фразе "Invalid license info", в каком файле это сообщение находится. Оказывается, что базовая строчка обнаруживается в файле "imfc0.dll", плюс к этому в пачке файлов локализации. Но нас интересует только файл imfc0.dll. Отправляем его на анализ в дизассемблер. В файле не все просто, оказывается, что это не обычная строка, а целая html-страница. Ну да, Qt так умеет.
Текст сообщения
Посмотрим, где вся эта красота используется. По перекрестным ссылкам в дизассемблере выходим на код, где это сообщение формируется и выводится диалоговое окно. Там ничего для нас интересного нет, поэтому прокручиваем код вверх и переходим на код, откуда это окно вызывается:
Code (Assembler) : Убрать нумерацию
- .text:100226DC loc_100226DC:
- .text:100226DC cmp [esp+5Ch+arg_4], ebp
- .text:100226E0 jnz short loc_10022753
- .text:100226E2 call ?instance@regLib@@SAPAV1@XZ
- ; regLib::instance(void)
- .text:100226E7 mov edi, eax
- .text:100226E9 mov ecx, edi
- .text:100226EB call ?updateState@regLib@@QAEXXZ
- ; regLib::updateState(void)
- .text:100226F0 cmp dword ptr [edi+0Ch], 0
- .text:100226F4 jz short loc_10022753
- ...
- ...
- ...
- .text:10022753 loc_10022753:
- .text:10022753 mov ecx, esi
- ; Окно с сообщением
- .text:10022755 call loc_1001F1F0
Функции регистрации
Можно сколько угодно не любить Qt, но за такие структурированные подарки приходится быть ему благодарным. Готов поспорить на что угодно, что функция GetRegInfo запрашивает регистрационные данные, SaveRegInfo сохраняет их куда надо, а самая интересная функция IsValidRegInfo отвечает за проверку корректности введенных данных. Вот ее и посмотрим, точнее самый ее конец:
Code (Assembler) : Убрать нумерацию
- .text:1008035E push eax
- .text:1008035F mov byte ptr [esp+50h+var_4], 0Ch
- ; Вызвать функцию проверки
- .text:10080364 call sub_10071DC0
- .text:10080369 add esp, 8
- .text:1008036C lea ecx, [esp+48h+var_20]
- ; Сохранить ее результат из AL в BL
- .text:10080370 mov bl, al
- .text:10080372 mov byte ptr [esp+48h+var_4], 0Bh
- .text:10080377 call ds:??1QByteArray@@QAE@XZ
- .text:1008037D lea ecx, [esp+48h+var_28]
- .text:10080381 mov byte ptr [esp+48h+var_4], 0Ah
- .text:10080386 call ds:??1QByteArray@@QAE@XZ
- .text:1008038C lea ecx, [esp+48h+var_2C]
- .text:10080390 mov [esp+48h+var_4], ebp
- .text:10080394 call ds:??1QString@@QAE@XZ
- ; Сохраненный результат нулевой?
- .text:1008039A test bl, bl
- .text:1008039C jz short loc_100803A3
- ; Записать во временную переменную 1
- .text:1008039E mov byte ptr [esp+48h+arg_8], 1
- .text:100803A3 loc_100803A3:
- ; Записать в AL значение из переменной
- .text:100803A3 mov al, byte ptr [esp+48h+arg_8]
- .text:100803A7 loc_100803A7:
- .text:100803A7 mov ecx, [esp+48h+var_C]
- .text:100803AB mov large fs:0, ecx
- .text:100803B2 pop ecx
- .text:100803B3 pop edi
- .text:100803B4 pop esi
- .text:100803B5 pop ebp
- .text:100803B6 pop ebx
- .text:100803B7 add esp, 34h
- ; На выход
- .text:100803BA retn 0Ch
Программа успешно "зарегистрирована"
Отлично теперь программа принимает любое регистрационное имя и любой серийник. Это быстрый вариант "для дома, для семьи", когда надо просто пару раз воспользоваться программой и не заморачиваться на красоте решений.
Теперь давайте попробуем отреверсить алгоритм генерации серийных номеров, ну или, как вариант, найти валидную пару для регистрации. Оба решения можно считать "красивыми", ведь ничего патчить в этом случае не придется. К сожалению, анализировать код на Qt в дизассемблере - это ужас, конструкции типа "динамического указателя на динамический указатель на указатель" тут обычное дело. Поэтому запустим программу под отладчиком и поставим точку останова в модуле imfc0.dll на адрес 10071DC0. Помните, по этому адресу располагается финальная функция проверки из функции IsValidRegInfo? Запускаем программу, пробуем зарегистрировать любым левым серийником. Когда сработает точка останова, продолжим выполнение под управлением отладчика в пошаговом режиме.
Code (Assembler) : Убрать нумерацию
- .text:10071E0C mov [esp+0A4h+var_60], edi
- .text:10071E10 mov [esp+0A4h+var_81], 0
- .text:10071E15 call ds:?left@QByteArray@@QBE?AV1@H@Z
- .text:10071E1B mov ecx, edi
- .text:10071E1D mov [esp+9Ch+var_4], 0
- .text:10071E28 call ds:?size@QByteArray@@QBEHXZ
- ; Длина строки серийника должна быть 27h символов
- .text:10071E2E cmp eax, 27h
- .text:10071E31 jnz loc_1007228D
- .text:10071E37 mov ecx, esi
- .text:10071E39 call ds:?isEmpty@QByteArray@@QBE_NXZ
- .text:10071E3F test al, al
- .text:10071E41 jnz loc_1007228D
Code (Assembler) : Убрать нумерацию
- .text:100720FF lea ecx, [esp+0A0h+var_88]
- .text:10072103 call ds:?data@QByteArray@@QAEPADXZ
- .text:10072109 push eax ; int
- .text:1007210A lea ecx, [esp+0A0h+var_58] ; this
- .text:1007210E call ??0CMD5@@QAE@PBDI@Z
- ; CMD5::CMD5(char const *,uint)
- .text:10072113 lea ecx, [esp+98h+var_58] ; this
- .text:10072117 mov byte ptr [esp+98h], 0Ah
- .text:1007211F call ?getMD5Digest@CMD5@@QAEPBDXZ
- ; CMD5::getMD5Digest(void)
- .text:10072124 push eax
- .text:10072125 lea ecx, [esp+9Ch+var_68]
- .text:10072129 call ds:??0QByteArray@@QAE@PBD@Z
Вызов генерации MD5
Если посмотреть на стек, то там находятся два параметра: указатель на указатель на хешируемую строку и длина строки.
Параметры для генерации хеша
Итого, длина хешируемых данных 53h символов. Переходим в дампе на адрес из стека (значение динамическое, поэтому у вас будет другой адрес). Там находится вот такая строка:
Блок для генерации хеша
Если отсчитать 53h символов, то как раз получится выделенный на скриншоте фрагмент. Дальше обратите внимание, из чего он состоит. Сперва идет символ "1", затем 10 нечетных символов из строки "xilisoftvideocutter2", чередующиеся с номерами этих символов, затем 10 четных символов этой же строки по очереди с их номерами, затем два нуля "00", затем первые 20 символов серийного номера и строка "xilisoftvideocutter2". Можно проверить с разными серийниками, алгоритм составления строки остается неизменным. Затем от этого блока данных считается хеш MD5 и преобразуется в строку.
Строка хеша MD5
После этого с полученной строкой выполняются преобразования и в финале выполняется самое обычное сравнение строк:
Code (Assembler) : Убрать нумерацию
- .text:100721FB mov edx, [esp+9Ch+var_60]
- ; Указатель на одну строку
- .text:100721FF push edx
- .text:10072200 lea eax, [esp+0A0h+var_88]
- ; Указатель на вторую строку
- .text:10072204 push eax
- ; Вызвать функцию сравнения строк
- .text:10072205 call sub_10070A00
- .text:1007220A add esp, 8
- ; Сохранить результат сравнения
- .text:1007220D mov [esp+9Ch+var_81], al
- .text:10072211 loc_10072211:
- .text:10072211 lea ecx, [esp+9Ch+var_6C]
- .text:10072215 mov byte ptr [esp+9Ch+var_4], 0Ah
- .text:1007221D call ds:??1QByteArray@@QAE@XZ
- ...
- ...
- ...
- ; Записать результат сравнения в AL, он же является результатом всей функции
- .text:100722A2 mov al, [esp+9Ch+var_81]
- .text:100722A6 mov ecx, [esp+9Ch+var_C]
- .text:100722AD mov large fs:0, ecx
- .text:100722B4 pop ecx
- .text:100722B5 pop edi
- .text:100722B6 pop esi
- .text:100722B7 pop ebp
- .text:100722B8 pop ebx
- .text:100722B9 mov ecx, [esp+88h+var_10]
- .text:100722BD xor ecx, esp
- .text:100722BF call @__security_check_cookie@4
- .text:100722C4 add esp, 88h
- .text:100722CA retn
Первая строка
Вторая строка
Получается, что валидный серийник "12345678908234567890954D-DA1C-1C5D-8821". Ненадолго вернемся к хешу MD5. Если посмотреть на него, то можно увидеть, что вторая половина серийника составлена из нечетных символов строки хеша. Регистрационное имя не используется. Вот и весь алгоритм генерации серийных номеров.
Программа успешно зарегистрирована
Пробуем зарегистрировать программу найденным серийником. Программа благодарит за регистрацию. Все ограничения сняты.
Программа успешно зарегистрирована
Точно так же нейтрализуется защита и на остальных продуктах Xilisoft. Вся разница только в строке, которая используется для хеширования. Универсальный патч или кейген для любой программы от Xilisoft вы теперь можете написать самостоятельно.
Просмотров: 2018 | Комментариев: 10
Метки: исследование защиты, мультимедиа
Внимание! Статья опубликована больше года назад, информация могла устареть!
Комментарии
Отзывы посетителей сайта о статье
xussr
(24.01.2018 в 23:23):
Пособие отличное спасибо...
arina-23
(24.01.2018 в 03:49):
Перед проверкой валидности регистрационных данных есть еще проверка длины номера и наличия в номере букв. Я, в свое время, на этом срезался. Если есть хотя-бы одна буква, сразу на выход. Должны быть только шестнадцатеричные цифры.
ManHunter
(23.01.2018 в 21:02):
Проверить другой путь, например, работу с реестром, ввод-вывод, все равно же где-то должно быть критическое место. Монстрячность Qt иногда играет в нашу пользу.
pawel97
(23.01.2018 в 20:41):
"А еще можно патчить функцию isFree в regLib, тоже интересно получается. Часть продуктов, видимо, планировались как халявные, а модуль защиты везде одинаковый."
Помнится у ashampoo ещё интересней было, там и патчить не надо было, просто один дворд в реестре менялся (shareware_type кажется). Но потом в отдельные фичи добавили онлайн активацию.
А как быть, если строки сообщений исключительно в qt-шных языковых файлах? По каким данным их в иде искать?
Там ещё id какие-то числовые есть, но только рядом с этими строками они и используются. Типа: присвоить такой-то строке такой-то id. Но мест, где по нему запрашивается нужная строка, не нахожу.
Помнится у ashampoo ещё интересней было, там и патчить не надо было, просто один дворд в реестре менялся (shareware_type кажется). Но потом в отдельные фичи добавили онлайн активацию.
А как быть, если строки сообщений исключительно в qt-шных языковых файлах? По каким данным их в иде искать?
Там ещё id какие-то числовые есть, но только рядом с этими строками они и используются. Типа: присвоить такой-то строке такой-то id. Но мест, где по нему запрашивается нужная строка, не нахожу.
ManHunter
(23.01.2018 в 13:16):
Noobie, не хочу разочаровывать, но по длине он точно такой же :))) И генерится точно по такому же алгоритму. Только разделители добавлены, вот и вся разница :)
А еще можно патчить функцию isFree в regLib, тоже интересно получается. Часть продуктов, видимо, планировались как халявные, а модуль защиты везде одинаковый.
А еще можно патчить функцию isFree в regLib, тоже интересно получается. Часть продуктов, видимо, планировались как халявные, а модуль защиты везде одинаковый.
Noobie
(23.01.2018 в 13:10):
Познавательно, спасибо! С практической точки зрения, для тех, кто не желает ничему учиться, есть старый, но поныне актуальный кейген от Lz0, там для получения данных используется файл pd.cfg из папки программы. Серийник получается короче, например: C322-1076-54A9-8FE3-DDFC-76D0-F138-6A73
ManHunter
(23.01.2018 в 13:08):
Совершенно верно.
user
(23.01.2018 в 12:54):
То есть, если патчится начало функции, RET делаем точно такой, как у оригинальной функции?
ManHunter
(23.01.2018 в 11:56):
Чтобы стек восстановить
user
(23.01.2018 в 11:48):
Для чего делается RET именно 0Ch?
Добавить комментарий
Заполните форму для добавления комментария