Снятие защиты Sentinel SuperPro (часть первая)

Внимание: Данная статья предназначена только для эмулирования легально купленных ключей, которые по каким-либо причинам не могут быть использованы в компьютере (отсутствие порта, его некорректная работа или занятость). Автор не несет никакой ответственности за нецелевое использование нижеизложенного материала.

Введение

Я уже имею некоторый опыт по реверсингу программы для локализации софта - Passolo. Некоторое время тому назад я написал три статьи, в которых рассказал о доработке демо-версий программы Passolo. В демо-версиях разработчики вырезали значительную часть кода, в результате чего программа не могла работать в полнофункциональном режиме. Проводя реверсинг демо-версии программы, мне пришлось дописывать код, и все равно в части обеспечения работы некоторых функций (создание лицензионных пакетов перевода) мне не удалось провести полный реверсинг этой замечательной программы. Один из иностранных участников форума дал мне адрес сайта, откуда я скачал полнофункциональную версию программы Passolo 5.0.005, а затем и 5.0.006. Однако эти программы, при своем запуске требовали установки электронного ключа защиты (Dongle), которого у меня, естественно, не было, и при нажатии на кнопку ОК, запускались в режиме демо-версии.
В интернете я читал об электронных ключах защиты (HASP, SafeKey и других), но никогда не занимался их анализом и реверсингом. В каталоге установки программы Passolo я обнаружил неизвестный для меня подкаталог Rainbow, которого не было в демо-версиях программы, а просмотрев содержимое этого подкаталога, увидел второй незнакомый для меня подкаталог Sentinel, в котором я обнаружил две папки - Win_9x и Win_NT, и несколько файлов. В файле readme.txt я прочитал, что имею дело с защитой Sentinel SuperPro, и в этом каталоге находятся драйвера, которые должны быть установлены для обеспечения работы электронного ключа защиты Sentinel.
Я залез в интернет, и нашел несколько интересных сайтов и форумов, в которых описывался взлом программ, защищенных электронными ключами Sentinel. Этих статей, к сожалению, оказалось немного. Не все они подробно описывают процесс снятия этой довольно сильной защиты, но они все-таки дали мне некоторый исходный материал для работы. Наиболее содержательной для меня оказалась статья CyberHeg "Удаление dongle в SentinelLM Wlscgen", на русский язык с некоторыми изменениями эта статья переведена Quantum "SentinelLM!Вступление" (находится на сайте WASM.RU, и Quantum разбил этот материал на 5 частей), а также статьи CrackZ, Goatass, и весьма неплохая статья Dr0x "Логгер-эмулятор API Sentinel SuperPro", находящаяся на сайте CRACKL@B. Я не буду в своей статье рассказывать подробно о принципах построения защиты Sentinel SuperPro, о ней вы можете прочитать в выше указанных статьях, а более подробно я буду излагать процесс снятия этой защиты.
В своей работы по снятию защиты Sentinel SuperPro я использовал полюбившийся мне отладчик OllyDbg v1.10, который не только удобен в работе, но и весьма наглядно может продемонстрировать весь процесс работы.
Итак, начинаем рассмотрение работы по снятию защиты Sentinel SuperPro в программе Passolo v5.006.

Снятие защиты Sentinel SuperPro

Итак, загружаем файл psl.exe в отладчик OllyDbg v1.10, и создаем дизассемблированный список секции кода нашей программы. Запускаем эту программу в отладчике, и получаем следующее сообщение:

[Скриншот]

Итак, нам надо найти место, откуда вызывается ключ защиты Sentinel SuperPro, который нам показывает весьма неприглядную для нас картинку.
Из статьи Dr0x "Логгер-эмулятор API Sentinel SuperPro" нам известно, что сначала нам надо найти и отключить функцию sproFindFirstUnit, которая проверяет принадлежность ключа защиты для данной программы.
Из статей CyberHeg "Удаление dongle в SentinelLM Wlscgen" и Quantum "SentinelLM!Вступление", нам известно, что защита Sentinel SuperPro применяет для своей идентификации фирменную метку - значение 7242. Поэтому, вводим поиск этой константы в отладчике, и останавливаемся здесь:

[Скриншот]

Здесь мы видим копирование в память машины числа 7242. Давайте поищем в программе еще ссылки на это "магическое" для нас число. В отладчике запускаем поиск этой константы, и получаем следующую таблицу:

[Скриншот]

Из всех этих значений, мы теперь должны найти, каким функциям Sentinel SuperPro принадлежат эти параметры. Здесь можно пойти двумя путями:

1. Запустить дизассемблер IDA с набором сигнатур от Sentinel SuperPro (они имеются на многих сайтах);

2. Воспользоваться анализом, приведенным в работе Dr0x "Логгер-эмулятор API Sentinel SuperPro".

Я решил пойти по второму пути, поскольку он позволяет более лучше понять использование API-функций Sentinel SuperPro.
Ставим точки прерывания на все найденные нами инструкции, содержащие магическое число 7242, запускаем программу…, и нигде не останавливаемся. В чем же дело? Оказывается, все очень просто. Мы не запустили драйвер Sentinel SuperPro. Запускаем этот драйвер из Главного меню, перезагружаем компьютер, загружаем программу в отладчик, снова запускаем ее, и… вот оно!. Мы останавливаемся здесь.

[Скриншот]

Здесь производится проверка функции Sentinel SuperPro - sproInitialize, которая проверяет инициализацию драйвера. Для нас эта функция не представляет интереса, поскольку она не связана с какой-либо ячейкой ключа защиты. Поэтому смело жмем кнопку Запустить, и останавливаемся здесь.

Функция sproFindFirstUnit



[Скриншот]

А вот это уже интересно. Что это за функция? Поднимемся немного вверх, в начало этой инструкции (адрес 005695D0), и внизу мы видим, что эта команда вызывается из

[Скриншот]

Переходим на адрес 004CFFE9

[Скриншот]

Наша функция принимает два значения. А из описания функций Sentinel SuperPro (они описаны в Руководстве разработчика, а также приведены в приложении к статье Dr0x "Логгер-эмулятор API Sentinel SuperPro") мы знаем, что только одна функция принимает два параметра, и эта функция - sproFindFirstUnit. Значение C5BD является developerID, которое является индивидуальным для каждого покупателя ключа защиты Sentinel SuperPro. Компания Rainbow для компании Passolo присвоила developerID равным C5BD. Таким образом, мы уже имеем значение одной ячейки ключа защиты - C5BD. Причем из Руководства разработчика мы знаем, что значение developerID записывается в ячейку 1 ключа защиты.
Дальнейший запуск программы опять нас приводит к появлению сообщения о необходимости подключения ключа защиты. Из вышеприведенных статей мы знаем, что функция sproFindFirstUnit возвращает значение 0, если ключ защиты имеется, или 3 - если такового нет. Поскольку ключа защиты у нас нет, то функция sproFindFirstUnit возвратила нам 3. Нам надо принудительно заставить функция sproFindFirstUnit вернуть 0, что можно сделать следующим образом

[Скриншот]

Как видим, ничего сложного здесь нет. Обнуляем EAX, и NOP'им остальные инструкции до возврата из стека значений регистров ESI и EBX.
Запускаем программу дальше, и мы останавливаемся здесь

Функция sproExtendedRead



[Скриншот]

Эта подпрограмма вызывается из

[Скриншот]

Проходим на адрес 004D0023

[Скриншот]

и здесь мы видим, что эта подпрограмма принимает 4 параметра, а из всех API-функций Sentinel SuperPro, 4 параметра принимает только функция sproExtendedRead. Ни в одной статье я не встречался с эмуляцией этой функции; везде описана только функция sproRead. Поэтому здесь пришлось немного подумать - а что делать с параметром accessCode?. Я решил с этим параметром пока ничего не делать, и сделать эмуляцию этой функции, как это описано в статьях CyberHeg, Quantum и CrackZ.

[Скриншот]

Адреса буферов для хранения содержимого ячейки ключа защиты и кода доступа мы берем из родного листинга по адресам 00569896 и 0056989E соответственно, а сами ячейки ключа мы размещаем в конце секции .data по адресу 00691F00 (на этом месте ничего не записывается, поскольку оно предназначено для выравнивания секции файла .data), поэтому вычитая из 00691F00 значение регистра EBP, мы получаем нужное нам смещение 128686. Номер ячейки ключа для чтения мы берем из стека с необходимым для нас смещением [ESP+14]. Все остальное понятно из приведенных возле команд комментариев.
Проходим до адреса 00569882, и внизу мы видим, что вызывается ячейка ключа защиты - 01

[Скриншот]

Немного теории. Из Руководства разработчика нам известно, что продавец ключей защиты Sentinel - Компания Rainbow - резервирует для себя первые 8 ячеек ключа защиты: от 00 до 08. В ячейке 00 записывается серийный номер ключа защиты (который постоянно увеличивается, как и для любого товара), а в ячейке 01 записывается developerID, т.е. идентификационный номер, который для каждого покупателя ключа - разработчика программ - сугубо индивидуальный. Для разработчика Passolo Компания Rainbow присвоила developerID = C5BD (это мы видели немного раньше, при просмотре функции sproFindFirstUnit). Таким образом, значение ячейки ключа защиты 01 должно быть C5BD. И это значение должно быть записано по адресу 00691А02

[Скриншот]

Запускаем программу далее, и опять останавливаемся на проверке функции sproExtendedRead. На этот раз читается ячейка ключа защиты 00. Но ведь у нас нет ключа защиты, а значение ячейки должно быть. Как же быть в этом случае? Я поступил очень просто. Поскольку серийный номер ключа защиты всегда разный, то я установлю его "от фонаря". Поскольку программа Passolo v5.0.006 была выпущена в 2005 году, и реверсинг этой программы я провожу тоже в этом году, то в ячейку 00 запишем значение 2005

[Скриншот]

Запускаем программу далее, и опять останавливаемся на проверке функции sproExtendedRead. На этот раз читается ячейка ключа защиты 0A. Оставляем значение этой ячейки равным 0000, и это значение записывается в буфер, находящийся по адресу 00692B08. Устанавливаем точку прерывания на доступе к памяти на этом адресе, запускаем программу далее, и смотрим, есть ли доступ к этому значению в программе. Видим, что нет. И опять останавливаемся на проверке функции sproExtendedRead. На этот раз читается ячейка ключа защиты 08. Опять устанавливаем точку прерывания на доступе к памяти на этом адресе, запускаем программу далее, и смотрим, есть ли доступ к этому значению в программе. Снова ничего. И опять останавливаемся на проверке функции sproExtendedRead. На этот раз читается ячейка ключа защиты 3D. Опять устанавливаем точку прерывания на доступе к памяти на этом адресе, запускаем программу далее, и смотрим, есть ли доступ к этому значению в программе. Ура, есть!!! Мы остановились на адресе 004CCFC3. Мы видим, что эта подпрограмма как-то обрабатывает значение ячейки ключа защиты 3D, при обработке использует значение ячейки 00 (в адресе 004CCFE5), и значение ячейки 01 (в адресе 004CCFE5). Немного протрассировав эту программу, мы проходим еще одну подпрограмму обработки по адресу 004CD000, и опять попадаем на подпрограмму 004CCFC0, после завершения которой идет сравнение значения регистра AX=5 (адрес 004CD381). Это означает, что в ячейке ключа защиты 3D должно быть такое значение, которое после обработки в подпрограмме 004CCFC0, в регистре AX даст нам 5. Для решения это проблемы я решил применить brute-force

[Скриншот]

Начальное значение ячейки 3D - 0000. Мы это значение на каждом цикле увеличиваем на 1, и сравниваем регистр AX с 5. Если АХ не равно 5, идем далее; при АХ=5, прыгаем на начало подпрограммы, и останавливаемся. Инструкция по адресу 004CCFF5 взята из команды по адресу 004CC9F2, которая следует сразу же после выполнения подпрограммы 004CCFC0. Все остальное понятно из приведенных комментариев. Запускаем наш brute-force, и в секции стека, по адресу 0012FD3C мы видим наше желанное значение - 0770!

[Скриншот]

Запускаем программу дальше. Идет чтение из ячеек 38, 39, 3A, 3B. Значения этих ячеек обрабатываются подпрограммой 004CCFC0, но сравнения значений нигде не видно. Поэтому оставляем их пока равными 0000. Затем читается ячейка 3E, 3C и 3F. И снова ничего. Но потом, на адресе 004CCB12 мы видим сравнение

[Скриншот]

И, как мы видим, значение регистра EAX не равно значению, хранящемуся в памяти по адресу 00685390. Поэтому заходим в CALL по адресу 004CD450, где мы опять видим вызов подпрограммы сравнения 004CCFC0. При этом, в стеке по адресу 0012FD4C, мы видим, что туда занесено значение из адреса 00692B06, относящееся к ячейке ключа 08. Все это сделано для того, чтобы намного затруднить жизнь нам, занимающимся реверсингом программ. Опять делаем brute-force, аналогично тому, что мы делали для ячейки 3D, но вместо команды сравнения AX=5, мы делаем команду сравнения AX=4150

[Скриншот]

И в стеке по адресу 0012FD4C мы видим наше желаемое значение ячейки 08 - 2DDE.
Ура!!! Мы нашли значение еще одной ячейки ключа.
Запускаем программу далее, и мы прерываемся здесь

Функция sproQuery



[Скриншот]

Эта подпрограмма вызывается из

[Скриншот]

Проходим на адрес 004D00CD, и здесь видим, что функция принимает 6 параметров

[Скриншот]

А 6 параметров принимает только одна функция - sproQuery. Нам нужно сделать эмулятор этой функции. Посмотрим немного работу этой функции. Запускаем программу до RETN, и в регистре EAX мы видим 3, что означает, что выполнение этой функции было неудачным. Исправляем значение регистре EAX на 0, делаем еще несколько раз RETN, пока мы не окажемся здесь

[Скриншот]

По адресу 004CDA21 происходит вызов функции sproQuery, поскольку мы принудительно сделали возврат функции sproQuery успешным, то произошел прыжок на адрес 004CDA32, на котором производится сравнение двух значений: из стека 0012FD78 и из регистра EDI. И, как мы видим, эти значения не равны!. Давайте посмотрим, откуда берется значение в регистре EDI. Пролистав немного листинг вверх, по адресу 004CD9F6 мы видим интересную константу [692AAC]

[Скриншот]

И в этой константе мы видим значение 0000042B. Если мы попытаемся изменить в календаре операционной системы день, то меняется и это значение в константе [692AAC]. На базе этой константы, по адресу 004CDA02 выполняется CALL 004CC3E0, который генерирует случайное число, связанное с днем года, и это случайное число по адресу 004CDA07 помещается в регистр EDI. Поэтому нам надо, при выполнении функции sproQuery, вернуть это число в стеке по адресу 0012FD78. Давайте найдем, где хранится в памяти программы это число. В отладчике входим в память, и включаем поиск сгенерированного программой случайного числа. Мы видим, что это число хранится в стеке, по адресу 0012FD7C, и в секции данных, по адресу 00692AA8

[Скриншот]

Нам, конечно, лучше всего брать это число из секции данных, которое находится там постоянно, на все время работы программы. Эмуляция функции sproQuery будет выглядеть так

[Скриншот]

Комментарии поясняют работу эмулятора функции sproQuery. По адресу 00569BB4 мы получаем значение ячейки ключа, которое сравниваем со значением ячейки, для которой создан этот эмулятор. Функция sproQuery вызывает много ячеек ключа, но, как мы увидим дальше, имеются только два вида эмулятора функции sproQuery - для ячейки ключа 0C и для ячеек ключа 10, 14, 18, 20, 24, 1С, 28, 2С, 30 и 34. Затем мы берем константу из секции .data, помещаем ее в буфер, и делаем успешной работу функции sproQuery, устанавливая в регистр EAX 0.
Запускаем программу, и, останавливаемся на чтении ячейки 09. Трассируя работу программы, мы приходим опять в подпрограмму сравнения значений ячеек ключа по адресу 004CCFC0. В этом случае производится проверка на наличия в регистре EAX значения 500. Делаем аналогичный brute-force, как мы делали его для ячеек 08 и 3D, и получаем значение, которое должно быть в ячейке 09 ключа - 0F5C. Итак, мы уже имеем значения 5 ячеек ключа защиты. Запускаем программу дальше, и, УРА!!!, программа запустилась.
Давайте посмотрим работу элементов меню программы.
Начнем с загрузки программ-надстроек. Запустив команду Параметры --> Программы-надстройки, мы прерываемся на проверке функции sproQuery. Здесь вызывается ячейка ключа 20. Нажимаем несколько раз кнопку отладчика Вернуться до Return, и мы попадаем сюда

[Скриншот]

т.е. мы попали в листинг Подпрограммы-надстройки PAIJava. Немного протрассируем, и мы попадаем сюда

[Скриншот]

Здесь опять производится сравнение значения регистра EAX со значением, находящимся по адресу [1D01A40]. Эмулятор функции sproQuery для этого и других значений ячеек (10, 14, 18, 20, 24, 1С, 28, 2С, 30 и 34) не сильно отличается от эмулятора функции sproQuery для ячейки 0C - только местом, откуда берется правильное значение константы для сравнения. Поэтому, я привожу листинг полного эмулятора функции sproQuery:

[Скриншот]

Все элементы меню программы (без созданного проекта) работают нормально.
Создадим проект и проверим работу программы при работе с проектом. И здесь видим, что все элементы меню для работы с проектом работают нормально. И вот мы добираемся до очень интересного момента - Создание лицензированного пакета перевода. Запустив команду на создание лицензированных проектов перевода, мы получаем следующую картинку:

[Скриншот]

Что-то не то. Опять загружаем нашу программу в отладчик, и начинаем думать. Почему у нас 0 бесплатных лицензий, -31509 используемых лицензий и 34027 импортируемых лицензий. Какие ячейки ключа влияют на эти данные. Я долго думал над этими вопросами, пока не обратился к справке по Passolo (раздел Лицензии пакетов перевода). Здесь на приведенном рисунке я увидел число бесплатных лицензий - 10, число используемых лицензий 0. И нет никакого числа импортируемых лицензий. Итак, первой задачей стало - убрать параметр Число импортируемых лицензий, и сделать Число используемых лицензий равным 0. Запускаем программу в отладчике, и останавливаемся На вызове ячеек ключа 38, 39, 3A и 3B в буфера по адресам 00692B14, 00692B16, 00692B18 и 00692B1A записывается значение EB84

[Скриншот]

Попытаемся в дампе исправить эти значения на 0000. И Число используемых лицензий стало равным 0. Опять делаем brute-force для ячеек ключа 38, 39, 3A и 3B, и получаем их значения - 275C. Итак, мы нашли значения еще 4-х ячеек ключа защиты. Снова запускаем программу в отладчике, и после чтения значения в ячейках ключа защиты 38, 39, 3A и 3B, читается ячейка 3E. И, нажимая несколько раз кнопку отладчика Вернуться до Return, мы попадаем во в это место

[Скриншот]

Здесь происходит сравнение двух регистров EAX и EBX. Значение регистра EBX определяется значениями ячеек 38, 39, 3A и 3B, а EAX - ячейкой 3E. И эта ячейка отвечает за число бесплатных лицензий. Я решил установить число бесплатных лицензий 32 (20 в Hex), и с помощью brute-force нашел значение ячейки ключа 3E - 265C. И теперь осталось убрать число импортируемых лицензий. Опять запускаем программу в отладчике, несколько раз нажимаем кнопку отладчика Вернуться до Return, и попадаем сюда

[Скриншот]

С помощью brute-force находим значение ячейки ключа 3F - D8A3. Снова запускаем программу, и получаем прекрасное окно приглашения для создания лицензированного пакета перевода.

[Скриншот]

Ставим флажок в окно Генерировать лицензированный пакет перевода, и прерываемся здесь

[Скриншот]

Функция sproActivate


Эта функция вызывается из

[Скриншот]

Перейдя на этот адрес, мы видим, что функция принимает 5 параметров, одним из которых является пароль на запись 4C46

[Скриншот]

И такой функцией является sproActivate. Эта функция перезаписывает имеющиеся значения ячеек ключа. Причем это значение программа прямо указывает по адресу 0056995F

[Скриншот]

Зная номер ячейки ключа, и его новое значение, нетрудно сделать эмулятор этой функции, который бы позволял записывать новые значения в ячейки ключа. Это действие я предлагаю желающим сделать самостоятельно. Дело в том, что от этой идеи я отказался практически сразу же, после небольшого обдумывания ситуации. Причиной же послужило следующее. Допустим, я записал новое значение в ячейку ключа, в памяти программы. Затем, когда я закрываю программу, естественно, все измененные данные теряются. И при новом открытии программы, мы получаем все значения ячеек ключа, связанные с экспортом лицензированного пакета перевода, в их первоначальном виде. И экспортировать назад лицензированный пакет перевода становится невозможным, поскольку программа выдает сообщение "Лицензия не использована". Сначала я попытался эту проблему решить в лоб, и отключить эту проверку, пропатчив один переход. Но разработчики программы включили в эту ветку кода проверку CRC, и программа молча закрывается. При этом переставали работать компиляция, программы-надстройки, сохранение проекта и т.д. Отключать эту проверку я не стал, а начал думать над тем, как использовать Реестр операционной системы для этой цели. И эта работа будет являться второй части статьи по реверсингу программы Passolo.

Заключение

В этой статье я постарался подробно рассказать о снятии защиты Sentinel SuperPro с реальной программы. Здесь приведено много рисунков, которые показывают сделанные мной те или иные шаги, и относительно подробные пояснения. Надо сказать, что разработчики программы Passolo смогли очень усложнить жизнь специалистов по реверсингу, введя, например, запись в ячейках ключа с помощью функции sproActivate. Довольно сложной оказалась подготовка значений из ячеек для сравнения, которые без написания brute-force, определить попросту невозможно. Но общим недостатком таких защит является необходимость сравнения значений, записанных в ячейках ключа защиты, с контрольными значениями, которые заложены в саму программу. И, несмотря на многочисленные ухищрения, эти сравниваемые элементы все-таки находятся, что и позволяет выполнить реверсинг программы. Буду очень признателен за замечания к этой статье, а также за Ваши отзывы и пожелания. Буду рад, если изложенный в этой статье материал поможет Вам провести реверсинг других программ, и хочу всем читателям этой статьи пожелать удачи.

13-07-2005
vnekrilov