"Генерация случайных чисел слишком важна, чтобы оставлять ее на волю случая" - Роберт Р. Кавью

Как-то поздним летним вечером мне пришлось разобраться, как устроены генераторы случайных чисел в Windows и Linux. Собственно, в этой статье я попробую привести саккумулированную информацию, и преподнести ее максимально простыми словами, без необходимости лезть в исходники, туториалы и статьи.

Генераторы псевдослучайных чисел

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

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

Немного про энтропию

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

Генератор псевдослучайных чисел - это функция, которая перемешивает входные биты, применяя к ним простые операции несколько раз, выдает наружу результат, который и является последовательностью случайных бит. Для примера рассмотрим алгоритм ChaCha20, применяемый в Linux, и SP800-90 AES-CTR-DRBG в Windows

  • ChaCha20 - это развитие алгоритма Salsa20. Он основан на комбинации операций: 32-битное сложение, XOR и побитовое вращение. Если кратко: матрица 4 * 4 заполняется особой константой, ключом, счетчиком и одноразовым числом для текущей итерации, а затем происходит перемешивание бит в течение 20 раундов алгоритма. При этом нечетные раунды отвечают за изменения бит по столбцам матрицы, а четные за изменения по диагоналям. Полученные на выходе биты и есть псевдослучайное число, которое к тому же является входным состоянием для следующего запуска генератора. Но, так как при таком подходе было бы очень легко предсказать все следующие числа, существует обязательная операция изменения ключа после каждого запроса;

Код перемешивания в ChaCha
// linux/lib/crypto/chacha.c
for (i = 0; i < nrounds; i += 2) {
// Odd round
    x[0]  += x[4];    x[12] = rol32(x[12] ^ x[0],  16);
    x[1]  += x[5];    x[13] = rol32(x[13] ^ x[1],  16);
    x[2]  += x[6];    x[14] = rol32(x[14] ^ x[2],  16);
    x[3]  += x[7];    x[15] = rol32(x[15] ^ x[3],  16);

    x[8]  += x[12];   x[4]  = rol32(x[4]  ^ x[8],  12);
    x[9]  += x[13];   x[5]  = rol32(x[5]  ^ x[9],  12);
    x[10] += x[14];   x[6]  = rol32(x[6]  ^ x[10], 12);
    x[11] += x[15];   x[7]  = rol32(x[7]  ^ x[11], 12);

    x[0]  += x[4];    x[12] = rol32(x[12] ^ x[0],   8);
    x[1]  += x[5];    x[13] = rol32(x[13] ^ x[1],   8);
    x[2]  += x[6];    x[14] = rol32(x[14] ^ x[2],   8);
    x[3]  += x[7];    x[15] = rol32(x[15] ^ x[3],   8);

    x[8]  += x[12];   x[4]  = rol32(x[4]  ^ x[8],   7);
    x[9]  += x[13];   x[5]  = rol32(x[5]  ^ x[9],   7);
    x[10] += x[14];   x[6]  = rol32(x[6]  ^ x[10],  7);
    x[11] += x[15];   x[7]  = rol32(x[7]  ^ x[11],  7);
// Even round
    x[0]  += x[5];    x[15] = rol32(x[15] ^ x[0],  16);
    x[1]  += x[6];    x[12] = rol32(x[12] ^ x[1],  16);
    x[2]  += x[7];    x[13] = rol32(x[13] ^ x[2],  16);
    x[3]  += x[4];    x[14] = rol32(x[14] ^ x[3],  16);

    x[10] += x[15];   x[5]  = rol32(x[5]  ^ x[10], 12);
    x[11] += x[12];   x[6]  = rol32(x[6]  ^ x[11], 12);
    x[8]  += x[13];   x[7]  = rol32(x[7]  ^ x[8],  12);
    x[9]  += x[14];   x[4]  = rol32(x[4]  ^ x[9],  12);

    x[0]  += x[5];    x[15] = rol32(x[15] ^ x[0],   8);
    x[1]  += x[6];    x[12] = rol32(x[12] ^ x[1],   8);
    x[2]  += x[7];    x[13] = rol32(x[13] ^ x[2],   8);
    x[3]  += x[4];    x[14] = rol32(x[14] ^ x[3],   8);

    x[10] += x[15];   x[5]  = rol32(x[5]  ^ x[10],  7);
    x[11] += x[12];   x[6]  = rol32(x[6]  ^ x[11],  7);
    x[8]  += x[13];   x[7]  = rol32(x[7]  ^ x[8],   7);
    x[9]  += x[14];   x[4]  = rol32(x[4]  ^ x[9],   7);
}
Заполнение массива раунда ChaCha
Заполнение массива раунда ChaCha
  • SP800-90 AES CTR DRBG (англ.: CounTeR mode Deterministic Random Byte Generator) - это криптографически стойкий генератор псевдослучайных чисел, основанный на блочном шифровании AES (англ.: Advanced Encryption Standard) в режиме счетчика. Текущее состояние генератора описывается тремя объектами: ключ K, вектор V, который является счетчиком, и счетчик повторного заполнения counter. В упрощенном виде процесс создания выходной последовательности можно разбить на 2 части:

    • Создание ключа K и начального вектора V при помощи алгоритма обновления и дополнительных входных данных;

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

Алгоритм обновления
Алгоритм обновления
Создание выходных данных
Создание выходных данных

Случайные события

Осталось разобраться с самым интересным - чем инициализировать ГПСЧ, чтобы получать действительно случайные числа? Кроме того, проблема состоит не только в инициализации, но и в постоянной переинициализации, которая необходима для предотвращения возможности предсказания следующего состояния. Вот тут и начинается самая интересная и важная работа по поиску случайности в событиях в системе.

Linux

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

В данный момент используется 4 типа источников случайных событий:

  • Информация от устройств, которая должна быть разной на физически разных машинах, например MAC адрес сетевой карты. Фактически, это не добавляет энтропии системе, но позволяет в очень плохих случаях (запуск с одного образа) на разных устройствах получать разные состояния;

  • Информация от таймера, прерывания, типа прерывания, значения;

  • Время прерывания;

  • Информация о времени поиска блока на диске. Однако на современных SSD это достаточно плохой источник случайности, так как у них время поиска сравнительно маленькое и примерно одинаковое всегда.

Чтобы инициализировать или переинициализировать ГПСЧ, необходимо из пула энтропии достать несколько случайных байт. Для этого, весь пул хэшируется алгоритмом SHA-1, а хэш-сумма выдается как случайный набор бит. При этом предпринимаются меры для обеспечения безопасности генератора в будущем. Во-первых, результат хэширования перемешивается с пулом, чтобы по выходному значению нельзя было восстановить текущее состояние. Во-вторых, происходит постоянная оценка оставшегося количества энтропии в пуле.

Из-за последнего существует 2 способа взаимодействия со случайными числами в Linux - /dev/random и /dev/urandom. Первый блокируется, когда оценка по количеству энтропии становится ниже нуля, а второй выдает числа всегда, даже если пул не пополняется случайными битами. При этом числа все еще могут оставаться достаточно случайными для требуемой задачи.

Стоит добавить, что на многих шагах, где это имеет смысл, в коде добавлены обращения к "железным" генераторам случайных чисел, которые работают быстрее и дают более качественную энтропию. Это, возможно, было бы не так важно, но Intel еще в Ivy Bridge добавили инструкции RDRAND и RDSEED, позже и AMD сделали это. Таким образом у многих современных компьютеров в CPU есть быстрый генератор случайных чисел. Почему это необходимо - будет объяснено в заключении.

Windows

В Windows процесс создания случайных чисел подчинен достаточно сложной древовидной структуре. Есть три типа источников энтропии, которые отличаются предназначением и качеством. Энтропия, получаемая из них, используется для инициализации и переинициализации корневого ГПСЧ - все случайные числа тем или иным образом получаются из него. Так как в современных многоядерных компьютерах было бы непозволительно медленно иметь только один генератор, то для каждого логического CPU, создается свой ГПСЧ, который инициализируется корневым. Далее, на каждый пользовательский процесс, заводятся и инициализируются свой ГПСЧ и его дочерние генераторы для каждого логического CPU. Таким образом мы получаем что-то вроде дерева генераторов.

Все генераторы, кроме корневого, переинициализируются, когда понимают, что их состояние устарело. Это происходит с помощью ведения и сравнения эпох. Счетчик инкрементируется каждый раз, когда корневой генератор переинициализируется. При этом каждый из его "потомков" в дереве локально запоминает состояние счетчика при его собственной переиницализации. Корневой генератор заполняется по расписанию - при загрузке системы, а далее с экспоненциально растущим периодом: 1, 3, 9, 27 секунд и т.д. Максимальное значение периода составляет 1 час.

В качестве источников энтропии в Windows используются:

  • Время прерываний (основной источник) - при каждом прерывании берется отметка о времени с помощью чтения с TSC (англ.: TimeStamp Counter) и записывается в специальный массив на 256 байт компактным образом;

  • TPM (англ.: Trusted Platform Module) - выдает 40 байт на старте и 64 байта при каждой переинициализации, но из-за ограничений, не может этого делать чаще, чем раз в 40 минут;

  • RDRAND/RDSEED - "железные" генераторы, предоставляемые процессором;

  • Seed файл - запись в реестре, которая создается ОС во время работы и используется во время следующей загрузки;

  • Внешняя энтропия - запись в реестре, которая делается установщиком для первого запуска системы, но также может быть использована пользователем в будущем, чтобы влиять на процесс инициализации;

  • ACPI-OEM0 - создается гипервизором Hyper-V и заполняет при каждом запуске гостевой ОС;

  • Данные из драйверов - хэшируются и представляются как очень плохой источник энтропии, который, однако, позволяет по-разному инициализировать систему на разных физически машинах;

  • UEFI - случайные числа из UEFI-драйвера;

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

  • Уникальное (не случайное) число от Hyper-V - помогает бороться с повторением состояния при запуске снапшота системы.

Hyper-V

Как правило, не только Hyper-V при работе с Windows предоставляет такие улучшения. Многие гипервизоры "прикидываются" Hyper-V, чтобы обеспечивать такой же функционал и использовать встроенные возможности по повышению производительности при работе с Windows.

Во время старта системы данные с 7 источников (Seed файл, внешняя энтропия, TPM, RDRAND, ACPI-OEM0, UEFI и время старта) хэшируются SHA-512 и используются для инициализации SP800-90 AES-CTR-DRBG. Уже во время работы системы, данные предоставленные источником, помещаются в пул (за исключением первого раза, когда они идут сразу на переинициализацию корневого ГПСЧ).

Заключение

Как можно было заметить, многие источники случайных событий связаны с текущим состоянием машины, следовательно при виртуализации могут начаться проблемы. В Linux в комментариях к коду иногда открыто признается эта проблема. В Windows с Hyper-V (или другим гипервизором, "прикидывающимся" им) пытаются с этим бороться, но сама проблема все же иногда проявляется. Ситуация несколько облегчается, тем фактом, что в современных процессорах есть "железные" генераторы случайных чисел, а так же существуют виртуализированные генераторы, которые подсовывают случайные числа хостовой ОС гостевой. Ведь нельзя оставлять это на волю случая...

Литература

Linux
Windows
ChaCha20
SP800-90 AES-CTR-DRBG

Комментарии (26)


  1. vilgeforce
    06.12.2021 00:16
    +4

    Статья немного не полна как минимум по части Windows: в каких версиях используется описываемый алкгоритм? Какие API-функции его используют? Мне известно минимум две функции WinAPI для генерации случайных чисел и еще 2-3 штуки для генерации криптоключей. И есть у меня подозрения, что описанный алгоритм может быть ими (не)использован поразному...


    1. M_ximus Автор
      06.12.2021 00:33

      Да, спасибо за комментарий. Боялся перегрузить и сделать статью огромной и нечитаемой. По Windows в основном полагался на статью для Windows 10. Основной API:

      • SystemPrng

      • ProcessPrng

      • BCryptGenRandom

      • CryptGenRandom

      • RtlGenRandom


  1. ifap
    06.12.2021 00:25

    Во-первых, эти события должны быть недетерминированные. Во-вторых, их должно быть тяжело пронаблюдать извне.

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


  1. mentin
    06.12.2021 00:46
    +3

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


  1. Hlad
    06.12.2021 10:41

    Почему-то вспомнилась лабораторная работа в университете: её целью было формирование нормального распределения при помощи генератора случайных чисел.
    Периодически у студентов вместо красивого горба из учебника получался «забор»: в определённых условиях ГСЧ вырождался, и начинал выдавать определённые числа по кругу. Интересно, существуют ли алгоритмы, у которых математически доказано отсутствие вырождение ГСЧ на большом промежутке времени?


    1. PrinceKorwin
      06.12.2021 10:45

      Интересно, существуют ли алгоритмы, у которых математически доказано отсутствие вырождение ГСЧ на большом промежутке времени?

      Немного не в теме, а числа после дроби в Пи можно такими считать?



  1. Tarakanator
    06.12.2021 12:52

    Мне вот недавно надо было сгенерировать надёжный пин код.
    Я задумался как сгенерировать случайный?
    Пришлось бросать 4 монеты,
    1,2,5,10 рублей.
    4 бита дают значения 0-15.
    если выпадают 10-15 значение отбрасывается.
    Если 0-9 вуаля, у нас есть одна цифра пина.

    P.S. нет 12 гранного кубика у меня не нашлось.


    1. amarao
      06.12.2021 15:12

      А я обычно делаю `echo $RANDOM` и этого достаточно для любых оффлайновых случайностей.


      1. Tarakanator
        06.12.2021 15:16

        Мне захотелось максимальной секъюрности.
        Правда это только с пин кодом работает. 256бит ключ я так поленился генерировать.


        1. amarao
          06.12.2021 17:02
          +1

          А в чём несекьюрность echo $RANDOM? Карго культ?


          1. Tarakanator
            06.12.2021 19:51
            +1

            Чтобы воспользоваться каким-то секьюрным генератором вначале надо почитать почему он считается секьюрным(и считается ли).
            Читать дольше, чем десяток раз подбросить монетки.
            Ну и была идея хоть что-то сделать правильнее некуда.


            1. amarao
              06.12.2021 23:29

              С точки зрения единичного числа-секрета в оффлайне достаточно знать, что любое число из диапазона вероятно.


              1. Tarakanator
                07.12.2021 09:19

                РАВНОвероятно.


                1. amarao
                  07.12.2021 15:01

                  А если оно не равновероятно, то для пин-кода угроза состоит в том, что...?


                  1. Tarakanator
                    07.12.2021 16:01

                    Ну давайте представим. Пин код 2 бита(для наглядности).
                    шанс угадать, при условии что биты случайные-1/2^2=0.25
                    Теперь представим себе что каждый бит с вероятностью 0.25=0 и с вероятностью 0.75=1
                    Тогда заявив что пин код 11 вы угадаете его в 0.75^2=0.5625 случаев. Падение безопасности более чем в 2 раза.


                    1. amarao
                      07.12.2021 16:08

                      Для этого надо знать, чем вы генерировали. Так что я всё равно спрошу, в чём риск для единичного оффлайн-секрета?


                      1. Tarakanator
                        07.12.2021 16:12

                        Для этого надо знать, чем вы генерировали.

                        Надо. Но:
                        1)можно предположить.
                        2)вы поручитесь что у вас на компе нет зловредов? я нет.


                      1. amarao
                        07.12.2021 16:18

                        Окей, даже если вы знаете, что человек набрал `echo $RANDOM`, что это даёт? Три попытки. Вы даже не знаете алгоритм отображения [0-65535] в [0-9999].

                        Да, я весьма уверен, что у меня на компьютере нет malware бытового уровня.


                      1. Tarakanator
                        07.12.2021 16:38

                        Вы даже не знаете алгоритм отображения [0-65535] в [0-9999].

                        Это значит, что я УЖЕ знаю, что 0-9999 будет вероятнее, чем 10000-65535


  1. amarao
    06.12.2021 15:11
    +3

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

    Фактически, спасение для виртуалок, у которых очень мало родной энтропии.


  1. lunacyrcus
    06.12.2021 16:04

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

    Например, один только блок питания может дать кусок аналоговой энтропии, так как он выдает множество разных напряжений и они никогда не бывают идеально выдержаны, так или иначе присутствуют колебания (зависимые и от состояния самого БП, и от окружающей среды/температуры, которая тоже колеблется в микро-пределах всегда, и от нагрузки на те или другие компоненты ПК, и от сети), и предсказать их практически невозможно.

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

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

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

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

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

    // да уж понеслась фантазия, ну можно реально до бесконечности, это уж получается переусложненное решение банальной проблемки

    В общем по мне так давно уже не проблема получить надежный ГПСЧ, вот в старые времена может это еще было проблемой, и то только потому что с этим вопросом тогда вообще не сильно заморачивались, вот как стало важно/актуально, так видно что быстро сделали хорошие генераторы.


    1. 13werwolf13
      07.12.2021 07:58
      +2

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

      а вот они то емнип задействованы, покрайней мере в linux (если исходить из того что мне когда-то преподавали в ВУЗе), да и в статье шла речь про драйверы устройств как источник в windows (почему-то плохой).

      а ещё вспоминается webmoney где надо было подёргать мышкой в пределах окна для получения энтропии и archlinux при установке которого надо подолбить по клавиатуре для тех же целей (или это уже пофиксили?).

      но если в современных ОС ГпСЧ хотя бы имеет документацию то вот спец приборы выдающие рандом обычно представляют собой чёрный ящик (помнится создатели "годвиля" такой купили за дорого и оказались разочарованы) непонятно как и насколько "случайно" работающий. вот про них я бы почитал..


  1. speshuric
    06.12.2021 23:05
    +1

    В Windows процесс создания простых чисел подчинен достаточно сложной древовидной структуре.

    А зачем там именно простые числа?


    1. M_ximus Автор
      06.12.2021 23:09
      +1

      Это очепятка, там конечно же, случайные, а не простые
      Спасибо огромное!


      1. speshuric
        06.12.2021 23:14

        Чёрт, ну я даже подумал же через Ctrl-Enter сообщить. Но решил, что это я дуб дубом и почему-то упустил, что где-то в AES-based ГПСЧ нужны именно простые.

        И тогда еще вопрос: я же правильно догадываюсь, что MS взял именно AES-based, чтобы на всех более менее современных процессорах использовать аппаратное ускорение AES?