
Verifica dimostrabile per CVE-2022-40769: la chiave privata di un EOA rientra nello spazio delle chiavi raggiungibile tramite Profanity?
Verifica dimostrabile per CVE-2022-40769: la chiave privata di un EOA si trova nello spazio delle chiavi raggiungibile dal generatore di vanity address Profanity? Se sì, la chiave è recuperabile e l'account (e tutto ciò che controlla) può essere svuotato da chiunque.
Nessuna euristica. Il risultato è un booleano supportato da una ricerca completa dell'esatto spazio delle chiavi, riproducibile rieseguendo lo stesso shard.
Dispatcher.cpp, createSeed():
std::random_device rd;
std::mt19937_64 eng(rd()); // seeded with only 32 bits <- CVE-2022-40769
std::uniform_int_distribution<cl_ulong> distr;
cl_ulong4 r;
r.s[0] = distr(eng); r.s[1] = distr(eng); // 256-bit seed, used directly
r.s[2] = distr(eng); r.s[3] = distr(eng); // as the private key
Il kernel OpenCL prende poi G^k per quella chiave e avanza, incrementando la chiave di uno a ogni round finché l'indirizzo non corrisponde al pattern richiesto. Quindi l'intero spazio delle chiavi raggiungibile è:
key(x, i) = mt19937_64(x) as a 256-bit scalar + i
x ∈ [0, 2^32) the 32-bit seed (four billion possibilities)
i ∈ [0, depth] rounds the generator ran
P_j = P − j·G per j ≤ depth. Se Profanity ha generato la chiave, uno di questi punti è una chiave pubblica di seed.x deriva mt19937_64(x) e il suo indirizzo.P_j è la chiave pubblica del seed, quindi la chiave privata del candidato è seed(x) + j — recuperabile, quindi svuotabile.Una corrispondenza è una prova. Nessuna corrispondenza sull'intero intervallo dei seed è la prova del contrario, fino alla profondità scelta.
std::mt19937_64 per i seed 0, 1, 12345 e 4294967295 sono stati confrontati limb per limb con un programma g++ che usa std::mt19937_64 + std::uniform_int_distribution<unsigned long long>; identici.k256 (Rust puro) e secp256k1 (binding libsecp256k1): seed 0 → 0xfef2583edde5637dad990bc5d05d52c8247019cf, seed 12345 → 0x7712c45360f5dfa3a622a815b6073f0351050f13.--selftest verifica derivazione, indirizzo, camminata sulla catena, recupero tramite camminata all'indietro e una scansione end-to-end dei seed che trova un seed noto.Eseguitelo voi stessi:
cargo run --release -- --selftest
profanity-verifier --address 0x... [--pubkey 0x...] --shard i/n [--depth 16777216]
--pubkey — la chiave pubblica non compressa del candidato (64 byte di x||y, con o senza il prefisso 0x04). Recuperala da qualsiasi transazione firmata dall'indirizzo. Senza di essa, si può testare solo la posizione 0 nella catena.--shard i/n — la porzione dello spazio dei seed a 32 bit da cercare, così il lavoro si parallelizza tra più macchine.--depth — quanto in avanti lungo la catena del generatore cercare. 2^24 copre un vanity a sei caratteri con margine; valori più profondi costano memoria e tempo.L'output è una singola riga JSON, ad esempio:
{"address":"0x...","shard":"0/64","depth":16777216,"seeds_checked":67108864,"seconds":833.4,"match":true}
Il seed e la chiave privata non vengono mai stampati né memorizzati. Vengono emessi solo il booleano, l'indirizzo e lo shard, così un risultato può essere pubblicato senza consegnare a nessuno la chiave.
.github/workflows/verify.yml distribuisce la ricerca su una matrice di job (candidate, shard) su runner pubblici.
Throughput misurato: ~20.000 seed/s su 2 core (libsecp256k1). L'intero spazio dei seed 2^32 è di ~59 core-ore, quindi 64 shard × 4 core ≈ 14 minuti di tempo reale per candidato — gratis su un repository pubblico.
Avvia con la lista dei candidati:
gh workflow run profanity-verify -f candidates='[{"address":"0x...","pubkey":"0x..."}]' -f shards=64
std::mt19937_64 di libstdc++, cosa che il confronto limb per limb copre. Build contro una libreria standard diversa avrebbero una mappatura diversa.depth limita la ricerca. Un candidato generato dopo una ricerca insolitamente lunga (pattern vanity molto lunghi) può trovarsi oltre tale limite.