
Технический анализ CVE-2008-0166 в Bitcoin 2009-2012, исследование слабостей PRNG OpenSSL 0.9.8c, сбоев энтропии Windows и реконструкции ключевого пространства.
Автор: анализ на основе bitcoin-code-r252 (0.1.5 / 0.2.0 / 0.3.24) и оригинального make-OpenSSL-0-9-8c-vulnerable-again.diff (THC) Дата: 2026-09-10
Bitcoin 0.1.5 (январь 2009) работал исключительно на Windows, используя OpenSSL
0.9.8c для генерации EC-ключей (EC_KEY_generate_key). В 2008 году
в пакете OpenSSL от Debian была обнаружена CVE-2008-0166, но на Windows
у проблемы была другая первопричина — не отсутствующий /dev/urandom, а
реализация RandAddSeed() в самом исходном коде Bitcoin.
В bitcoin-code-r252 (tags-0.1.5) последовательность инициализации RNG
(util.cpp):
CInit::CInit() {
RAND_screen(); // (a) screen bitmap scrape
RandAddSeed(true); // (b) QPC + PerfMon
}
RandAddSeed (util.cpp:57-92):
QueryPerformanceCounter -> RAND_add(&PerformanceCount, 8, 1.5)RegQueryValueEx(HKEY_PERFORMANCE_DATA, "Global", ..., buf=250000)
-> if ERROR_SUCCESS: SHA256(pdata) -> RAND_add(&hash, 32, entropy)Генерация ключа (key.h:75-78):
CKey::MakeNewKey() {
EC_KEY_generate_key(pkey); // internally: RAND_bytes(32)
}
Важно: версии с 0.1.5 по 0.3.24 не имели keypool. GetRand()
использует RAND_bytes(8) для других целей (сетевые nonce, никнеймы IRC), тем самым
нарушая состояние PRNG между последовательными вызовами GenerateNewKey().
Версия 0.4.0 (сентябрь 2011) представила TopUpKeyPool() с пулом
по умолчанию из 100 ключей (GetArg("-keypool", 100)).
Группа THC (The Hackers Choice) разработала thc-btc-rng-bruteforce,
используя модифицированный OpenSSL 0.9.8c. Три критических изменения:
THC_hitme() (md_rand.c:133-180)THC_hitme(0): обнуляет state_num, state_index, entropy,
initialized, stirred_pool, md_count[0..1], md[], state[]
— полный сброс PRNG.THC_hitme(pid): сохраняет pid в статической переменной thc_pid.ssleay_rand_add() — содержимое буфера игнорируется (строка 344)- MD_Update(&m, buf, j);
+ //MD_Update(&m, buf, j); // commented out!
RAND_add по-прежнему продвигает state_index на num и увеличивает
md_count[1], но содержимое буфера не влияет на состояние PRNG.
ssleay_rand_bytes() — подстановка PID (строки 550-561)getpid() удалён.curr_pid = thc_pid — константное значение из THC_hitme.MD_Update(&m, &curr_pid, sizeof(curr_pid)) внедряет
PID как единственный вход внешней переменной.В пропатченном OpenSSL состояние PRNG зависит только от:
THC_hitme)RAND_add (параметр num, а не содержимое буфера)RAND_bytes перед генерацией ключаВ OpenSSL 0.9.8c BN_ULONG определяется через opensslconf.h:
#ifdef SIXTY_FOUR_BIT_LONG -> BN_ULONG = unsigned long (64-bit)
#ifdef THIRTY_TWO_BIT -> BN_ULONG = unsigned long (32-bit)
На 64-битном Linux OpenSSL по умолчанию использует SIXTY_FOUR_BIT_LONG, изменяя
sizeof(BN_ULONG) с 4 на 8 байт. Это влияет на:
static long md_count[2] (8 против 16 байт)1FkLYqPpfKPAR6EZh2RC6sDwTUA6Axb1XQ1JbxBrkBiSwpJUJN2bXrYJc51pH5rjrzTDBitcoin 2009 работал на Windows XP 32-bit, поэтому корректная реконструкция ключа
требует компиляции с принудительным THIRTY_TWO_BIT (отредактируйте
include/openssl/opensslconf.h перед сборкой).
В посте от 27 сентября 2012 года (topic=113496) Серхио Лернер описал:
(a) RandAddSeed() вызывает QueryPerformanceCounter() — требуется
выравнивание аргумента по QWORD. Компилятор gcc на Windows мог не
выровнять стековую переменную по 8 байт, что приводило к тихому сбою.
(b) RegQueryValueExA(HKEY_PERFORMANCE_DATA, "Global", ...) использует
фиксированный буфер размером 250 000 байт. На Windows XP данные о производительности
занимали ~280 КБ — функция возвращала ERROR_MORE_DATA и больше никогда не
вызывалась с буфером большего размера. В debug.log не было предупреждения.
Если оба механизма отказывали, единственным источником энтропии был RAND_screen()
(растровый снимок экрана). В пропатченном OpenSSL даже RAND_screen не имеет значения,
поскольку содержимое буфера игнорируется.
Лернер рекомендовал логировать эти сбои в debug.log — это исправление было
включено в более поздние версии Bitcoin Core.
Проверено в bitcoin-code-r252 (tags: 0.1.5, 0.2.0, 0.3.0) и в репозитории bitcoin/bitcoin (tags: 0.3.24, 0.4.0, 0.5.0, 0.6.0):
| Версия | Дата | Инициализация RNG (Windows) | Keypool |
|---|---|---|---|
| 0.1.5 | 2009-01 | RAND_screen + RandAddSeed(true) | нет |
QPC->RAND_add(8) + perfmon->SHA256->RAND_add(32) | |||
| 0.2.0 | 2009-12 | RAND_screen (только __WXMSW__) | нет |
QPC->RAND_add(8), perfmon отдельно каждые 10 мин | |||
| 0.3.0 | 2010 | то же | нет |
| 0.3.24 | 2011-07 | perfmon каждые 10 мин, RegQueryValueExA | нет |
| 0.4.0 | 2011-09 | то же | TopUpKeyPool 100 |
| 0.5.0 | 2011-12 | то же | 100 |
| 0.6.0 | 2012-03 | то же | 100 |
RAND_screen() использовался на всех версиях Windows вплоть до 2012 года.SHA256(pdata) -> RAND_add(32), более поздние версии
передавали сырой pdata в RAND_add (разный размер num имеет значение в
пропатченном OpenSSL, поскольку на состояние влияет только размер, а не содержимое).Пространство ключей для атаки (в пропатченном OpenSSL):
PID (1..32767)
x poll (count of RAND_add calls in init, modeling Toolhelp32 on WinXP)
x keypool position (1 for pre-0.4.0, 100 for 0.4.0+)
x profile (RAND call sequence for each Bitcoin version)
x architecture (le32 / le64)
Параметры tick, screen, cursor, hwnd, queue в этой модели
не имеют значения, поскольку содержимое буфера RAND_add игнорируется.
Оригинальные авторы THC писали: "We did not find any." — они не нашли никаких ключей в блокчейне. Анализ подтверждает их вывод: вероятность существования уязвимого ключа в блокчейне полностью зависит от того, был ли какой-либо биткоин-кошелёк действительно сгенерирован на системе Windows XP со сломанным OpenSSL и тихо отказывающими perfmon/QPC.
[1] Sergio Lerner, "Possible new vulnerability: poor entropy in Windows generated keypairs", Bitcointalk 2012-09-27: link
[2] Bitcoin StackExchange — "Was Satoshi using Windows or Linux?": link
[3] THC, thc-btc-rng-bruteforce: link
[4] Bitcoin Core tags (0.1.5, 0.2.0, 0.3.0, 0.3.24, 0.4.0, 0.5.0, 0.6.0): link
[5] OpenSSL 0.9.8c + patch make-OpenSSL-0-9-8c-vulnerable-again.diff
[6] Analiza_entropy_win.txt — технический отчёт, 2026-09-10