Случайные числа, RNG и безопасность

Случайности неслучайны - иногда настолько, что это надо учитывать в администрировании

Случайные числа – очень нужная штука в IT-технологиях. Особенно когда речь заходит о системах, связанных с безопасностью. Практически не найти технологии из этой категории, в которой не были бы нужны случайные числа.

Любая безопасная сетевая сессия – что SSL/TLS, что IPsec/VPN – и вам надо создавать сессионные ключи. Любой протокол аутентификации с challenge/response – и надо создавать nonce, притом каждый раз новый. Установка простой TCP-сессии – два раза придумывать случайные числа для handshake. Хранение паролей – пожалуйста, придумайте salt. Защищённый DNS, чтобы не подделали ответы – придумайте много salt. Шифруете алгоритмами с секретным ключом, используя ряд распространённых режимов шифрования – нужен IV, и послучайнее. Распределение блоков памяти при ASLR – и опять вопрос случайности.

Примеров масса – однако вопрос “откуда эти числа берутся и насколько они случайны” обычно заминается. Хотя генерация “хороших” случайных чисел – действительно один из фундаментальных вопросов в сетевой (да и не только) безопасности.

Поговорим про это, акцентируя внимание именно на генераторах случайных чисел у распространённых ОС и современных проблемах – чтобы оставить разговор про “а как потом из этих хороших случайных чисел генерятся, допустим, ключи” – на следующий раз.

Стартовая диспозиция

Рассматриваемое ПО будет иметь следующие параметры по умолчанию:

  • Windows – NT 6.2 и старше, если явно не указано иное.
  • *nix – Так как чаще всего мне приходится пересекаться с POSIX-совместимыми системами, похожими на Scientific Linux, примеры будут на широко распространённом дистрибутиве CentOS последней версии, 7.4, с ядром kernel-ml 4.15.

Про что же поговорим?

Начнём.

RNG – Random Number Generators – виды и варианты

Говоря про RNG, мы столкнёмся со следующей терминологией.

  • RNG – просто некий генератор случайных чисел, без детализации “какой”;
  • PRNG – генератор случайных чисел, который честно говорит, что создаёт псевдослучайные числа (P – Pseudo); он создаёт бесконечную последовательность, опираясь на некий инициализирующий набор бит, называемый seed
  • TRNG – генератор случайных чисел, работающий без seed, потому что он Тру (T – True), т.е. истинный; встречается редко, остерегайтесь подделок.
  • CSPRNG – генератор случайных чисел, который честно говорит, что создаёт псевдослучайные числа, но пригодные для криптографического применения (CS – Cryptographically Secure);

Конечно, нам интереснее всего последний вариант – CSPRNG – поэтому сразу зададим очевидный вопрос.

Зачем вообще нужны генераторы не-истинных, не-криптографически пригодных случайных чисел?

Один из первых вопросов выглядит именно так – зачем т.е. вообще пытаться создавать “небезопасные” числа? Если можно просто всегда делать “максимально хорошие” по всем-всем-всем мыслимым критериям?

Пример задачи, когда это нужно – типовая онлайн-игра:

Чтобы сгенерить карту – со всеми оформительскими деталями, разбросанными камушками и кустиками, фурнитурой и деталями интерьера, делающими местность “менее синтетической” – можно передавать с сервера на клиента список с описанием каждого объекта, его координатами и прочей атрибутикой. А можно просто синхронизировать генерацию псевдослучайных чисел и тогда клиент “случайно” сгенерит уровень со всеми деталями – идентичный такому же уровню со стороны сервера. Просто потому что стартовые случайные числа будут одинаковы, и генератор будет работать одинаково, и в результате вся система выдаст цепочку таких же псевдослучайных значений.

В результате передача информации о карте займёт единицы байт – нужно будет только отдать “стартовое” значение, а дальше PRNG сгенерит предсказуемую цепочку.

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

Как выглядит хороший CSPRNG?

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

Хорошее описание критериев, определяющих качество случайных бит есть в FIPS 140-2. Чтобы не превращать статью в пересказ соседней, перейдём к тому, как сейчас обстоит дело с реализациями.

Текущая ситуация с реализациями RNG

RNG в Windows NT

Начиная с NT 5.1 (ещё более древние версии для простоты не рассматриваем), в библиотеке advapi32.dll зашит код функции CryptGenRandom, которая умеет возвращать нужное число байт, декларируемых как пригодные для любых применений, включая криптографические. До вызова этой функции надо инициализировать криптопровайдера – соответственно, выбрать или один из встроенных, или принести свой (подписанный Microsoft).

Функция CryptGenRandom работает не только на выход – если при её вызове заранее заполнить буфер какими-либо данными, они будут использоваться для ещё большего улучшения качества выходящей псевдослучайной последовательности. Как именно – заминается, но есть мнение (не основанное на дизассемблировании, т.к. лицензия Windows в явном виде запрещает это), что входящий буфер будет xor’иться с имеющимся seed генератора. Эксперименты показывают, что простые тесты на качество результирующей последовательности не замечают какой-то разницы между характеристиками итогового блока псевдослучайных байт.

Встретить CryptGenRandom можно только в ПО, которое работает с до-NT 6.0 версиями Windows. Дело в том, что функция начинает быть deprecated с момента выхода CNG, и надо использовать новую.

Это не мешает CryptGenRandom обновляться, и в NT 6.0 SP1 получить модернизацию до статуса “я теперь работаю в соответствии с NIST SP 800-90“. Технически это говорит о том, что она (функция) теперь использует для обработки бит энтропии AES-CTR с блоком в 128 бит – впрочем, данная доработка вызвана не стремлением к высокому уровню безопасности для клиентов, а тем, что если RNG будет сделан иначе, то сбыт ПО, скомпилированного с вызовом этой функции, натолкнётся на затруднения. То есть с определённого момента надо будет или переписать софт, или нельзя будет ставить его в американские гос.органы. Проще, безусловно, подправить функцию прямо в dll-ке, что фирма Microsoft и сделала.

Начиная же с NT 6.0 вместо данной функции рекомендуется использовать BCryptGenRandom – из CNG’шной bcrypt.dll. Из интересного можно отметить интересную и неочевидную деталь функционирования.

Дело в том, что данная функция также допускает помощь в генерации псевдослучайных чисел – можно до её вызова заполнить передаваемый буфер нужными значениями, которые повлияют на random seed – для этого теперь надо явно указать флаг BCRYPT_RNG_USE_ENTROPY_IN_BUFFER. Но начиная с Windows 8 эта настройка тихо игнорируется – т.е. вы можете в буфер до вызова функции добавить свои данные, при вызове функции добавить флаг – но на результат это не повлияет. Сделано это, как понятно, “в целях безопасности”. А то вдруг кто-то Правильные Случайные Числа своими Неправильными Случайными Числами сделает слишком случайными?

Есть ещё различные возможности запросить случайные битовые последовательности через другие API – например, в случае managed code, через .NET, но все они обычно будут врапперами – те же дотнетовские сборки будут дёргать p/invoke, притом настолько ловко, что их реализация не является сертифицированной АНБ – т.е. для действительно серьёзных задач написать систему, использующую для криптографически стойких операций .NET’овский генератор случайных чисел – хоть он и будет запрашивать WinAPI – не получится. Поэтому рассматривать их как отдельные сущности не имеет смысла.

По факту мы имеем ситуацию, когда всё сводится или к устаревшей CryptGenRandom, или к обновлённой с NT 6.0 CNG’шной BCryptGenRandom. В любом варианте, функция создаст нам не просто битовую последовательность, а битовую последовательность от специфичного криптопровайдера. Список криптопровайдеров, умеющих генерить случайные числа, можно запросить моей утилитой ATcmd, предсказуемой командой show crypto functions:

Визуально их три – BCRYPT_RNG_ALGORITHM (стандартный, по NIST SP 800-90 – единственный, кстати, подпадающий под FIPS 140-2), BCRYPT_RNG_DUAL_EC_ALGORITHM (про него чуть дальше) и BCRYPT_RNG_FIPS186_DSA_ALGORITHM – но по факту выбора у вашего приложения под Windows не будет. Почему?

BCRYPT_RNG_FIPS186_DSA_ALGORITHM нужен только для работы систем, использующих DSA-подпись – а следовательно сейчас практически не будет использоваться. До кучи он не сертифицирован на FIPS 140-2 – следовательно, любой софт, который может продаваться в американские гос.структуры, не использует этот вариант.

С BCRYPT_RNG_DUAL_EC_ALGORITHM вышла ещё более весёлая история – NIST (т.е. АНБ) пропихнула его в стандарт, несмотря на имеющиеся с 2006 года подтверждённые проблемы с безопасностью. Там вообще, в CNG, если поглядеть, одна засада на другой – что с RNG, что с эллиптическими кривыми.

Проблемы существуют, DualEC используется – пока в 2013 году Эд Сноуден не палит всю контору, объявляя о наличии у АНБ программы Bullrun. АНБ начинает стандартно хныкать про клевету, идиоты начинают стандартно ныть про “да вы что… да в нормальных-то странах… да никогда такого быть не может… да их же там бы засудили всех сразу… да там же не бывает такого потому что не бывает” – и тут в 2014 году коммерческая фирма RSA окончательно добивает ситуацию, честно признавшись, что за 10 миллионов долларов США государственная структура купила молчание крупнейшей коммерческой организации, чтобы та, зная о проблеме, продолжала рекомендовать использовать DualEC DRBG, и добавила его в BSAFE как “провайдер по умолчанию”. Шок.

Полгода идёт сопение под ковром, после чего АНБ говорит “ну ладно, задрали вы нас уже – всё, всё, уговорили, давайте убирать DualEC RNG”. В Windows 10 его уже нет. Риторические вопросы вида “а что делать с трафиком, который защищали с использованием ключевого материала, созданного опираясь на эти сильно случайные числа между 2006 и 2014 годами, и который [трафик] заснифили в АНБ?” подвисают в воздухе, т.к. плотность атмосферы доверия такая, что хоть топор вешай.

В итоге всё богатство криптографических возможностей по генерации случайных чисел на платформе Windows уже лет 10 как представлено 1 (одной) функцией и 1 (одним) провайдером. Несмотря на то, что по идее написать свой криптопровайдер и подписать его в Microsoft – чтобы он стал доступен в Windows – может любая организация, всё упирается в то, что помимо Microsoft, данный вопрос решается ещё и АНБ. И если криптопровайдер не обладает “крышей” уровня государства – как в случае с некоторыми разработчиками ГОСТ’овых криптопровайдеров – то возможность подписи (а, следовательно, и использования) останется призрачной.

Получается, что архитектура системы открытая и модульная, а доступный выбор – из одного модуля. Рыночек порешал, так сказать.

Да, и штатной схемы “как приложению А сказать использовать генератор случайных чисел Б” – нет. Всё hardcoded, а для части ПО, работающего для скорости как часть ОС – например, стека IPsec или веб-сервера IIS – даже теоретически трудно представить схему “работающий на уровне ядра процесс будет бегать в userspace за битовыми строками”.

Если детально посмотреть реализацию, то RNG в Windows берёт энтропию из следующих источников:

  • От чипа TPM, если он есть у системы;
  • От процессорного блока RdRand, если он есть у CPU;
  • От интерфейса UEFI, если система загружается с UEFI, а не BIOS;
  • От текущего времени у RTC;
  • От счётчика тактов у CPU;
  • От содержимого таблицы OEM0 в ACPI;
  • От свободного места на диске, с которого была загрузка системы;
  • От NETBIOS-имени компьютера;
  • От ключа реестра ExternalEntropy, если его задали;

Что ж, немало. Что у других?

Аппаратные реализации

Первые реализации массовых и доступных для обычных пользователей RNG (то есть реализованных не на специально докупаемой плате расширения) появились достаточно давно – например Intel добавил аппаратный генератор случайных чисел в чипсете 810 (весьма древняя линейка, 1999 год – обычно помнят следующий, 815й, и следующий за ним 845й – они стали популярны из-за приемлемого качества интегрированной графики), а VIA – в процессорах линейки C3.

Сейчас массовый аппаратный RNG представлен как расширение RdRand, добавленное в процессорах Intel поколения Ivy Bridge и AMD Zen.

*nix-системы

В Unix случайные числа обычно добываются в трёх местах – /dev/urandom (основной вариант), /dev/random (классический вариант, блокируется в случае нехватки энтропии) и /dev/srandom (почти /random у ряда BSD-систем). В дальнейшем я буду чаще писать про /dev/urandom, т.к. практически все сейчас используют именно этот вариант.

Технически, если очень рамочно, есть три пула – основной, блокирующийся и неблокирующийся.

Основной пул энтропии (Primary, 512 байт, из которого черпают свои 128 байт пулы /random и /urandom) пополняется так – каждое событие из категорий “отслеживаемые” – от клавиатуры, мыши, HDD, новых IRQ – добавляет в пул два DWORD, в одном из которых тип события, а в другом – метка времени. Что первый DWORD, что второй – весьма предсказуемы по формату-структуре, а если некоторых категорий нет вообще, так ситуация ещё более упрощается. Метка времени предсказуема и линейно увеличивается, тип события – число из фиксированного и небольшого перечня. То есть каждое пополнение номинально добавляет 64 бита, а по факту – куда как меньше.

В блокирующемся (/dev/random) и неблокирующемся (/dev/urandom) по 128 байт. Берутся эти байты из основного пула, и у каждого пула бежит счётчик “сколько взяли”. Основное различие между пулами – не в качестве исходной энтропии, а в том, что блокирующийся умеет останавливаться по исчерпанию энтропии, а неблокирующийся выдаёт бесконечную битовую строку, перемешивая имеющиеся биты при помощи криптографических функций. Второе отличие – в том, что вы можете писать в /dev/urandom, но не в /dev/random. Сделано это затем, чтобы нельзя было сторонними действиями нарушить тезис “в /dev/random – только действительно собранная с различных источников энтропия”. Если бы вы могли без ограничений добавлять свои биты в /dev/random, то могла бы возникнуть ситуация, когда биты считаются “натуральными”, а по факту добавлены кем-то, кто знает логику работы пула, и в результате появляется возможность влиять на генерацию. Неблокирующийся же пул /dev/urandom не позиционируется как “если вы запросите миллион бит, то все они будут собраны из натуральных аппаратных источников разной природы”, поэтому в него можно производить запись – но, что показательно, счётчик количества бит энтропии это не сдвинет.

Источники информации для основного пула будут такими:

  • Функция add_device_randomness() будет брать сетевые адреса, серийные номера устройств (у тех, кто умеет) и показания таймера; в силу предсказуемости части параметров данная функция пригодна лишь для инициализации генератора.
  • Функция add_disk_randomness() будет брать seek time от устройств хранения; как понятно, у виртуальных машин и SSD с этим будут проблемы;
  • Функция add_input_randomness() будет ловить события от клавиатуры и мышки. Если их нет – ну, вы понимаете.
  • Функция add_interrupt_randomness() будет записывать timestamp и значение event у прерывания, выдавая 64, а по сути 11-12 бит энтропии;

Как видно, негусто. Обсудим это чуть позже.

Примеры RNG в другом ПО и ОС

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

В PHP, допустим, современная функция random_bytes(), которая клянётся отдавать строго cryptographically secure pseudo-random bytes, честно умеет делать следующие вещи:

  • В случае Windows NT – работать враппером вокруг CryptGenRandom или BCryptGenRandom (в зависимости от версии ОС);
  • В случае линукса – вызывать getrandom();
  • В случае других *nix-ОС – запрашивать /dev/urandom;
  • Во всех остальных случаях – ловко впадать в невменоз, выбрасывая exception;

То есть это чистый враппер, ничего не умеющий делать самостоятельно.

И это даже хорошо, т.к. наглядный пример с тем, как “давайте мы улучшим имеющуюся функцию, не особо вникая в детали”, имеется прямо рядом – в том же PHP есть функция openssl_random_pseudo_bytes(), которая, судя по названию, куда как “специализированнее” в плане генерации надёжных случайных чисел. Правда, потом выяснилось, что генерила она что-то куда как менее надёжное, чем встроенный в ОС алгоритм.

В Cisco IOS вы можете разве что натолкнуться на ситуацию, когда энтропия кончилась – выглядит это примерно так:

%ENTROPY-0-ENTROPY_ERROR: Unable to collect sufficient entropy

и вам предложат стандартный набор костылей – “обновите IOS”, “создайте инцидент в TAC”. Это никак не решит ситуацию – впрочем, натолкнуться на такое можно разве что совместив очень слабое устройство (например, коммутатор линейки Catalyst 29xx) и задачу “генерить много ключей, например длинные RSA-ключи для x.509-сертификата”. В принципе, кроме как “не генерить сертификаты на встроенном в IOS криптомодуле” задача не решается.

Проблемы генерации случайных чисел в современных системах

Обычно конкретика “как именно в данной системе создаются случайные числа” скрыта от системного администратора, поэтому обычно предполагается, что числа создаются хорошо.

Однако это не совсем так – и с развитием технологий ситуация только ухудшилась. Почему?

Виртуализация

Типовой современный сервер – виртуальная машина. Ей выделяется фиксированный или достаточно хорошо прогнозируемый квант времени хоста. У неё нет RTC – “натуральный” кварц эмулируется, притом с определённых моментов (например, Gen2 у виртуальных машин Hyper-V) – совершенно символически. Она работает с SSD или виртуальными дисковыми устройствами, представляющими из себя нечто доступное по сети и распределённое – соответственно, биты энтропии, ранее забираемые от seek time и подобного – всегда одинаковые или нулевые. Устройства ввода – клавиатура и мышь – в сценарии “виртуальный сервер” также не дадут никакой энтропии, от них просто не поступает событий.

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

Сетевые устройства и IoT

Проблема не только в виртуализации – если взять типичное сетевое устройство, например роутер, то, несмотря на отсутствие виртуализации в привычном понимании этого слова, там будет та же проблематика. Генератор случайных чисел у сетевого устройства будет в 99% случаев выдран из опенсорсной *nix, опираться будет на “привычные физические источники”, а ни клавиатуры, ни мыши, ни других устройств ввода, ни жёсткого диска (вместо него обычно флэшка, доступная через ATA-интерфейс, что позволит без изменения кода считать её IDE-диском, но с нулевыми показателями по части датчиков и таймеров) – подключено не будет в принципе.

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

А если посмотрим в сторону IoT, то там ситуация ещё “лучше” – устройством там может быть нечто с ещё меньшим комплектом периферии, чем у сетевых устройств, однако случайные числа будут генериться той же функцией getrandom(), которая, в силу использования /dev/urandom по умолчанию, будет имитировать “всё хорошо”, не блокируясь от недостатка энтропии, хотя опираться может разве что на PID и таймер.

Сетевые интерфейсы

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

Однако современные сетевые адаптеры – даже самые дешёвые – весьма функциональные устройства. Одно из преимуществ у них – управление прерываниями (обычно называется interrupt moderation или как-то так). В результате применения этой технологии сетевой адаптер экономит время системы, дёргая CPU не по каждому чиху, а только когда накопит некое количество данных, либо вообще вызывает прерывания фиксированное число раз в секунду. Соответственно выходит, что данный источник энтропии в результате развития технологий тоже лишь ухудшился – “прокачанная” сетевая карта дёргает IRQ по расписанию и не чаще, чем указано в настройках.

Аппаратные закладки

Время от времени индустрия “осчастливливает” массы чем-нибудь “прорывным” в плане безопасности, а потом тема тихо сливается по причине кучи выяснившихся мелочей, перечёркивающих стартовое заявление.

Генерация случайных чисел – не исключение.

Появление чипа TPM (Trusted Platform Module) подавалось как “Ну вот и всё. Счастье для каждого. Всё безопасно – шутка ли, специальный одобренный товарищем майором для ширнармасс криптографический чип, которому можно доверять!”. Одной из функций TPM версии 1.2 (в принципе, первой появившейся на публике) и последующей 2.0 (в принципе, последней из имеющих смысл к обсуждению) был как раз встроенный генератор случайных чисел. Который, безусловно, в силу реализации на отдельном чипе решал любые вопросы вида “а откуда брать энтропию, когда система только-только включается”. Но в силу того, что чип просто-таки взасос расхваливали DoD (который United States Department of Defense) и NSA, а на вопросы об аудите отвечали хитрым смехом, TPM запомнился разве что тем, что добавил проблем для экспортёров – очевидно, что любое серьёзное применение криптографических средств никак не может опираться на получаемые из TPM значения. Энтропия от TPM, в случае наличия оного на хосте, добавляется в пул у той же Windows, но именно что добавляется, а не формирует его.

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

Чипсет Intel 810 давно почил в бозе, но память о нём сохраняется – и с ним тоже были приключения; встроенный RNG не был особо доверенным, т.к. несмотря на заявления о “технически случайные числа берутся от датчика температуры” были найдены некоторые зависимости, говорящие о другом. Возможно поэтому Intel тихо свернул тему, начатую с 810 и продолженную в 815, 830 и 845G.

В современных процессорах Intel есть, как и упоминалось в начале статьи, модуль, отвечающий за случайные числа – RdRand. Анализ работы этого модуля доступен, но весьма настораживает, что он существует в единственном экземпляре, и проведён калифорнийской Cryptography Research, Inc. по контракту с Intel. Ситуация “о полном отсутствии коррупции в МВД заявили эксперты из ГИБДД” во весь рост. Впрочем, опять же – добавлять энтропию из RdRand в пул – неплохо; плохо опираться на неё как на единственный источник (как сделано в некотором ПО, с радостью сообщающем о “полной поддержке последних поколений процессоров Intel и AMD, делающих интернет безопаснее”).

Множество имеющихся в распространённых USB-токенах генераторов также обычно вызывают вопросы – во многом благодаря тому, что за формулировками вида “У нас всё по ANSI X9.31, там на двух ключах TDES’ом обработанная энтропия выдаётся, по FIPS 140-2 норм” скрывается отсутствие информации про “а откуда вы её там берёте-то, в этой штуке, в USB-порт воткнутой, в таком количестве” и “а можно ли снаружи влиять на этот процесс?”. По моему опыту, доверять таким генераторам не имеет смысла, т.к. дважды пришлось столкнуться с низкоуровневыми утилитами от вендора, которые могли что выгрузить полный посекторный дамп токена (оба раза – очень широко известных фирм, прямо-таки известнейших), что задать ему seed для PRNG.

Обновление BIOS до UEFI также среди плюсов содержало тему про улучшение генерации случайных чисел. По факту же речь лишь о том, что появился стандартный API для запроса случайных чисел на ранней фазе старта системы. UEFI не обязан самостоятельно генерить криптографически хорошие числа – поэтому иногда встречающийся тезис вида “Hyper-V VM Gen2 безопаснее, чем Gen1, потому что в UEFI встроена новая криптуха” ложный. Просто теперь есть API, через которое тот же ntoskernel дёргает 64 байта, добавляемых в seed. А байты эти выдаёт NT-сервис на хостовой системе. Не генерит, а выдаёт.

Рост потребления бит энтропии

В добавок к вышеперечисленному растёт и количество нужных в единицу времени случайных чисел.

Простой пример – ещё лет 10 назад технология PFS существовала, но особой популярностью или критичностью применения не отличалась. Да, тогда тоже были веб-сервера – и они могли работать по SSL. Но прекрасно обходились ситуацией “разово сгенерили ключевой материал и подключаем абонентов”. Как только требования по безопасности выросли – до уровня “чтобы каждому новому, кто запросит установку сессии, индивидуальный комплект бит ключевого материала” – потребность в количестве бит энтропии выросла многократно, зачастую на порядки. На порядки – это не оборот речи; это ситуация, когда у сервера, держащего 500 сессий, раньше была задача “разово получить битовую строку, используемую для генерации ключевого материала первой и последующих сессий”, а теперь надо сделать почти то же самое, но 500 раз.

Используемые алгоритмы также стали потреблять больше энтропии – скажем, если ранее использовались группы DH 1,2,5, то сейчас будут группы DH 14-16. Числа больше, бит надо больше (ну, если исключить “звёздные” реализации, когда ключ любой длины генерится от 32х битового rand(), что влечёт за собой эпичную безопасность такой системы).

Что же делать?

Как посмотреть текущую ситуацию с RNG

В случае с Windows у нас нет встроенных инструментов оценки качества PRNG (видимо, встраивать в коммерческую ОС инструмент для оценки одного из её компонентов, притом критичного для безопасности и интегрированного – плохая примета). Однако мы сможем сделать анализ сторонними средствами – например, используя PractRand. Тестов в этом комплекте достаточно много – есть и статистические, и на FIPS 140-2, и другие.

В случае с unix-системами можно поступить проще, например так:

cat /dev/urandom | rngtest -c 10000

, где десять тысяч – число тестов.

Просмотреть доступную энтропию – именно в “исходном” пуле, /dev/random, можно вот так:

cat /proc/sys/kernel/random/entropy_avail

Учитывайте, что данная команда выводит данные о пуле-источнике, куда попадают все события, собираемые для последующего формирования бесконечного /dev/urandom. Поэтому малое число бит в /dev/random, как ни странно, не влечёт за собой вывод “а значит система стала небезопасна” – нет, генерация бесконечной битовой строки на основании стартового комплекта информации запущена, и пополнение /dev/random – лишь дополнительное “перемешивание” того, что будет влиять на результат. Впрочем, про это надо остановиться поподробнее.

Почему /dev/urandom, а не /dev/random

Миф гласит о том, что в /dev/random – годная, кошерная безопасность, с закруглёнными уголками и для успешных людей – ну а в /dev/urandom безопасность обычная, ординарная, ширпотреб, низкий класс и для нищебродов.

Это не так. Чисто технически данные файлы решают разные задачи – хоть и берут данные из одного, первичного пула в 512 байт, куда ОС складывает все считающиеся “достаточно случайными” события в виде пар DWORD’ов.

/dev/random на данное время обладает лишь одним заметным отличием от /dev/urandom – блокируется, когда из него пытаются запросить больше бит энтропии, чем есть в первичном пуле ядра.

Это не значит, что выдаваемая далее битовая строка псевдослучайных бит – плохая. Это чисто административное ограничение, накладываемое счётчиком “сколько бит израсходовали”, и, пока пул не будет пополнен новой информацией о каком-либо событии, и не будет перемешан с ней, используя какую-либо криптографическую функцию (ту же ChaCha20) – /random будет ждать. Факт поступления нового события (+ 64 условно случайных бита в пул) в данном случае пытаются приравнять к тому, что пул стал опять пригодным для использования, т.к. поступила новая энтропия. Было 300 байт – исчерпали их – счётчик на нуле; ждём; приехало ещё 8 байт – добавили их в пул, а после прошлись по пулу функцией шифрования – оп, у нас теперь есть 308 байт, и можем отдавать их.

Представьте себе, что вы вбрасываете 1024 случайных бита в генератор, который на основании оных выдаёт бесконечную битовую строку. Будут ли все биты после 1024 плохими с криптографической точки зрения? Безусловно нет. Стартовое число, которым вы инициализировали генератор – около 300 десятичных разрядов. Подобрать или угадать его – нереально. Вы можете инициализировать генератор любым случайным числом – и пока генератор выдаёт криптографически “хорошую” последовательность бит, обладающую всеми нужными характеристиками и похожую на “белый шум”, а атакующий не знает это число и механизм работы генератора, всё будет в порядке. Ваша задача – делая re-seeding, т.е. вбрасывая в генератор новую порцию энтропии, поддерживать такой уровень непредсказуемости, что техническая возможность восстановить поведение системы от старта работы была бы невозможной.

/dev/urandom – это именно такой, “разово поджигаемый” и выдающий бесконечную битовую строку генератор, забота о котором состоит в грамотной инициализации (seed) и регулярном подбрасывании дров в топку энтропии (re-seeding). Он не является менее безопасным, чем /dev/random, исключая разве что специально созданные ситуации типа “давайте инициализируем генерацию случайных чисел одним байтом и опубликуем этот байт”. В этом случае /dev/urandom, если запросить у него энтропию для генерации, например, ключа шифрования, выдаст предсказуемую последовательность, а /dev/random не сможет отдать нужное число байт. Но этот случай – чистая синтетика; стартовых 512 байт, попадающих в пул ядра после старта системы, более чем достаточно, чтобы такая ситуация не была бы возможна на практике.

В BSD-системах эта ситуация решается просто – пока не будет набрано критически нужное число бит энтропии, /dev/urandom тоже блочится, чтобы никогда не отдавать предсказуемые биты. В linux ситуация иная, и реально можно создать – именно создать ситуацию с предсказуемым выводом /dev/urandom. Но следует ли из этого, что /dev/urandom – “менее безопасная версия /dev/random“? Нет. Вам надо использовать /dev/urandom – вопрос в том, как его правильно готовить. К этому и перейдём.

Что же делать с RNG?

Итак, перейдём к действиям.

Настройка системного RNG на платформе Windows

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

С NT 6.2 вы также не можете повлиять на этот процесс из приложения, “добавляя” к пулу свою энтропию – единственный оставшийся в работе интерфейс, BCrypt, будет игнорировать эти данные. То есть если даже разработчик приложения озаботился тем, чтобы иногда вбрасывать в пул энтропию, “перемешивая” – теперь это просто игнорируется.

Это значит, что в условиях “нет своего внешнего генератора энтропии, который регистрирует своего криптопровайдера и оный выдаёт случайность” вы имеете минимальную возможность влиять на ситуацию. Однако она всё же есть.

В разделе реестра HKEY_LOCAL_MACHINE\SYSTEM\RNG существует три значения, которые будут нам в данном случае интересны. Это Seed, ExternalEntropyCount и ExternalEntropy.

Значение Seed будет представлять из себя 12 фиксированных байт – надпись “Running!” и DWORD 00 D0 07 00 – после которых будет идти 64 байта, добавляющихся к пулу энтропии и меняющиеся на каждой перезагрузке. Можно, в принципе, впасть в окончательную паранойю и регулярно менять это значение, используя некий “свой”, доверенный RNG – однако партия обо всём подумала и будет это игнорировать. То есть Windows просто сотрёт то, что вы добавите в этот ключ, и перезапишет его после перезагрузки на 12 фиксированных байт и 64 новых.

Что вообще очень подозрительно, потому что получается, что RNG “поджигается” при помощи псевдослучайной последовательности, которая потом публикуется в ключе реестра. Зачем? Ведь очевидно, что ни для чего, кроме как для попытки предсказать выход генератора, стартовый seed не нужен – его надо разово использовать и уничтожить.

Чтобы когда-нибудь не узнать о том, что очередной софт от АНБ умеет забегать в этот ключ реестра и читать данное значение, вы можете стирать его после загрузки – ну или модифицировать. Не бойтесь, вы можете делать с Seed что угодно – если это действительно просто seed RNG, свою задачу он уже выполнил, инициализировав на фазе загрузки ядра ОС генератор случайных чисел.

С этим разобрались – теперь про штатную возможность добавлять свою энтропию.

Вы можете записывать байты от доверенного генератора случайных чисел в значение ExternalEntropy (оно не создано по умолчанию, тип – REG_BINARY). Рекомендуется добавлять как минимум 64 байта – это также не случайность , в дальнейшем вся энтропия пойдёт под обработку SHA-2/512, а хорошей традицией является предоставлять криптографической хэш-функции на вход больше данных, чем после ожидать на выходе. Система съест эти данные на старте ядра и сотрёт ключ.

Осталось только значение ExternalEntropyCount – с ним всё просто. Это – “сколько мы использовали на старте источников энтропии”. У вас оно, должно быть, равно 2.

Система не умеет самостоятельно создавать этот ключ, поэтому удалять его не надо – после перезагрузки он не появится. Значение не будет обновляться, пока вы не подкинете новый ключ ExternalEntropy – тогда при загрузке система его использует, подкинув дровишек в entropy pool, и добавит +1 к ExternalEntropyCount.

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

Таким образом, влиять на встроенный RNG в Windows мы можем, но увы – только на фазе инициализации. Что тоже неплохо. Однако разрабатывая ПО, которому нужны хорошие случайные числа, учитывайте, что вы никак не сможете “закрутить гайки” на встроенном в Windows PRNG – например, время от времени добавляя ему энтропии из внешнего источника. Поэтому имеет смысл использовать другие PRNG.

Настройка системного RNG на Unix-системах

Задачу “как стартово поджигать генератор на перезагрузке системы” решили в данном случае достаточно давно – в момент shutdown’а у системы сохраняется 512 байт от /dev/urandom, а после включения – они же пишутся в этот файл. Так как после добавления любого количества данных весь пул “перемешивается”, то данный метод снимает вопрос с “система только включилась, и нужны доп.действия, чтобы выжать из генератора что-то похожее на случайные числа”.

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

Например, можно раз в час добавлять в пул энтропии данные о текущих сетевых подключениях. Это несложно – в /etc/cron.hourly создать файл, в котором будет вызываться что-то типа такого:

netstat -onWtu > /dev/urandom.

В этом примере параметры ограничат выгружаемые в пул данные tcp- и udp-подключениями, у нас точно не будет задержек из-за отключения трансляции адресов в имена, а общая масса данных может быть даже избыточна (ну, в этом случае можно и урезать вывод). Главное, что свою задачу такой вариант решит без проблем – в данных будут и IP-адреса + порты со стороны клиентов, и таймеры подвисших в состоянии TIME_WAIT TCP-сессий, а если запрашивать это раз в час, то данные с гарантией будут совсем другими.

Вариант будет очень неплох для сценария “виртуальная машина без стандартных источников энтропии со стороны устройств ввода и HDD, без TPMа на хосте, без данных от UEFI, с неизвестно каким стартовым seed”.

По аналогии, кстати, можно работать и с сетевым оборудованием на различных unix-ОС, к которому возможно удалённое подключение по SSH – сделать на них или локально запускаемый по cron’у и добавляющий энтропии скрипт (он может даже в случае очень урезанной сетевой ОС перенаправлять вывод чего угодно, что даёт энтропию – хоть ping’а удалённого узла – и все равно в этом случае вывод будет непредсказуем в части времени в миллисекундах, что нам подойдёт), или подключающийся удалённо по расписанию и вбрасывающий в /dev/urandom.

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

ps --sort -time -evf | head -n 10 > /dev/urandom

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

Такого можно придумать множество.

В случае, если у вас сетевое устройство, где никак не получается повлиять на генерацию случайных чисел – подумайте о том, чтобы это устройство никогда не создавало себе x.509-сертификаты, а также не пытайтесь использовать его для задач вида “VPN-концентратор” или “терминирует TLS-сессии”. Иначе, опять же, вы рискуете узнать о своих достижениях в плане безопасности из очередных откровений очередного Сноудена. Никто и никогда не будет отвечать за то, что конкретная железка X придумывает “случайные” числа исходя из, допустим, миллисекунд текущего времени.

Внешние RNG

Использование практически любого внешнего RNG сопряжено с единственным вопросом – насколько вы доверяете тому, что говорит производитель.

Производитель любого криптографического устройства будет подпадать под лицензирование в своей стране (так вышло, что на планете Земля нет территорий где развёрнуты производства криптографического оборудования, но находящихся при этом вне гос.границ). Из этого будет следовать, что он – фирма, конкретные физ.лица – будут подписывать конкретные бумаги и нести ответственность за то, чтобы продукция соответствовала регулированию по законодательству.

Рекламные заявления идут параллельно этому и содержат информацию, которая способствует продажам.

Ни один производитель не объяснит, что парни из АНБ вызвали его на ковёр и попросили, чтобы каждое чётное “случайное число”, которое выдаётся генератором, было бы младшим байтом от SHA-2/384 от текущей микросекунды. А подобных задач по “разумному упрощению сложности” – масса, и решаются они крайне хорошо. Вы не сможете вычислить сторонним анализом то, что такая закладка есть – выходящая битовая строка может выглядеть как “белый шум”, проходить все тесты, но при этом обладать свойствами, позволяющими и предсказывать будущие, и угадывать предыдущие результаты.

Вот, например, прекрасная цепочка байт:

6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b
d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35
4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce
4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a
ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d

Если выдавать её как бесконечную битовую последовательность – она удачно пройдёт все тесты на “чтобы групп из единиц и нулей подряд было бы разумное число”. Но при этом является SHA-2/256 хэшами от натуральных чисел от 1 до 5. Тот, кто знает это, по группе из четырёх семёрок подряд догадается, что генератор обрабатывает число 4, а потом будет выдавать хэш от 5. Минимальное усложнение такой схемы приводит к невозможности анализа, т.к. выходящие данные криптографически безупречны.

Поэтому до того как покупать аппаратный RNG, либо использовать внешний сервис, либо разворачивать какой-нибудь сетевой сервис в своей сети – подумайте о целесообразности. Рациональнее корректно администрировать и “подпитывать” энтропией (притом не зависящей от конкретного вендора ПО или оборудования) имеющийся генератор, изучив его работу, чем на каждый чих ставить отдельный костыль, решающий только данную задачу – и при этом добавляющий другие.

Если развернуть, например, сетевой источник энтропии – типа того же EGD – то появятся вопросы “как обеспечить доступность его по сети во всех случаях / что делать с появляющимися задержками на запросах / что делать в случае высокой загруженности сети”. А также вопросы совместимости (например, источник написан на скриптовом языке конкретной версии, интерпретатор нельзя обновлять либо надо специфически настраивать в случае пересборки). Плодите сущности только в случае прямых медицинских показаний.

Для примера можно разобрать пару распространённых программных решений.

Дополнительный источник энтропии – HAVEGEd

Устанавливается тривиально, работает как сервис, пополняет пул энтропии, используя непредсказуемость работы кэша CPU и связанных с этим таймингов. Пополняет только когда нужно – ловит write_low_watemark и перекачивает биты из своего пула энтропии в ядро. Перечень зависимостей, от которых и достаются эти биты, достаточно приличен:

  • Correct or incorrect branch prediction (both direction and target)
  • Hit or miss of the instructions in the ITLB
  • Hit or miss of the instructions on the instruction cache
  • Hit or miss of the data load on the data cache
  • Hit or miss of the data load on the data TLB
  • In case of miss on one of those caches, hit or miss on the L2 cache.
  • + очевидная зависимость от “разные инструкции по разному обрабатываются в pipeline”

Часть из них, безусловно, будут предсказуемее в сценарии “виртуальная система”, чем “физический хост”. А часть – наоборот, т.к. будут зависеть от количества VM на хосте, их настроек в плане “кому сколько ядер процессоров”, а также фактической загрузки.

HAVEGE daemon можно смело использовать как дополнительный источник энтропии.

Дополнительный источник энтропии – EGD

EGD, который Entropy Gathering Daemon, достаточно древний проект, представляющий из себя скрипт на Perl.

Несмотря на то, что проект достаточно давно не дорабатывается, его иногда используют. Причина проста – у OpenSSL в документации есть страница про то, как можно использовать внешние источники энтропии, где EGD мало того, что совпадает с названием класса источников (т.е. EGD и конкретный продукт и все внешние демоны энтропии), так ещё и имеет прямую ссылку.

Впрочем, если вчитаться в текст, то OpenSSL начинает рассказ о том, как подцепить EGD с того, что “это было нужно на старых системах, где не было /dev/urandom“. Поэтому вопрос о применяемости EGD – особенно на ядрах старше 4.8, с современным и быстрым /dev/urandom – весьма спорный, EGD банально не нужен. А в сценарии “доступен по сети” – и подавно; задержки, дополнительные зависимости – всё это может лишь ухудшить ситуацию. Добивает ситуацию то, что EGD может заблочиться от интенсивных запросов. Поэтому лучше, чтобы OpenSSL брал энтропию из локального /dev/urandom.

Подкормка /dev/random – RNGd

RNGd делает чуть другую задачу, чем вышеперечисленные товарищи – он отслеживает фактическое число энтропии в пуле и, если оно падает ниже половины (256 байт), подкачивает её из /dev/hwrandom блоками по 64 байта (все значения, если надо, настраиваемы). RNGd нужен если вы имеете источник энтропии (это может быть и внешнее устройство, например TPM, и реальный файл) и хотите автоматически пополнять из него системный пул. RNGd не создаёт энтропию.

Если у вас нет аппаратного устройства, и пул уже дополнительно пополняется другим способом, RNGd не нужен.

Советы

Используйте на Windows-хостах возможность добавления своей энтропии для стартового seed и организуйте зачистку seed после старта ОС. Это единственное, что вы можете сделать на уровне системы (и это работает, проверено дизассемблированием – энтропия из реестра действительно добавляется). Если у вас Windows младше NT 6.1, то дополнительный повод обновиться – наличие DualEC и использование его для ряда системных задач. В случае разработки криптографического ПО озаботьтесь тем, чтобы не использовать встроенный в Windows RNG, либо добавляйте энтропию к его выводу (например, организуйте свой пул, набейте его псевдослучайными числами и используйте вывод CNG’шного RNG как ключ для HMAC на SHA-2/512 над пулом).

Используйте на *nix-хостах /dev/urandom. Добавляйте энтропию в пул как минимум двумя способами – и чем-то типа HAVEGEd, чтобы пополнялся именно первичный пул, и периодическими записями в /dev/urandom, чтобы “перемешивалось” то, что используется для формирования исходящей последовательности.

Не создавайте ключи на системах, где нет возможности влиять на генерацию случайных чисел. Как минимум – реализуйте создание сертификатов для сетевых устройств через SCEP.

Проще добавлять энтропии в существующий в системе RNG, чем плодить сущности.

Не доверяйте ПО, которое вместо ответов на “зачем у вас свой личный специальный RNG” и аналитики содержит массу лозунгов и мемов про Свободу, Распределённость, Революционность, Открытость, Меняя Реальность, Прорывность и прочее. Примером можно считать штатный АНБшный инструмент для харвеста дурачков называющийся Tor, у которого семь новых RNG на неделе, не считая восьмого, реализованного через remote code execution. Туда вообще можно добавить что угодно с криками “не время сейчас думать и рефлексировать – мы такие-то безопасные, против Системы и Родаков, поэтому срочно обновляйтесь, будет только лучше, вам Распределённое Сообщество будет числа генерить”.

Подробный анализ заклинаний вида “Это не просто случайные числа, это Одобренные Внешним Кворумом Анонимных Распределённых Участников, а значит Святые случайные числа, поэтому аксептить их не просто полезно а богоугодно, плохое не пришлют, а сомнение суть ересь, веруем а не мыслим”, увы, выходит за рамки технической статьи.

Напоследок

Как видите, у задач сетевой (и не только) безопасности – много тонкостей, с которыми лучше ознакомиться заранее.

Данный материал не предназначен для переубеждения тех, у кого вродь работает, а также острое ахаха да кому мы нужны лол – задачей является расширение той зоны, в которой происходят понятные системному инженеру события. Чем больше частей комплексного процесса известны и не представляются чёрными ящиками, описываемыми в терминах веры – тем лучше. Чем больше потенциальных возможностей и “закладок на будущее” будет выявлено и аннулировано – тем меньше перспективных граблей будут ждать за линией горизонта.

К тому же, это ещё и просто интересно.

Удачного использования!

Вернуться к полному списку статей Knowledge Base @ Advanced Training

Ruslan V. Karmanov

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