
Веб-разработчик знает, что скрипты, созданные в коммерческих целях, могут пойти гулять по сети с затёртыми копирайтами; не исключено, что скрипт начнут перепродавать от чужого имени. Чтобы скрыть исходный код скрипта и препятствовать его изменению, применяются обфускаторы, минификаторы и т.д. Один из самых давних и известных инструментов для шифрования скриптов на PHP это ionCube. Появившийся в 2002, он продолжает следить за развитием PHP и заявляет о поддержке последних версий платформы. Как я покажу в этой статье, с поддержкой PHP 7 у ionCube далеко не всё в порядке...
Модель использования PHP-шифровщиков такая, что программист продаёт зашифрованный скрипт, а покупатель скрипта на своём сервере должен установить модуль расширения, который позволит выполнять зашифрованные скрипты. Скрипт, зашифрованный ionCube, выглядит примерно так:
<?php //0059b// 10.2 72// // IONCUBE ONLINE ENCODER EVALUATION// THIS FILE IS LICENSED TO BE USED FOR ENCODER TESTING// PURPOSES ONLY AND SHOULD NOT BE DISTRIBUTED// if(!extension_loaded('ionCube Loader')){$__oc=strtolower(substr(php_uname(),//skipped?>HR+cPn5yR+EksbFLjyZwm7EQh7Q0Y6YO6pLddgsuLRlBWUC5JWhAm3KcPBcRdP9D0zkMmdPNk5VGrMP1GxIwsA5NinHkQjWqG2pHL5nIZUvatUW+XMas3Knjf4wz9+DJoq47N1qZLDXwVzpOOupqa+Y4k8PPXt8WNYXL4gbJnVu6NrqBqqwOrtlHUE9Sc30fMfAAEDTAVfa7ADHT2egTb5xxy9RGlDCjGlmaRxoL1LvxvYcfe48f44x/H+GVTM7dPaYyy9DozcJjt3l8EDxcD73d67cWOtDgQGixQEmBlYJO7CvhIAfeCBywIrDMgWfCC80uEIX+WtSmt/PuI7OXMgsNG3yVZu2HXJvXFRmXvc6748uxr+Zh0uZnAqeLpkJB5K9H5qbMr4YM/Aig+7MhwVG3KJ0kQCEhKxJe7+7Un/jSGcwQ8HKa/90ePzH2EXazm3T87pf2hXL/exl4L7hutt/MfDGjculaEOCaoDLlUJjJqeXJL3kFDUsiPFfEL/BwAYUqe2pJAMjWXn7YIUt7Y1DdTUD4ob/5fwE9wQwfG6PfDLPFkrGVKFpkBa95sRuA7qgtXATacXAVzsfYMxZgbwF3RcI5IxQoHTgnCg57vWmM/u6swJrgkz+747DWZRS1TfJZnKbdbmWIHAW11HG2FloKdWWSIronfqnuXTI/j2/R9hX1Uim6mQowBwjS5zHZY8WFU9xE1KgETkCTsaDZODg9NYTICKs5aAdujzAtzxLWSicHZCfmpgzdFRhqYTYE1B9wktZsItkssDaq+xlyTZ+0LGnXAC6eaH7npS7w3NRBRj9ySVTRYPXBraVuJViMIX+U4IzHJDFSNiT818GtS7erlLKcbGn4OZ40Ee3XEiicFzVrOfOvH0rJT3LZgVqY+KMtjqaQike2P4DdA0SOuqOlFgQitYoo
Пользователи давно обратили внимание, что когда модуль ionCube Loader загружен, то в PHP появляются две глобальные функции с очень странными именами:
_dyuweyrj4
и
_dyuweyrj4r
. Если вызвать одну из этих функций, то PHP
напечатает один из двух китайских афоризмов, и завершит
выполнение:C:\php>php -r "_dyuweyrj4(); echo 'Hmm..';"
A rat who gnaws at a cat's tail invites destruction.
C:\php>php -r "_dyuweyrj4(); echo 'Hmm..';"
Do good, reap good; do evil, reap evil.
C:\php>php -r "_dyuweyrj4r(); echo 'Hmm..';"
Do good, reap good; do evil, reap evil.
Видно, что выводимая строка не зависит от вызванной функции, и выбирается случайно.
Напрашивается вопрос: зачем эти функции нужны, и что они делают?
Простые эксперименты
Для начала посмотрим, какие параметры
_dyuweyrj4
принимает:C:\php>php -r "_dyuweyrj4(1,2,3);"
PHP Warning: _dyuweyrj4() expects at most 2 parameters, 3 given in
Standard input code on line 1
C:\php>php -r "_dyuweyrj4('foo', 'bar');"
PHP Warning: _dyuweyrj4() expects parameter 1 to be int, string
given in Standard input code on line 1
C:\php>php -r "_dyuweyrj4(1, 'bar');"
PHP Warning: _dyuweyrj4() expects parameter 2 to be int, string
given in Standard input code on line 1
C:\php>php -r "_dyuweyrj4(1, 2);"
A rat who gnaws at a cat's tail invites destruction.
Похоже, что принимает два числа, но какие бы числа ни были, печатает те же самые афоризмы.
Поиск использования
На каком-то мутном индонезийском форуме удаётся найти запощенный в 2013 пример скорее всего, полученный каким-то дизассемблером байткода PHP:
function tconnect( ){ $__tmp = _dyuweyrj4( 21711392, 920173696 ); return $__tmp[0]; return 1;}function tvariable( ){ $__tmp = _dyuweyrj4( 21720496, 920165136 ); return $__tmp[0]; return 1;}
Что интересного в парах чисел (21711392, 920173696) и (21720496, 920165136)? Внимательный исследователь заметит, что XOR чисел в каждой паре даёт 932443808. Попробуем сами вызвать
_dyuweyrj4
с парой чисел, дающих в результате XOR
932443808:C:\php>php -r "_dyuweyrj4(0,
932443808);"
Не напечаталось ничего!
C:\php>php -r "_dyuweyrj4(932443808,
0);"

Погружаемся в отладчик

Видим, что выполняется попытка чтения по адресу
[ecx+40h]
, причём ecx
равен
0x3793f6a0
переданному нами в функцию числу. Значит,
функция ожидает получить в качестве параметра значение адреса в
памяти процесса PHP, и к dword по адресу
[ecx+40h]
прибавит единицу (команда inc dword ptr
[eax]
видна чуть ниже точки крэша). Попробуем передать такой
адрес: для этого обратим внимание, что адрес, по которому
загружается основной модуль php.exe, не изменяется до перезагрузки
Windows. В моём случае это 0x00980000
. Открыв php.exe
в IDA, смотрим, какие данные доступны для перезаписи:
В качестве эксперимента попробуем перезаписать первый указатель в структуре
cli_sapi_module
.На него есть ссылка из
main+22
; при загрузке php.exe по адресу
0x00980000
эта ссылка будет находиться по адресу
0x00982d63
. Значит, в функцию
_dyuweyrj4
нам надо передать значение, меньшее на
0x40
, т.е. 0x00982d23
:C:\php>php -r "_dyuweyrj4(0x00982d23, 0x00982d23 ^
0x3793f6a0);"

Опять крэш; но уже в другой функции внутри ioncube_loader_win_7.3.dll. Что интереснее, адрес, по которому был прочитан нулевой указатель
0x14c32820+0x78
не имеет
ничего общего с переданным в функцию значением. (Парадоксально, что
проверка на нулевой указатель test ebx, ebx
осуществляется сразу же послеобращения по этому указателю.)
Заглянув в память по адресу 0x14c32820
, находим там
структуру _zend_op_array
, т.е. определение функции.
Заглянуть внутрь него удобнее всего через Immediate Window:(_zend_op_array*)0x14c32820
0x14c32820 {type=0x01 '\x1' arg_flags=0x14c32821 ""
fn_flags=0x00000100 ...}
type: 0x01 '\x1'
arg_flags: 0x14c32821 ""
fn_flags: 0x00000100
function_name: 0x06f66850 {gc={refcount=0x00000001
u={type_info=0x000001c6 } } h=0xacaf1bdb len=0x0000000a ...}
scope: 0x00000000 <NULL>
prototype: 0x00000000 <NULL>
num_args: 0x00000000
required_num_args: 0x00000000
arg_info: 0x00000000 <NULL>
cache_size: 0x131183d0
last_var: 0x06ee0e98
T: 0x00000000
last: 0x00000000
opcodes: 0x00000000 <NULL>
run_time_cache: 0x00000000 {???}
static_variables: 0x00000000 <NULL>
vars: 0x00000000 {???}
refcount: 0x600df45e {???}
last_live_range: 0x88008c00
last_try_catch: 0x00000001
live_range: 0x00000100 {var=??? start=??? end=??? }
try_catch_array: 0x14c2d958 {try_op=0x00000001 catch_op=0x000001c6
finally_op=0xddab5409 ...}
filename: 0x14c1a538 {gc={refcount=0x00000001
u={type_info=0x14c00668 } } h=0x00000000 len=0x00000001 ...}
line_start: 0x00000000
line_end: 0x00000002
doc_comment: 0x00000002 {gc={refcount=??? u={type_info=??? } }
h=??? len=??? ...}
last_literal: 0x714beccc
literals: 0x71180110
{php7.dll!zif_xmlwriter_write_pi(_zend_execute_data *, _zval_struct
*)} {value={lval=0x3314ec83 ...} ...}
reserved: 0x14c3288c {0x06ee0bc0, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000}
((_zend_op_array*)0x14c32820)->function_name
0x06f66850 {gc={refcount=0x00000001 u={type_info=0x000001c6 } }
h=0xacaf1bdb len=0x0000000a ...}
gc: {refcount=0x00000001 u={type_info=0x000001c6 } }
h: 0xacaf1bdb
len: 0x0000000a
val: 0x06f66860 "_dyuweyrj4"
&((_zend_op_array*)0)->reserved[3]
0x00000078 {???}
Как видим, нулевой указатель, вызвавший крэш это поле
_zend_op_array.reserved[3]
в определении функции
_dyuweyrj4
. Видимо, это поле используется ionCube
Loader в каких-то своих внутренних целях. Для проверки прогоним
файл из одной строчки
<?php _dyuweyrj4(0x00982d23, 0x00982d23 ^ 0x3793f6a0);
через их Online PHP Encoder. (Результат шифрования приведён
в самом начале поста.) К сожалению, это не помогает:
_zend_op_array.reserved[3]
остаётся нулевым. Зато
убеждаемся, что у выполняющейся (безымянной) функции
_zend_op_array.reserved[3]
теперь заполняется:executor_globals.current_execute_data->func->op_array
{type=0x02 '\x2' arg_flags=0x14a72141 "" fn_flags=0x08000000
...}
type: 0x02 '\x2'
arg_flags: 0x14a72141 ""
fn_flags: 0x08000000
function_name: 0x00000000 <NULL>
scope: 0x00000000 <NULL>
prototype: 0x00000000 <NULL>
num_args: 0x00000000
required_num_args: 0x00000000
arg_info: 0x00000000 <NULL>
cache_size: 0x00000004
last_var: 0x00000000
T: 0x00000001
last: 0x00000005
opcodes: 0x14a72280 {handler=0x999fc7ce op1={constant=0x00000000
var=0x00000000 num=0x00000000 ...} op2={constant=...} ...}
run_time_cache: 0x14a74018 {0x14c27ba0}
static_variables: 0x00000000 <NULL>
vars: 0x00000000 {???}
refcount: 0x14a74030 {0x00000002}
last_live_range: 0x00000000
last_try_catch: 0x00000000
live_range: 0x00000000 <NULL>
try_catch_array: 0x00000000 <NULL>
filename: 0x14a66230 {gc={refcount=0x00000001
u={type_info=0x00000006 } } h=0x00000000 len=0x00000032 ...}
line_start: 0x00200001
line_end: 0x00000001
doc_comment: 0x00000000 <NULL>
last_literal: 0x00000005
literals: 0x14a66280 {value={lval=0x0716a738
dval=5.875681226702e-316#DEN counted=0x0716a738
{gc={refcount=0x00000001 ...} } ...} ...}
reserved: 0x14a721ac {0x00000000, 0x00000000, 0x00000000,
0x14a6b200, 0x00000000, 0x00000000}
Баг багом вышибают
Как мы видим,
_zend_op_array.reserved[3]
заполнен
только у зашифрованных функций. (Это не единственная их
отличительная черта: на распечатке выше можно заметить ещё и
line_start=0x00200001
вместо правдоподобного номера
строки.) С другой стороны, указатель на
_zend_op_array
, который приходит в крэшащуюся функцию,
берётся из execute_data
, переданного в
_dyuweyrj4
неявным первым параметром так что
этот _zend_op_array
всегда соответствует вызываемой
функции, а именно, самой _dyuweyrj4
. Эта функция не
зашифрована, и поэтому у неё
_zend_op_array.reserved[3]
всегда будет нулевым. Отсюда
делаем вывод: вызов _dyuweyrj4
с правильными
параметрами неизбежно ведёт к крэшу(а с неправильными, как
мы видели к печати китайских афоризмов). Замечательно в этом то,
что получающийся крэш не преднамерен, а вызывается использованием
нулевого указателя перед его проверкой. Такой баг в коде поймал бы
любой инструмент статического анализа; но видимо, в ionCube ничем
подобным не пользуются.Что получится, если пофиксить баг в ioncube_loader_win_7.3.dll, поставив проверку указателя перед его использованием? Для этого удобнее всего использовать x64dbg:

Запускаем
php -r "_dyuweyrj4(0x00982d23, 0x00982d23 ^
0x3793f6a0);"
с пропатченным ionCube Loader, и получаем крэш
уже в новом месте опять при использовании нулевого указателя из
_zend_op_array.reserved[3]
:
Значит, от (некорректной) проверки на нулевой указатель толку всё равно не было: в следующей вызываемой функции этот же указатель используется уже без проверки. Делаем вывод, что потенциальная уязвимость, позволявшая бы нам изменять память процесса PHP по произвольному адресу, и посредством этого сбежать из сэндбокса например, вызывать функции, запрещённые администратором сервера в ionCube Loader закрыта последовательностью багов, приводящих к непреднамеренному крэшу php.exe.
Что имел в виду автор?
Я полагаю, что изначально
_dyuweyrj4
появилась для
того, чтобы привязать к зашифрованным функциям какой-то формально
корректный массив "zend_op
-ов прикрытия" потому что
ionCube Loader далеко не единственный модуль расширения, который
залазит в эти массивы. (Один из распространённых случаев, когда
расширения залазят в zend_op
-ы функций это кэширование
этих zend_op
-ов между запусками одного и того же
скрипта; другой PHP-дизассемблеры вроде того, вывод которого
запощен на индонезийском форуме.) В качестве аргумента
_dyuweyrj4
получала указатель на
_zend_op_array
зашифрованной функции, и передавала
управление расшифровщику, которым ionCube Loader заменяет
стандартную функцию zend_execute_ex
. Единственный
сценарий, когда _dyuweyrj4
могла бы вызываться это если
посторонний модуль расширения закэширует "zend_op
-ы
прикрытия" отдельно от зашифрованной функции, и потом попытается
эти zend_op
-ы выполнить. В этом случае вызов
_dyuweyrj4
с указателем на
_zend_op_array
зашифрованной функции превратится в
расшифровку и запуск самой этой функции.При переходе к PHP 7 изменилсяABI функций расширения: вместо четырёх неявных параметров
ht, return_value_ptr, this_ptr,
return_value_used
стала использоваться структура
_zend_execute_data
. Тут программисты ionCube
запутались, потому что _dyuweyrj4
теперь получает два
указателя на _zend_op_array
: один через поле
_zend_execute_data.func
, второй явно переданным
параметром. Первый соответствует самой _dyuweyrj4
,
второй зашифрованной функции, которую требуется вызвать. И вот тут
мы встречаем очередной баг: инкрементировав поле
refcount
зашифрованной функции,
_dyuweyrj4
полностью о ней забывает,и в
дальнейшем работает только со своим собственным
_zend_op_array
. Естественно, что попытка вызвать
функцию расширения, как если бы это была зашифрованная функция PHP,
приводит ко крэшу и хорошо ещё, что не к бесконечной рекурсии,
потому что _dyuweyrj4
пытается вызывать сама себя!Напрашивается вопрос: как QA в ionCube пропустил в релиз функцию, которая в принципе никогда не способна работать как задумано? То, что в план тестирования она не попала, видно ещё и потому, что в 64-битной версии ionCube Loader параметры у
_dyuweyrj4
остаются 32-битными это значит, что
указатель на _zend_op_array
зашифрованной функции
обрезается до 32 бит ещё до инкремента refcount
, и тот
крэш, который мы поймали самым первым, гарантированно случается
вообще при любом вызове _dyuweyrj4
.