Resumo Breve
A partir de 30 de julho de 2026, fundos foram varridos de carteiras de hardware Bitcoin COLDCARD em múltiplas ondas que continuaram a se desdobrar nos dias seguintes. Não houve uma única transação de ataque on-chain nem contrato vulnerável; a perda veio de um problema de recuperação de seed offline seguido de varrimentos on-chain. Em 7 de agosto de 2026, o rastreamento on-chain havia verificado um piso de cerca de 1.405 BTC (~$91M ao preço de $64.700 em 7 de agosto) drenados de aproximadamente 4.925 endereços [1], a atribuição por onda chegou a cerca de 1.433 BTC em dez ondas [2], e a reconciliação por canais privados com vítimas colocou o valor em até 2.055 BTC (~$133M) [3].
A causa raiz foi uma falha de entropia da carteira: uma migração de firmware em 2021 roteou a geração de seed através de ngu.random.bytes(), que recorreu a um gerador de software determinístico em vez do RNG de hardware STM32 pretendido. Para os dispositivos afetados, isso reduziu a recuperação de seed de um problema criptograficamente inviável para uma busca offline, permitindo que um atacante enumerasse seeds candidatos e os confrontasse com dados públicos de carteira para recuperar as chaves privadas. Com base em nossa análise semanal anterior do incidente, esta análise aprofundada quantifica a entropia residual para cada geração de dispositivo afetado, rastreia como vítimas e fundos roubados foram identificados on-chain, e examina uma regressão separada de firmware pós-hotfix que pode negar serviço antes do login.
Contexto
O COLDCARD é uma carteira de hardware Bitcoin. Como outras carteiras de autocustódia, sua segurança depende fundamentalmente da imprevisibilidade da frase seed gerada durante a configuração da carteira. Uma frase seed BIP-39 é legível por humanos, mas a propriedade de segurança subjacente ainda é a entropia dos bytes aleatórios usados para criá-la. Se a saída de geração de seed for previsível, a segurança da carteira entra em colapso, por mais cuidadosamente que a seed seja posteriormente armazenada.
O COLDCARD abrange as revisões de hardware Mk1 a Mk5 e o modelo Q. Mk2 e Mk3 usam a linha de firmware legada. Mk4, Mk5 e Q usam trilhas de lançamento Standard e Edge separadas.
A maior parte da lógica de aplicação do COLDCARD é Python rodando em MicroPython, com módulos C nativos fornecendo operações de hardware e criptografia. Um caminho seguro de geração de seed deve ler 32 bytes do RNG de hardware STM32, falhar se o periférico travar ou repetir um valor, calcular o hash do resultado e então codificá-lo como palavras BIP-39. Na v3.2.2, make_new_wallet() seguia esse caminho:
make_new_wallet()
|- ckcc.rng_bytes(seed)
| `- random_buffer()
| |- leitura do STM32 RNG->DR
| `- falha em timeout ou saída repetida
|- SHA-256(seed)
`- palavras seed BIP-39
Análise de Vulnerabilidade
A causa raiz foi um erro de build e integração em torno de MICROPY_HW_ENABLE_RNG. A configuração de placa de produção do COLDCARD definiu essa macro como 0, porque o firmware usava seu próprio wrapper de RNG de hardware local em vez da implementação de RNG de hardware do MicroPython. No entanto, o caminho de geração de carteira havia sido migrado para ngu.random.bytes(32), e o caminho STM32 da libngu dependia em última instância do símbolo global rng_get().
O caminho afetado entrava em my_random_bytes() da libngu. Para cada palavra de saída, ele aplicava XOR ao valor retornado por CHIP_TRNG_32() com sua própria saída Yasmarang:
generate_seed()
`- ngu.random.bytes(32)
`- my_random_bytes() [libngu]
|- CHIP_TRNG_32()
| `- rng_get() [MicroPython]
| `- Yasmarang A
| `- UID + SysTick + RTC
|
|- my_yasmarang() [libngu]
| `- Yasmarang B
| |- Mk2/Mk3: estado inicial público
| `- Mk4/Q/Mk5: reseed de pad de 32 bits
|
`- saída = Yasmarang A XOR Yasmarang B
O Yasmarang A era o fallback do MicroPython por trás de rng_get(), inicializado a partir do estado do dispositivo e dos temporizadores. O Yasmarang B pertencia à libngu: usava valores iniciais públicos no Mk2/Mk3, enquanto Mk4/Q/Mk5 substituía apenas seu pad de 32 bits com dados derivados do elemento seguro. O RNG STM32 local da placa ainda existia, mas ngu.random.bytes() não o chamava.
A incompatibilidade é visível diretamente na configuração de build. O mpconfigboard.h do Mk4 desativava o ramo de RNG de hardware do MicroPython:
// Temos nossa própria versão deste código.
#define MICROPY_HW_ENABLE_RNG (0)
O CHIP_TRNG_32() da libngu ainda tratava a macro como prova suficiente de um RNG de hardware porque verificava apenas se a macro existia e então chamava rng_get():
extern uint32_t rng_get(void);
#define CHIP_TRNG_32() rng_get()
#ifndef MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz"
#endif
Essa guarda perde o caso perigoso: uma macro definida como 0 ainda está definida. O build, portanto, teve sucesso, e rng_get() resolveu para o módulo RNG STM32 do MicroPython em vez do wrapper local de placa separado do COLDCARD (random32() / random_buffer(), exposto ao Python como ckcc.rng_bytes). Na seleção de rng_get() do MicroPython, MICROPY_HW_ENABLE_RNG == 0 selecionava o ramo de fallback de software:
#if MICROPY_HW_ENABLE_RNG
// RNG de hardware STM32
#else
// Fallback de software Yasmarang
#endif
Mk2/Mk3: Cerca de 40 Bits
O fallback compilado pyb_rng_yasmarang() inicializava e avançava o Yasmarang da seguinte forma:
STATIC uint32_t pyb_rng_yasmarang(void) {
static bool seeded = false;
static uint32_t pad = 0, n = 0, d = 0;
static uint8_t dat = 0;
if (!seeded) {
seeded = true;
rtc_init_finalise();
pad = *(uint32_t *)MP_HAL_UNIQUE_ID_ADDRESS ^ SysTick->VAL;
n = RTC->TR;
d = RTC->SSR;
}
pad += dat + d * n;
pad = (pad << 3) + (pad >> 29);
n = pad | 2;
d ^= (pad << 31) + (pad >> 1);
dat ^= (char)pad ^ (d >> 8) ^ 1;
return pad ^ (d << 5) ^ (pad >> 18) ^ (dat << 1);
}
As variáveis ocupam 104 bits, mas o tamanho do estado não é entropia. dat começa em zero, e os outros valores são metadados fixos ou leituras de temporizador correlacionadas em vez de segredos independentes. Sob o modelo frouxo Mk2/Mk3 usado para a figura aproximada de 40 bits:
| Entrada | Valores candidatos | Custo de enumeração |
|---|---|---|
UID_low32 conhecido |
1 |
2^0 |
SysTick->VAL |
80.000 |
2^16,29 |
RTC->TR hora do dia |
86.400 |
2^16,40 |
RTC->SSR subsegundo |
256 |
2^8 |
Tratar todos os campos de temporizador como independentes fornece um teto intencionalmente amplo:
80.000 * 86.400 * 256
= 1.769.472.000.000
= 2^40,69 estados iniciais candidatos
Uma busca exaustiva, portanto, leva no máximo 2^40,69 tentativas e, sob uma suposição de posição uniforme, cerca de 2^39,69 tentativas em média. Este é um limite superior de enumeração, não 40 bits de entropia criptográfica. Se os registradores RTC forem estáticos durante uma inicialização a frio normal, apenas o SysTick permanece, reduzindo o teto para cerca de 2^16,29. Se o UID, os temporizadores e o número de chamadas RNG anteriores forem conhecidos, há exatamente um fluxo: 2^0. O histórico de chamadas desconhecido acrescenta apenas o número de rastros de execução plausíveis, não uma nova fonte de entropia. Da mesma forma, gerar oito palavras de 32 bits para uma seed de 256 bits não multiplica o espaço de busca: cada palavra é determinada pelo mesmo estado inicial.
Na camada libngu, my_random_bytes() misturou o fallback do MicroPython acima com o gerador Yasmarang separado da libngu. O código-fonte não contém literalmente chip = rng_get(): CHIP_TRNG_32() expande para rng_get(). No Mk2/Mk3, o segundo gerador começava a partir de constantes públicas:
static uint32_t yasmarang_pad = 0x0a8ce26f, yasmarang_n = 69, yasmarang_d = 233;
static uint8_t yasmarang_dat = 0;
uint32_t chip = CHIP_TRNG_32();
// ... verificação de saúde de saída adjacente ...
chip ^= my_yasmarang();
Ambos os fluxos eram, portanto, reproduzíveis uma vez que o estado de fallback do MicroPython e o histórico de chamadas fossem conhecidos. O XOR alterou os valores de saída, mas não adicionou entropia. Mesmo que UID_low32 seja desconhecido, UID_low32 ^ SysTick ainda colapsa ambas as entradas em um único pad de 32 bits; suas contagens nominais de bits não podem ser somadas.
Mk4/Q/Mk5: Cerca de 72 Bits
Os modelos posteriores mantiveram a mesma construção de dois geradores, mas rng_seeding() adicionou material do elemento seguro ao gerador da libngu durante a inicialização:
a = callgate.read_rng(1) # SE1
b = callgate.read_rng(2) # SE2
n = ngu.hash.sha256d(a + b)
n, = ustruct.unpack('I', n[0:4])
ngu.random.reseed(n)
Embora 40 bytes tenham entrado no hash, apenas seus primeiros quatro bytes chegaram a reseed() [4]. A implementação de random_reseed() então substituiu apenas a palavra pad de 32 bits da libngu:
STATIC mp_obj_t random_reseed(mp_obj_t arg)
{
yasmarang_pad = mp_obj_get_int_truncated(arg);
return mp_const_none;
}
As outras palavras de estado da libngu mantiveram seus valores públicos, e o fallback do MicroPython não foi resemeado. A figura aproximada de 72 bits combina o reseed de 32 bits da libngu com um limite superior frouxo para o estado do temporizador dos modelos posteriores:
Fallback do MicroPython:
120.000 valores SysTick * 86.400 horários RTC * 256 subsegundos
= 2^41,27 estados
Reseed seguro da libngu:
2^32 valores
Teto combinado:
2^41,27 * 2^32 = 2^73,27 candidatos
Enumeração média:
2^73,27 / 2 = 2^72,27 tentativas
Esta é a origem da figura "cerca de 72 bits". É uma estimativa média de trabalho de ataque, não 72 bits fornecidos pelos elementos seguros. Os campos do temporizador são correlacionados e podem ser reconstruídos; se o estado de fallback do MicroPython for conhecido, apenas os 2^32 valores de reseed permanecem, com média de 2^31 tentativas. Calcular o hash dos 32 bytes aleatórios finais não pode aumentar o número de seeds possíveis.
Versões Afetadas
| Dispositivo e trilha | Fora desta regressão | Firmware de geração de seed afetado | Segurança em bits efetiva | Primeira versão corrigida |
|---|---|---|---|---|
| Mk1 | Até v3.0.6 | Nenhum | - | N/A |
| Mk2/Mk3 | Até v3.2.2 | v4.0.0-v4.1.9 (aviso oficial a partir de v4.0.1) | Cerca de 40 bits quando afetado | v4.2.0 |
| Mk4/Mk5 Standard | N/A | Antes de v5.6.0 | Cerca de 72 bits antes da correção; pelo menos 128 bits depois | v5.6.0 |
| Q Standard | N/A | Antes de v1.5.0Q | Cerca de 72 bits antes da correção; pelo menos 128 bits depois | v1.5.0Q |
| Mk4/Mk5 Edge | N/A | Antes de v6.6.0X | Cerca de 72 bits antes da correção; pelo menos 128 bits depois | v6.6.0X |
| Q Edge | N/A | Antes de v6.6.0QX | Cerca de 72 bits antes da correção; pelo menos 128 bits depois | v6.6.0QX |
A versão relevante é o firmware que gerou a seed, não o firmware atualmente instalado. Novas seeds geradas na versão corrigida ou posteriores usam o caminho corrigido, mas atualizar não repara uma seed existente. O intervalo oficial afetado para Mk2/Mk3 começa em v4.0.1 [5], enquanto a análise em nível de código-fonte também inclui v4.0.0 [4]. A entropia independente de dados pode aumentar a segurança da seed, enquanto uma passphrase BIP-39 forte adiciona uma barreira separada sem reparar a seed em si [6].
Best Security Auditor for Web3
Validate design, code, and business logic before launch
Rastreamento de Vítimas e Fundos
Não houve transação de exploit on-chain para rastrear; a recuperação foi necessariamente offline. Visando uma seed gerada por firmware afetado, um atacante poderia restringir e enumerar os estados candidatos de RNG descritos acima, reproduzir o fluxo de geração de seed de cada candidato, derivar as chaves de carteira resultantes e confrontá-las com dados públicos de carteira, depois varrer on-chain qualquer carteira financiada correspondente. Isso só atingiu carteiras deriváveis apenas da seed: uma passphrase BIP-39 forte e única mistura entropia independente fornecida pelo usuário — que a falha do RNG nunca tocou — na derivação de chaves através de PBKDF2, colocando tais carteiras fora da enumeração pura de seed. As carteiras que foram varridas eram necessariamente aquelas sem tal proteção [6]. Como o roubo surgiu apenas como varrimentos on-chain em vez de um exploit rastreável, identificar as vítimas e seguir os fundos tornou-se uma questão de análise forense on-chain.
Vários esforços independentes rastrearam os fundos roubados: sites públicos de rastreamento (Coldcard Sweep Watch [1], coldcard.rip [2] e o Coldcard Hack Tracker [7]) e uma reconciliação por canal privado pela Galaxy Research [3], com seus totais reportados comparados abaixo. Como o Coldcard Sweep Watch publicou sua metodologia, a usamos para ilustrar o processo de identificação, um ciclo de retroalimentação entre relatórios off-chain e análise on-chain:
- Âncoras off-chain. Vítimas e pesquisadores forneceram endereços públicos ou IDs de transação, além de contexto de dispositivo e geração de seed quando disponível. Cada relato foi tratado como uma pista e verificado on-chain; nenhuma frase seed, chave privada ou xpub foi necessária.
- Expansão on-chain. A partir de âncoras confirmadas, os scanners pesquisaram blocos relevantes com as mesmas características de varredura: carteiras esvaziadas sem troco, tipos de entrada semelhantes, timing apertado, taxas de taxa repetidas, destinos comuns ou co-gastos posteriores.
- Verificações cruzadas off-chain. Novos relatos de vítimas, conjuntos de dados de pesquisadores e atribuição de serviços foram usados para confirmar ou rejeitar ondas candidatas. Clusters verificados e candidatos heurísticos permaneceram separados.
O Bitcoin identifica endereços, não pessoas ou modelos de carteira. Uma carteira pode controlar muitos endereços, portanto a contagem de endereços não é a contagem de vítimas. A primeira onda principal amplamente relatada, 960188, contabilizou 594,48 BTC; varreduras e relatos contínuos elevaram o total. Em 7 de agosto de 2026, o Coldcard Sweep Watch reportou um piso verificado de cerca de 1.405,07 BTC (~$91M ao preço de $64.700 em 7 de agosto) de aproximadamente 4.925 endereços [1], enquanto o snapshot de 3 de agosto do coldcard.rip atribuiu até 1.433,13 BTC brutos, 1.432,48 BTC para destinos após taxas, de 5.477 endereços em dez ondas [2]. Uma reconciliação separada por canal privado pela Galaxy Research, baseada em correspondência com vítimas, colocou o valor ainda mais alto, de cerca de 1.596 BTC até 2.055 BTC (~$133M ao mesmo preço) [3]. As diferenças refletem limiares de evidência, tempo de descoberta e qual canal de confirmação cada rastreador utiliza.
Os fundos foram então seguidos através de três camadas observadas: endereços de origem varridos, destinos diretos de varredura (holding) e destinos de consolidação subsequentes (vault). A tabela mostra a contagem de endereços distintos em cada camada:
| Padrão | Rotas de exemplo | Consequência para o rastreamento |
|---|---|---|
| Muitas varridas para um ou dois endereços holding, depois um vault | 960183: 204 -> 2 -> 1; 960188: 500 -> 1 -> 1 |
A convergência de destino torna o cluster comparativamente forte e fácil de seguir |
| Varridas param nos endereços holding sem consolidação posterior | 960352: 352 -> 1 -> 0; 960668: 795 -> 1 -> 0 |
O endereço holding permanece rastreável, mas não há co-gasto posterior para fortalecer a atribuição |
| Destinos novos por varredura, às vezes seguidos por vaults separados | 960359: 13 -> 13 -> 0; 960395: 1.918 -> 294 -> 293 |
Um detector de coletor compartilhado falha; o agrupamento depende de timing, taxa de taxa, template de transação e corroboração off-chain |
Quando as saídas rastreadas se movem, a análise segue divisões, fusões e cadeias de descascamento conservando o valor após taxas e limitando a atribuição ao valor varrido. Fundos que entram em uma exchange ou outro serviço commingled reduzem a confiança; o serviço não é adicionado ao cluster do atacante.
Uma Correção que Precisava de Correção: Uma Possível Regressão de Bloqueio Permanente?
Separadamente da falha de entropia, o hotfix que a resolveu introduziu uma regressão de firmware distinta [8, 9]. Ao restaurar o caminho do RNG de hardware, a correção deixou uma condição de erro de seed de hardware sem tratamento, o que pode negar serviço antes do login e gerou alegações de dispositivos permanentemente bloqueados. O quanto essa afirmação mais forte se sustenta depende dos detalhes a nível de registrador.
O RNG de hardware STM32 expõe um registrador de controle (RNG_CR), um registrador de status (RNG_SR) e um registrador de dados de 32 bits (RNG_DR). O status relevante é:
| Bit | Função |
|---|---|
RNGEN |
Habilita o RNG e suas fontes de ruído analógico |
DRDY |
Indica que os dados estão prontos em RNG_DR; o software ainda deve rejeitar zero |
SECS / SEIS |
Falha atual no teste de saúde de seed / status de erro de seed travado |
CECS / CEIS |
Falha atual no clock do RNG / status de erro de clock travado |
A distinção entre status atual e travado é importante. SECS descreve a condição presente da fonte de ruído, enquanto SEIS registra que um erro de seed ocorreu até que o software o limpe. No STM32L4S da família Mk4/Q, um erro de seed interrompe a geração de novos números aleatórios; no STM32L4 do Mk3, os dados podem permanecer disponíveis, mas não devem ser confiados. Erros de clock são separados e não estabelecem esse bloqueio de erro de seed.
A sequência de recuperação necessária depende da geração do STM32:
| Família de dispositivo | Recuperação de erro de seed documentada |
|---|---|
| Mk3 STM32L4 (RM0351, gerenciamento de erros RNG [10]) | Limpar SEIS, depois limpar e definir RNGEN |
| Família Mk4/Q STM32L4S (RM0432, gerenciamento de erros RNG [11]) | Limpar SEIS, ler e descartar 12 palavras RNG_DR, depois confirmar que SEIS permanece limpo |
O hotfix de entropia de 31 de julho corretamente fez rng_get() resolver para o TRNG de hardware, mas o rng_get_or_fault() da família Mk4/Q não implementa recuperação de erro de seed. rng_init() age apenas quando RNGEN está limpo, enquanto o loop de leitura verifica apenas DRDY. Se um erro de seed deixar RNGEN habilitado, mas suprimir DRDY, a inicialização se torna uma operação nula; cada leitura aguarda 10 ms e levanta OSError(EFAULT) sem limpar SEIS.
Isso pode atingir a UI antes do login. Tanto o teclado numérico mempad._start_scan() quanto o Q keyboard._start_scan() embaralham sua ordem de varredura a partir da interrupção de pressionamento de tecla. Uma exceção de RNG ali pode bloquear a entrada de PIN e o menu normal de atualização de firmware pelo restante daquela sessão de hardware.
O caminho de falha no nível de código é plausível, mas a afirmação mais forte de "ataque de bloqueio permanente" não está estabelecida. Os bits de controle e status do RNG são redefinidos para zero em um reset de hardware, portanto, um único erro transitório não deve danificar permanentemente o periférico; a persistência após um ciclo completo de energia não foi demonstrada. Tampouco existe uma maneira remota ou confiavelmente controlada demonstrada de acionar a falha no teste de saúde de seed. Uma publicação no X [8] afirmou que o bloqueio foi confirmado, mas o PR apontado, o PR #692 [9] enviado pela comunidade, tem o próprio autor declarando que analisou a falha com um mock de registrador, não a reproduziu em hardware Mk4/Q real e não confirmou independentemente os relatos de campo. A classificação mais bem fundamentada é, portanto, uma potencial negação de serviço pré-login e regressão de confiabilidade, não um ataque de bloqueio permanente confirmado.
A própria correção dos mantenedores, PR #693 [12], verifica os flags de erro de seed, adiciona recuperação e novas tentativas limitadas, rejeita amostras suspeitas e captura apenas o erro esperado do teclado; foi mesclado em 5 de agosto de 2026, substituindo o PR #692 [9] da comunidade, que foi fechado sem mesclar em 4 de agosto de 2026.
Conclusão
Este incidente foi uma falha de entropia de carteira que transformou a recuperação de seed de criptograficamente inviável em um problema de busca offline para dispositivos e fluxos de trabalho afetados. A principal falha de engenharia foi que o firmware enviado não provou que a API de geração de seed crítica para segurança realmente atingiu o RNG de hardware pretendido. As guardas de build devem verificar tanto a existência da macro quanto o valor da macro, fallbacks para entropia criptográfica devem falhar de forma fechada, e o CI deve verificar a proveniência dos símbolos e o fluxo de entropia de ponta a ponta na imagem final do firmware. Para os usuários afetados, o remédio não é uma atualização de firmware: atualizar não repara uma seed já gerada sob o caminho defeituoso, e adicionar uma passphrase depois não protege os fundos já mantidos nos endereços dessa seed. Esses fundos devem ser movidos para uma carteira construída a partir de uma nova seed em uma versão corrigida; apenas os fundos que já estavam protegidos por uma passphrase forte e única ficaram fora da enumeração somente por seed [6].
Referências
- [1] Coldcard Sweep Watch — conjunto verificado de endereços drenados e rastreador de perdas
- [2] coldcard.rip — registro de incidentes, rotas e atribuição
- [3] Galaxy Research — estimativa de perdas do COLDCARD a partir de relatos de vítimas
- [4] Block Engineering — Fallback de RNG Previsível e Reseed de 32 Bits no Firmware COLDCARD
- [5] Coinkite — Aviso de segurança do COLDCARD e intervalos de firmware afetados
- [6] Coinkite — Análise técnica aprofundada do problema de entropia
- [7] Coldcard Hack Tracker — totais de varredura ao vivo por onda
- [8] Publicação no X alegando que a falha de RNG pós-hotfix poderia bloquear permanentemente o dispositivo
- [9] PR #692 do firmware Coldcard — proposta de recuperação de falha TRNG da comunidade (fechado sem mesclar)
- [10] STMicroelectronics RM0351 — Registradores RNG STM32L4 e gerenciamento de erros
- [11] STMicroelectronics RM0432 — Registradores RNG STM32L4S e gerenciamento de erros
- [12] PR #693 do firmware Coldcard — recuperação TRNG com novas tentativas limitadas mesclada e tratamento de erro do teclado



