
Verificação comprovável para CVE-2022-40769: a chave privada de uma EOA está no espaço de chaves alcançável pelo Profanity?
Verificação comprovável para CVE-2022-40769: a chave privada de uma EOA está no espaço de chaves alcançável pelo gerador de endereços vanity Profanity? Se sim, a chave é recuperável e a conta (e tudo o que ela controla) pode ser drenada por qualquer pessoa.
Sem heurísticas. O resultado é um booleano respaldado por uma busca completa do espaço de chaves exato, reproduzível ao reexecutar o mesmo 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
O kernel OpenCL então toma G^k para essa chave e avança, incrementando a chave em um por rodada até que o endereço corresponda ao padrão solicitado. Portanto, todo o espaço de chaves alcançável é:
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. Se o Profanity gerou a chave, um desses pontos é uma chave pública semente.x, deriva mt19937_64(x) e seu endereço.P_j é a chave pública semente, então a chave privada do candidato é seed(x) + j — recuperável, portanto drenável.Uma correspondência é prova. A ausência de correspondência em toda a faixa de sementes é prova do oposto, até a profundidade escolhida.
std::mt19937_64 para as sementes 0, 1, 12345 e 4294967295 foram comparadas limb a limb contra um programa g++ usando std::mt19937_64 + std::uniform_int_distribution<unsigned long long>; idênticas.k256 (Rust puro) e secp256k1 (bindings libsecp256k1): semente 0 → 0xfef2583edde5637dad990bc5d05d52c8247019cf, semente 12345 → 0x7712c45360f5dfa3a622a815b6073f0351050f13.--selftest verifica a derivação, o endereço, a caminhada na cadeia, a recuperação por caminhada descendente e uma varredura de sementes de ponta a ponta que encontra uma semente conhecida.Execute você mesmo:
cargo run --release -- --selftest
profanity-verifier --address 0x... [--pubkey 0x...] --shard i/n [--depth 16777216]
--pubkey — a chave pública não comprimida do candidato (64 bytes de x||y, com ou sem o prefixo 0x04). Recupere-a de qualquer transação que o endereço tenha assinado. Sem ela, apenas a posição 0 da cadeia pode ser testada.--shard i/n — a fatia do espaço de sementes de 32 bits a ser pesquisada, para que o trabalho seja paralelizado entre máquinas.--depth — até onde ao longo da cadeia do gerador procurar. 2^24 cobre um vanity de seis caracteres com folga; mais profundo custa memória e tempo.A saída é uma única linha JSON, por exemplo:
{"address":"0x...","shard":"0/64","depth":16777216,"seeds_checked":67108864,"seconds":833.4,"match":true}
A semente e a chave privada nunca são impressas ou armazenadas. Apenas o booleano, o endereço e o shard são emitidos, para que um resultado possa ser publicado sem entregar a chave a ninguém.
.github/workflows/verify.yml distribui a busca em uma matriz de jobs (candidate, shard) em runners públicos.
Taxa de transferência medida: ~20.000 sementes/s em 2 núcleos (libsecp256k1). O espaço completo de 2^32 sementes é ~59 horas-núcleo, então 64 shards × 4 núcleos ≈ 14 minutos de tempo real por candidato — gratuito em um repositório público.
Dispare com a lista de candidatos:
gh workflow run profanity-verify -f candidates='[{"address":"0x...","pubkey":"0x..."}]' -f shards=64
std::mt19937_64 do libstdc++, o que a comparação limb a limb cobre. Builds contra uma biblioteca padrão diferente teriam um mapeamento diferente.depth limita a busca. Um candidato gerado após uma busca excepcionalmente longa (padrões vanity muito longos) pode estar além dela.