vuke
Research tool for analyzing and reproducing vulnerable Bitcoin key generation.
Features
- Modular architecture - pluggable sources and transforms
- Multiple input sources
- Numeric ranges (test weak seeds)
- Wordlists (brainwallet analysis)
- Timestamps (time-based PRNG exploitation)
- Stdin streaming (pipeline integration)
- Historical vulnerability transforms
- Direct (raw bytes as key)
- SHA256 (classic brainwallet)
- Double SHA256 (Bitcoin-style hashing)
- MD5 (legacy weak hashing)
- SHA256 chain (iterated/indexed deterministic derivation)
- Milksad (MT19937 PRNG - CVE-2023-39910)
- MultiBit HD (seed-as-entropy bug)
- Electrum pre-BIP39 (2011-2014 deterministic derivation)
- Armory (legacy HD derivation)
- Key origin analysis - reverse detection of vulnerable generation methods
- Parallel processing via Rayon
- Address matching for scanning known targets
- File output for saving results
- Cloud storage - async upload to S3/R2/MinIO
- Pure Rust implementation
Why This Project?
This tool is designed for security research - understanding how vulnerable keys were generated in the past helps improve modern wallet security.
Historical vulnerabilities this tool can reproduce:
| Vulnerability | Year | Impact |
|---|---|---|
| Brainwallets | 2011-2015 | SHA256(passphrase) easily cracked |
| Weak PRNGs | 2013-2023 | Predictable seeds (timestamps, PIDs) |
| Milksad | 2023 | libbitcoin bx used MT19937 with 32-bit seeds |
| MultiBit HD | 2014-2016 | 64-byte BIP39 seed used as entropy |
| Electrum pre-BIP39 | 2011-2014 | Custom deterministic derivation with weak stretching |
| Armory HD | 2012-2016 | Pre-BIP32 deterministic derivation |
| LCG PRNGs | 1990s-2010s | glibc rand(), MINSTD, MSVC - only 31-32 bit state |
| Xorshift PRNGs | 2003-present | V8/SpiderMonkey Math.random() - 64-128 bit state |
| SHA256 chains | 2010s-present | Deterministic key derivation from weak seeds |
Installation
Cargo
From source
Usage
Generate single key from passphrase
Output:
Passphrase: "correct horse battery staple"
Transform: sha256
Source: correct horse battery staple
---
Private Key (hex): c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a
WIF (compressed): L3p8oAcQTtuokSCRHQ7i4MhjWc9zornvpJLfmg62sYpLRJF9woSu
---
P2PKH (compressed): 1JwSSubhmg6iPtRjtyqhUYYH7bZg3Lfy1T
P2WPKH: bc1qfnpg7ceg02y64qrskgz0drwp3y6hma3q6wvnzr
Scan wordlist for known addresses
Data Providers
Instead of file paths, use provider references for dynamic target loading:
# Scan all unsolved b1000 puzzles
# Scan specific collection with filter
# Available filters: all, unsolved, solved, with-pubkey
Provider syntax: provider:collection:filter or provider:collection:id
Available collections (boha provider):
b1000- Bitcoin puzzle 1000 (256 puzzles, 1-256 bits)gsmg- GSMG puzzlebitaps- Bitaps puzzlehash_collision- Hash collision puzzleszden- Zden puzzlesbitimage- Bitimage puzzles
Test numeric range (weak seeds)
Test LCG-based keys
# Generate keys using glibc rand() (default big-endian)
# Use MINSTD variant with big-endian byte order
# Test all LCG variants at once
Test xorshift-based keys
# Generate keys using all xorshift variants (requires cascade filter for analysis)
# Use specific variant
Test timestamp-based keys
Derive keys from files (Bitimage)
# Generate key from a single file
# Scan directory recursively
# With custom derivation path and passphrase
# Derive multiple addresses per file
# Brute-force passphrases from wordlist
Multiple transforms
Pipe from stdin
|
Save results to file
Persistent storage (Parquet)
Store results in Parquet format for TB-scale analysis (requires storage feature):
# Build with storage support
# Store generated keys to Parquet
# Configure chunk rotation
# Configure compression (default: zstd level 3)
# No compression (fastest writes)
# Available algorithms: none, snappy, gzip, lz4, zstd
Query stored results (SQL)
Query stored Parquet files using SQL (requires storage-query feature):
# Build with query support
# Count results by transform
# Find matches
# Export to JSON
# Export to CSV
# Show schema
Output formats: table (default), json, csv
Cloud upload (S3-compatible)
Upload Parquet results to S3-compatible storage (requires storage-cloud feature):
# Build with cloud upload support
# Set credentials (AWS S3)
# Upload to AWS S3
# Upload to Cloudflare R2
# Upload to MinIO (self-hosted)
# Delete local files after successful upload
# Fail fast on upload errors (default: continue on failures)
Cloud upload features:
- Streaming multipart upload (memory-efficient for large files)
- Automatic retry with exponential backoff (5 retries, 100ms→30s)
- Concurrent uploads with configurable parallelism
- Only deletes local files that were successfully uploaded
Iceberg catalog registration
Register uploaded Parquet files in an Apache Iceberg catalog for SQL querying (requires storage-iceberg feature):
# Build with Iceberg support
# Set credentials
# Generate, upload, and register in Iceberg catalog
# With custom namespace and table name
# Using environment variables
Iceberg catalog features:
- REST catalog protocol (compatible with Apache Polaris, Nessie, Tabular, etc.)
- Automatic table creation with schema and partition spec
- Partitioned by
transform(identity) andtimestamp(day) - Credentials shared with cloud upload (CLOUD_/AWS_ env vars)
Benchmark transforms
Analyze private key origin
Check if a private key could have been generated by a vulnerable method:
Output:
Private Key: c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a
Bit Length: 256
Hamming Weight: 144
---
Analysis:
✗ milksad: NOT_FOUND (checked 4294967296 seeds)
✗ direct: NOT_FOUND (no direct patterns detected)
? heuristic: UNKNOWN (entropy=5.00, hamming=144)
Fast mode (skip brute-force):
JSON output:
Specific analyzer:
LCG analyzer
Check if a key was generated using a Linear Congruential Generator:
# Check all LCG variants
# Check specific variant and endianness
# With masking for puzzle analysis
Masked key analysis (BTC1000-style puzzles)
Some Bitcoin puzzles use a masking scheme where:
- A full 256-bit key is generated (e.g., from MT19937)
- The key is masked to N bits with highest bit forced to 1
Formula: masked_key = (full_key & (2^N - 1)) | 2^(N-1)
# Analyze 5-bit puzzle key 0x15
Output:
Private Key: 0000000000000000000000000000000000000000000000000000000000000015
Bit Length: 5
Hamming Weight: 3
---
Analysis:
✓ milksad: CONFIRMED (seed=1610000002, full_key=7ed2...5055, masked=0x15, mask_bits=5, formula=(key & 0x1f) | 0x10)
# Analyze 10-bit puzzle key
Cascading filter (multi-puzzle verification)
When analyzing masked keys, a single small-bit match has high false positive rates. The cascading filter verifies candidates against multiple known puzzle keys:
# Verify seed against multiple puzzles with increasing bit widths
Output:
Private Key: 0000000000000000000000000000000000000000000000000000000000000016
Bit Length: 5
Hamming Weight: 3
---
Analysis:
✓ milksad: CONFIRMED (seed=100 (0x00000064))
P5: target=0x16, full_key=08961c8b18dbd0ab4337434767df7b69572fad6c4f00c186b03f43d88af70a26
P10: target=0x273, full_key=5e413501b4371e2862271f1f3550bc2f4236b6abe29ec9350e166bd322c3e673
P15: target=0x7a85, full_key=f133ff22f0aac1de185139938f664d10e4ac2de46be7d29f3c458e353a1efa85)
The cascade format is bits:target,bits:target,... where:
bitsis the mask width (1-64)targetis hex (with 0x prefix) or decimal
Probability analysis:
- P5 alone: 1/16 chance of false positive
- P5 + P10: 1/16 × 1/512 = 1/8192
- P5 + P10 + P15: virtually impossible false positive
Provider-based analysis
Use data providers to automatically configure puzzle analysis:
# Auto-set mask from puzzle bits (puzzle #5 = 5 bits)
# Build cascade from solved neighbors (3 puzzles before #5)
# Verify key against entire collection
# JSON output for scripting
The --puzzle flag:
- Loads puzzle context from provider
- Auto-sets
--maskfrom puzzle.key.bits - Displays expected address for verification
The --cascade with provider:
- Format:
boha:collection:puzzle_id:neighbor_count - Builds cascade from N solved puzzles before target
- Default: 5 neighbors if count not specified
MT19937-64 analyzer (64-bit seeds)
For testing 64-bit seed hypotheses, the mt64 analyzer requires cascade filter
(64-bit seed space is not exhaustively searchable):
# MT19937-64 cascade search - REQUIRES cascade filter
Progress shows search rate and cascade filter hits:
⠋ Searched: 1200000 seeds | Rate: 850K/s | Elapsed: 1.4s | Cascade hits: 73
Xorshift analyzer (64-bit seeds)
Xorshift PRNGs (used in V8/SpiderMonkey JavaScript engines) also require cascade filter due to 64-bit seed space:
# Test all xorshift variants
# Test specific variant
Supported variants:
xorshift:64- Classic 64-bit state xorshiftxorshift:128- 128-bit state xorshift (seed initialized as(seed, 0))xorshift:128plus- Xorshift128+ (used in V8/SpiderMonkey Math.random())xorshift:xoroshiro- Xoroshiro128** (modern variant)
MultiBit HD analyzer (seed-as-entropy bug)
MultiBit HD Beta 7 (2014-2016) had a bug where the 64-byte BIP39 seed was passed
directly to BitcoinJ's DeterministicSeed constructor as entropy (expected 16-32 bytes).
This created incompatible keys that cannot be recovered with standard BIP39 tools.
# Check if a key was generated by the MultiBit HD bug
# Test with a specific passphrase
# Dictionary attack with mnemonic file
Generate buggy keys from a known mnemonic:
Output (first key at m/0'/0/0):
P2PKH (compressed): 1LQ8XnNKqC7Vu7atH5k4X8qVCc9ug2q7WE
This matches the buggy address from MultiBit HD issue #445.
The correct BIP39 address would be 12QxtuyEM8KBG3ngNRe2CZE28hFw3b1KMJ.
Electrum pre-BIP39 keys (2011-2014)
Electrum wallets before BIP39 adoption used a custom deterministic derivation scheme:
- 128-bit hex seed stretched via 100,000 SHA256 iterations
- Child keys derived as
(master + sequence) mod n - Uncompressed public keys for addresses
Generate keys from an old Electrum seed:
# Generate receiving chain addresses (first 20 keys)
# Generate change chain addresses
Output (receiving address 0):
P2PKH (uncompressed): 1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo
SHA256 chain analyzer
Detect keys generated using deterministic SHA256 chains (key[n] = SHA256(key[n-1]) or SHA256(seed || n)):
# Check with iterated chain (default): key[n] = SHA256(key[n-1])
# Check indexed variant: key[n] = SHA256(seed || n as bytes)
# With masking for puzzle analysis
# With cascade filter for reduced false positives
Supported variants:
sha256_chainorsha256_chain:iterated- Chain derivation: key[n] = SHA256(key[n-1])sha256_chain:indexedorsha256_chain:indexed:be- Indexed: SHA256(seed || n) with big-endiansha256_chain:indexed:le- Indexed with little-endian byte ordersha256_chain:counter- String indexed: SHA256(seed || "n")
SHA256 chain transform
Generate keys using SHA256 chain derivation:
# Generate iterated chain keys from numeric seeds
# Generate indexed chain with counter strings
# Generate with specific chain depth
Supported Transforms
| Transform | Description | Use Case |
|---|---|---|
direct |
Raw bytes padded to 32 bytes | Testing raw numeric seeds |
sha256 |
SHA256(input) | Classic brainwallets |
double_sha256 |
SHA256(SHA256(input)) | Bitcoin-style hashing |
md5 |
MD5(input) duplicated to 32 bytes | Legacy weak hashing |
milksad |
MT19937 PRNG with 32-bit seed | CVE-2023-39910 (libbitcoin) |
mt64 |
MT19937-64 PRNG with 64-bit seed | 64-bit seed hypothesis testing |
multibit |
MultiBit HD seed-as-entropy bug | 2014-2016 MultiBit HD wallets |
armory |
Armory HD derivation chain | Pre-BIP32 wallets |
electrum |
Electrum pre-BIP39 derivation | 2011-2014 Electrum wallets |
electrum:change |
Electrum change chain | 2011-2014 Electrum change addresses |
lcg[:variant][:endian] |
LCG PRNG with 32-bit seed | Legacy C stdlib rand() |
xorshift[:variant] |
Xorshift PRNG with 64-bit seed | V8/SpiderMonkey Math.random() |
sha256_chain[:variant] |
Deterministic SHA256 chain | Iterated/indexed key derivation |
bitimage |
File→base64→SHA256→BIP39→HD | Bitimage puzzle key derivation |
Supported Analyzers
| Analyzer | Method | Use Case |
|---|---|---|
milksad |
Brute-force 2^32 seeds | Check if key is Milksad victim |
milksad --mask N |
Brute-force with N-bit masking | BTC1000-style puzzle analysis |
milksad --cascade |
Multi-target sequential verification | Reduce false positives in puzzle research |
mt64 --cascade |
Brute-force 2^64 with cascade filter | BTC1000 64-bit PRNG hypothesis |
multibit-hd --mnemonic |
Test mnemonic against key | Verify MultiBit HD bug origin |
multibit-hd --mnemonic-file |
Dictionary attack | Find mnemonic for MultiBit HD key |
direct |
Pattern detection | Detect small seeds, ASCII strings |
heuristic |
Statistical analysis | Entropy, hamming weight anomalies |
lcg[:variant][:endian] |
Brute-force 231-232 seeds | Detect glibc/minstd/msvc/borland rand() |
xorshift[:variant] --cascade |
Brute-force 2^64 with cascade filter | V8/SpiderMonkey xorshift PRNGs |
sha256_chain[:variant] |
Brute-force 2^32 seeds with chain depth | Deterministic SHA256 key chains |
Library Usage
use KeyDeriver;
use ;
Architecture
src/
├── main.rs # CLI entry point
├── lib.rs # Library exports
├── derive.rs # Private key → address derivation
├── matcher.rs # Address matching against targets
├── network.rs # Bitcoin network handling
├── benchmark.rs # Performance testing
├── lcg.rs # LCG PRNG shared logic
├── xorshift.rs # Xorshift PRNG shared logic
├── mt64.rs # MT19937-64 PRNG shared logic
├── multibit.rs # MultiBit HD bug logic (PBKDF2, BIP32)
├── electrum.rs # Electrum pre-BIP39 deterministic derivation
├── sha256_chain.rs # SHA256 chain shared logic (iterated/indexed)
├── bitimage.rs # Bitimage puzzle derivation logic
├── analyze/
│ ├── mod.rs # Analyzer trait and types
│ ├── key_parser.rs # Parse hex/WIF/decimal keys
│ ├── milksad.rs # MT19937 brute-force
│ ├── mt64.rs # MT19937-64 brute-force (requires cascade)
│ ├── multibit.rs # MultiBit HD mnemonic verification
│ ├── lcg.rs # LCG brute-force (glibc, minstd, msvc, borland)
│ ├── xorshift.rs # Xorshift brute-force (requires cascade)
│ ├── sha256_chain.rs # SHA256 chain brute-force
│ ├── direct.rs # Pattern detection
│ ├── heuristic.rs # Statistical analysis
│ └── output.rs # Plain text and JSON formatting
├── source/
│ ├── mod.rs # Source trait and types
│ ├── range.rs # Numeric range source
│ ├── wordlist.rs # File-based wordlist
│ ├── timestamps.rs # Date range → Unix timestamps
│ ├── stdin.rs # Streaming from stdin
│ └── files.rs # File/directory source for binary data
├── storage/
│ ├── mod.rs # StorageBackend trait
│ ├── parquet_backend.rs # Parquet file writer
│ ├── query.rs # DuckDB SQL executor
│ ├── schema.rs # Arrow schema definitions
│ └── cloud/ # S3-compatible upload
│ ├── mod.rs # CloudUploader trait
│ ├── s3.rs # S3/R2/MinIO implementation
│ ├── sync.rs # Batch upload with concurrency
│ ├── progress.rs # Upload progress tracking
│ └── error.rs # Cloud error types
├── transform/
│ ├── mod.rs # Transform trait and types
│ ├── input.rs # Input value representation
│ ├── direct.rs # Raw bytes transform
│ ├── sha256.rs # SHA256 hashing
│ ├── double_sha256.rs # Double SHA256
│ ├── md5.rs # MD5 hashing
│ ├── milksad.rs # MT19937 PRNG (CVE-2023-39910)
│ ├── mt64.rs # MT19937-64 PRNG transform
│ ├── multibit.rs # MultiBit HD seed-as-entropy bug
│ ├── electrum.rs # Electrum pre-BIP39 deterministic derivation
│ ├── lcg.rs # LCG PRNG transform
│ ├── xorshift.rs # Xorshift PRNG transform
│ ├── sha256_chain.rs # SHA256 chain transform
│ ├── bitimage.rs # File-derived HD keys (Bitimage puzzle)
│ └── armory.rs # Armory HD derivation
└── output/
├── mod.rs # Output trait
└── console.rs # Console output handler
Requirements
- Rust 1.70+
Disclaimer
This tool is for educational and security research purposes only. Do not use it to access wallets you do not own. The authors are not responsible for any misuse.
License
MIT License - see LICENSE for details.
References
- Milksad vulnerability - CVE-2023-39910
- MultiBit HD issue #445 - Seed-as-entropy bug
- Brainwallet attacks - Academic paper
- Armory documentation - Legacy HD wallet
- Linear Congruential Generator - Wikipedia
- Xorshift PRNGs - Wikipedia
- Electrum 1.x key derivation - Pre-BIP39 source code