Главная » Хабрахабр » [Перевод] Книга «Безопасность в PHP» (часть 5). Нехватка энтропии для случайных значений

[Перевод] Книга «Безопасность в PHP» (часть 5). Нехватка энтропии для случайных значений

Книга «Безопасность в PHP» (часть 1)
Книга «Безопасность в PHP» (часть 2)
Книга «Безопасность в PHP» (часть 3)
Книга «Безопасность в PHP» (часть 4)

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

  1. Для случайного выбора опций из пула или диапазона известных опций.
  2. Для генерирования векторов инициализации при шифровании.
  3. Для генерирования непредсказуемых токенов или одноразовых значений при авторизации.
  4. Для генерирования уникальных идентификаторов, например ID сессий.

Если атакующий угадает или предскажет выходные данные вашего генератора случайных чисел (RNG, Random Number Generator) или генератора псевдослучайных чисел (PRNG, Pseudo-Random Number Generator), то он сможет вычислить токены, соли, одноразовые значения и криптографические векторы инициализации, создаваемые с помощью этого генератора. Во всех этих случаях имеется характерная уязвимость. е. Поэтому очень важно генерировать высококачественные случайные значения, т. Ни в коем случае не допускайте предсказуемости токенов сброса паролей, CSRF-токенов, ключей API, одноразовых значений и токенов авторизации! те, которые крайне трудно предсказать.

В PHP со случайными значениями связаны ещё две потенциальные уязвимости:

  1. Раскрытие информации (Information Disclosure).
  2. Нехватка энтропии (Insufficient Entropy).

Подобные утечки могут сильно облегчить предсказывание будущих выходных данных PRNG. В данном контексте «раскрытие информации» относится к утечке внутреннего состояния генератора псевдослучайных чисел — его начального значения (seed value).

Не слишком хорошие новости для PHP-программистов. «Нехватка энтропии» описывает ситуацию, когда вариативность начального внутреннего состояния (seed) PRNG или его выходных данных столь мала, что весь диапазон возможных значений относительно легко перебирается брутфорсом.

Но сначала давайте разберёмся, что на самом деле представляет собой случайное значение, когда речь идёт о программировании на PHP. Мы подробно рассмотрим обе уязвимости с примерами сценариев атак.

Что делают случайные значения?

Несомненно, вы слышали о разнице между криптографически стойкими случайными значениями и расплывчатыми «уникальными» значениями «для других видов использования». Путаница относительно предназначения случайных величин усугубляется и общим непониманием. Я считаю это впечатление фальшивым и контрпродуктивным. Основное впечатление — используемые в криптографии случайные значения требуют высококачественной случайности (или, точнее, высокой энтропии), а значения для других областей применения могут обойтись меньшей энтропией. Это вообще исключает криптографию из рассмотрения вопроса. Реальное различие между непредсказуемыми случайными значениями и теми, что нужны для тривиальных задач, лишь в том, что предсказуемость вторых не влечёт за собой вредных последствий. Иными словами, если вы используете случайное значение в нетривиальной задаче, то автоматически должны выбрать гораздо более сильные RNG.

Энтропия — это мера неопределённости, выраженная в «битах». Сила случайных значений определяется затраченной для их генерирования энтропией. Если атакующий не знает точное значение, то мы имеем энтропию 2 бита (т. Например, если я возьму двоичный бит, его значение может быть 0 или 1. подбрасывание монеты). е. Также количество бит может находиться в диапазоне от 0 до 2. Если атакующий знает, что значение всегда равно 1, то мы имеем энтропию 0 бит, поскольку предсказуемость — антоним неопределённости. Так что чем более неопределённые двоичные биты мы выбираем, тем лучше. Например, если 99 % времени двоичный бит равен 1, то энтропия может чуть-чуть превышать 0.

Функция mt_rand() генерирует случайные значения, это всегда цифры. В PHP это можно увидеть более наглядно. Это означает, что на каждый байт у атакующего приходится гораздо меньше догадок, т. Она не выдаёт буквы, специальные символы или иные значения. энтропия низкая. е. Очевидно, что этот вариант гораздо лучше, потому что обеспечивает значительно больше бит энтропии. Если заменить mt_rand() чтением байтов из Linux-источника /dev/random, то мы получим по-настоящему случайные байты: они генерируются на основе шума, формируемого драйверами системных устройств и прочими источниками.

В нём реализован алгоритм под названием «вихрь Мерсенна» (Mersenne Twister), который генерирует числа, распределённые таким образом, чтобы результат получался приближенным к результату работы генератора истинно случайных чисел. О нежелательности mt_rand() говорит и то, что это генератор не истинно случайных, а псевдослучайных чисел, или, как его ещё называют, детерминированный генератор случайных двоичных последовательностей (Deterministic Random Bit Generator, DRBG). mt_rand() использует только одно случайное значение — начальное (seed), на его основе фиксированный алгоритм генерирует псевдослучайные значения.

Взгляните на этот пример, вы можете протестировать его самостоятельно:

mt_srand(1361152757.2); for ($i=1; $i < 25; $i++) { echo mt_rand(), PHP_EOL;
}

Оно было получено на выходе функции, приводимой в качестве примера в документации к mt_srand() и использующей текущие секунды и микросекунды. Это простой цикл, исполняемый после того, как PHP-функция вихря Мерсенна получила начальное, заранее установленное значение. Они выглядят случайными, никаких совпадений, всё прекрасно. Если выполнить приведённый код, то он выведет на экран 25 псевдослучайных чисел. Что-нибудь заметили? Снова выполним код. Запустим в третий, четвёртый, пятый раз. Именно: выводятся ТЕ ЖЕ САМЫЕ числа. В старых версиях PHP результат может быть разным, но это не относится к проблеме, поскольку она характерна для всех современных версий PHP.

Так что защита начального значения — дело первостепенной важности. Если атакующий получит начальное значение такого PRNG, то он сможет предсказать все выходные данные mt_rand(). Если вы его потеряете, то больше не имеете права генерировать случайные значения…

Вы можете сгенерировать начальное значение одним из двух способов:

  • вручную, с помощью функции mt_srand(),
  • вы проигнорируете mt_srand() и позволите PHP сгенерировать его автоматически.

Второй вариант предпочтительнее, но и сегодня легаси-приложения зачастую наследуют применение mt_srand(), даже после портирования на более современные версии PHP.

В результате любое приложение после подобной утечки становится уязвимым для атаки раскрытия информации. Это повышает риск того, что атакующий восстановит начальное значение (атака Seed Recovery Attack), что даст ему достаточно информации для предсказания будущих значений. Утечка информации о локальной системе способна помочь атакующему в последующих атаках, что нарушит принцип эшелонированной защиты. Это самая настоящая уязвимость, несмотря на её очевидно пассивную природу.

Случайные значения в PHP

В PHP используется три PRNG, и если злоумышленник получит доступ к начальным значениям, применяемым в их алгоритмах, то он сможет предсказывать результаты их работы:

  1. Линейный конгруэнтный генератор (Linear Congruential Generator, LCG), lcg_value().
  2. Вихрь Мерсенна, mt_rand().
  3. Локально поддерживаемая C-функция rand().

Это означает, что злоумышленник может предсказывать выходные данные этих и других функций, использующих внутренние PRNG языка PHP, если заполучит все необходимые начальные значения. Также эти генераторы применяются для внутренних нужд, для функций вроде array_rand() и uniqid(). Особенно это касается open source приложений. Также это означает, что не получится улучшить защиту, запутав нападающего с помощью многочисленных обращений к генераторам. Злоумышленник способен предсказать ВСЕ выходные данные для любого известного ему начального значения.

В Linux обычно применяют /dev/urandom, можно считывать его напрямую либо обращаться не напрямую, с помощью функций openssl_pseudo_random_bytes() или mcrypt_create_iv(). Чтобы повысить качество генерируемых случайных значений для нетривиальных задач, PHP нужны внешние источники энтропии, предоставляемой операционной системой. Иными словами, убедитесь, что в вашей серверной версии PHP включено расширение OpenSSL или Mcrypt. Обе они могут использовать и криптографически безопасный генератор псевдослучайных чисел (CSPRNG) в Windows, но в PHP в пользовательском пространстве пока нет прямого метода получения данных от этого генератора без расширений, обеспечиваемых этими функциями.

Это делает его неинтересной целью для злоумышленника. /dev/urandom — PRNG, но зачастую он получает новые начальные значения от высокоэнтропийного источника /dev/random. Если он исчерпает энтропию, то все чтения будут заблокированы, пока снова не наберётся достаточно энтропии от системного окружения. Мы стараемся избегать прямого чтения из /dev/random, потому что это блокирующий ресурс. Хотя для наиболее важных задач следует использовать именно /dev/random.

Всё это приводит нас к правилу:

Все процессы, подразумевающие применение нетривиальных случайных чисел, ДОЛЖНЫ использовать openssl_pseudo_random_bytes(). В качестве альтернативы вы МОЖЕТЕ попытаться напрямую считывать байты из /dev/urandom. Если ни один вариант не сработал и у вас нет выбора, то вы ДОЛЖНЫ генерировать значение с помощью сильного смешивания данных от нескольких доступных источников случайных или секретных значений.

Как обычно, внутренности PHP предпочитают усложнять жизнь программистам вместо прямого включения безопасных решений в ядро PHP. Базовую реализацию этого правила вы найдёте в эталонной библиотеке SecurityMultiTool.

Хватит теории, теперь давайте посмотрим, как можно атаковать приложение, вооружившись вышеописанным.

Атака на генераторы случайных чисел в PHP

По ряду причин в PHP для решения нетривиальных задач используются PRNG.

3. Функция openssl_pseudo_random_bytes() была доступна только в PHP 5. 3. В Windows она создавала проблемы с блокировкой, пока не вышла версия 5. Также в PHP 5. 4. До этого в Windows поддерживался только MCRYPT_RAND — по сути, тот же системный PRNG, используемый для внутренних нужд функцией rand(). 3 функция mcrypt_create_iv() в Windows стала поддерживать источник MCRYPT_DEV_URANDOM. 3 было немало пробелов, так что многие легаси-приложения, написанные на предыдущих версиях, могли и не переключиться на более сильные PRNG. Как видите, до появления PHP 5.

Поскольку нельзя положиться на их доступность даже на серверах с PHP 5. Выбор расширений Openssl и Mcrypt — на ваше усмотрение. 3, приложения часто используют PRNG, встроенные в PHP, в качестве запасного варианта для генерирования нетривиальных случайных значений.

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

Представим, что мы нашли в онлайне приложение, использующее следующий код для генерирования токенов, которые применяются в разных задачах по всему приложению:

$token = hash('sha512', mt_rand());

Здесь используется только один вызов mt_rand(), захешированный с помощью SHA512. Есть и более сложные средства генерирования токенов, но это неплохой вариант. Например, к некриптографическим случаям относятся токены доступа, CSRF-токены, одноразовые значения API и токены сброса паролей. На практике, если программист решит, что функции случайных значений в PHP «достаточно случайны», то он наверняка выберет упрощённый подход, пока не прозвучит слово «криптография». Прежде чем продолжить, я подробно распишу всю степень уязвимости этого приложения, чтобы вы лучше понимали, что вообще делает приложения уязвимыми.

Характеристики уязвимого приложения

На практике список характеристик может отличаться! Это не исчерпывающий список.

1. Сервер применяет mod_php, который при использовании KeepAlive позволяет обслуживать несколько запросов одним и тем же PHP-процессом

Если мы можем сделать к процессу два запроса и более, то он будет использовать одно и то же начальное значение. Это важно потому, что генераторы случайных чисел в PHP получают начальные значения единожды на один процесс. е. Суть атаки заключается в том, чтобы применить раскрытие одного токена для извлечения начального значения, которое нужно для предсказания другого токена, генерируемого на основе ТОГО ЖЕ начального значения (т. Поскольку mod_php идеально подходит для использования нескольких запросов для получения связанных случайных значений, то иногда с помощью всего одного запроса можно извлечь несколько значений, относящихся к mt_rand(). в том же процессе). Например, часть энтропии, используемой для генерирования начального значения для mt_rand(), может утечь через ID сессий или выходные значения в том же запросе. Это делает избыточными любые требования к mod_php.

2. Сервер раскрывает CSRF-токены, токены сброса паролей или подтверждения аккаунтов, сгенерированные на основе mt_rand()-токенов

Причём даже неважно, как оно используется. Для извлечения начального значения нам нужно напрямую проверить число, созданное генераторами в PHP. Подойдут даже косвенные источники, у которых случайное значение определяет иное поведение на выходе, что раскрывает это самое значение. Мы можем извлечь его из любого доступного значения, будь то выходные данные mt_rand(), или хешированный CSRF, или токен подтверждения аккаунта. А это уязвимость «раскрытие информации». Главное ограничение заключается в том, что оно должно быть из того же процесса, генерирующего второй токен, который мы пытаемся предсказать. Обратите внимание, что уязвимость не ограничена единственным приложением: вы можете считывать выходные данные PRNG в одном приложении на сервере и применять их для определения выходных данных в другом приложении на том же сервере, если оба они используют один PHP-процесс. Как мы скоро увидим, утечка выходных данных PRNG может быть крайне опасна.

3. Известный слабый алгоритм генерирования токенов

Вы можете вычислить его:

  • покопавшись в исходниках open source приложения,
  • дав взятку сотруднику с доступом к личному исходному коду,
  • найдя бывшего сотрудника, затаившего обиду на бывшего работодателя,
  • или просто предположив, какой алгоритм тут может быть.

По-настоящему слабые средства генерирования отличаются использованием одного из генераторов случайных чисел PHP (например, mt_rand()), слабой энтропией (нет других источников неопределённых данных) и/или слабым хешированием (например, MD5 или вообще без хеширования). Некоторые методы генерирования токенов более очевидны, некоторые — более популярны. Также я использовал хеширование SHA512, чтобы продемонстрировать, что маскировка — это всегда неудовлетворительное решение. Рассмотренный выше пример кода как раз имеет признаки слабого метода генерирования. е. SHA512 — слабое хеширование, поскольку оно быстро вычисляется, т. И не забывайте, что закон Мура тоже ещё действует, а значит, скорость брутфорса будет расти с каждым новым поколением CPU/GPU. атакующий может с невероятной скоростью брутфорсить входные данные на любых CPU или GPU. Поэтому пароли должны хешироваться с помощью инструментов, взлом результатов которых требует фиксированного времени вне зависимости от производительности процессоров или закона Мура.

Выполнение атаки

В рамках подключения к PHP-процессу мы проведём быструю сессию и отправим два отдельных HTTP-запроса (запрос А и запрос Б). Наша атака достаточно проста. Запрос А нацелен на получение какого-нибудь доступного токена вроде CSRF, токена сброса пароля (отправляется атакующему по почте) или чего-то подобного. Сессия будет удерживаться сервером, пока не будет получен второй запрос. д. Не забывайте и о других возможностях вроде встроенной разметки (inline markup), используемых в запросах произвольных ID и т. Всё это — часть атаки с восстановлением начального значения: когда у начального значения такая маленькая энтропия, что его можно брутфорсить или поискать в заранее вычисленной радужной таблице. Мы будем мучить исходный токен, пока он нам не выдаст своё начальное значение.

Давайте сделаем запрос на сброс локального администраторского пароля. Запрос Б будет решать более интересную задачу. Этот токен будет храниться в базе данных в ожидании момента, когда администратор воспользуется ссылкой сброса пароля, отправленной ему на почту. Это запустит генерирование токена (с помощью случайного числа на базе того же начального значения, которые мы вытаскиваем с помощью запроса А, если оба запроса успешно отправляются на один и тот же PHP-процесс). А значит, сможем перейти по ссылке сброса до того, как администратор прочитает письмо! Если мы сможем извлечь начальное значение для токена из запроса А, то, зная, как генерируется токен из запроса Б, мы предскажем токен сброса пароля.

Вот последовательность развития событий:

  1. С помощью запроса A получаем токен и подвергаем его обратному инжинирингу для вычисления начального значения.
  2. С помощью запроса Б получаем токен, сгенерированный на базе того же начального значения. Этот токен хранится в базе данных приложения для будущего сброса пароля.
  3. Взламываем хеш SHA512, чтобы достать сгенерированное сервером случайное число.
  4. С помощью полученного случайного значения брутфорсим начальное значение, которое было сгенерировано с его помощью.
  5. Используем начальное значение для вычисления серии случайных значений, которые, вероятно, могут лежать в основе токена сброса пароля.
  6. Используем этот токен(-ы) для сброса пароля администратора.
  7. Получаем доступ к администраторскому аккаунту, развлекаемся и получаем выгоду. Ну, как минимум развлекаемся.

Займёмся хакингом...

Пошаговый взлом приложения

Шаг 1. Осуществляем запрос А для извлечения токена

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

Шаг 2. Осуществляем запрос Б для получения токена сброса пароля, сгенерированного для администраторского аккаунта

Токен будет сохранён в базе данных и отправлен пользователю по почте. Этот запрос представляет собой простую отправку формы сброса пароля. Если характеристики сервера точны, то запрос Б использует тот же PHP-процесс, что и запрос А. Нам нужно правильно вычислить этот токен. Можно даже использовать запрос А для захвата CSRF-токена формы сброса, чтобы включить ввод данных (submission) ради упорядочивания процедуры (исключаем промежуточный round trip). Следовательно, в обоих случаях вызовы mt_rand() будут применять одно и то же начальное значение.

Шаг 3. Взламываем хеширование SHA512 токена, полученного по запросу А

Однако в методе генерирования токенов, выбранном нашей жертвой, есть одна проблема — случайные значения ограничены только цифрами (т. SHA512 внушает программистам благоговейный трепет: у него крупнейший номер во всём семействе алгоритмов SHA-2. степень неопределённости, или энтропия, ничтожна). е. Это ограниченное количество возможностей делает SHA512 уязвимым для брутфорса. Если вы проверите выходные данные mt_getrandmax(), то обнаружите, что наибольшее случайное число, которое может сгенерировать mt_rand(), — 2,147 миллиарда с мелочью.

Если у вас есть дискретная видеокарта одного из последних поколений, то можно пойти следующим путём. Только не верьте мне на слово. Это одна из самых быстрых версий hashcat, она есть для всех основных операционных систем, включая Windows. Поскольку мы ищем одиночный хеш, то я решил воспользоваться замечательным инструментом для брутфорса — hashcat-lite.

С помощью этого кода сгенерируйте токен:

$rand = mt_rand();
echo "Random Number: ", $rand, PHP_EOL;
$token = hash('sha512', $rand);
echo "Token: ", $token, PHP_EOL;

Этот код воспроизводит токен из запроса А (он содержит нужное нам случайное число и спрятан в хеш SHA512) и прогоняет через hashcat:

./oclHashcat-lite64 -m1700 --pw-min=1 --pw-max=10 -1?d -o ./seed.txt <SHA512 Hash> ?d?d?d?d?d?d?d?d?d?d

Вот что означают все эти опции:

  • -m1700: определяет алгоритм хеширования, где 1700 означает SHA512.
  • --pw-min=1: определяет минимальную входную длину хешируемого значения.
  • --pw-max=10: определяет максимальную входную длину хешируемого значения (10 для mt_rand()).
  • -1?d: определяет, что нам нужен кастомный словарь из одних лишь цифр (т. е. 0—9).
  • -o ./seed.txt: файл для записи результатов. На экран ничего не выводится, так что не забудьте задать этот параметр!
  • ?d?d?d?d?d?d?d?d?d?d: маска, задающая используемый формат (все цифры максимум до 10).

Да, минут. Если всё сработает правильно и ваш GPU не расплавится, Hashcat вычислит захешированное случайное число за пару минут. Убедитесь в этом сами. Ранее я уже объяснял, как работает энтропия. Так что бессмысленно было хешировать выходные данные mt_rand(). У функции mt_rand() так мало возможностей, что SHA512-хеши всех значений реально вычислить за очень короткое время.

Шаг 4. Восстанавливаем начальное значение с помощью свежевзломанного случайного числа

Вооружившись случайным значением, мы можем запустить другой инструмент для брутфорса — php_mt_seed. Как мы видели выше, на извлечение из SHA512 любого сгенерированного mt_rand() значения требуется всего пара минут. Скачайте текущую версию, скомпилируйте и запустите. Эта маленькая утилита берёт выходные данные mt_rand() и после брутфорса вычисляет начальное значение, на основании которого могло быть сгенерировано анализируемое. Если появятся проблемы с компиляцией, попробуйте более старую версию (с новыми у меня были проблемы с виртуальными средами).

./php_mt_seed <RANDOM NUMBER>

На приличном процессоре утилита найдёт весь возможный диапазон начального значения за несколько минут. Это может занять немного больше времени, чем взлом SHA512, поскольку выполняется на CPU. е. Результат — одно или несколько возможных значений (т. Повторюсь: мы наблюдаем результат слабой энтропии, только на этот раз в отношении генерирования в PHP начальных значений для функции вихря Мерсенна. значений, на основании которых могло быть получено данное случайное число). Позднее мы рассмотрим, как были сгенерированы эти значения, так что вы увидите, почему можно так быстро выполнять брутфорс.

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

Шаг 5. Генерируем возможные токены сброса пароля администраторского аккаунта

Теперь начнём предсказывать токены, используя ранее вычисленные возможные начальные значения: Предположим, что в рамках запросов А и Б было сделано всего два запроса к mt_rand().

function predict($seed) { /** * Передаём в PRNG начальное значение */ mt_srand($seed); /** * Пропускаем вызов функции из запроса А */ mt_rand(); /** * Предсказываем и возвращаем сгенерированный в запросе Б токен */ $token = hash('sha512', mt_rand()); return $token;
}

Эта функция предсказывает токен сброса для каждого возможного начального значения.

Шаги 6 и 7. Сбрасываем пароль администраторского аккаунта и веселимся!

Возможно, выяснится, что на форуме или в статье можно публиковать нефильтрованный HTML (частое нарушение принципа эшелонированной защиты). Теперь нужно собрать URL, содержащий токен, который позволит сбросить администраторский пароль благодаря уязвимости приложения и получить доступ к аккаунту. Серьёзно, зачем останавливаться на одном лишь получении доступа? Это позволит вам выполнить обширную XSS-атаку на всех остальных пользователей приложения, инфицировав их компьютеры зловредом и средствами мониторинга «человек в браузере» (Man-In-The-Browser). Хакинг — это как игра в аркадный файтинг, когда вам нужно быстро нажать нужную комбинацию, чтобы провести серию мощных ударов. Суть этих, на первый взгляд, пассивных и не слишком опасных уязвимостей заключается в том, чтобы помочь злоумышленнику медленно проникнуть туда, откуда он сможет наконец достичь своей главной цели.

Анализ после атаки

Риски столь очевидны, что теперь мы можем считать любые слабо скрытые выходные значения mt_rand(), доступные нападающему в любой форме, уязвимостью «раскрытие информации». Вышеприведённый сценарий и простота шагов должны ясно продемонстрировать вам опасность mt_rand().

Например, если вы зависите от библиотеки, которая невинно использует mt_rand() для каких-то важных задач, пусть и не выдавая получаемые значения, то, воспользовавшись для своих нужд «дырявым» токеном, вы скомпрометируете эту библиотеку. Более того, у этой истории есть и вторая сторона. Надо ли винить пользователя за утечку значений mt_rand() — или библиотеку за то, что она не применяет более качественные случайные значения? И это проблема, потому что библиотека или фреймворк никак не помогают смягчить атаку с восстановлением начального значения.

Библиотеке не следует выбирать mt_rand() (или любой другой одиночный источник слабой энтропии) для важных задач в качестве единственного источника случайных значений. На самом деле оба достаточно виноваты. Так что — да, можно начать обвинительно тыкать пальцем в неграмотные примеры использования mt_rand(), даже если это не приводит к прямым утечкам. А пользователь не должен писать код, из которого утекают значения mt_rand().

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

А теперь всё то же самое

е. Теперь мы знаем, что использование PRNG, встроенных в PHP, считается уязвимостью нехватки энтропии (т. Можно расширить нашу атаку: снижение неопределённости облегчает брутфорс).

$token = hash('sha512', uniqid(mt_rand()));

Чтобы понять причину, давайте внимательнее посмотрим на PHP-функцию uniqid(). Уязвимость раскрытия информации делает этот метод генерирования токенов совершенно бесполезным. Её определение:

На основе текущего времени в микросекундах получает уникальный префикс-идентификатор.

Из-за уязвимости раскрытия информации возможна утечка значений, генерируемых mt_rand(), так что использование mt_rand() в качестве уникального префикса-идентификатора добавляет нулевую неопределённость. Как вы помните, энтропия — это мера неопределённости. Но оно точно НЕ является неопределённым. В нашем примере единственный другой вид входных данных для uniqid() — это время. А у предсказуемых значений крайне низкая энтропия. Оно изменяется линейно и предсказуемо.

е. Конечно, определение указывает на «микросекунды», т. Это даёт нам 1 000 000 возможных чисел. на миллионные доли секунды. Прежде чем углубиться в детали, давайте препарируем функцию uniqid() и посмотрим на её С-код: Здесь я игнорирую значения больше 1 секунды, поскольку их фракция и измеряемость так велики (например, заголовок HTTP Date в отклике), что это почти ничего не даёт.

gettimeofday((struct timeval *) &tv, (struct timezone *) NULL);
sec = (int) tv.tv_sec;
usec = (int) (tv.tv_usec % 0x100000); /* usec может иметь максимальное значение 0xF423F, так что мы используем * usecs только пять шестнадцатеричных чисел. */
if (more_entropy) { spprintf(&uniqid, 0, "%s%08x%05x%.8F", prefix, sec, usec, php_combined_lcg(TSRMLS_C) * 10);
} else { spprintf(&uniqid, 0, "%s%08x%05x", prefix, sec, usec);
} RETURN_STRING(uniqid, 0);

Если это выглядит слишком сложно, то можно реплицировать всё в старый добрый PHP:

function unique_id($prefix = '', $more_entropy = false) else { return sprintf('%s%08x%05x', $prefix, $sec, $usec); }
}

Первые 8 символов — текущая временная метка в Unix (в секундах), выраженная в шестнадцатеричной форме. Этот код говорит нам, что простой вызов uniqid() без параметров вернёт нам строку из 13 символов. Иными словами, базовая функция uniqid() обеспечивает очень точное измерение системного времени, которое можно извлечь из простого вызова uniqid() с помощью подобного кода: Последние 5 символов — дополнительные микросекунды в шестнадцатеричной форме.

$id = uniqid();
$time = str_split($id, 8);
$sec = hexdec('0x' . $time[0]);
$usec = hexdec('0x' . $time[1]);
echo 'Seconds: ', $sec, PHP_EOL, 'Microseconds: ', $usec, PHP_EOL;

Точное системное время в выходных данных никогда не бывает скрыто, вне зависимости от параметров: Посмотрите на С-код.

echo uniqid(), PHP_EOL; // 514ee7f81c4b8
echo uniqid('prefix-'), PHP_EOL; // prefix-514ee7f81c746
echo uniqid('prefix-', true), PHP_EOL; // prefix-514ee7f81c8993.39593322

Брутфорс уникальных идентификаторов

Происходит утечка очень точного системного времени, которое можно использовать для прогнозирования входных данных для последующих вызовов uniqid(). Если поразмыслить, то становится очевидно, что раскрытие злоумышленнику любого значения uniqid() — ещё один пример потенциальной уязвимости раскрытия информации. Поскольку об этой утечке можно было упомянуть позже, то в нашем примере технически она не нужна. Это помогает решать любые дилеммы, возникающие при попытке предсказывания микросекунд, за счёт сужения 1 000 000 возможностей до более узкого диапазона. Давайте снова посмотрим на оригинальный код токена uniqid():

$token = hash('sha512', uniqid(mt_rand()));

Если вам нужен узкий диапазон временных меток без использования утечки системного времени из uniqid(), то проанализируем серверные отклики, которые обычно содержат заголовок HTTP Date. Из этого примера мы видим, что, выполнив против mt_rand() атаку с восстановлением начального значения в совокупности с раскрытием информации из uniqid(), мы вычислим относительно небольшой набор SHA512-хешей, которые могут оказаться сбросом пароля или другими важными токенами. А поскольку в этом случае энтропия равна одному миллиону возможных значений микросекунд, то можно забрутфорсить её за несколько секунд! Отсюда можно почерпнуть точные серверные временные метки.

<?php
echo PHP_EOL; /** * Генерирует токен для взлома без утечки микросекунд */
mt_srand(1361723136.7);
$token = hash('sha512', uniqid(mt_rand())); /** * Теперь взламываем токен без измерения микросекунд, * но помните, что секунды получены из заголовка HTTP Date и начального значения * для mt_rand() с помощью более раннего сценария атаки ;) */
$httpDateSeconds = time();
$bruteForcedSeed = 1361723136.7;
mt_srand($bruteForcedSeed);
$prefix = mt_rand(); /** * Инкрементируем HTTP Date на несколько секунд, чтобы исключить возможность * пересечения отсчёта секунд (second tick) между вызовами uniqid() и time(). */
for ($j=$httpDateSeconds; $j < $httpDateSeconds+2; $j++) { for ($i=0; $i < 1000000; $i++) { /** Replicate uniqid() token generator in PHP */ $guess = hash('sha512', sprintf('%s%8x%5x', $prefix, $j, $i)); if ($token == $guess) { echo PHP_EOL, 'Actual Token: ', $token, PHP_EOL, 'Forced Token: ', $guess, PHP_EOL; exit(0); } if (($i % 20000) == 0) { echo '~'; } }
}

Спасёт ли нас увеличение энтропии?

Конечно, есть возможность добавить энтропию в uniqid() путём присвоения второму параметру функции значения TRUE:

$token = hash('sha512', uniqid(mt_rand(), true));

Эта функция выводится в пользовательское пространство посредством функции lcg_value(), которую я применял в своём PHP-преобразовании функции uniqid(). Как показывает С-код, новый источник энтропии использует выходные данные внутренней функции php_combined_lcg(). Ниже представлен код, который снабжал генераторы этими начальными значениями. По сути, она комбинирует два значения, сгенерированных с помощью двух линейных конгруэнтных генераторов, получивших отдельные начальные значения. Как в случае с mt_rand(), они генерируются один раз для каждого PHP-процесса и многократно используются во всех последующих вызовах.

static void lcg_seed(TSRMLS_D) /* {{{ */
{ struct timeval tv; if (gettimeofday(&tv, NULL) == 0) { LCG(s1) = tv.tv_sec ^ (tv.tv_usec<<11); } else { LCG(s1) = 1; } #ifdef ZTS LCG(s2) = (long) tsrm_thread_id(); #else LCG(s2) = (long) getpid(); #endif /* Add entropy to s2 by calling gettimeofday() again */ if (gettimeofday(&tv, NULL) == 0) { LCG(s2) ^= (tv.tv_usec<<11); } LCG(seeded) = 1;
}

Мониторы нынче дороги. Если вы будете слишком долго на это смотреть и захотите кинуть чем-нибудь в монитор, то лучше не надо.

Нужно отметить, что оба вызова реализованы в исходном коде, так что значение счётчика microsecond() между ними будет минимальным, что снижает вносимую неопределённость. Оба начальных значения используют в С функцию gettimeofday() для захвата текущего времени в секундах и микросекундах начиная с Unix Epoch (относится к серверным часам). Конечно, можно вручную поднять границу примерно до 4 миллионов, изменив /proc/sys/kernel/pid_max, но это очень нежелательно. Второе начальное значение также окажется подмешано в ID текущего процесса, который в большинстве случаев под Linux не превысит 32 768.

К примеру, помните наше начальное значение mt_rand()? Получается, что первичный источник энтропии, используемый этими LCG, это микросекунды. Угадайте, как оно вычисляется.

#ifdef PHP_WIN32
#define GENERATE_SEED() (((long) (time(0) * GetCurrentProcessId())) ^ ((long) (1000000.0 * php_combined_lcg(TSRMLS_C))))
#else
#define GENERATE_SEED() (((long) (time(0) * getpid())) ^ ((long) (1000000.0 * php_combined_lcg(TSRMLS_C))))
#endif

Даже несколько раз смешиваются одинаковые входные данные. Это означает, что все используемые в PHP начальные значения взаимозависимы. Вы даже можете вычислить дельту в микросекундах между другими вызовами gettimeofday(), имея доступ к исходному коду (открытость кода PHP играет на руку). Возможно, вы ограничите диапазон начальных микросекунд, как мы это обсуждали выше: с помощью двух запросов, когда первый делает переход между секундами (так что микровремя будет 0 + время выполнения следующего С-вызова gettimeofday()). Не говоря уже о том, что брутфорс начального значения mt_rand() даёт вам финальное начальное значение, позволяющее выполнять офлайновую верификацию.

Это нижнеуровневая реализация функции lcg_value() из пользовательского пространства, которая получает начальное значение один раз на каждый PHP-процесс. Однако основная проблема кроется в php_combined_lcg(). Если разгрызть этот орешек — то всё, игра окончена. И если вам известно это значение, то вы можете предсказать выходные данные.

Для этого есть приложение...

Не так просто получить два начальных значения, используемых php_combined_lcg(), — возможно, прямую утечку не получится организовать. Я уделил немало внимания практическим вещам, так что давайте снова к ним вернёмся. Я не хочу предотвращать утечку значения для lcg_value(), но это непопулярная функция. Функция lcg_value() относительно малоизвестна, и программисты чаще полагаются на mt_rand(), когда им нужен PRNG, встроенный в PHP. Однако есть надёжный источник, напрямую предоставляющий данные для брутфорса начальных значений: идентификаторы сессий в PHP. Пара скомбинированных LCG также не отражают функцию генерирования начального значения (так что не получится просто поискать вызовы к mt_srand(), чтобы определить дырявый механизм создания начальных значений, унаследованный из чьего-то легаси-кода).

spprintf(&buf, 0, "%.15s%ld%ld%0.8F", remote_addr ? remote_addr : "", tv.tv_sec,
(long int)tv.tv_usec, php_combined_lcg(TSRMLS_C) * 10);

Учитывая существенное снижение количества микровременных возможностей (этому коду нужна 1 для генерирования ID и 2 для php_combined_lcg(), что приводит к минимальной разнице между ними), мы теперь можем выполнить брутфорс. Этот код генерирует предхешевое (pre-hash) значение для ID сессии, используя IP, временную метку, микросекунды и… выходные данные php_combined_lcg(). Ну, вероятно.

Это сделано для предотвращения брутфорса ID сессий, в ходе которого можно быстро (это не займёт часы) получить два начальных значения для объединённых с помощью php_combined_lcg() LCG-генераторов. Как вы, наверное, помните, PHP теперь поддерживает новые опции сессий наподобие session.entropy_file и session.entropy_length. 3 или ниже, то, возможно, неправильно настроили эти опции. Если вы используете PHP 5. А это означает наличие другой полезной уязвимости раскрытия информации, позволяющей выполнять брутфорс ID сессий ради получения начальных значений для LCG.

Для подобных случаев существует Windows-приложение, позволяющее вычислять LCG-значения.

Кстати, знание состояний LCG позволяет понять, как mt_rand() получает начальное значение, так что это ещё один способ обойти нехватку утечек значения mt_rand().

Что всё это означает с точки зрения добавления энтропии в возвращаемые значения uniqid()?

$token = hash('sha512', uniqid(mt_rand(), true));

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

Если приложение Х полагается на uniqid(), но пользователь или другое приложение на том же сервере допускает утечку внутреннего состояния LCG, то нужно принимать меры в обеих ситуациях. Опять же, кого винить? Пользователи должны быть уверены, что ID сессий используют достаточно высокую энтропию, а сторонние программисты должны понимать, что их методам генерирования случайных значений не хватает энтропии, поэтому нужно переключиться на более подходящие альтернативы (даже если доступны только источники слабой энтропии!).

В поисках энтропии

Здесь даже нет базового API для передачи данных из PRNG-генераторов уровня операционной системы, являющихся надёжными источниками сильной энтропии. PHP сам по себе не способен генерировать сильную энтропию. Они предлагают функции, которые гораздо лучше своих дырявых, предсказуемых, низкоэнтропийных родственниц. Поэтому вам нужно полагаться на опциональное наличие расширений openssl и mcrypt.

Когда такое случается, нужно дополнять слабую энтропию mt_rand() с помощью дополнительных источников неопределённости, смешивая их данные в единый пул, из которого можно черпать псевдослучайные байты. К сожалению, поскольку оба расширения опциональны, в некоторых случаях у нас нет иного выбора, кроме как полагаться на источники слабой энтропии в качестве последнего отчаянного рубежа. Вот что следует по возможности делать программистам. Подобный случайный генератор, использующий миксер сильной энтропии, уж реализовал Энтони Феррара в своей библиотеке RandomLib.

Всё это повторит злоумышленник, как только он узнает первичное начальное значение. Избегайте соблазна скрыть слабость своей энтропии с помощью хеширования сложных математических преобразований. Не забывайте: чем меньше энтропия, тем меньше неопределённости; чем меньше неопределённости, тем меньше возможностей необходимо брутфорсить. Подобные ухищрения лишь незначительно увеличат объём вычислений при брутфорсе. Единственное оправданное решение — любыми доступными способами увеличить пул используемой вами энтропии.

Например, можно смешать выходные данные mt_rand(), uniqid() и lcg_value(), добавить PID, потребление памяти, ещё какое-то измерение микровремени, сериализацию $_ENV, posix_times() и т. Библиотека RandomLib генерирует случайные байты путём смешивания данных из разных источников энтропии и локализации информации, которая может понадобиться злоумышленнику для предположений. Или пойти ещё дальше, это позволяет расширяемость RandomLib. д. е. Допустим, использовать какие-то дельты в микросекундах (т. измерять, сколько микросекунд нужно какой-то функции для работы с псевдослучайными входными данными наподобие вызовов hash()).

/** * Генерируем 32-байтное случайное значение. Можно использовать и другие методы: * — generateInt() для получения целочисленных вплоть до PHP_INT_MAX * — generateString() для получения значений в определённом диапазоне символов */
$factory = new \RandomLib\Factory;
$generator = $factory->getMediumStrengthGenerator();
$token = hash('sha512', $generator->generate(32));

Возможно, в связи с доступностью расширений OpenSSL и Mcrypt и потреблением памяти (footprint) библиотекой RandomLib вы будете использовать RandomLib в качестве запасного варианта, как в классе PRNG-генератора SecurityMultiTool.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Философия Джефа Безоса: «День 1»

13 сентября Джеф Безос стартовал филантропический проект «День 1». Копнем, что же стоит за этим названием. Какова философия «Дня 1» Джеффа Безоса? Изначально этот вопрос появился на ‘Quora’: месте для получения и обмена знаниями, позволяющим людям учиться у других и ...

Very Special Event: как мы смотрели презентацию Apple и что об этом думаем

Тем не менее, мы в Авито не могли пропустить это событие. От презентации Apple, которая должна была пройти 12 сентября, ничего особенного не ждали: три новых модели iPhone и новую версию Apple Watch — об этих новинках знали заранее. Посмотреть ...