Книга «Безопасность в PHP» (часть 5). Нехватка энтропии для случайных значений |
||
МЕНЮ Искусственный интеллект Поиск Регистрация на сайте Помощь проекту ТЕМЫ Новости ИИ Искусственный интеллект Разработка ИИГолосовой помощник Городские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Техническое зрение Чат-боты Авторизация |
2018-11-05 23:02 Случайные значения в PHP повсюду. Во всех фреймворках, во многих библиотеках. Вероятно, вы и сами написали кучу кода, использующего случайные значения для генерирования токенов и солей, а также в качестве входных данных для функций. Также случайные значения играют важную роль при решении самых разных задач:
Во всех этих случаях имеется характерная уязвимость. Если атакующий угадает или предскажет выходные данные вашего генератора случайных чисел (RNG, Random Number Generator) или генератора псевдослучайных чисел (PRNG, Pseudo-Random Number Generator), то он сможет вычислить токены, соли, одноразовые значения и криптографические векторы инициализации, создаваемые с помощью этого генератора. Поэтому очень важно генерировать высококачественные случайные значения, т. е. те, которые крайне трудно предсказать. Ни в коем случае не допускайте предсказуемости токенов сброса паролей, CSRF-токенов, ключей API, одноразовых значений и токенов авторизации! В PHP со случайными значениями связаны ещё две потенциальные уязвимости:
В данном контексте «раскрытие информации» относится к утечке внутреннего состояния генератора псевдослучайных чисел — его начального значения (seed value). Подобные утечки могут сильно облегчить предсказывание будущих выходных данных PRNG. «Нехватка энтропии» описывает ситуацию, когда вариативность начального внутреннего состояния (seed) PRNG или его выходных данных столь мала, что весь диапазон возможных значений относительно легко перебирается брутфорсом. Не слишком хорошие новости для PHP-программистов. Мы подробно рассмотрим обе уязвимости с примерами сценариев атак. Но сначала давайте разберёмся, что на самом деле представляет собой случайное значение, когда речь идёт о программировании на PHP. Что делают случайные значения? Путаница относительно предназначения случайных величин усугубляется и общим непониманием. Несомненно, вы слышали о разнице между криптографически стойкими случайными значениями и расплывчатыми «уникальными» значениями «для других видов использования». Основное впечатление — используемые в криптографии случайные значения требуют высококачественной случайности (или, точнее, высокой энтропии), а значения для других областей применения могут обойтись меньшей энтропией. Я считаю это впечатление фальшивым и контрпродуктивным. Реальное различие между непредсказуемыми случайными значениями и теми, что нужны для тривиальных задач, лишь в том, что предсказуемость вторых не влечёт за собой вредных последствий. Это вообще исключает криптографию из рассмотрения вопроса. Иными словами, если вы используете случайное значение в нетривиальной задаче, то автоматически должны выбрать гораздо более сильные RNG. Сила случайных значений определяется затраченной для их генерирования энтропией. Энтропия — это мера неопределённости, выраженная в «битах». Например, если я возьму двоичный бит, его значение может быть 0 или 1. Если атакующий не знает точное значение, то мы имеем энтропию 2 бита (т. е. подбрасывание монеты). Если атакующий знает, что значение всегда равно 1, то мы имеем энтропию 0 бит, поскольку предсказуемость — антоним неопределённости. Также количество бит может находиться в диапазоне от 0 до 2. Например, если 99 % времени двоичный бит равен 1, то энтропия может чуть-чуть превышать 0. Так что чем более неопределённые двоичные биты мы выбираем, тем лучше. В PHP это можно увидеть более наглядно. Функция О нежелательности Взгляните на этот пример, вы можете протестировать его самостоятельно: mt_srand(1361152757.2); for ($i=1; $i < 25; $i++) { echo mt_rand(), PHP_EOL; } Это простой цикл, исполняемый после того, как PHP-функция вихря Мерсенна получила начальное, заранее установленное значение. Оно было получено на выходе функции, приводимой в качестве примера в документации к Если атакующий получит начальное значение такого PRNG, то он сможет предсказать все выходные данные Вы можете сгенерировать начальное значение одним из двух способов:
Второй вариант предпочтительнее, но и сегодня легаси-приложения зачастую наследуют применение Это повышает риск того, что атакующий восстановит начальное значение (атака Seed Recovery Attack), что даст ему достаточно информации для предсказания будущих значений. В результате любое приложение после подобной утечки становится уязвимым для атаки раскрытия информации. Это самая настоящая уязвимость, несмотря на её очевидно пассивную природу. Утечка информации о локальной системе способна помочь атакующему в последующих атаках, что нарушит принцип эшелонированной защиты. Случайные значения в PHP В PHP используется три PRNG, и если злоумышленник получит доступ к начальным значениям, применяемым в их алгоритмах, то он сможет предсказывать результаты их работы:
Также эти генераторы применяются для внутренних нужд, для функций вроде Чтобы повысить качество генерируемых случайных значений для нетривиальных задач, PHP нужны внешние источники энтропии, предоставляемой операционной системой. В Linux обычно применяют
Всё это приводит нас к правилу: Все процессы, подразумевающие применение нетривиальных случайных чисел, ДОЛЖНЫ использовать openssl_pseudo_random_bytes(). В качестве альтернативы вы МОЖЕТЕ попытаться напрямую считывать байты из /dev/urandom. Если ни один вариант не сработал и у вас нет выбора, то вы ДОЛЖНЫ генерировать значение с помощью сильного смешивания данных от нескольких доступных источников случайных или секретных значений. Базовую реализацию этого правила вы найдёте в эталонной библиотеке SecurityMultiTool. Как обычно, внутренности PHP предпочитают усложнять жизнь программистам вместо прямого включения безопасных решений в ядро PHP. Хватит теории, теперь давайте посмотрим, как можно атаковать приложение, вооружившись вышеописанным. Атака на генераторы случайных чисел в PHP По ряду причин в PHP для решения нетривиальных задач используются PRNG. Функция Выбор расширений Openssl и Mcrypt — на ваше усмотрение. Поскольку нельзя положиться на их доступность даже на серверах с PHP 5.3, приложения часто используют PRNG, встроенные в PHP, в качестве запасного варианта для генерирования нетривиальных случайных значений. Но в обоих случаях мы имеем нетривиальные задачи, которые применяют случайные значения, сгенерированные с помощью PRNG с низкоэнтропийными начальными значениями. Это делает нас уязвимыми к атакам с восстановлением начальных значений. Давайте рассмотрим простой пример. Представим, что мы нашли в онлайне приложение, использующее следующий код для генерирования токенов, которые применяются в разных задачах по всему приложению: $token = hash('sha512', mt_rand()); Есть и более сложные средства генерирования токенов, но это неплохой вариант. Здесь используется только один вызов Характеристики уязвимого приложения Это не исчерпывающий список. На практике список характеристик может отличаться! 1. Сервер применяет mod_php, который при использовании KeepAlive позволяет обслуживать несколько запросов одним и тем же PHP-процессом Это важно потому, что генераторы случайных чисел в PHP получают начальные значения единожды на один процесс. Если мы можем сделать к процессу два запроса и более, то он будет использовать одно и то же начальное значение. Суть атаки заключается в том, чтобы применить раскрытие одного токена для извлечения начального значения, которое нужно для предсказания другого токена, генерируемого на основе ТОГО ЖЕ начального значения (т. е. в том же процессе). Поскольку mod_php идеально подходит для использования нескольких запросов для получения связанных случайных значений, то иногда с помощью всего одного запроса можно извлечь несколько значений, относящихся к 2. Сервер раскрывает CSRF-токены, токены сброса паролей или подтверждения аккаунтов, сгенерированные на основе mt_rand()-токенов Для извлечения начального значения нам нужно напрямую проверить число, созданное генераторами в PHP. Причём даже неважно, как оно используется. Мы можем извлечь его из любого доступного значения, будь то выходные данные 3. Известный слабый алгоритм генерирования токенов Вы можете вычислить его:
Некоторые методы генерирования токенов более очевидны, некоторые — более популярны. По-настоящему слабые средства генерирования отличаются использованием одного из генераторов случайных чисел PHP (например, Выполнение атаки Наша атака достаточно проста. В рамках подключения к PHP-процессу мы проведём быструю сессию и отправим два отдельных HTTP-запроса (запрос А и запрос Б). Сессия будет удерживаться сервером, пока не будет получен второй запрос. Запрос А нацелен на получение какого-нибудь доступного токена вроде CSRF, токена сброса пароля (отправляется атакующему по почте) или чего-то подобного. Не забывайте и о других возможностях вроде встроенной разметки (inline markup), используемых в запросах произвольных ID и т. д. Мы будем мучить исходный токен, пока он нам не выдаст своё начальное значение. Всё это — часть атаки с восстановлением начального значения: когда у начального значения такая маленькая энтропия, что его можно брутфорсить или поискать в заранее вычисленной радужной таблице. Запрос Б будет решать более интересную задачу. Давайте сделаем запрос на сброс локального администраторского пароля. Это запустит генерирование токена (с помощью случайного числа на базе того же начального значения, которые мы вытаскиваем с помощью запроса А, если оба запроса успешно отправляются на один и тот же PHP-процесс). Этот токен будет храниться в базе данных в ожидании момента, когда администратор воспользуется ссылкой сброса пароля, отправленной ему на почту. Если мы сможем извлечь начальное значение для токена из запроса А, то, зная, как генерируется токен из запроса Б, мы предскажем токен сброса пароля. А значит, сможем перейти по ссылке сброса до того, как администратор прочитает письмо! Вот последовательность развития событий:
Займёмся хакингом... Пошаговый взлом приложения Шаг 1. Осуществляем запрос А для извлечения токена Мы исходим из того, что целевой токен и токен сброса пароля зависят от выходных данных Шаг 2. Осуществляем запрос Б для получения токена сброса пароля, сгенерированного для администраторского аккаунта Этот запрос представляет собой простую отправку формы сброса пароля. Токен будет сохранён в базе данных и отправлен пользователю по почте. Нам нужно правильно вычислить этот токен. Если характеристики сервера точны, то запрос Б использует тот же PHP-процесс, что и запрос А. Следовательно, в обоих случаях вызовы Шаг 3. Взламываем хеширование SHA512 токена, полученного по запросу А SHA512 внушает программистам благоговейный трепет: у него крупнейший номер во всём семействе алгоритмов SHA-2. Однако в методе генерирования токенов, выбранном нашей жертвой, есть одна проблема — случайные значения ограничены только цифрами (т. е. степень неопределённости, или энтропия, ничтожна). Если вы проверите выходные данные Только не верьте мне на слово. Если у вас есть дискретная видеокарта одного из последних поколений, то можно пойти следующим путём. Поскольку мы ищем одиночный хеш, то я решил воспользоваться замечательным инструментом для брутфорса — hashcat-lite. Это одна из самых быстрых версий hashcat, она есть для всех основных операционных систем, включая Windows. С помощью этого кода сгенерируйте токен: $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 Вот что означают все эти опции:
Если всё сработает правильно и ваш GPU не расплавится, Hashcat вычислит захешированное случайное число за пару минут. Да, минут. Ранее я уже объяснял, как работает энтропия. Убедитесь в этом сами. У функции Шаг 4. Восстанавливаем начальное значение с помощью свежевзломанного случайного числа Как мы видели выше, на извлечение из SHA512 любого сгенерированного ./php_mt_seed <RANDOM NUMBER> Это может занять немного больше времени, чем взлом SHA512, поскольку выполняется на CPU. На приличном процессоре утилита найдёт весь возможный диапазон начального значения за несколько минут. Результат — одно или несколько возможных значений (т. е. значений, на основании которых могло быть получено данное случайное число). Повторюсь: мы наблюдаем результат слабой энтропии, только на этот раз в отношении генерирования в PHP начальных значений для функции вихря Мерсенна. Позднее мы рассмотрим, как были сгенерированы эти значения, так что вы увидите, почему можно так быстро выполнять брутфорс. Итак, до этого мы пользовались простыми инструментами взлома, доступными в сети. Они заточены под вызовы Шаг 5. Генерируем возможные токены сброса пароля администраторского аккаунта Предположим, что в рамках запросов А и Б было сделано всего два запроса к function predict($seed) { /** * Передаём в PRNG начальное значение */ mt_srand($seed); /** * Пропускаем вызов функции из запроса А */ mt_rand(); /** * Предсказываем и возвращаем сгенерированный в запросе Б токен */ $token = hash('sha512', mt_rand()); return $token; } Эта функция предсказывает токен сброса для каждого возможного начального значения. Шаги 6 и 7. Сбрасываем пароль администраторского аккаунта и веселимся! Теперь нужно собрать URL, содержащий токен, который позволит сбросить администраторский пароль благодаря уязвимости приложения и получить доступ к аккаунту. Возможно, выяснится, что на форуме или в статье можно публиковать нефильтрованный HTML (частое нарушение принципа эшелонированной защиты). Это позволит вам выполнить обширную XSS-атаку на всех остальных пользователей приложения, инфицировав их компьютеры зловредом и средствами мониторинга «человек в браузере» (Man-In-The-Browser). Серьёзно, зачем останавливаться на одном лишь получении доступа? Суть этих, на первый взгляд, пассивных и не слишком опасных уязвимостей заключается в том, чтобы помочь злоумышленнику медленно проникнуть туда, откуда он сможет наконец достичь своей главной цели. Хакинг — это как игра в аркадный файтинг, когда вам нужно быстро нажать нужную комбинацию, чтобы провести серию мощных ударов. Анализ после атаки Вышеприведённый сценарий и простота шагов должны ясно продемонстрировать вам опасность Более того, у этой истории есть и вторая сторона. Например, если вы зависите от библиотеки, которая невинно использует На самом деле оба достаточно виноваты. Библиотеке не следует выбирать Волноваться нужно не только по поводу уязвимостей раскрытия информации. Необходимо помнить и об уязвимостях нехватки энтропии, оставляющих приложения беззащитными перед брутфорсом важных токенов, ключей или одноразовых значений, которые технически не относятся к криптографии, но используются в работе нетривиальных функций приложений. А теперь всё то же самое Теперь мы знаем, что использование PRNG, встроенных в PHP, считается уязвимостью нехватки энтропии (т. е. снижение неопределённости облегчает брутфорс). Можно расширить нашу атаку: $token = hash('sha512', uniqid(mt_rand())); Уязвимость раскрытия информации делает этот метод генерирования токенов совершенно бесполезным. Чтобы понять причину, давайте внимательнее посмотрим на PHP-функцию На основе текущего времени в микросекундах получает уникальный префикс-идентификатор. Как вы помните, энтропия — это мера неопределённости. Из-за уязвимости раскрытия информации возможна утечка значений, генерируемых Конечно, определение указывает на «микросекунды», т. е. на миллионные доли секунды. Это даёт нам 1 000 000 возможных чисел. Здесь я игнорирую значения больше 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) { list($usec, $sec) = explode(' ', microtime()); $usec *= 1000000; if(true === $more_entropy) { return sprintf('%s%08x%05x%.8F', $prefix, $sec, $usec, lcg_value()*10); } else { return sprintf('%s%08x%05x', $prefix, $sec, $usec); } } Этот код говорит нам, что простой вызов $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 Брутфорс уникальных идентификаторов Если поразмыслить, то становится очевидно, что раскрытие злоумышленнику любого значения $token = hash('sha512', uniqid(mt_rand())); Из этого примера мы видим, что, выполнив против <?phpphp 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)); Как показывает С-код, новый источник энтропии использует выходные данные внутренней функции 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; } Если вы будете слишком долго на это смотреть и захотите кинуть чем-нибудь в монитор, то лучше не надо. Мониторы нынче дороги. Оба начальных значения используют в С функцию Получается, что первичный источник энтропии, используемый этими 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 начальные значения взаимозависимы. Даже несколько раз смешиваются одинаковые входные данные. Возможно, вы ограничите диапазон начальных микросекунд, как мы это обсуждали выше: с помощью двух запросов, когда первый делает переход между секундами (так что микровремя будет 0 + время выполнения следующего С-вызова Однако основная проблема кроется в Для этого есть приложение... Я уделил немало внимания практическим вещам, так что давайте снова к ним вернёмся. Не так просто получить два начальных значения, используемых 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); Этот код генерирует предхешевое (pre-hash) значение для ID сессии, используя IP, временную метку, микросекунды и… выходные данные Как вы, наверное, помните, PHP теперь поддерживает новые опции сессий наподобие session.entropy_file и session.entropy_length. Это сделано для предотвращения брутфорса ID сессий, в ходе которого можно быстро (это не займёт часы) получить два начальных значения для объединённых с помощью Для подобных случаев существует Windows-приложение, позволяющее вычислять LCG-значения. Кстати, знание состояний LCG позволяет понять, как Что всё это означает с точки зрения добавления энтропии в возвращаемые значения $token = hash('sha512', uniqid(mt_rand(), true)); Это другой пример потенциальной уязвимости нехватки энтропии. Нельзя полагаться на энтропию с утечками (даже если вы не отвечаете за них!). Благодаря утечке информации об ID сессии злоумышленник может предсказать и то значение энтропии, что было дополнительно добавлено в этот ID. Опять же, кого винить? Если приложение Х полагается на В поисках энтропии PHP сам по себе не способен генерировать сильную энтропию. Здесь даже нет базового API для передачи данных из PRNG-генераторов уровня операционной системы, являющихся надёжными источниками сильной энтропии. Поэтому вам нужно полагаться на опциональное наличие расширений openssl и mcrypt. Они предлагают функции, которые гораздо лучше своих дырявых, предсказуемых, низкоэнтропийных родственниц. К сожалению, поскольку оба расширения опциональны, в некоторых случаях у нас нет иного выбора, кроме как полагаться на источники слабой энтропии в качестве последнего отчаянного рубежа. Когда такое случается, нужно дополнять слабую энтропию Избегайте соблазна скрыть слабость своей энтропии с помощью хеширования сложных математических преобразований. Всё это повторит злоумышленник, как только он узнает первичное начальное значение. Подобные ухищрения лишь незначительно увеличат объём вычислений при брутфорсе. Не забывайте: чем меньше энтропия, тем меньше неопределённости; чем меньше неопределённости, тем меньше возможностей необходимо брутфорсить. Единственное оправданное решение — любыми доступными способами увеличить пул используемой вами энтропии. Библиотека RandomLib генерирует случайные байты путём смешивания данных из разных источников энтропии и локализации информации, которая может понадобиться злоумышленнику для предположений. Например, можно смешать выходные данные /** * Генерируем 32-байтное случайное значение. Можно использовать и другие методы: * — generateInt() для получения целочисленных вплоть до PHP_INT_MAX * — generateString() для получения значений в определённом диапазоне символов */ $factory = new RandomLibFactory; $generator = $factory->getMediumStrengthGenerator(); $token = hash('sha512', $generator->generate(32)); Возможно, в связи с доступностью расширений OpenSSL и Mcrypt и потреблением памяти (footprint) библиотекой RandomLib вы будете использовать RandomLib в качестве запасного варианта, как в классе PRNG-генератора SecurityMultiTool. Источник: m.vk.com Комментарии: |
|