
Technical analysis of CVE-2008-0166 in Bitcoin 2009-2012, examining OpenSSL 0.9.8c PRNG weaknesses, Windows entropy failures, and key-space reconstruction.
Author: analysis based on bitcoin-code-r252 (0.1.5 / 0.2.0 / 0.3.24) and the original make-OpenSSL-0-9-8c-vulnerable-again.diff (THC) Date: 2026-09-10
Bitcoin 0.1.5 (January 2009) ran exclusively on Windows, using OpenSSL
0.9.8c for EC key generation (EC_KEY_generate_key). In 2008,
CVE-2008-0166 was discovered in Debian's OpenSSL package, but on Windows
the issue had a different root cause — not a missing /dev/urandom, but
the implementation of RandAddSeed() in the Bitcoin source code itself.
In bitcoin-code-r252 (tags-0.1.5) the RNG initialization sequence
(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 generation (key.h:75-78):
CKey::MakeNewKey() {
EC_KEY_generate_key(pkey); // internally: RAND_bytes(32)
}
Important: versions 0.1.5 through 0.3.24 had no keypool. GetRand()
uses RAND_bytes(8) for other purposes (network nonces, IRC nicknames), thus
disrupting the PRNG state between successive GenerateNewKey() calls.
Version 0.4.0 (September 2011) introduced TopUpKeyPool() with a default
pool of 100 keys (GetArg("-keypool", 100)).
The THC group (The Hackers Choice) developed thc-btc-rng-bruteforce,
using a modified OpenSSL 0.9.8c. Three critical changes:
THC_hitme() (md_rand.c:133-180)THC_hitme(0): zeroes state_num, state_index, entropy,
initialized, stirred_pool, md_count[0..1], md[], state[]
— complete PRNG reset.THC_hitme(pid): stores pid in the static variable thc_pid.ssleay_rand_add() — buffer content ignored (line 344)- MD_Update(&m, buf, j);
+ //MD_Update(&m, buf, j); // commented out!
RAND_add still advances state_index by num and increments
md_count[1], but the buffer content has no effect on PRNG state.
ssleay_rand_bytes() — PID substitution (lines 550-561)getpid() call removed.curr_pid = thc_pid — constant value from THC_hitme.MD_Update(&m, &curr_pid, sizeof(curr_pid)) injects
PID as the only external variable input.In the patched OpenSSL, PRNG state depends only on:
THC_hitme)RAND_add calls (num parameter, not buffer content)RAND_bytes calls before key generationIn OpenSSL 0.9.8c, BN_ULONG is defined by opensslconf.h:
#ifdef SIXTY_FOUR_BIT_LONG -> BN_ULONG = unsigned long (64-bit)
#ifdef THIRTY_TWO_BIT -> BN_ULONG = unsigned long (32-bit)
On 64-bit Linux, OpenSSL defaults to SIXTY_FOUR_BIT_LONG, changing
sizeof(BN_ULONG) from 4 to 8 bytes. This affects:
static long md_count[2] (8 vs 16 bytes)1FkLYqPpfKPAR6EZh2RC6sDwTUA6Axb1XQ1JbxBrkBiSwpJUJN2bXrYJc51pH5rjrzTDBitcoin 2009 ran on Windows XP 32-bit, so correct key reconstruction
requires compilation with THIRTY_TWO_BIT enforced (edit
include/openssl/opensslconf.h before building).
In a post on 27 September 2012 (topic=113496), Sergio Lerner described:
(a) RandAddSeed() calls QueryPerformanceCounter() — requires
QWORD alignment of its argument. The gcc compiler on Windows could fail
to 8-byte align the stack variable, causing silent failure.
(b) RegQueryValueExA(HKEY_PERFORMANCE_DATA, "Global", ...) uses a
fixed buffer of 250,000 bytes. On Windows XP, the performance data was
~280 KB — the function returned ERROR_MORE_DATA and was never
called again with a larger buffer. debug.log contained no warning.
If both mechanisms failed, the only entropy source was RAND_screen()
(screen bitmap). In the patched OpenSSL even RAND_screen is irrelevant
because buffer content is ignored.
Lerner recommended logging these failures to debug.log — this fix was
incorporated into later Bitcoin Core versions.
Verified in bitcoin-code-r252 (tags: 0.1.5, 0.2.0, 0.3.0) and the bitcoin/bitcoin repository (tags: 0.3.24, 0.4.0, 0.5.0, 0.6.0):
| Version | Date | Init RNG (Windows) | Keypool |
|---|---|---|---|
| 0.1.5 | 2009-01 | RAND_screen + RandAddSeed(true) | none |
QPC->RAND_add(8) + perfmon->SHA256->RAND_add(32) | |||
| 0.2.0 | 2009-12 | RAND_screen (__WXMSW__ only) | none |
QPC->RAND_add(8), perfmon separate every 10 min | |||
| 0.3.0 | 2010 | same | none |
| 0.3.24 | 2011-07 | perfmon every 10 min, RegQueryValueExA | none |
| 0.4.0 | 2011-09 | same | TopUpKeyPool 100 |
| 0.5.0 | 2011-12 | same | 100 |
| 0.6.0 | 2012-03 | same | 100 |
RAND_screen() was used on all Windows versions through 2012.SHA256(pdata) -> RAND_add(32), later versions
passed raw pdata to RAND_add (different num size matters in the
patched OpenSSL because only the size, not the content, affects state).The key space for the attack (in patched 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)
The tick, screen, cursor, hwnd, queue parameters are
irrelevant in this model because RAND_add buffer content is ignored.
The original THC authors wrote: "We did not find any." — they did not find any keys on the blockchain. The analysis confirms their finding: the probability of a vulnerable key existing on-chain depends entirely on whether any Bitcoin wallet was actually generated on a Windows XP system with the broken OpenSSL and silently-failing 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 — technical report, 2026-09-10