Как стать автором
Обновить
2675.18
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Пасхалка в ionCube — попытка разработчиков замести мусор под ковер?

Время на прочтение9 мин
Количество просмотров15K

Веб-разработчик знает, что скрипты, созданные в коммерческих целях, могут пойти гулять по сети с затёртыми копирайтами; не исключено, что скрипт начнут перепродавать от чужого имени. Чтобы скрыть исходный код скрипта и препятствовать его изменению, применяются обфускаторы, минификаторы и т.д. Один из самых давних и известных инструментов для шифрования скриптов на 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.



Теги:
Хабы:
Всего голосов 49: ↑49 и ↓0+49
Комментарии8

Публикации

Информация

Сайт
ruvds.com
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия
Представитель
ruvds