
Comprobación demostrable de CVE-2022-40769: ¿está la clave privada de una EOA dentro del espacio de claves alcanzable por Profanity?
Comprobación demostrable de CVE-2022-40769: ¿está la clave privada de una EOA dentro del espacio de claves alcanzable por el generador de direcciones vanidosas Profanity? Si es así, la clave es recuperable y la cuenta (y todo lo que controla) puede ser vaciada por cualquiera.
Sin heurísticas. El resultado es un booleano respaldado por una búsqueda completa del espacio de claves exacto, reproducible al volver a ejecutar el mismo 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
El kernel de OpenCL entonces toma G^k para esa clave y avanza, incrementando la clave en uno por ronda hasta que la dirección coincide con el patrón solicitado. Así que todo el espacio de claves alcanzable es:
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 para j ≤ depth. Si Profanity generó la clave, uno de estos puntos es una clave pública semilla.x deriva mt19937_64(x) y su dirección.P_j es la clave pública semilla, por lo que la clave privada del candidato es seed(x) + j — recuperable, por lo tanto vaciable.Una coincidencia es una prueba. La ausencia de coincidencia en todo el rango de semillas es prueba de lo contrario, hasta la profundidad elegida.
std::mt19937_64 para las semillas 0, 1, 12345 y 4294967295 se compararon miembro a miembro contra un programa g++ que usa std::mt19937_64 + std::uniform_int_distribution<unsigned long long>; idénticas.k256 (Rust puro) y secp256k1 (bindings de libsecp256k1): semilla 0 → 0xfef2583edde5637dad990bc5d05d52c8247019cf, semilla 12345 → 0x7712c45360f5dfa3a622a815b6073f0351050f13.--selftest verifica la derivación, la dirección, el recorrido de la cadena, la recuperación por recorrido descendente y un escaneo de semillas de extremo a extremo que encuentra una semilla conocida.Ejecútalo tú mismo:
cargo run --release -- --selftest
profanity-verifier --address 0x... [--pubkey 0x...] --shard i/n [--depth 16777216]
--pubkey — la clave pública sin comprimir del candidato (64 bytes de x||y, con o sin el prefijo 0x04). Recupérala de cualquier transacción que la dirección haya firmado. Sin ella, solo se puede probar la posición 0 de la cadena.--shard i/n — la porción del espacio de semillas de 32 bits a buscar, para que el trabajo se paralelice entre máquinas.--depth — hasta dónde mirar a lo largo de la cadena del generador. 2^24 cubre una vanidosa de seis caracteres con margen de sobra; más profundidad cuesta memoria y tiempo.La salida es una única línea JSON, por ejemplo:
{"address":"0x...","shard":"0/64","depth":16777216,"seeds_checked":67108864,"seconds":833.4,"match":true}
La semilla y la clave privada nunca se imprimen ni se almacenan. Solo se emiten el booleano, la dirección y el shard, de modo que un resultado puede publicarse sin entregarle la clave a nadie.
.github/workflows/verify.yml distribuye la búsqueda sobre una matriz de trabajos (candidate, shard) en runners públicos.
Rendimiento medido: ~20.000 semillas/s en 2 núcleos (libsecp256k1). El espacio completo de 2^32 semillas es de ~59 horas-núcleo, así que 64 shards × 4 núcleos ≈ 14 minutos de reloj por candidato — gratis en un repositorio público.
Lánzalo con la lista de candidatos:
gh workflow run profanity-verify -f candidates='[{"address":"0x...","pubkey":"0x..."}]' -f shards=64
std::mt19937_64 de libstdc++, lo cual cubre la comparación miembro a miembro. Las compilaciones contra una biblioteca estándar diferente tendrían un mapeo distinto.depth acota la búsqueda. Un candidato generado tras una búsqueda inusualmente larga (patrones vanidosos muy largos) puede quedar más allá.