Веб-разработчик знает, что скрипты, созданные в коммерческих целях, могут пойти гулять по сети с затёртыми копирайтами; не исключено, что скрипт начнут перепродавать от чужого имени. Чтобы скрыть исходный код скрипта и препятствовать его изменению, применяются обфускаторы, минификаторы и т.д. Один из самых давних и известных инструментов для шифрования скриптов на 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+EksbFLjyZwm7EQh7Q0Y6YO6pLddgsuLRlBWUC5JWhAm3KcPBcRdP9D0zkMmdPNk5VG
rMP1GxIwsA5NinHkQjWqG2pHL5nIZUvatUW+XMas3Knjf4wz9+DJoq47N1qZLDXwVzpOOupqa+Y4
k8PPXt8WNYXL4gbJnVu6NrqBqqwOrtlHUE9Sc30fMfAAEDTAVfa7ADHT2egTb5xxy9RGlDCjGlma
RxoL1LvxvYcfe48f44x/H+GVTM7dPaYyy9DozcJjt3l8EDxcD73d67cWOtDgQGixQEmBlYJO7Cvh
IAfeCBywIrDMgWfCC80uEIX+WtSmt/PuI7OXMgsNG3yVZu2HXJvXFRmXvc6748uxr+Zh0uZnAqeL
pkJB5K9H5qbMr4YM/Aig+7MhwVG3KJ0kQCEhKxJe7+7Un/jSGcwQ8HKa/90ePzH2EXazm3T87pf2
hXL/exl4L7hutt/MfDGjculaEOCaoDLlUJjJqeXJL3kFDUsiPFfEL/BwAYUqe2pJAMjWXn7YIUt7
Y1DdTUD4ob/5fwE9wQwfG6PfDLPFkrGVKFpkBa95sRuA7qgtXATacXAVzsfYMxZgbwF3RcI5IxQo
HTgnCg57vWmM/u6swJrgkz+747DWZRS1TfJZnKbdbmWIHAW11HG2FloKdWWSIronfqnuXTI/j2/R
9hX1Uim6mQowBwjS5zHZY8WFU9xE1KgETkCTsaDZODg9NYTICKs5aAdujzAtzxLWSicHZCfmpgzd
FRhqYTYE1B9wktZsItkssDaq+xlyTZ+0LGnXAC6eaH7npS7w3NRBRj9ySVTRYPXBraVuJViMIX+U
4IzHJDFSNiT818GtS7erlLKcbGn4OZ40Ee3XEiicFzVrOfOvH0rJT3LZgVqY+KMtjqaQike2P4Dd
A0SOuqOlFgQitYoo
Пользователи давно обратили внимание, что когда модуль 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
.
Автор: ruvds