Queda al descubierto una vulnerabilidad en Coldcard: robo de 1.719 BTC en un ataque en 2026

Resumen del mercado generado por IA
Un fallo de entropía en el firmware de Coldcard, según se ha informado, permitió la recuperación mediante fuerza bruta sin conexión de las semillas en dispositivos con configuración predeterminada, lo que llevó al robo confirmado de al menos 1.719 BTC en miles de direcciones. El incidente es significativo porque replantea el riesgo del "almacenamiento en frío" en torno a la calidad de la generación de claves, más que a un compromiso de red, lo que podría presionar la confianza en los monederos de hardware, impulsar actualizaciones urgentes del firmware e incrementar a corto plazo los movimientos de fondos impulsados por la seguridad y el escrutinio en las prácticas de custodia de BTC.
Nivel de impacto
● Alto
Activos afectados
BTC/USDT-1.62%
Ideas de IA · BTC/USDTIdeas de IA
▼ Bajista
Haz trading ahora
⚠️ Las ideas generadas por IA se basan en contenido de noticias y se proporcionan solo con fines informativos. No constituyen asesoramiento de inversión ni representan los puntos de vista de BingX. Invertir implica riesgos. Opera de forma responsable.
Autor: Johan & Lisa | Edición: 77 Contexto El 30 de julio de 2026, una serie de direcciones en la blockchain comenzó a mover fondos de forma secuencial. En solo 41 minutos, 1.196 direcciones de firma única quedaron vaciadas, con la desaparición de alrededor de 1.082 bitcoins. Ese fue el primer golpe. A comienzos de agosto, las pérdidas confirmadas ascendían al menos a 1.719 bitcoins (aprox. 111 millones de dólares), con más de 5.200 direcciones afectadas y un patrón de tres a cuatro oleadas. El elemento más desconcertante era el perfil de las carteras: buena parte de los fondos estaba en "cold wallets", sin movimientos durante meses o incluso años. El problema no fue operativo, sino de claves privadas. Todas las claves comprometidas se habían generado con wallets hardware Coldcard sin añadir entropía extra con dados ni activar una passphrase BIP39. Los casos afectados compartían la configuración más simple y por defecto. El alcance varía por línea de firmware. En Mk2 y Mk3 con versiones 4.0.1 a 4.1.9, la entropía efectiva cae a unos 40 bits, el escenario más grave. En Mk4, Mk5 y Q, en determinadas versiones, la entropía ronda 72 bits, todavía dentro del rango de ataques por fuerza bruta offline. Coinkite reaccionó con rapidez: el 30 de julio emitió una advertencia y, al día siguiente, publicó firmware corregido. Recomendó actualizar Mk2 y Mk3 a 4.2.0 o superior; Mk4 y Mk5 a 5.6.0 o superior; y Q a 1.5.0Q o superior. Además, destruyó el inventario con versiones vulnerables y suspendió envíos. La intrusión no está relacionada con acceso remoto ni con manipulación de la cadena de suministro. El atacante no necesitó tocar los dispositivos: obtuvo las claves mediante fuerza bruta offline, probando sistemáticamente cada semilla posible dentro del espacio de búsqueda. La debilidad llevaba años latente hasta que investigadores externos desarrollaron una herramienta de fuerza bruta y la sacaron a la luz. Más tarde, Muan reprodujo toda la cadena del ataque. Este informe parte del código del firmware. Toma como ejemplo el Mk3 con firmware 4.1.9 para reconstruir la línea que permitió el vaciado masivo de miles de dispositivos Coldcard. Causa raíz de la vulnerabilidad Coldcard está diseñado para generar semillas usando el TRNG (generador de números aleatorios verdadero) integrado del chip STM32L475. Dos errores en el firmware se combinaron para convertir el proceso en un PRNG software con un estado casi idéntico en los dispositivos a nivel mundial. Ese PRNG, llamado Yasmarang, reduce la entropía efectiva del Mk3 a aproximadamente 40 bits. Error 1: se desactivó de forma explícita el RNG hardware El primer fallo está en la configuración de compilación: Coldcard deshabilita el soporte del RNG hardware de MicroPython en mpconfigboard.h. // stm32/COLDCARD/mpconfigboard.h // The team has implemented their own version of ckcc.rng_bytes, so disable MicroPython's builtin version #define MICROPY_HW_ENABLE_RNG (0) A primera vista puede parecer inocuo. Coinkite dispone de ckcc.rng_bytes, que llama directamente al TRNG del STM32 y ofrece más control que la capa port de MicroPython. El problema aparece al expandir macros: my_random_bytes() de libngu obtiene aleatoriedad con CHIP_TRNG_32(), que termina llamando a rng_get() del port STM32 de MicroPython y lo trata como TRNG hardware. Con MICROPY_HW_ENABLE_RNG a 0, rng_get() degrada silenciosamente su fuente y abre la puerta al segundo error. Error 2: el "fallback" de rng_get() se trató como si fuera TRNG hardware El análisis de libngu random.c, siguiendo las expansiones de macro, lleva a la rama else de ports/stm32/rng.c. // external/libngu/ngu/random.c #ifdef MICROPY_PY_STM // ports/stm32/rng.c extern uint32_t rng_get(void); # define CHIP_TRNG_SETUP() # define CHIP_TRNG_32() rng_get() #endif ... void my_random_bytes(uint8_t *dest, uint32_t count) { uint32_t chip = CHIP_TRNG_32(); // Assumes reading from hardware TRNG, but may actually receive Yasmarang output if (chip == last) ... // Reports error if two consecutive words are identical chip ^= my_yasmarang(); // XORs with a global constant Yasmarang stream (pad=0x0a8ce26f) ... } rng_get() decide su comportamiento con #if MICROPY_HW_ENABLE_RNG. Si el macro es distinto de cero, lee RNG>DR (TRNG hardware). Si es cero, compila la rama alternativa y devuelve la salida de un PRNG software. En Coldcard el valor es exactamente 0, por lo que cualquier Mk3 de fábrica recorre esta ruta. // external/micropython/ports/stm32/rng.c #if MICROPY_HW_ENABLE_RNG uint32_t rng_get(void) { ... read RNG>DR hardware TRNG ... } #else // MICROPY_HW_ENABLE_RNG // Vulnerability point: Fallback exists, and seed is almost entirely predictable 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; // pad = UID ^ SysTick n = RTC>TR; // n = RTC_TR d = RTC>SSR; // d = RTC_SSR } pad += dat + d * n; pad = (pad > 29); n = pad | 2; d ^= (pad > 1); dat ^= (char)pad ^ (d >> 8) ^ 1; return pad ^ (d > 18) ^ (dat VAL when rng_get() is first called. } #endif Según el desglose, SysTick>VAL se toma en la primera llamada a rng_get(). El chip opera a 80 MHz con un valor de recarga de 80.000, lo que produce valores entre 0 y 79.999, equivalentes a unas 17 bits de entropía. RTC_TR y RTC_SSR son registros del RTC justo tras el encendido, antes de que la aplicación los inicialice. Cada uno añade una dimensión de enumeración con espacio de valores muy pequeño. En los vectores on-chain verificados, ambos valores son 0. Sumando componentes: el UID aporta aproximadamente 14 bits, SysTick alrededor de 17 bits, ambas entradas RTC aportan 0 bits y las pulsaciones añaden en torno a 5 bits. El resultado es una fuente de entropía real de 36 a 37 bits para toda la generación de semilla. La cifra de Coinkite de "unos 40 bits" encaja con el espacio de búsqueda derivado de la ingeniería inversa: un espacio que un clúster de GPU puede recorrer en pocos días. Proceso del ataque En el primer arranque del Mk3, desde el código hasta la clave privada, el consumo de aleatoriedad sigue tres perfiles típicos. La tarea clave del atacante es modelar cada perfil y reconstruir con precisión cada paso del PRNG. Fase 1: estado inicial Nada más encender el dispositivo ocurren dos cosas: Encendido └── libngu Yasmarang estático (ngu/random.c) pad=0x0a8ce26f n=69 d=233 dat=0 ← Constante global └── rng_get() primera llamada y "seeding" (ports/stm32/rng.c) pad=UID^SysTick n=RTC_TR=0 d=RTC_SSR=0 dat=0 En este punto, toda la "aleatoriedad" queda comprimida en un pad de 32 bits, más dos dimensiones pequeñas enumerables. El pad global del Mk3 queda confinado a una caja reducida y agotable. Fase 2: operaciones con botones Durante la configuración inicial, el usuario debe fijar un PIN, aceptar términos pulsando OK y navegar por el menú. Cada pulsación dispara _start_scan() en shared/mempad.py, que llama a _rand_below() tres veces mediante shuffle(self.scan_order). La longitud de scan_order es NUM_ROWS, igual a 4. shuffle se define en shared/random.py; randbelow está enlazado directamente a ngu.random.uniform, el _rand_below a nivel C en libngu. # shared/random.py import ngu randbelow = ngu.random.uniform bytes = ngu.random.bytes def shuffle(lst): # Fisher-Yates for i in reversed(range(1, len(lst))): j = randbelow(i + 1) lst[i], lst[j] = lst[j], lst[i] # shared/mempad.py # Cada pulsación > _start_scan() > shuffle(self.scan_order) # scan_order length = NUM_ROWS = 4 Estos patrones deben modelarse por separado. Del análisis del firmware v4.1.9 se desprenden tres perfiles. Perfil A: configuración inicial típica Antes de aceptar términos se pulsa dos veces; settings.save() busca un hueco libre entre 32 slots, excluyendo my_pos, quedando 31 slots, lo que implica 30 llamadas a _rand_below. Luego se introduce el PIN, se navega por el menú, se realizan varias pulsaciones más y finalmente se recoge entropía con random_bytes(32). kpad_a × shuffle(4) # pulsaciones antes de accept_terms (kpad_a = 2) shuffle(31) # settings.save(): 30 llamadas a _rand_below kpad_b × shuffle(4) # PIN + navegación (kpad_b enumera [4, 34]) random_bytes(32) # recoge entropía Perfil B: primer arranque tras flasheo o borrado con NVRAM vacía nvstore ejecuta un shuffle(32) y después consume 3.072 pasos sincronizados en 3 slots × 16 bloques × 256 bytes, sin realimentación de salida y avanzando solo el estado. En el arranque con NVRAM en blanco, se añade otro shuffle(32), luego consumo por pulsaciones y, por último, my_random_bytes(32). nvstore shuffle(32) # 31 llamadas a _rand_below nvstore blanking 3×16×256B # 3072 consumos sin feedback perfil de onboarding NVRAM vacía # shuffle(32) adicional consumo por pulsaciones (kpad_b en [4, 34]) my_random_bytes(32) Perfil C: "paper wallet" La función de "paper wallet" imprime claves para almacenamiento offline, habitual en regalos o custodia de largo plazo. En el menú Paper Wallets de Coldcard, tras existir una wallet, reentrar en el menú requiere entre 8 y 25 pulsaciones; luego my_random_bytes(32) se usa directamente como clave privada, sin pasar por la lista de palabras BIP39. Fase 3: construcción de semilla y derivación de dirección Desde random_bytes(32) hasta la dirección final, el Mk3 sigue esta tubería determinista: raw_bytes = random_bytes(32) entropy = ngu.hash.sha256s(raw_bytes) # SHA256 simple, no sha256d mnemonic = BIP39(entropy, wordlist=english) # mnemónico de 24 palabras seed = PBKDF2-HMAC-SHA512(mnemonic, "mnemonic", 2048) master = HMAC-SHA512("Bitcoin seed", seed) child = m/{44,49,84}'/0'/0'/0/0 address = bech32(hash160(compressed_pubkey)) # BIP84 por defecto Cada etapa es reproducible y común a los tres perfiles. Con un pad acertado, el atacante puede recalcular toda la cadena sin conexión. Fase 4: fuerza bruta y acierto en GPU La estrategia descrita se resume en cuatro pasos: 1) Construir el conjunto de pads candidatos. Tomar coordenadas X e Y de oblea entre 0 y 72, combinarlas con un espacio UID de unas 5.300 posibilidades, y hacer producto cartesiano con SysTick entre 0 y 79.999. Resultado: 424 millones de pads candidatos. Suena grande, pero en GPU son pocos días. 2) Enumerar el número de pulsaciones por pad, con kpad_b entre 4 y 34. Dado que rtc_tr y rtc_ssr aparecen como 0 en los casos verificados, se sitúan en el bucle externo y se simplifica el interior. 3) Ejecutar en el kernel de GPU toda la tubería: SHA256, BIP39, PBKDF2 (2048 rondas), BIP32, Hash160 y Bech32. El flujo constante de libngu puede precomputarse; el kernel recupera palabras por índice, evitando que cada hilo avance el PRNG constante por su cuenta. 4) Buscar coincidencias con un Bloom filter o un array ordenado de hash160 con complejidad O(log n), apuntando al conjunto de direcciones P2WPKH de firma única en la red. Coste estimado: ejecutar el espacio de la Fase A en una GPU Apple M1, con UID 72×72, 80.000 variaciones de SysTick y 31 conteos de pulsaciones, generó aproximadamente 14,8 mil millones de candidatos y tardó unos 8,6 días. Un clúster A100 de nivel centro de datos puede reducirlo a pocas horas. El espacio de ~72 bits de Mk4, Mk5 y Q es 2^32 veces mayor que el de Mk3, pero comparte el mismo origen. El enfoque de modelado por perfiles es el mismo; lo que cambia es la escala: de una máquina a un clúster, y de días a semanas, un coste que atacantes siguen asumiendo. Por eso estos modelos también aparecen en las listas de víctimas.