Апокриф 03

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

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

Криптография держится на секретах, а секреты берутся из случайности. Ключ шифрования, nonce, IV, соль, одноразовый токен, коэффициент ECDSA — всё это должно быть непредсказуемым для противника. Если генератор предсказуем, стойкость алгоритма не имеет значения: противник не подбирает ключ, он его вычисляет. Эта тема разбирает, чем криптографический генератор отличается от обычного, как восстанавливается состояние популярных PRNG и какие реальные катастрофы произошли из-за одного неверного вызова.

Она опирается на тему 3 основного курса и на апокриф 2 (модели атак, перебор). Атаки на nonce в ECDSA подробно разбираются в апокрифе 10; здесь — общая механика и восстановление состояния генератора.

Где криптографии нужна случайность

Секреты и одноразовые значения

Случайность входит в систему в четырёх ролях. Первая — долговременные ключи: их энтропия определяет стойкость всей системы. Вторая — одноразовые значения: nonce в AEAD, IV в режимах, k в ECDSA, соль в хешировании паролей. Третья — идентификаторы и токены: сессионные ключи, токены сброса пароля, промокоды. Четвёртая — маскирование и blinding в реализациях, защищающих от сторонних каналов.

Требования к этим значениям разные. Ключ должен быть равномерно распределён и секретен. Nonce — уникален для данного ключа, и его уникальность важнее секретности: в GCM повтор nonce ломает аутентификацию, даже если nonce был «непредсказуем». Соль — уникальна и не обязана быть секретной. Токен сброса — непредсказуем и одноразов.

Свойства хорошего генератора

Криптографически стойкий генератор псевдослучайных чисел (CSPRNG) обязан выдерживать различение: никакой эффективный противник, зная алгоритм и все прошлые выходы, не может предсказать следующий бит с преимуществом лучше случайного. Это сильнее статистической «похожести на шум»: последовательность может пройти все тесты NIST SP 800-22 и всё равно быть предсказуемой, если её внутреннее состояние восстанавливается 5.

Отсюда два независимых требования: хорошая статистика и стойкость к восстановлению состояния. Обычные PRNG обеспечивают первое и не обеспечивают второе.

Некриптографические генераторы и их вскрытие

Линейный конгруэнтный генератор

LCG задаётся рекуррентностью x_{n+1} = (a·x_n + c) mod m; выход — обычно старшие биты x_n. Параметры подбираются так, чтобы период был большим, но линейность остаётся. По нескольким последовательным выходам параметры и состояние восстанавливаются алгебраически, но число нужных выходов зависит от того, что известно. Если модуль m известен и наблюдаются полные значения x_n, трёх выходов достаточно, чтобы найти a и c. Если известны только старшие биты, а не всё состояние, трёх значений уже недостаточно: младшие разряды потеряны, и их приходится перебирать. Если неизвестен и m, выхода три мало — модуль ищется как делитель величины t_{i+2}·t_i − t_{i+1}^2, где t_i = x_{i+1} − x_i, и на практике нужны четыре-пять выходов и перебор делителей. После восстановления параметров следующий выход вычисляется точно.

Пусть m = 2^31 − 1, a = 48271, c = 0, и известны x_1, x_2, x_3. Тогда x_2 ≡ a·x_1, x_3 ≡ a·x_2, откуда a ≡ x_3·x_2^{-1} mod m; для c ≠ 0 используется разность x_{n+2} − x_{n+1} = a·(x_{n+1} − x_n), из которой a находится по обратному элементу. В лабораторной работе восстанавливаются параметры по трём-четырём выходам и проверяется предсказание четвёртого.

Mersenne Twister

MT19937, стандартный генератор Python (random), PHP, Ruby и многих других сред, имеет состояние из 624 32-битных слов и период 2^19937 − 1 1. Он отлично проходит статистические тесты и совершенно не годится для криптографии: каждое выданное 32-битное число после обратимой «закалки» (tempering) соответствует одному слову состояния. Собрав 624 последовательных выхода, противник обращает закалку, получает все слова состояния и может вычислить любое будущее (и прошлое) значение.

На рисунке 1 показан весь путь от выходов к предсказанию: 624 выхода, обратная закалка, состояние, следующий выход.

Клонирование MT19937 по 624 выходам
Рисунок 1. Восстановление состояния MT19937: обратная закалка каждого выхода и предсказание следующего значения

Закалка устроена как цепочка y ^= y >> 11, y ^= (y << 7) & 0x9D2C5680, y ^= (y << 15) & 0xEFC60000, y ^= y >> 18. Каждое преобразование обратимо: для x ^ (x >> s) старшие s бит результата совпадают с x, по ним восстанавливаются следующие s бит и так далее; для x ^ ((x << s) & mask) аналогично восстанавливаются младшие биты. После 624 обращений состояние известно полностью.

На рисунке 2 показана последовательность атаки на приложение: сбор выходов, клонирование, запрос сброса и подстановка предсказанного токена.

Атака на токен сброса пароля
Рисунок 2. Мэллори собирает 624 промокода, клонирует генератор и предсказывает токен сброса Алисы

В лабораторной работе это применяется к учебному приложению: промокоды и токены сброса пароля берутся из одного random.Random. Собрав 624 промокода, противник клонирует генератор и вычисляет токен сброса чужой учётной записи, не зная пароля и не имея доступа к «почте».

Почему статистика не спасает

MT19937 проходит тесты на равномерность, серийность и корреляцию. Проверка «выглядит случайно» отвечает на вопрос о распределении, а не о предсказуемости. Криптографическая стойкость — это невозможность восстановить состояние и предсказать следующий выход, а она требует нелинейности и секретного состояния. Именно поэтому CSPRNG строятся на хешах, блочных шифрах или ARX-функциях с ключом, а не на регистрах сдвига и линейных рекуррентностях.

CSPRNG и генераторы операционной системы

Конструкции

Практические CSPRNG делятся на три семейства. Первое — на блочных шифрах: CTR_DRBG шифрует счётчик ключом, который обновляется после каждого запроса. Второе — на хешах: Hash_DRBG и HMAC_DRBG смешивают энтропию и состояние через хеш, что даёт хорошее перемешивание и простую реализацию 2. Третье — на поточных шифрах: ChaCha20-based DRBG, используемый в ядре Linux и в OpenBSD.

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

Linux: getrandom и /dev/urandom

В Linux источником энтропии служат прерывания, джиттер таймеров, драйверы и инструкции RDRAND/RDSEED. Ядро поддерживает пул энтропии и выдаёт из него значения через getrandom(2), /dev/urandom и /dev/random. Историческое различие «urandom не блокируется, random блокируется» устарело: с ядра 5.6 /dev/random блокируется только до инициализации пула, а после неё ведёт себя как /dev/urandom 7. Правильный способ получить криптографические байты — os.urandom или secrets в Python, getrandom в C.

В Python random — это MT19937 без криптографических гарантий 8; secrets.token_bytes, secrets.token_hex, secrets.randbelow используют os.urandom. Замена random на secrets в коде приложения — не косметика, а устранение целого класса атак.

Dual_EC_DRBG и доверие к параметрам

NIST SP 800-90A включал Dual_EC_DRBG — генератор на эллиптических кривых, который после разоблачений 2013 года был заподозрен в наличии «черного хода»: знание секретного соотношения между константами P и Q позволяло предсказывать выход по небольшому фрагменту 2. Генератор был исключён из стандарта. Урок шире конкретной истории: если генератор содержит фиксированные константы неизвестного происхождения, доверие к нему основано на вере, а не на проверке. По этой же причине существуют сомнения в параметрах NIST P-256 (апокриф 10).

Реальные провалы

Debian OpenSSL 2008

В 2006 году мейнтейнер Debian убрал из OpenSSL код, который, по его мнению, был «неинициализированным чтением памяти», и вместе с ним — почти всю энтропию. В результате генератор ключей OpenSSL стал детерминированным: пространство возможных ключей сжалось до нескольких десятков тысяч вариантов. За два года было выпущено множество уязвимых сертификатов, SSH- и TLS-ключей; их пришлось перевыпускать, а список уязвимых ключей — рассылать по всему миру 6. Атака на такой ключ — не «взлом RSA», а перебор 15–17 бит.

Общие простые множители RSA

Heninger, Durumeric, Wustrow и Halderman в 2012 году собрали миллионы открытых RSA-ключей из Интернета и обнаружили, что у заметной доли ключей есть общий простой множитель с другим ключом: слабый генератор выдавал одинаковые простые числа разным устройствам. НОД двух модулей мгновенно раскрывает оба ключа 4. Это ещё один пример, когда алгоритм в порядке, а случайность — нет.

Повтор nonce в ECDSA

Sony PlayStation 3 (2010) и Android Bitcoin wallet (2013) использовали один и тот же k для разных сообщений. Детерминированный nonce, выводимый из сообщения и ключа по RFC 6979, снимает эту проблему 9. Из двух подписей (r, s1), (r, s2) восстанавливается k = (z1 − z2)/(s1 − s2) mod n, а затем закрытый ключ d = (s1·k − z1)/r mod n. Атака не требует перебора и работает за миллисекунды; подробный разбор — в апокрифе 10. Здесь важен общий вывод: одноразовое значение, использованное дважды, перестаёт быть одноразовым.

Клонирование состояния: fork, снимки, восстановление

Отдельный класс провалов не связан с качеством генератора: состояние просто оказывается скопированным. Классический случай — fork() в Unix: если процесс-родитель инициализировал CSPRNG и затем породил потомка, оба могут получить одну и ту же последовательность, потому что состояние памяти скопировано целиком. Именно так в 2008 году в Debian усугубилась история с OpenSSL: одинаковые ключи у процессов, стартовавших с одного снимка. Лечение — явная реинициализация или «перемешивание» состояния после fork (в OpenSSL для этого есть RAND_poll и резервирование в pid), а для приложений — не полагаться на наследование состояния вообще.

Та же ошибка встречается в виртуализации: снимок (snapshot) работающей машины сохраняет состояние генератора, и после отката к снимку два экземпляра выдают одинаковые nonce и ключи. Восстановление из резервной копии, повторный запуск контейнера из образа с зафиксированным seed и «тёплый» старт прошивки без переинициализации энтропии дают тот же результат. Практическое правило: любое событие, меняющее контекст исполнения — новый процесс, новая машина, откат, восстановление, — требует нового источника энтропии, а не продолжения старой последовательности.

Полезно разделять три уровня, которые часто называют одним словом «генератор». Источник энтропии — физический процесс (джиттер, прерывания, RDSEED); он даёт неопределённость, но не обязан быть быстрым и равномерным. DRBG — детерминированный алгоритм, который растягивает зерно в последовательность и обновляет состояние; его стойкость зависит от зерна. Генератор приложения — то, что вызывает разработчик (secrets, os.urandom, BCryptGenRandom); он должен обращаться к системному DRBG, а не к языковому random. Путаница уровней и порождает ошибки вида «мы же используем криптостойкий алгоритм» при том, что зерно взято из времени запуска.

Тесты и их ограничения

NIST SP 800-22

Набор статистических тестов проверяет частоты, серии, корреляции, энтропию и другие свойства последовательности 5. Тесты полезны для поиска грубых дефектов: смещённых битов, коротких периодов, линейных зависимостей. Они не доказывают криптографическую стойкость: MT19937 проходит большинство тестов, но полностью предсказуем; последовательность, полученная шифрованием счётчика, может не пройти отдельный тест из-за малой выборки и всё равно быть стойкой.

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

Практическая проверка

В лабораторной работе сравниваются LCG, MT19937 и secrets по трём критериям: статистика (частоты, серии), восстановление состояния по выходам и предсказание следующего значения. Ожидаемый результат: LCG и MT восстанавливаются, secrets — нет; статистические тесты при этом могут не различать их вовсе.

Итоги темы

Случайность — не «шум», а секретное состояние, из которого выводятся значения. LCG и MT19937 линейны и восстанавливаются по нескольким сотням выходов; их статистическая безупречность не имеет отношения к криптографии. CSPRNG строятся на криптографических функциях, обновляют состояние и используют энтропию ОС; в Python это secrets и os.urandom, а не random.

Реальные катастрофы — Debian 2008, общие простые множители RSA, повтор nonce в ECDSA — показывают, что слабый генератор ломает систему целиком, независимо от стойкости алгоритма. Статистические тесты ловят грубые дефекты, но не предсказуемость. Следующая тема — поточные шифры — покажет, что происходит, когда одноразовое значение всё-таки повторилось.

Литература

  1. Matsumoto M., Nishimura T. Mersenne Twister: A 623-Dimensionally Equidistributed Uniform Pseudo-Random Number Generator. — ACM Transactions on Modeling and Computer Simulation, 1998, Т. 8, № 1, С. 3–30. DOI: 10.1145/272991.272995
  2. NIST. Recommendation for Random Number Generation Using Deterministic Random Bit Generators. — SP 800-90A Rev. 1, 2015. https://doi.org/10.6028/NIST.SP.800-90Ar1
  3. Eastlake D., Schiller J., Crocker S. Randomness Requirements for Security. — RFC 4086, IETF, 2005. https://www.rfc-editor.org/rfc/rfc4086
  4. Heninger N., Durumeric Z., Wustrow E., Halderman J. A. Mining Your Ps and Qs: Detection of Widespread Weak Keys in Network Devices. — USENIX Security Symposium, 2012.
  5. NIST. A Statistical Test Suite for Random and Pseudorandom Number Generators for Cryptographic Applications. — SP 800-22 Rev. 1a, 2010. https://doi.org/10.6028/NIST.SP.800-22r1a
  6. Yilek S., Rescorla E., Shacham H., Enright B., Savage S. When Private Keys are Public: Results from the 2008 Debian OpenSSL Vulnerability. — Internet Measurement Conference, 2009. DOI: 10.1145/1644893.1644896
  7. Linux man-pages project. getrandom(2). — https://man7.org/linux/man-pages/man2/getrandom.2.html
  8. Aumasson J.-P. Serious Cryptography. — 2-е изд. — No Starch Press, 2024.
  9. Pornin T. Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA). — RFC 6979, IETF, 2013. https://www.rfc-editor.org/rfc/rfc6979