php-elephant[1]Да, мы снова возвращаемся к теме поиска уязвимостей в PHP-скриптах. Предугадываю твой скептический настрой, но не закисай так быстро! Я постараюсь освежить твой взгляд на возможности исследования кода. Сегодня мы посмотрим, как можно найти уязвимости в условиях плохой видимости, а также ты узнаешь о возможностях динамического анализа кода, которые нам рад предложить сам интерпретатор PHP.

Цель дайвинга

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

А теперь зададимся таким вопросом — как ты ищешь уязвимости в веб-приложениях? Дай угадаю. Скачиваешь движок, начинаешь ознакомление с исходниками, грепаешь его на предмет наличия разных сомнительных функций или ищешь уязвимые куски кода по шаблону, запускаешь сканер, наподобие RIPS… Ну, если исходник уже проверен и знаком, то можно задачу упростить и просто сравнить версии движков инструментом типа WinMerge. Но что делать, если, допустим, движок обфусцирован или занимает необъятные просторы жесткого диска? Конечно, можно пытаться проделать всю черную работу вручную. Допустим, попробовать деобфусцировать, но результат не всегда удовлетворяет нашим требованиям. Можно положиться на уже упомянутые методы обнаружения уязвимостей и копать исходники до посинения. В общем, это не вариант, когда есть другой метод — если не более перспективный, то уж точно необходимый.

Исходный код мы вообще не будем трогать, нам даже смотреть на него не нужно. Я предлагаю тебе спуститься на уровень чуть ниже, чем тот, на котором ты привык работать с веб-приложениями, в частности с PHP. Ты определенно слышал что-то про Zend, хакинг ядра PHP, опкоды и тому подобное. А может быть и вовсе писал расширение для PHP, пусть даже “hello, world”? Тогда тебе будет еще проще, но обо всем по порядку.

Инструктаж

Хочу тебя предупредить, что документирован Zend Engine весьма скудно. Есть книга, посвященная тому, как расширять PHP — “Extending and Embedding PHP”, но той уже пять лет, да и не вся информация там присутствует. Кое-какая информация представлена в книге “Advanced PHP Programming”, но, опять же, книге целых семь лет. Есть кое-что в самом мануале PHP, периодически встречаются разные огрызки в интернете… Большинство актуальной и нужной информации можно узнать из исходников других проектов, интерпретатора PHP и различных презентаций. Чтобы работать с инструментарием, о котором я расскажу чуть позже, тебе нужно понять, что вообще происходит с кодом, когда его выполняет интерпретатор PHP. Я не буду сильно вдаваться в подробности, так как это может занять объем дюжины журналов и выходит за рамки темы. Но данных деталей тебе вполне хватит, чтобы понять сабж и двигаться дальше самому.

Если представлять картину обработки веб-приложения в упрощенном виде, то участвуют четыре компонента. Первый — ядро PHP, которое разбирает запросы и занимается файловыми и сетевыми операциями. Второй компонент — это виртуальная машина Zend Engine, в которой происходят нас интересующие процессы: компиляция и выполнение скрипта, а также распределение памяти и ресурсов. Третий компонент — это обычные расширения PHP типа mysql, zlib, curl и тому подобные. Четвертый — это SAPI или серверное API, такое как CLI, mod_php, fastcgi.


Примерно так выглядит цикл пищеварения PHP-скриптов

Теперь разберемся с тем, что происходит со скриптом, когда тот попадает на выполнение PHP. Для краткости я пропускаю весь процесс инициализации и действия, совершаемые после того, как выполнилось приложение, — нам сейчас это не важно. В общем, после завершения инициализации происходит лексический анализ файла — разбор на токены, затем синтаксический анализ, где определяется их грамматическая структура. Образуется байт-код. Это этап, который называется компиляцией. Затем полученный байт-код (op_array) выполняется при помощи zend_execute(). Проход по массиву опкодов осуществляется два раза, так как необходимо заполнить недостающую информацию, недоступную после первого прохождения. Одна из многих причин такого алгоритма — это необходимость в нахождении адресов для таких опкодов как разновидности JMP, CALL, SWITCH. Еще имей в виду, что при инклуде скрипта процесс возвращается к точке компиляции файла, а при вызове метода или функции — к выполнению байт-кода. Глянь на соответствующую картинку, это должно помочь тебе сориентироваться. Кстати, расширение APC, закешировав опкод, в дальнейшем пропускает весь процесс компиляции, за счет чего и добивается прироста производительности.

Ну а теперь поподробней про байт-код. Байт-код, про который я говорю, это своего рода ассемблер для виртуальной машины Zend. Он представляет из себя упорядоченный набор инструкций — массивы опкодов op_array. Здесь содержится такая информация как название функции и ее тип, имя файла, номер исполняемой строки, строки опкодов и так далее. Строки опкодов, в свою очередь, вмещают в себя то, что представлено в структуре _zend_op. Данная структура определена в файле Zend/zend_compile.h и выглядит следующим образом:

struct _zend_op {
opcode_handler_t handler;
znode result;
znode op1;
znode op2;
ulong extended_value;
uint lineno;
zend_uchar opcode;
};

Операнды op1 и op2, которые также представляют из себя структуры, могут иметь один из пяти типов:

  • VAR — представляет из себя ссылку на реальную переменную (символ $);
  • TMP — временная переменная для содержания промежуточных значений во время таких операций, как математические вычисления, конкатенации (символ ~);
  • CV — компилированная переменная, оптимизированный вариант VAR (символ !);
  • CONST — константные значения типа чисел, строк, и так далее;
  • UNUSED — неопределенный операнд;

Результирующий операнд result, который не всегда заполняется, может иметь типы VAR, TMP, CV. Самый последний элемент — это один из номеров опкодов, от 0 до 153 (PHP 5.3.6), которые определены в Zend/zend_vm_opcodes.h. От версии к версии их число может меняться, а опкоды с 116 до 131 не определены.

Вообще, многие внутренние механизмы PHP регулярно подвергаются самым разным изменениям ради целей оптимизации и внедрения нового функционала. И про версию 4 забудь, акцент ставится на версию 5.1. и выше. Стоит отметить, что весьма существенные изменения произошли как раз в версии 5.1, в том числе был добавлен тип CV, а на каждый опкод стало 25 обработчиков опкодов. Между прочим, это является одной из главных причин, позитивно повлиявших на скорость работы интерпретатора. А когда ты увидишь !n в листингах, то знай, что в прошлой жизни это была самая обычная переменная PHP типа $var.

Кстати, не задумывался ли ты о том, что происходит с текстом (допустим, html), когда тот не включен в PHP, вот как тут:

<?php
$var = 1;
?>
<html>
...

PHP делает просто — компилирует в выражения ECHO. То есть такой, казалось бы, незадействованный участок тоже участвует в процессе обработки кода. И даже если там будет одинокий символ пробела или перенос строки, то PHP обработает и их. Все аналогично тому, как происходило бы, будь там echo(). Ну это так, тебе на заметку.

Акваланг, ласты и прочее

Некоторые коварности bytekit’а.

Хочу предупредить о том, что могут быть проблемы при дампе опкодов на версиях PHP 5.2.*. Лично у меня на некоторых платформах графики строились не совсем корректно. В то же время на PHP 5.3. все работает как положено. Также советую увеличить объем памяти, доступный PHP, — я себе выставил 384 Мб, так как некоторые скрипты (например, scan_eval.php) пожирают нещадно много памяти. При поиске уязвимостей на том уровне, про который мы говорим, можно работать непосредственно с опкодом, а можно и вовсе реализовать автоматический мониторинг всего и вся — переменных, методов, функций. Конечно, последний вариант более предпочтителен, но для начала нужно и в первый вникнуть. А потом уж все в твоих руках.

Для дампа опкодов PHP существуют как минимум два расширения — Vulcan Logic Dumper (vld) и bytekit. Это самые надежные варианты из тех, что я нашел, да нам больше и не нужно. Установка расширений достаточно проста — вводишь в консоли следующие команды:

phpize
confi gure
make
make install

Теперь остается поправить php.ini, добавив такие строки:

extension=bytekit.so
extension=vld.so

Хотя можно подключить расширение, приписав строку -d extension=bytekit.so во время вызова PHP. Вот и все, готов к труду и обороне.


Так выглядит дамп vld

Пожалуй, наиболее полезное и интересное расширение — это bytekit (который сначала назывался bytedis) от Стефана Эссера. Он как раз и создавался для наших целей — в нем реализованы дизасм опкодов, визуализация потока выполнения приложения (дампя информацию в формате *.dot) и недоступная простым смертным улучшенная визуализация при помощи Zynamics BinNavi (используя скрипт php2sql). Вообще, идея написания такого расширения появилась из-за неудовлетворенности уже существующим расширением parsekit, которое больше не поддерживается, работает крайне нестабильно, вываливаясь в segfault, да и криво к тому же. Изначально при помощи bytekit Стефан решал задачу облегчения поиска уязвимостей в приложениях, накрытых защитой типа ZenGuard, ionCube. Однако для получения опкодов сначала необходимо их восстановить, решая проблемы обфускации, защиты перехвата функций и так далее. Но эта весьма объемная тема и заслуживает отдельной статьи, поэтому для начала обратим наше внимание на более простые вещи. К тому же наша статья не про снятие защиты, а про поиск уязвимостей.

Еще одно интересное расширение (опять от Стефана Эсера) — это evalhook. Вероятно, по названию ты уже догадался о его принципе работы и назначении — перехват всех eval() а также preg_replace() с модификатором e, create_function(), assert(). Когда скрипт попытается выполнить код при помощи данной конструкции или одной из функций, evalhook перехватит такой вызов, покажет строку, которую необходимо выполнить, и спросит, продолжить ли выполнение. Реализация расширения достаточно проста — ставится хук на zend_compile_string(), который компилирует строку, при необходимости спрашивает пользователя о дальнейших действиях и затем отдает управление обратно оригинальной функции. Стефан представил evalhook в прошлом году во время проекта “Month Of PHP Bugs”.


Визуализация потока выполнения PHP-скрипта

Однако мне evalhook не понравился тем, что его можно запускать только из консоли. Поэтому я решил добавить функционала в расширение — теперь можно гонять скрипты из браузера, а расширение на фоне будет писать в лог-файл все то, что попадает в вышеупомянутые функции. Более про него рассказывать не буду — подробную инструкцию по применению и само расширение ищи в Сети.

В принципе, это все, что доступно публично для динамического анализа и имеет какой-то смысл. Но дальше ты увидишь, что не так уж и проблематично построить мощный инструментарий. Остается только гадать, что есть в арсенале у серьезных исследователей :).

Начинаем погружение

Вот мы и добрались до самого интересного момента — практики. В качестве примера возьмем DVWA версии 1.0.7 и рассмотрим некоторые, так сказать, стандартные уязвимости — SQL-инъекцию и FI. Но сначала небольшая настройка — лучше отключить XDebug, он будет вставлять нам ненужные опкоды. В системе у тебя должен присутствовать dot, ну и не забудь установить наше заведомо уязвимое приложение. Вместе с расширением bytekit поставляются скрипты, которые делают много полезных вещей. Позже мы рассмотрим парочку, но сейчас нам нужно лишь получить графическое представление потока исполнения приложения в виде опкодов. Скрипты лежат в папке examples, зайди туда и запусти такую команду:

php php2dot_simple.php /var/www/htdocs/h/dvwa/vulnerabilities/sqli/source/low.php sqli-l

В качестве первого аргумента данный скрипт принимает название тестируемого скрипта, второй аргумент — это название папки, куда дампить результат. В папке должны появиться *.dot-и *.svg-файлы. Если тебе не по нраву *.svg, то из *.dot можно сконвертировать в *.png такой командой:

dot -Tpng -o ./xxx.png xxx.dot

Думаю, здесь ничего пояснять не нужно. Ну а теперь приступаем к анализу. Открыв график, обрати внимание на второй блок слева, который мы будем исследовать. У тебя должно быть примерно такое же полотно…

Почему примерно? Просто очень вероятно, что наши листинги не будут совпадать тютелька в тютельку из-за разницы версий PHP, но это не критично. Еще один момент — листинг, который ты видишь тут, отличается от графического наличием двух колонок впереди опкодов. Я просто сделал дамп при помощи vld:

php -d extension=vld.so -dvld.active=1 /var/www/dvwa/vulnerabilities/sqli/source/low.php

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

Итак, перед нами самая банальная SQL-инъекция. Где же это видно? Начнем с наиболее понятного: нам знакома строка SQL-запроса на линии 9, под номером опкода 6. Во временную переменную ~5 сохраняется данная строка, затем, на следующей линии, к этой же переменной добавляется компилированная переменная !0. Последний символ, который сохраняется в этой переменной — это 39, что означает кавычку. Опкод ASSIGN завершает все действия 9 строки присвоением переменной !1 значения ~5. В данном квартете интерес представляет компилированная переменная !0. Нам важно понять, откуда у нее растут ноги. Для этого вернемся в самое начало исследуемого блока…

На первых двух строках дампа происходит проверка наличия индекса ‘Submit’ в массиве $_GET, а JMPZ хочет прыгнуть по адресу 48 в случае, если результат — 0, то есть, когда проверяемый элемент отсутствует.

На графике видно, что это прыжок к выходу — RETURN 1. На линии под номером 7 довольно очевидно, что последующие три строки делают какие-то манипуляции с глобальной переменной $_GET. Здесь FETCH_R читает значение массива в $2, затем FETCH_DIM_R получает значение элемента ‘id’ и записывает в $3. Обрати внимание на *_R — это означает чтение ака read. Есть еще и *_W — write, для записи, и *_RW — read/write, для чтения и записи. Ну а далее в дампе находится нам уже знакомый опкод присвоения, который занимается тем, что снова копирует значение переменной $3 (не путай с обычной переменной PHP) в !0. Идем далее. Следующий опкод SEND_VAR занимает место первого аргумента для последующей функции, читая значение первого операнда, в данном случае !1. Второй операнд означает порядковый номер аргумента. Судя по графику, DO_FCALL вызывает функцию mysql_query() и полученное значение сохраняет в $7. Вероятно, ты заметил, что на данном участке не было никаких других вызовов функций, а также прыжков в какие-либо другие места.

Это отчетливо говорит о том, что здесь отсутствуют какие-либо проверки переменной, а значит — есть место для уязвимости. Данный блок завершает опкод JMPNZ_EX. Что он делает? Делает он самый обычный xor над переменными ~9 и $8. В том случае, если результатом операции является 0, то управление передается на адрес 19 (исходя из дампа vld). На графике видно, что по данному адресу находится такой вот дамп…

Здесь тебе должно быть все ясно. Ну, может быть, кроме опкода FREE — он просто высвобождает ресурсы, занятые указанной переменной. В этом примере больше нет ничего интересного, плывем дальше.

По аналогии с предыдущим примером сделай дампы dvwa/vulnerabilities/fi/index.php и dvwa/vulnerabilities/fi/source/medium.php. Открой график для дампа индекса — поищем там инклуд файлов. В первую очередь тут следует обратить внимание на опкоды групп INCLUDE, REQUIRE, и от них уже можно двигаться в обратном направлении. Допустим, самый первый REQUIRE_ONCE не представляет для нас никакого интереса — он пытается заинклудить файл, имя которого находится во временной переменной ~2. А собирается эта переменная лишь из константных значений. Следующий такой же оператор встречается в самом последнем блоке. Здесь в переменную ~24 склеились две других переменных такого же типа — ~22 и ~23. Но и они принимают значения констант. Подозрительной тут выглядит компилированная переменная !1 — ее следует искать в других блоках. Нашли, но видим строки следующего типа…

Что означает не что иное, как PHP код, подобный такому:

$variable = 'low.php';

Название переменной я придумал сам, ибо в дампах имена переменных отсутствуют. Хотя их совсем не сложно получить. Но не отвлекаемся, уязвимости тут снова нет, значит, идем к следующему опкоду в том же самом блоке — INCLUDE. Он пытается заинклудить имя файла, содержащееся в !2. Но если ты посмотришь на график, то определения такой переменной ты не найдешь. Как же так? Все просто — в данном файле она не определена, поэтому нужно смотреть, какие файлы инклудит данный скрипт.

Теперь открой второй график для medium.php. Тут вообще один одинокий блок. Имей в виду, что нумерация снова начинается с нуля, поэтому не ищи здесь !2. В данном блоке видна всего лишь одна компилированная переменная !0, с которой и происходят всякие манипуляции. В принципе, тут есть уже все известные нам опкоды, и тут ты уже должен определить, что происходит слабенькая фильтрация !0 при помощи функции str_replace(). В самом конце блока видно финальное присваивание и выход из скрипта. Таким образом, можно установить, что данный скрипт содержит потенциальную уязвимость. Но в нашем случае, уже имея на руках анализ файла index.php, можно уверенно сказать, что здесь присутствует уязвимость типа инклуд файлов.

Мы разобрали с тобой примеры, но какой вывод можно сделать из всего этого, и на что нужно обращать внимание? Главным образом тебя должны заинтересовать “потенциально небезопасные” опкоды. А это опкоды типа DO_FCALL, DO_FCALL_BY_NAME, INCLUDE_OR_EVAL, ECHO. Степень их риска можно определить по тому, к какому типу принадлежат операнды конкретного опкода, и что делает эта функция. Ну например, если мы видим операнд-константу, которая никак не изменяется, то вполне ясно, что данный опкод или даже группу можно спокойно игнорировать.

Если же нечто иное, то повод задуматься. Хотя нечего думать, надо делать обратную трассировку. Но это уже ближе к концу. А с чего начинать анализ? Тут все как обычно — анализ начинается с поиска глобальных переменных, как в примере с SQL-инъекцией, а также с других участков кода, где данные поступают на вход, будь то файловые функции, функции с базой данных и так далее.

Опкоды FETCH_R, FETCH_W помогут тебе идентифицировать места записи и чтения переменных. А семейство ASSIGN выявит любые присвоения переменных PHP. Таким образом, зная, что делает каждый опкод и в каких комбинациях операндов, их типов и значений есть угроза безопасности, можно вынести вердикт конкретной переменной.

Во избежание кессонной болезни

Конечно, чтобы вручную копаться в опкодах, нужно иметь терпение и время. Фактически, данный подход ничем не уступает по сложности анализу самого обычного ассемблерного полотна, которое мы видим в IDA. Но в случае с PHP, если есть нормальный исходник, то нет никакого смысла в поиске уязвимостей среди опкодов.

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

Еще из серии того, на что способны расширения PHP: bytekit предоставляет API, при помощи которого можно самому конструировать полезные утилиты. Например в той же папке examples/ есть утилита для быстрой проверки наличия уязвимостей типа FI:

php -d extension=bytekit.so bytekit-0.1.1/examples/check_include.php index.php

index.php(30): require_once DVWA_WEB_PAGE_TO_ROOT.
"vulnerabilities/fi /source/{$vulnerabilityFile}";
index.php(35): include($fi le);

И еще один суперский инструмент, перехватывает все подозрительные eval’ы:

/var/www$ php -d extension=bytekit.so bytekit-0.1.1/examples/scan_eval.php ./

/var/www/dvwa/external/phpids/0.6/lib/ IDS/vendors/htmlpurifi er/HTMLPurifi er/ VarParser/Native.php(17): $result = eval("\$var = $expr;");

PHP Warning: bytekit_disassemble_fi le(): bytekit_get_next_oplines: found throw outside of try/catch in /home/ams/Desktop/bytekit-0.1.1/examples/scan_eval.php on line 19
/var/www/dvwa/external/phpids/0.6/lib/ IDS/vendors/htmlpurifi er/HTMLPurifier/ConfigSchema/ InterchangeBuilder.php(140): return eval('return array('. $contents .');');

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

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

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

Ну, приплыли!

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

Ну или можешь просто написать свое небольшое расширение под конкретную задачу, либо улучшить уже другой существующий проект. Так что, бери на заметку, фантазируй и погружайся в глубины PHP, там много интересного :).

Links:

  1. Расширение bytekit;
  2. Расширение vld;
  3. Расширение evalhook;
  4. Мануал по внутренностям PHP;
  5. Презентация от Stefan Esser по поиску уязвимостей в закрытом коде PHP-приложений;
  6. DVWA.

By Ruslan Novikov

Интернет-предприниматель. Фулстек разработчик. Маркетолог. Наставник.