Pérdida de más de 88 millones de dólares: Análisis de la vulnerabilidad del billetera hardware Coldcard y rastreo de los fondos robados El 31 de julio, se robaron un total de 594 bitcoins de aproximadamente 500 billeteras hardware Coldcard, valorados en unos 38 millones de dólares. Posteriormente, Coinkite, la empresa responsable de la producción de las billeteras Coldcard, confirmó que existía una vulnerabilidad de seguridad en el proceso de generación de claves, afectando múltiples generaciones de productos, incluyendo Coldcard Mk2, Mk3, Mk4, Q y Mk5.
Actualmente, el monto perdido debido a este ataque de vulnerabilidad ha superado los 88 millones de dólares, y el ataque sigue en curso. Los usuarios de billeteras Coldcard deben transferir sus fondos a otras direcciones lo antes posible. A continuación se presenta el análisis de Beosin sobre esta vulnerabilidad y el seguimiento de los fondos robados.
I. Análisis de la vulnerabilidad
Al analizar los registros de confirmación del firmware de Coldcard, se puede observar que en la confirmación anterior 37e4af5451c260c1e7d429fe8972c4cb5e68ee59, el equipo de desarrollo actualizó varios códigos relacionados con la configuración de MK4, que incluyen:
// Tenemos nuestra propia versión de este código.#define MICROPY_HW_ENABLE_RNG (0)
En la plataforma MicroPython STM32, esta macro controla la ruta de compilación para el generador de números aleatorios hardware predeterminado (RNG) y la implementación general de números aleatorios. Establecerla en 0 hace que la ruta del RNG hardware predeterminado no se utilice como backend para rng_get().
Su nota indica que el desarrollador implementará el RNG; al revisar el rng.h personalizado, se descubre que solo se declaran dos objetos MicroPython:
MP_DECLARE_CONST_FUN_OBJ_0(pyb_rng_get_obj);MP_DECLARE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj);
La implementación correspondiente es:
/// \function pyb_rng_get()///// Devuelve un número aleatorio generado por hardware de 30 bits: ¡o falla!//STATIC mp_obj_t pyb_rng_get(void){ // Obtener y devolver el nuevo número aleatorio return mp_obj_new_int(rng_get_or_fault() >> 2);}
/// \function rng_get_bytes()/// Llena un búfer con bits aleatorios; el llamador debe proporcionar un búfer de tamaño adecuado.STATIC mp_obj_t pyb_rng_get_bytes(mp_obj_t buffer_io) {
mp_buffer_info_t bufinfo; mp_get_buffer_raise(buffer_io, &bufinfo, MP_BUFFER_WRITE);
mp_uint_t count = bufinfo.len; if(count < 1) { mp_raise_ValueError(NULL); }
// Leer palabras de 32 bits y desempaquetarlas en el búfer proporcionado random_buffer(bufinfo.buf, count);
return mp_const_none;}
MP_DEFINE_CONST_FUN_OBJ_0(pyb_rng_get_obj, pyb_rng_get);MP_DEFINE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj, pyb_rng_get_bytes);
rng_get_or_fault() lee directamente el periférico RNG de STM32:
static uint32_t rng_get_or_fault(void){ // Habilitar el periférico RNG si aún no está habilitado rng_init();
// Esperar a que esté disponible un nuevo número aleatorio, tarda aproximadamente 10 us uint32_t start = HAL_GetTick();
while (!(RNG->SR & RNG_SR_DRDY)) { if (HAL_GetTick() - start >= RNG_TIMEOUT_MS) { // fallo de hardware... ¡no devolver nada! mp_raise_OSError(MP_EFAULT); } }
// Obtener y devolver el nuevo número aleatorio last_value = RNG->DR;
return last_value;}
Esto indica que el código personalizado de Coldcard realmente desea utilizar el RNG de hardware, pero solo garantiza el uso de esta función de hardware al llamar a pyb_rng_get* o su internal random_buffer().
Y durante la creación del monedero, se llama efectivamente al siguiente código, donde la función de creación de monedero en shared/seed.py es:
async def make_new_wallet(nwords): # Elegir una nueva semilla aleatoria. await ux_dramatic_pause('Generando...', 3) seed = generate_seed() words = await approve_word_list(seed, nwords) if words: await commit_new_words(words)
Esta llamada entra primero en el módulo shared/random.py de Coldcard. En versiones anteriores, random.py dependía explícitamente de ngu.random y conservaba:
# random.py - subconjunto del módulo random, sin compatibilidad y utilizando RNG de calidad criptográfica
# para bytes, use ngu.random.byte(len)
# bytes = ngu.random.bytes
El proceso de inicialización de la billetera utiliza random.bytes, no pyb.rng(); la función pyb_rng_get_obj personalizada no reemplaza automáticamente random.bytes.
Dado que MICROPY_HW_ENABLE_RNG está configurado como 0, al generar la billetera no se utilizó el RNG de hardware, sino que se llamó a pyb_rng_yasmarang en micropython/ports/stm32/rng.c:
#if MICROPY_HW_ENABLE_RNG
uint32_t rng_get(void) { // usar el generador de números aleatorios hardware de STM32 ...}
#else
// Para MCU que no tienen un RNG, aún necesitamos proporcionar una función rng_get()// Un generador pseudoaleatorio no es realmente ideal, pero lo usamos por ahora.
// Generador de números aleatorios Yasmarangstatic uint32_t pyb_rng_yasmarang(void) { static bool seeded = false; static uint32_t pad = 0, n = 0; ...}
uint32_t rng_get(void) { return pyb_rng_yasmarang();}
#endif
Y pyb_rng_yasmarang es un generador de números pseudoaleatorios, que es extremadamente inseguro para la generación de semillas de billeteras hardware, ya que los atacantes pueden obtener las claves mediante fuerza bruta. Actualmente, Coinkite ha excluido explícitamente stm32/rng.c en el archivo makefile:
# No compilar el PRNG de respaldo de MicroPython. El archivo rng.c específico de la placa
# proporciona rng_get(), y este objeto vacío satisface la lista de objetos upstream.
$(BUILD)/rng.o: CFLAGS += -Dpyb_rng_yasmarang=error-do-not-want-this
$(BUILD)/rng.o:
$(ECHO) "SKIP stm32/rng.c"
$(Q)$(CC) $(CFLAGS) -x c -c /dev/null -o $@
II. Rastreo de fondos robados
Actualmente, los fondos de varias carteras de víctimas han sido transferidos y reunidos en múltiples direcciones, sin haberse realizado aún una limpieza adicional. Beosin Trace, mediante inteligencia de amenazas y análisis de comportamiento en la cadena, ha monitoreado las siguientes direcciones de reunión:
bc1qq85v2c926eg6pgxhwp6q7lf6cnsz80qs3fcu9r (562 BTC)
bc1qx76cae2706qd5q576feh7xq8rfcsjpf2htfhe3 (398.47 BTC)
Además, existen las siguientes direcciones de agrupación, cuyos flujos de fondos son similares a los de la imagen anterior y por el momento no han habido transferencias adicionales:
- bc1q8jy96fe5lf8vfugydnte3cguk92gpev7kwtp3q (89.62 BTC)
- bc1q0rvn88w08j75k4h48lf9fvhan7unjp7vjf5q6m (64.9 BTC)
- bc1qtfrwa4j6rmj9rsgspv6a0yjumkg39js2numu75 (45,9 BTC)
- bc1qmd5m5ktv7m5ffujxv4248fxv36myvdx79n8jp6 (30.18 BTC)
Los ataques contra la billetera Coldcard aún están en curso, y el equipo de Beosin continúa monitoreando más direcciones de agrupación y analizando los flujos de fondos relacionados.
Tres, conclusión
Este importante incidente de seguridad en la billetera hardware Coldcard se debió a un error en la implementación del generador de números aleatorios por parte del equipo de desarrollo, que utilizó un generador de números pseudoaleatorios en el proceso crítico de generación de la semilla de la billetera. El equipo de desarrollo debe realizar pruebas y auditorías continuas y exhaustivas del código, y los usuarios de la billetera Coldcard deben transferir sus activos lo antes posible y mantenerse atentos a los siguientes anuncios de seguridad de Coinkite.
Beosin es una empresa líder en tecnología de seguridad blockchain y cumplimiento normativo, enfocada en auditorías de seguridad de contratos inteligentes antes del lanzamiento de proyectos, monitoreo y bloqueo de riesgos de seguridad durante la operación del proyecto, recuperación de activos robados, lucha contra el lavado de dinero (AML) para activos virtuales, e investigación y rastreo. Beosin ha proporcionado productos y servicios de cumplimiento blockchain "todo en uno" a más de 200 proveedores de activos virtuales y 4.500 proyectos Web3 en más de 20 países y regiones del mundo.

