Русский
Русский
English
Статистика
Реклама

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


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



Источник: habr.com
К списку статей
Опубликовано: 16.06.2020 14:11:48
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ruvds.com

Php

Отладка

Реверс-инжиниринг

Php 7

Ioncube

Скрипты

Шифрование

Категории

Последние комментарии

© 2006-2020, personeltest.ru