Кодирование и декодирование PHP кода

Я занимаюсь восстановлением исходников PHP из закодированного вида.
В этой статье я расскажу о том, как обстоят дела с кодированием и декодированием PHP в настоящее время.

Очень краткий ликбез по внутреннему устройству интерпретатора PHP


При выполнении PHP-скрипта, он парсится и компилируется в опкоды внутренней виртуальной машины PHP.
Из каждого файла PHP получаются:
— массив классов: в каждом классе — информация о классе, свойства класса и массив методов класса
— массив функций
— «тело скрипта» — код вне классов и функций

// Классы
class A
{
	public $prop1 = NULL;
	
	public function method1() { }
}

// Функции
function FFF() { }

// Тело скрипта
echo "Hello, world!";


Для краткости, всю внутреннюю структуру скомпилированного файла, готового к выполнению, в этой статье я называю "опкодами".

Сами опкоды (операции внутренней виртуальной машины PHP) внутри какой-нибудь функции выглядят так:

  
      [0000]ZEND_INIT_FCALL_BY_NAME               - , "defined" -> - 
      [0001]ZEND_SEND_VAL (61)                    "MVMMALL", -  -> - 
      [0002]ZEND_DO_FCALL_BY_NAME (1)             - , -  -> $_z_var_120
      [0003]ZEND_JMPZ                             $_z_var_120, #0008 -> - 
      [0004]ZEND_INIT_FCALL_BY_NAME               - , "defined" -> - 
      [0005]ZEND_SEND_VAL (61)                    "IN_ADMINCP", -  -> - 
      [0006]ZEND_DO_FCALL_BY_NAME (1)             - , -  -> $_z_var_120
      [0007]ZEND_JMPNZ                            $_z_var_120, #0009 -> - 
      [0008]ZEND_EXIT                             "Access Denied", -  -> - 


Важный момент: файлы в скомпилированном виде достаточно сильно отличаются даже между подверсиями интерпретатора PHP. Оно и понятно: сам для себя скомпилировал — сам и выполнил.

Как работают энкодеры



Существуют два принципиально разных типа энкодеров.

Первые — работают исключительно средствами самого языка. Они делают код нечитаемым с помощью base64-кодирования, zip-ования, разных манипуляций со строками, и все в конце концов используют функцию eval(). Все это очень похоже на обфускаторы в Javascript. Выглядит это как-то так:

eval(base64_decode("DQplcnJvcl9yZXBvcnRpbmcoMCk7DQokcWF6c --- [cut] --- KfQ0KfQ=="));


Снимается такая защита очень просто, в самых сложных случаях — за несколько часов. Еще один крупный минус — серьезно страдает производительность. Поэтому для серьезного применения такую защиту использовать не рекомендуется.

Второй тип энкодеров использует свои подключаемые модули для интерпретатора PHP, которые называются загрузчиками (loader-ы). В этом случае, как правило, кодируется не сам исходный код, а результаты его компиляции, т.е. внутренние структуры и опкоды. Это уже гораздо более серьезная защита — даже если раскодировать сами опкоды, по ним еще надо восстановить исходный PHP-код. К тому же, с точки зрения производительности, дополнительные затраты на раскодирование часто компенсируются экономией на компилировании кода, т.е. скорость выполнения закодированных скриптов зачастую даже выше, чем у исходного кода.

Во время загрузки интерпретатора PHP, loader-ы энкодеров вешают свои обработчики на функции загрузки PHP-файлов, компиляции и выполнения, для того, чтобы работа с закодированными файлами была бы прозрачной для самого интерпретатора.

Основная сложность для энкодеров — это сделать так, чтобы опкоды, скомпилированные под одной версией PHP во время кодирования, работали бы под другой версией PHP при декодировании. Практически все loader-ы у всех энкодеров после декодирования делают необходимые правки, чтобы обеспечить такую совместимость. Главный игрок на этом рынке — IonCube — в свое время приложил огромные усилия для решения этой задачи, и его loader-ы могут на лету корректно выполнить опкоды от PHP 4.x на PHP 5.x, а по возможности — даже наоборот!

Обфускация


Также, для дополнительной защиты, большинство энкодеров дает возможность обфусцировать идентификаторы: имена переменных, названия функций, классов. Этот процесс, как правило, односторонний — наподобие хэширования, и к тому же в результате часто получаются имена с непечатными символами, которые отлично работают, но которые нельзя использовать напрямую в декомпилированных текстах. Например, как записать функцию с именем… *диктую по байтам* 0x0D, 0x07, 0x03, 0x0B, 0x02, 0x04, 0x06?

Отдельно внимание уделяется тому, чтобы обфусцированные имена работали бы корректно. Например, в коде вызывается функция checkLicense — loader обфусцирует название на лету, получает { 0x0D, 0x07, 0x03, 0x0B, 0x02, 0x04, 0x06 } и ищет уже этот ключ в хэш-таблице с названиями функций.

Zend Guard даже предоставляет run-time функции zend_obfuscate_function_name и zend_obfuscate_class_name, которые позволяют вычислить обфусцированные имена для функций и классов, чтобы облегчить связывание закодированных файлов с незакодированными.

Декодеры наносят ответный удар



Для создания декодера нужны две вещи: получать расшифрованные опкоды и уметь декомпилировать их в исходный код PHP.

Для получения опкодов кому-то пришла в голову светлая идея — сделать свою сборку интерпретатора PHP, которая бы вместо выполнения раскодированного скрипта — отправляла бы его на декомпилирование. Не нужно возиться с чтением формата энкодера и его защитами — loader энкодера сам делает всю нужную работу!

Какое-то время это работало неплохо, потом авторы некоторых энкодеров додумались заменять раскодированные функции заглушками, а реальный код прятать и доставать каждую вызываемую функцию только в момент ее непосредственного выполнения.
В ответ, авторы декодеров стали модифицировать loader-ы от энкодеров, чтобы те не применяли такие заглушки.
Довольно большой минус оказался в том, что для каждой версии PHP у каждого энкодера были свои loader-ы, которые к тому же частенько обновлялись. Приходилось много и часто патчить, хотя и несложно — просто отключить вызов функции-другой.

И вот наконец авторы одного популярного энкодера сделали следующий шаг: они стали дополнительно кодировать отдельные операнды в некоторых инструкциях и вешать свои обработчики для соответствующих команд виртуальной машины PHP. Например, код $a = 0; превращался в $a = 5;, а в момент исполнения кастомный обработчик правил 5 обратно в 0.

Это надолго затормозило тех, кто «патчил loader-ы». Во-первых, пришлось долго разбираться, почему вроде бы правильно вытащенные опкоды декомпилируются с ошибками. Во-вторых, тут уже не получалось просто поменять пару байт в loader-е.

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

Вторая часть в работе декодера — декомпилирование. Это — сложная, но интересная, чисто алгоритмическая задача.

Когда-то светлые головы написали пару неплохих алгоритмов декомпилирования для PHP. Большинство тех, кто занимается декодированием PHP сейчас, написать свой декомпилятор не могут, поэтому используют те, что есть, с минимальными правками.

Все декомпиляторы в открытом доступе правильно восстанавливают только 90-95%% кода. Остальное приходится исправлять вручную, и тут очень большую роль играют опыт программирования на PHP и опыт декомпилирования, т.к. ошибки возникают обычно типовые.

Подводя итог: никакого полностью автоматического декодирования для основных коммерческих энкодеров пока нет.

Как защититься от декодирования



Ясно, что рано или поздно любой закодированный код будет вскрыт, если это будет нужно. Но зная, как устроена работа декодеров, можно серьезно осложнить этот процесс:

  • по возможности, используйте новые версии PHP и сам язык по-полной: namespaces, traits, lambdas
  • обязательно используйте обфускацию имен, причем старайтесь не использовать короткие и типовые имена: $ch, $ci, $arr, 'license', 'valid' ...
  • декодеры «обожают» конструкции вида
    connect(...) or die(...);
    и их вариации вида:
    defined('MYCONST') or define('MYCONST', true);
    или
    ($_alias = $object_name) OR $_alias = $class;
  • особенно «хорошо» декодеры понимают редкие конструкции вида:

    $valid ? $a : exit('Error!');

    $valid ? $valid : print('Error!'); // вопрос знатокам PHP: знаете ли вы, почему тут именно print ? ;)
  • используйте «любимый» элемент языка: list( , , $c, $d) и конструкции вида while(list($k, $v) = each($arr))
  • попробуйте «десерт для декомпилятора»:

    switch($eatThis)
    	{
    	   default:
    	      $doNothing = 0;
        }
    


    (юмор в том, что декомпиляторы обычно ожидают увидеть хоть один CASE, без этого они и не понимают, что здесь была конструкция switch)
  • часть публично доступных декодеров сбоит на сложных именах методов или свойств: $obj->{'alpha' . $beta}
  • другая часть сбоит с magic-методами, включая даже __construct


Юридические аспекты



Вообще говоря, декодирование PHP-файлов после коммерческих энкодеров — нелегально. Технически это связано с тем, что для полноценного декодирования нужно декомпилировать и анализировать сами энкодеры, а закон и пользовательские соглашения это прямо запрещают.

На территории Евросоюза есть такая лазейка: разрешается «обеспечивать совместимость экземпляров ПО, которыми вы владеете, и для этого, при необходимости, обходить встроенные системы защиты». При этом прямой запрет на reverse engineering у каждого энкодера все-таки имеет приоритет.

Получается, что «я скачал программу из Интернета, которая достала мне незашифрованные опкоды» или «я использовал специальную сборку интерпретатора PHP, которая сохраняет расшифрованные опкоды» — это условно-легальные способы декодирования. «Условно» — потому что если дело все-таки дойдет до суда, еще непонятно, кто окажется прав.

Понятное дело, что создатели энкодеров предпочли бы, чтобы закодированные файлы никто и никогда не мог бы раскодировать. Но у тех, кто остался с закодированные кодом после недобросовестных фрилансеров, или после исчезновения компании-разработчика (что бывает очень часто), мнение про декодирование — диаметрально противоположное.

Интересные факты и байки



Большая часть энкодеров последние пару лет всего лишь чуть-чуть меняет формат файлов «под капотом», и выпускается под видом новой версии.

При обфускации коротких имен достаточно часто возникают коллизии. Видимо, в таких случаях, тех.поддержка энкодеров просто советует не пользоваться обфускацией.

Фрилансеры настолько часто используют куски кода из документации PHP и со StackOverflow, что словарь, составленный из идентификаторов, взятых оттуда из примеров, позволяет обычно деобфусцировать под 90% всех имен в среднем проекте.

За все время работы я встретил всего лишь пять различных декомпиляторов PHP. Три из них были написаны русскоязычными программистами, еще один — китайцем и еще один — божились, что французом. Мелочь, а приятно — горд за «наших» :)

При этом большинство русскоязычных клиентов просит по-свойски сделать работу бесплатно :)

И напоследок - пара историй
Один араб после длительного обсуждения его проекта, сообщил, что «мой бюджет — $15, но мы все понимаем… работы тут много, так что ты просто вышли все свои программы, а мы тут сами как-то все раскодируем».

Несколько раз получалось так, что декодировать определенный формат файлов мог только я. И одни и те же файлы приходили на декодирование через нескольких разных посредников одновременно.
Особенно меня повеселила такая история: негр с африканским именем и со швейцарским гражданством, разругался с фрилансером-программистом из Австралии, не заплатил ему за работу и остался с парочкой закодированных недоделанных файлов на своем сайте. Долго искал на фрилансерских биржах того, кто их раскодирует, пока наконец один индус не впарил ему свои услуги.
Три недели этот индус кормил заказчика завтраками, а сам усиленно искал реального исполнителя. Параллельно заказчик (жук еще тот) под другим именем и сам продолжал искать других декодировщиков на все тех же фрилансерских биржах. Нашел меня, отдал мне проект… и тут же, буквально через полчаса, ко мне постучался индус и с чувством явного облегчения стал уговоривать сделать и его проект тоже. Я сравнил файлы, и…
Конечно, стоило бы в воспитательных целях взять у обоих 100% предоплаты… но я просто заставил их пообщаться и разобраться между собой.
По итогам, индус до сих пор не забывает поздравить меня с днем рождения.
Заказчик даже дал мне бонус, а сейчас переехал в Эстонию (!) потому что там дешевле жить, и периодически уговаривает меня поучаствовать в каких-то его сомнительных прожектах.


UPD. Пришлось вырезать часть примера с eval-закодированым кодом, потому что Kaspersky выдавал на него предупреждение. Спасибо nokimaro!
Поделиться публикацией

Комментарии 25

    0
    Однажды по плохому стечению обстоятельств был слит движок, закоденный в ионкубе. На этом собственно порыв «хакеров» или заказчиков закончился.
    По моему это самый взломостойкий энкодер. Ходили даже слухи, что китайцы эти файлы по 15$ за штуку декодируют)
    Всё остальное, что я видел — поддается декодингу. Как сейчас, увы, не знаю.
      0
      Да, действительно слили движок Ioncube версии 6. На данный момент, актуальная версия — 8.

      Ioncube — самый качественный и проработанный энкодер, но не самый взломостойкий, если под взломостойкостью понимать 100% восстановление исходного кода.

      Текущая цена на декодирование — от $6 за файл до $10 за 100Kb, в зависимости от жадности декодировщика :)
        0
        Как на счет Zend Guard по сравнению с Ioncube по качеству?
          +1
          С точки зрения тех, кто декодирует — двояко: опкоды достаются проще (соответственно, можно всякие пароли в тексте подсмотреть даже без декомпилирования). Зато Zend Guard перед кодированием использует свой optimizer, а это усложняет декомпилирование. Причем, чем сложнее была структура исходного кода (вложенные циклы, switch-и, разветвленные if-ы), тем декомпиляторы больше запутываются.
          0
          А приходилось ли сталкиваться с Nu-Coder'ом?
            0
            За все время работы я видел только один запрос на декодирование файлов после Nu-Coder'а, это было на одном из форумов.
            По-моему, публично доступные декодеры справляются с такими файлами.

            А вообще, компания NuSphere мне нравится тем, что они создали свой стэк для PHP-разработки.
              0
              У них еще и своя IDE отличная была когда-то для PHP :)
        +1
        А никто не встречал сервисов по кодированию Zend Guard, по аналогии как на сайте ionCube?
          0
          Создатели Zend Guard решили не делать «дешевый» онлайн-энкодер для единичных скриптов. Видимо, больше мороки, чем отдачи.

          Про аналогичные пиратские сервисы я ничего не слышал и сомневаюсь, что они существуют.

          Один из сайтов по декодированию предлагает за деньги сделать так, чтобы ваши скрипты не могли бы декодировать другие сервисы — очень напоминает шутку про то, как «русский луноход отбирает образцы грунта у американского» :)
            0
            Речь про перевод кода в плохо-декодируемые конструкции такого вида?

            switch($eatThis) {
                default:
                    $doNothing = 0;
            }
            
              0
              Такие конструкции только затрудняют автоматическое декодирование, а там вроде речь шла о полной защите.

              Я ради интереса отправил запрос этому товарищу, о результатах сообщу.
                0
                Довольно быстро ответил:

                Hello I use namespace because public dezenders can not bypass it.

                <?php
                namespace name;
                class myClass {}
                
          +1
          Есть такие товарищи, которые и без обфускатора так напишут, что чтение их кода всегда вызывает реакцию «Поделись травой, которую курил» :(
            0
            Кстати с ZendGuard не все так гладко. Пару раз встречался с таким кодом (с весьма обычным на первый взгляд), который по-разному работал в закодированном и не закодированном виде. Сколько я тогда промучился с дебагом…
              0
              Это как раз тот случай, когда нужно было отрепортить баг создателям ZendGuard. Хотя, по моему субъективному мнению, только создатели IonCube серьезно относятся к таким запросам — быстро разбираются и исправляют. Могу быть и неправ.
                0
                Та Zend Guard'овцы вообще не очень адекватны, мало того что их продукт стоит намного дороже всех, он довольно спокойно открывается, еще и только недавно выпустили версию под PHP 5.4, который уже больше года как вышел.
              –1
              Как декодеры относятся к позднему связыванию?

              $var1 = 'Blah' . 'Blah' . 'Blah';
              $var2 = new $var1;
              


              А к замыканиям?
              $arr  = array(1,2,3); $z = 100;
              usort($arr, function($x, $y) (use $z) { ... });
              
                0
                Позднее связывание вообще никаких проблем не вызывает. Один из публично доступных декомпиляторов глючит с ключевым словом «new», но это скорее исключение.

                Замыкания, как я подозреваю, сейчас вообще никто из декодеров не поддерживает — т.е. корректно не декодирует. Во многом потому, что большинство кода, который приходится декодировать, пишут индусы в худшем значении этого слова :)
                +1
                Городок в западно-американской степи. Салун. За столом сидят два ковбоя, местный и приезжий, и пьют виски. Вдруг по улице кто-то проносится на огромной скорости, паля во все стороны из пистолетов. В салуне никто и ухом не ведёт. Приезжий местному:
                — Билл?
                — Да, Гарри?
                — Что это было, Билл?
                — Это был Неуловимый Джо, Гарри.
                — А почему его зовут Неуловимым Джо, Билл?
                — Потому что его никто ещё не поймал, Гарри.
                — А почему его никто ещё не поймал, Билл?
                — Потому что он нафиг никому не нужен, Гарри.
                  0
                  вопрос знатокам PHP: знаете ли вы, почему тут именно print? ;)

                  Только его и можно так использовать, print возвращает результат и ведёт себя как функция, а echo просто выводит строку на экран.
                    0
                    Абсолютно верно. Языковой конструкт print, в отличие от echo, возвращает значение, и может быть использован в выражениях.
                    0
                    Можно ещё написать скрипт, который пробежится по всему вашему проекту и заменит все константы и содержимое текстовых переменных с помощью chr(), перед этим пропустив через обфускатор а затем bcompiler. Находил на форуме каком-то тему где человек писал что декодирует любые скрипты, в итоге мне отказал ) так что схема неплохая, рекомендую.
                      0
                      Я бы не сказал, что это удачный подход. Во-первых, массовое использование chr ударит по производительности. Во-вторых, bcompiler — отлично декодируется, тем более, что это проект с открытым исходным кодом. И, к слову, с bcompiler-ом больше мороки тем, кто кодирует, чем тем, кто декодирует :)

                      Не знаю, почему ваш «человек с форума» отказался декодировать ваш вариант. Возможно, поленился возиться.
                      0
                      Если у вас такой серьезный код на продажу, который требует кодирования — думаю, можно критичные места вынести в extensions.
                        0
                        Столько воды, а как в итоге восстанавливать php из опкодов, так и не ясно…

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое