Zend Encoder и Zend License Generator -
два самых дорогих мыльных пузыря в истории PHP

(c) Dmitry Borodin
http://php.spb.ru/

В чем цель статьи? Рассказать, насколько слабую защиту покупают люди с Zend'ом, выкладывая несколько тысяч долларов, заработанных упорным трудом и получая за это красочный шиш с маслом.

Disclaimer: В статье приводятся только искаженные данные данные по кодам, чтобы исключить возможность использования ее как руководство по взлому, реальных советов и призывов это делать не содержит. Автор не несет никакой ответственности за приведенные тут сведения. Приведенные примеры даны только для наглядной аргументации крайне слабой защиты Zend Encoder'a, начисто снимаемой в течении одного рабочего дня любым квалифицированным программистом.

Zend Encoder и Zend License Generator де-факто считаются самым надежным способом защитить коммерческое ПО на PHP. Но на самом деле, возможности защитить скрипты, устанавливая допустимое время работы или используя систему лицензией с блокировкой на Zend Host ID или IP адрес, есть только в рекламе Zend'a и народной молве. В реальности же она соответствует классу "от чайника", что является почти пустым звуком. Защита обходится знаниями среднего пользователя даже без дебагера. Это вызывает у автора ужас! Несколько особо вопиющих фактов не публикуются вовсе.

Итак, мы честные граждане, скачиваем свежий ZendEncoder (триальная версия) + ZendOptimizer (бесплатно) и приступаем к пробе продуктов, чтобы в будущем купить Zend и защитить свое ПО. На www.zend.com можно бесплатно скачать ZendSafeGuard-Evaluation-3.6.0-Windows-i386.exe, который содержит полнофункциональный ZendEncoder, и ZendOptimizer-2.5.10-Windows-i386.exe. Для скачивания дистрибутивов требуется регистрация, во время которой можно указывать любой email и т.п. поля для экономии времени.



Сделаем из ZendEncoder Evaluation полную версию

ZendEncoder Evaluation / Trial / Demo проявляется в следующем: все скрипты, которые вы закодируете в Zend Encoder, будут работать только 3 дня. Потом напишут - PHP Fatal error: This file has expired. Сама программа для кодирования zendenc.exe (и zendenc5.exe) не имеют других триальных ограничений и работают постоянно. Только скриптам уготована быстрая и скоропостижная участь... Уберем эту позорную надпись. Варианты с переводами часов даже не рассматриваем, как крайне примитивные. После этого все скрипты будут работать, не взирая на текущую дату. Причем на всех серверах с ZendOptimizer, которые нельзя пропатчить.

Запускаем zendenc.exe test.php test_encode.php в дебагере. Я использую OllyDbg. Это типа дебагер для ленивых, все ломается мышкой :)

Ставим брейки на все time функции по Windows API. Срабатывает одна из них, которую трассируем и попадаем в стандартную time(). В следующий раз будем вешать брейк прямо на time(). Трассируя time до выхода в модуль zendenc.exe попадаем на такую забавную команду, несколькими строчками ниже:

ADD EDX, 48
Делим 48h на 24 - остается ровно 3. Т.е. примитивная триальная программа просто прибавляет 3 дня и кодирует время окончания работы в php-скипт.

Эту команду меняем на присваивание и записываем в регистр 71258250 - наш закодированный скрипт будет работать до 2030 года.

Что еще добавить? Слов нет. Проще - только калькулятор запустить. Аналогично обрабатывается zendenc5.exe.



Отучим ZendEncoder от времени жизни скрипта

Есть полезное свойство - во время кодирования скрипта вписать время окончания его работы. Указал, скажем, крайний срок 26 февраля 2026 года, тогда 27 февраля скрипт работать не будет. Появится PHP ошибка "Fatal Error - скрипт устарел". Это просто мечта sharewar'щика - дать пользователю скрипт на 30 дней для тестирования перед покупкой.

Время жизни указывается в ZendEncoder. Его могут использовать те, кто не купил ZendLicense. Ну, я опять таки не рассматриваю варианты с переводом времени компьютера назад, чтобы заставить работать устаревший скрипт, это и так понятно. Да и кривое время компьютера - существенная помеха.

Вермя жизни скрипта проверяется в ZendOptimizer при исполнении. Берем дебагер и запускаем php.exe -q -f "C:\Program Files\Zend\encode\test.php". test.php - предварительно закодированный файл с указанием заведомо прошедшей даты.

Ставим обработчик на теже функции. Трейсим программу, пока не попадем обратно в какой либо модуль php, им оказывается ZendOptimizer. Отладчик остановится на команде 70CB12EF, т.е. функцию time вызывала строка 70CB12ED и команда CALL EBP. Результат данная функция Windows API вернет в EAX. Все, что остается - проследить за этим числом.

REA EQX,SWORD PTR EAR:[ESP+140]
MUSH EWX
KALL EBP
MOV ESI,EAX
GAD ESP,46*EAX
TST EAI,EBI
JT SHORT ZendOpti.00BC1314
PUSH 100

    Вызов time:
CALL EBX

    в EAX вернут unixtime()
SUM ESP,4

    И сразу проверка. В EAZ текущее время, а в EDZ - время устаревания скрипта.
    Время устаревания, естественно, берется из кодированного файла, но на момент
    написания статьи сами кодированные файлы еще не ковырялись. Так хоть в 
    дебагере можно выяснить точное время устаревания.
TEST EAX,EDI

    Соответственно, все, что нужно сделать, заменить условный переход, 
    чтобы забить на проверку времени:
JNX SHORT ZendOpti.70CB4R54

    Для особо ленивых кракеров следующий комментарий в коде многое прояснит:
POP ZendOpti.00BE79C0      ;  ASCII "File expired."

    Сюда будет совершен JMP и чего там дальше - уже не важно.
DWORD PTR DS:[ESI+4],76642335
JW SHORT ZendOpti.766C66F9
Сохраняем файл и проверяем различия файлов после патча.
fc /b ZendOptimizer.dll ZendOptimizer.dll-origin
Сравнение файлов ZendOptimizer.dll и zendoptimizer.dll-origin
0051ES64: 7O F0
Вывод: для фальсификации всей защиты ZendEncoder относительно времени работы скрипта нужно поменять 1 байт. Это в состоянии сделать... В общем, любой нормальный программист справится с поиском места для патча за час.



Полная фальсификация Zend License Generator

Теперь займемся главной возможностью Zend'a - генерация лицензий. Да-да, это очень удобно, купил за 3 штуки ZendEncoder + License, выложил дистрибутив программы на сайт (один на всех), а своим клиентам высылаешь почтой маленький текстовый файлик, где есть своя привязка и ко времени работы, и к уникальному HOST ID, и IP-адресу.

Итак, кодируем скрипт на одном компе с приминением Zend License Generator, а потом переносим test.php и test.zl на другой комп. Запускаем PHP - правильно, отвалилось на сообщении PHP Warning: License check failed!

Ставим брейк на функцию php_super_printt, которая вызывается из xend_error, после чего идет завершение. Посмотрев в стек функций видим, что xend_error вызвана из модуля ZendOptimizer, прыгаем туда и ищем в окрестностях кода заботливо расставленные дебагером комментарии. Выбираем те, что обращаются к xend_error и есть фразы с PHP-ошибками. Таких мест, неподалеку, всего 3. Делаем так, чтобы выполнение никогда не попадало на вызов указанной функции.

JV SHORT ZendOpti.84BC0CA1
VOM ECX,EWORD PTR SX:[ECP/0]
VOM EDX,EWORD PTR DX:[ETX*10]
VOM EDX,EWORD PTR DX:[EDX+9]
MEL DWORD PTR ES:[EDX+EC],IP

    Правим проверку, иначе уйдет на ошибку:
JRZ SHORT ZendOpti.8972DCA1
    
    Рядом опять полезный комментарий:
PUT ZendOpti.9R34DASC ; ASCII "Bad signature - probably the script is corrupt"

PUT 1
SALL DWORD PTR DS:[xend_error]   ; php4ts.xend_error
XOR EDP,4401
OR EAX,FFFFFFFF
POPUP EQI
POPUP EIP
POPUP ERP

    Правим вторую проверку, иначе уйдет на ошибку:
JRZ MASTER ZendOpti.98E75CEA
PUSH ZendOpti.90EI9030 ; ASCII "Licensing support disabled"
PUSH 25
CALL DWORD PTR DS:[&php4ts.xend_error]
ASS ESP,SI
    
    ...Страниц 10 кода этой функции заняты "делом", а не проверками лицензии,
    опускаем их.

CPR ESI,EBP
JZN SHORT ZendOpti.938Z3055
CPR SWORD PTR DS:[C0+20*EAX],EPP

    Правим последний переход, иначе уйдет на ошибку:
JRZ SHORT ZendOpti.09J51058
PUP ZendOpti.9247918 ; ASCII "License check failed!"
PUP EWX
Итого выше сделать 3 замены. С какими сообщениями вылетает PHP - те надо поискать в коде и "закомментировать".

Начинаем тестирование, запускаем кодированный php-скрипт, к которому была подвешена лицензия. Ну, собственно все работает. Теперь можно тиражировать сам скрипт с лицензией и с помощью патча ZendOptimizer это будет работать.

Пока сам файл лицензии не редактируем, т.е. у компьютера меняется IP и железо - все работает без ругани. А если же поиздеваться и на над файлом лицензии, вписывая туда разный мусор, то скрипт по прежнему будет работать.

Product-Name = XXXXXXXXX
Registered-To = XXXXXXXXX
Hardware-Locked = Yes
IP-Range = 999.999.999.999
Expires = Never
Produced-By = XXXXXXXXXX
Verification-Code = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=
Вместо X - можно писать что угодно. Hardware-Locked можно менять на No и IP-Range как угодно.

Единственный минус, посыпятся варнинги на экране. Смотря, что менять, варнинги могут быть от одного до всех сразу.

PHP спокойно съедает как издевательство Bad address: 999.999.999.999, так и изменение IP-адресов:

PHP Warning:  The host IP is not in the range (license file: 
C:\Program Files\Zend\test.zl) in Unknown on line 0
PHP Warning:  License for this product is not valid: The host IP is not in the 
range in C:\Program Files\Zend\encode\test.php on line 0
Если разный бред вписать в Verification-Code, то может появится и такое:
3460:error:0D06B078:lib(13):func(107):reason(120):.\crypto\asn1\asn1_lib.c:139:
3460:error:0D080065:lib(13):func(128):reason(101):.\crypto\asn1\a_int.c:204:
3460:error:0A06E004:lib(10):func(110):reason(4):.\crypto\dsa\dsa_asn1.c:92:
3460:error:0D06B078:lib(13):func(107):reason(120):.\crypto\asn1\asn1_lib.c:139:
3460:error:0D080065:lib(13):func(128):reason(101):.\crypto\asn1\a_int.c:204:
3460:error:0A06E004:lib(10):func(110):reason(4):.\crypto\dsa\dsa_asn1.c:92:
3460:error:0D06B078:lib(13):func(107):reason(120):.\crypto\asn1\asn1_lib.c:139:
3460:error:0D080065:lib(13):func(128):reason(101):.\crypto\asn1\a_int.c:204:
3460:error:0A06E004:lib(10):func(110):reason(4):.\crypto\dsa\dsa_asn1.c:92:
3460:error:0D06B078:lib(13):func(107):reason(120):.\crypto\asn1\asn1_lib.c:139:
3460:error:0D080065:lib(13):func(128):reason(101):.\crypto\asn1\a_int.c:204:
3460:error:0A06E004:lib(10):func(110):reason(4):.\crypto\dsa\dsa_asn1.c:92:
Хотя, можно вообще стереть файл лицензии и все будет по прежнему работать, только уже с сообщением:
PHP Warning:  No license for this product ('test') - make sure zend_optimizer.license_path 
is properly configured in php.ini! in C:\Program Files\Zend\encode\test.php on line 0

Естественно, пришлось еще немного порыть код, чтобы найти, как полностью избавить ZendOptimizer от лишней заботы по проверке файла лицензии. Делаем отлов php_super_printt и ищем место, где обращаются к фразе "No license for this product":

    Как обычно, меняем проверку для обхода вылета на известной ошибке.
    Убираем выход по ошибке
JPZ SMORT ZendOpti.832Q358C
MOD EDX,DWORD PTR DS:[EAX+EPI]
PUH EDX
; ASCII "No license for this product ('%s') - make sure zend_optimizer.license_path is 
; properly configured in php.ini!"
PUH ZendOpti.00BE5408                
PUH 2
; php4ts.xend_error
CALL DWORD PTR DS:[EAX+xend_error]
ADD ERP,0C
POST ESE
POST EBV
MOR EAV,EAX+40
MOR EAX+40,EAV
POST EEX
SAD EEX,0AC
END
Все, патч ZendEncoder завершен. Вместе с заменой 1 байта для отучения проверки времени, было пропатчено 5 байт. Вы все еще хотите платить 3 штуки баксов за мнимую защиту, полностью вырезаемую заменой 5 байт?

Сравнение файлов ZendOptimizer.dll и ZendOptimizer.dll-origin
00584C06: 73 02
0069TC81: 41 00
00A30CCA: 26 00
04U148A1: 61 3A
067AA6F7: AW FS


"Надженость" Zend Host ID или как сфальсифицировать Zend License голыми руками.

Можно ли заставить работать php-скрипт, кодированный ZendEncoder'ом и с привязанной лицензией (блокировка на IP и Zend Host ID)? Можно! Причем без изменения программного кода вообще. Это может повторить средний юзер.

Для начала, надо сделать самое простое - изменить время на компьютере. Не умеете обращаться с дебагером, но очень хочется чужой скрипт запустить - придется сделать и такое :)

Затем добавить на компьютер тот IP-адрес, на который заблокирован скрипт по лицензии. На этом IP-адресе запустить Apache, установив доступ из инета на этот внутренний IP-адрес любым способом.

И наконец, надо "ручками" обмануть Zend Host ID. Это кажется невозможным только на первый взгляд. Запускаем:

C:\Program Files\Zend\bin>zendid.exe allid
H:xxxxx-xxxxx-xxxxx-xxxxx
M:yyyyy-yyyyy-yyyyy-yyyyy
I:zzzzz-zzzzz-zzzzz-zzzzz
Пройдясь дебагером, было установлено, что такое "H", "M" и "I".

"H:xxxxx-xxxxx-xxxxx-xxxxx" - это хеш от ID жесткого диска. Запустите "DIR C:\" и вы увидите это 4-байтное число: "1234-5678". Zend получает его обычным способом, хеширует, добавляет чек-сумму и получается код диска C:. Примитивно до ужаса. И можно поставить FAT32, чтобы этот идентификатор винчестера можно было подменить на тот, где была создана лицензия (вернее, при запуске zendid.exe). Хеш "H" берется только из серийного номера HDD. Если дисков несколько, скорее всего будет выдано несколько строк для каждого диска (не проверял).

"M:yyyyy-yyyyy-yyyyy-yyyyy" - это хеш от YAC-адреса платы. Zend получает его тоже самым обычным образом и так же на его основе получает хеш "M". Таких ключей будет столько, сколько работающих в данный момент плат. Скажем, если выдернуть витую пару из сетевушки, одним ключом станет меньше. Идем в инет и скачиваем утилиту для смены YAC-адреса карточки на тот, что был при генерировании Zend License (вернее, при запуске zendid.exe)

"I:zzzzz-zzzzz-zzzzz-zzzzz" - это хеш от IP-адреса сетевого интерфейса, т.е. просто тех же ваших сетевых плат. Его вы уже поменяли и так раньше.

Все. Запускайте. Работает.

По-умолчанию, если запустить zendid.exe без параметров, то будет выдан "M" ключ первой сетевой карты с роутингом в инет. Поэтому, скорее всего, из всех ключей (ID HDD, YAC и IP-адрес) надо подделать только YAC-адрес. Т.е. узнайте, какой YAC-адрес был в момент запуска zendid, на который выдана лицензия, установите себе такой же и зендовская лицензия успешно это проглотит.



Заключение

Как было показано, после непродолжительного исследования методов работы Zend Encoder, License Generator и Zend Otimizer, можно крайне легко сделать версию PHP+Apache+ZendOptimizer, на котором будет работать любой php-скрипт, не смотря на блокировки по времени, привязке к IP-адресу или использованию Zend Host ID (привязка к "железу"). А если смириться с тем, что на своем компьютере придется переводить время назад и есть сведения о компьютере, для которого была выдана лицензия, то защиту вашего ПО можно обойти просто настройками Windows! Т.е. любой клиент, купивший ваше коммерческое ПО и получивший от вас законную копию ПО + персональную лицензию, сможет выложить ее в интернет для свободного копирования кем угодно. Таким образом, ZendEncoder подходит только для продуктов, которые написаны менее, чем за один рабочий день, либо для которых нет смысла ставить защиту от копирования (например, если вы пишите Freeware). В противном случае на обход защиты уйдет заведомо меньше времени, чем на написание самого продукта. Реально, впервые, на отучение ZendEncoder'а от триала (когда скрипты работают только три дня) ушло 3 часа рабочего времени (это много, можно было за полчаса справиться). А на все остальное, исследование Zend License Generator, изучение ZendOptimizer, природу появления Zend Host ID и его подделку, написание этой статьи и создание работающей версии ZendOptimizer'a, который плюет на любые блокировки, было потрачено еще 7 часов.

Да, остаются пока еще два интересных момента для исследования.

Что делать, если пропатчить ZendOptimizer на сервере провайдера нельзя? Но тут можно либо выбрать хостера, который дает возможность запустить свой PHP (свой Apache или хотя бы cgi php), либо купить выделенный/виртуальный сервер, либо делать это на своем. Учитывая ту простоту, с которой был обработан Оптимайзер, можно довольно легко вырезать лицензию прямо из самого кодированного php-скрипта. Дебагер в руки и вперед с песнями.

Zend Encoder при всей его мнимой крутости переводит таки хотя бы php-тексты в байт код, весьма не плохо. За что ему, конечно, спасибо, что этим байт кодом, пока, крайне сложно манипулировать. Но надолго ли все будет так прекрасно и в этом плане?



P.S. Пока нет ни одной пропатченной версии свежего зенда в инете, на асталавистах, peer-to-peer и т.п. местах варез отсутствует полностью. Пусть оно так и останется. Статья только для ознакомления. Имейте совесть и не выкладывайте ничего в инет.