Coldcard硬件钱包RNG漏洞深度剖析: 一个宏定义如何导致8800万美元比特币被盗 Coldcard硬件钱包“熵崩溃”事件深度技术复盘一个宏定义如何导致8800万美元蒸发2026年7月30日一场针对Coldcard硬件钱包的攻击震惊了整个加密世界。攻击者在41分钟内清空了1,196个比特币地址盗走1,082.65 BTC按当时价格计算价值约7020万美元。此后攻击持续发酵截至8月初累计已有约1,367枚比特币从4,585个地址中被盗损失接近8900万美元。这不是一次物理入侵不是供应链攻击也不是钓鱼诈骗。攻击者从未接触过任何一台受害者的实体设备。他们只是读懂了五年前一行代码的“潜台词”。这不是“你的密钥不是你的币”的反面——密钥确实从未离开过冷钱包但它的出生证明本身就是伪造的。作为开发者我们需要回答一个问题一行宏定义怎么就能让一个以“安全”为卖点的硬件钱包全军覆没一、漏洞的“出生证明”2021年3月的那次提交漏洞的根源可以追溯到2021年3月1日发布的Coldcard固件4.0.0版本。在Commit37e4af5451c260c1e7d429fe8972c4cb5e68ee59中开发团队在mpconfigboard.h中写下了一行看似无害的配置// We have our own version of this code.#defineMICROPY_HW_ENABLE_RNG(0)在MicroPython STM32端MICROPY_HW_ENABLE_RNG这个宏控制着默认硬件随机数生成器RNG的绑定和编译路径。将其设为0的直接后果是默认硬件RNG路径不会作为通用rng_get()的后端使用。开发者为什么要把它设成0根据社区分析当时开发团队在整合自有RNG封装libngu时与MicroPython的现有实现发生了编译冲突。证据显示开发者在冲突后选择将MICROPY_HW_ENABLE_RNG设为0以允许固件编译通过这是一个典型的“先让编译跑起来”的决策但在安全攸关的场景下代价是毁灭性的。二、代码层面的完整剖析漏洞是如何“静默生效”的2.1 自定义RNG看上去很美的“替代方案”Coldcard开发团队的注释说他们“会自行实现RNG”。在自定义的rng.h中他们确实声明了两个MicroPython对象MP_DECLARE_CONST_FUN_OBJ_0(pyb_rng_get_obj);MP_DECLARE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj);对应的实现为/// \function pyb_rng_get()/// Return a 30-bit hardware generated random number: or fail!STATICmp_obj_tpyb_rng_get(void){returnmp_obj_new_int(rng_get_or_fault()2);}/// \function rng_get_bytes()/// Fill a buffer with random bits; caller must provide sized buffer.STATICmp_obj_tpyb_rng_get_bytes(mp_obj_tbuffer_io){mp_buffer_info_tbufinfo;mp_get_buffer_raise(buffer_io,bufinfo,MP_BUFFER_WRITE);mp_uint_tcountbufinfo.len;if(count1){mp_raise_ValueError(NULL);}random_buffer(bufinfo.buf,count);returnmp_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()本身确实读取了STM32硬件RNG外设staticuint32_trng_get_or_fault(void){rng_init();uint32_tstartHAL_GetTick();while(!(RNG-SRRNG_SR_DRDY)){if(HAL_GetTick()-startRNG_TIMEOUT_MS){mp_raise_OSError(MP_EFAULT);}}last_valueRNG-DR;returnlast_value;}问题来了Coldcard自定义代码确实“想要”使用硬件RNG但它只保证在调用pyb_rng_get*或其内部random_buffer()时才会走这条路。而钱包创建流程并没有走这条路径。2.2 钱包创建真正的“凶手”在这里钱包创建的实际入口在shared/seed.py中asyncdefmake_new_wallet(nwords):# Pick a new random seed.awaitux_dramatic_pause(Generating...,3)seedgenerate_seed()wordsawaitapprove_word_list(seed,nwords)ifwords:awaitcommit_new_words(words)该调用进入Coldcard的shared/random.py模块。钱包初始化使用的是ngu.random.bytes而不是pyb.rng()。自定义的pyb_rng_get_obj并未自动覆盖ngu.random.bytes。这就像你在厨房装了一台高级净水器但日常喝的水却是从水龙头直接接的因为水管根本没接对。2.3 静默回退Yasmarang的“幽灵”由于MICROPY_HW_ENABLE_RNG被设为0系统静默回退到了MicroPythonports/stm32/rng.c中的pyb_rng_yasmarang#ifMICROPY_HW_ENABLE_RNGuint32_trng_get(void){// use STM32 hardware RNG...}#else// For MCUs that dont have an RNG we still need to provide a rng_get() function// A pseudo-RNG is not really ideal but we go with it for now.// Yasmarang random number generatorstaticuint32_tpyb_rng_yasmarang(void){staticbool seededfalse;staticuint32_tpad0,n0;// ... deterministic PRNG logic}uint32_trng_get(void){returnpyb_rng_yasmarang();}#endifpyb_rng_yasmarang是一个确定性软件伪随机数生成器PRNG由芯片唯一ID和定时器寄存器初始化且在初始化之后不再收集新的熵。更致命的是libngu库在检查这个宏时只看它是否被定义#ifdef而不检查它是否被启用#if。宏被定义为0但#ifdef仍然为真于是编译通过系统安静地绑定了MicroPython的软件备用方案。2.4 Makefile的“障眼法”开发团队在Makefile中明确排除了stm32/rng.c的编译# Do not compile MicroPythons fallback PRNG. The board-specific rng.c # provides rng_get(), and this empty object satisfies the upstream object list. $(BUILD)/rng.o: CFLAGS -Dpyb_rng_yasmarangerror-do-not-want-this $(BUILD)/rng.o: $(ECHO) SKIP stm32/rng.c $(Q)$(CC) $(CFLAGS) -x c -c /dev/null -o $开发团队自认为已排除MicroPython的备用PRNG但实际上MICROPY_HW_ENABLE_RNG0这个设置在编译时静默地将rng_get()指向了pyb_rng_yasmarang。Makefile跳过了rng.c的编译但宏定义仍然在发挥作用导致了一个“幽灵PRNG”的存在。热修复后stm32/rng.o改为从/dev/null编译rng_get()解析为uint32_trng_get(void){returnrng_get_or_fault();}这才真正读取了硬件TRNG可惜为时已晚。2.5 熵值崩溃从128位到40位根据Coinkite的官方评估型号受影响固件版本有效熵值预期熵值Mk2/Mk34.0.0 – 4.1.9~40位128位Mk4/Mk55.6.0之前~72位128位Coldcard Q1.5.0Q之前~72位128位Block工程团队并未给出一个确切数字但设定了低于240.7**和**273.3的条件上限并特别警告后者并不等同于73位的密码学安全强度。对于Mk2/Mk3在已知UID、定时器状态和调用历史的情况下种子生成几乎是确定性的。对于较新型号虽然安全元件在启动时加入了熵但最终只保留了4个字节32位搜索空间仍然极为有限。40位熵意味着什么2^40 ≈ 1万亿在现代计算能力面前这个搜索空间完全可以暴力破解。Block的分析指出攻击者只要能确定或充分限定设备UID、定时器状态以及此前的RNG调用历史就无需接触设备即可离线重现候选输出流。三、攻击者的四步棋第一步识别漏洞攻击者通过分析Coldcard开源固件代码发现了MICROPY_HW_ENABLE_RNG (0)配置以及pyb_rng_yasmarang的存在。他们确认了不同型号的熵值差异并锁定了攻击目标2021年3月之后、修复固件发布之前创建的所有单签名钱包。第二步重建候选助记词空间攻击者利用PRNG的确定性本质种子来自芯片唯一ID、定时器状态等可预测信息在自己的计算环境中批量生成所有可能的低熵助记词组合。第三步链上地址匹配将生成的每个候选助记词按照BIP-32/BIP-44标准推导出对应的比特币地址然后与公开的区块链UTXO数据进行比对筛选出存有资金的地址。第四步批量自动化盗取一旦匹配成功直接推导私钥并发起转账。整个过程完全不需要接触受害者的实体设备。第一波攻击的自动化程度令人咋舌所有交易使用相同的30 sat/vB手续费率且无任何找零输出这是典型的自动化工具在批量操作的痕迹。Galaxy Research指出这种扫掠模式“看起来与币主自己选择转移币的行为相同”这也是为什么攻击如此难以被提前发现。四、完整时间线时间事件2021年3月1日Coldcard固件4.0.0发布引入MICROPY_HW_ENABLE_RNG (0)配置错误2021年3月 – 2026年7月漏洞持续存在于后续版本中潜伏超过5年2026年7月30日 01:10-01:51 UTC第一波攻击41分钟内从1,196个地址盗走1,082.65 BTC2026年7月30日Coinkite发布安全警告2026年7月31日Coinkite扩大警告范围至Mk4、Mk5和Q型号发布紧急修复固件2026年7月31日后第二波、第三波攻击累计受害地址达4,585个被盗约1,367枚BTC2026年8月2日之后疑似第四波攻击启动累计损失可能超过1.14亿美元一个值得深思的细节首波攻击发生在Coinkite公开披露漏洞的同一天攻击者可能比厂商更早发现了这个漏洞。Coinkite首席执行官Rodolfo Novak曾表示AI可能已经在公司开源固件中发现了这个存在五年的漏洞。五、热修复暴露的第二个问题2026年7月31日的热修复commit ca724637将rng_get()正确解析到了板级TRNG访问器。但随即暴露了第二个严重问题rng_get_or_fault()没有针对STM32 RNG错误标志的恢复路径。问题出在rng_init()的实现上staticvoidrng_init(void){if(!(RNG-CRRNG_CR_RNGEN)){__HAL_RCC_RNG_CLK_ENABLE();RNG-CR|RNG_CR_RNGEN;// TODO: throw out some samples?}}根据STM32参考手册RM0432 §25.3.7和RM0351当发生种子错误时SECS被设置SEIS锁存DRDY停止断言RNGEN保持设置状态恢复的正确做法是清除SEIS然后toggle RNGEN关闭→再开启。但rng_init()只检查RNGEN在故障状态下它什么也不做直接返回。rng_get_or_fault()随后忙等待10ms然后抛出异常每次调用都如此跨Python层重启也不恢复。更麻烦的是rng_get()现在被用在了键盘扫描路径上# mempad.py / keyboard.py :: _start_scan()shuffle(self.scan_order)# We scan in random order, because Tempest.-random.randbelow-ngu.random.uniform-_rand_below()-CHIP_TRNG_32()-rng_get()_start_scan()在每次按键中断press_irq最高60Hz时被调用在登录之前就运行。每次按键可能触发3次以上的TRNG读取每次最坏情况10ms。一旦OSError在那里抛出用户会看到一个错误屏幕无法输入PIN也无法进入升级菜单。这正是热修复后部分用户报告的“卡在错误屏幕/无法启动/疑似变砖”问题的根源。一个旨在修复安全漏洞的补丁却因为硬件错误处理逻辑的缺失导致设备变成“电子砖头”。这提醒我们安全修复本身也需要安全测试。六、正确修复方案完整版6.1 第一层正确的宏配置// 在 mpconfigboard.h 中#defineMICROPY_HW_ENABLE_RNG(1)// 启用硬件RNG效果确保rng_get()指向硬件TRNG而非软件PRNG。6.2 第二层硬件TRNG的故障恢复staticvoidrng_init(void){// 检查是否有挂起的错误标志if(RNG-SR(RNG_SR_SEIS|RNG_SR_CEIS)){// 清除错误标志RNG-SR~(RNG_SR_SEIS|RNG_SR_CEIS);// 关闭RNGRNG-CR~RNG_CR_RNGEN;// 等待关闭完成for(volatileinti0;i100;i);// 重新开启RNG-CR|RNG_CR_RNGEN;// 丢弃初始化后的前几个采样参考手册建议for(inti0;i4;i){while(!(RNG-SRRNG_SR_DRDY));(void)RNG-DR;}}elseif(!(RNG-CRRNG_CR_RNGEN)){__HAL_RCC_RNG_CLK_ENABLE();RNG-CR|RNG_CR_RNGEN;// 同样丢弃初始采样for(inti0;i4;i){while(!(RNG-SRRNG_SR_DRDY));(void)RNG-DR;}}}效果当硬件TRNG出现种子错误或时钟错误时能够自动恢复而不是永久锁死。6.3 第三层读取时的错误检查staticuint32_trng_get_or_fault(void){rng_init();uint32_tstartHAL_GetTick();while(!(RNG-SRRNG_SR_DRDY)){if(HAL_GetTick()-startRNG_TIMEOUT_MS){mp_raise_OSError(MP_EFAULT);}}// 在读取之前再次检查是否有错误标志锁存if(RNG-SR(RNG_SR_SEIS|RNG_SR_CEIS)){rng_init();// 会清除错误并重新初始化startHAL_GetTick();while(!(RNG-SRRNG_SR_DRDY)){if(HAL_GetTick()-startRNG_TIMEOUT_MS){mp_raise_OSError(MP_EFAULT);}}}returnRNG-DR;}效果确保不会将硬件故障时的“坏数据”当作有效随机数返回。6.4 第四层构建时的防御性检查在构建系统中加入编译时检查# 在构建脚本中 ifneq ($(MICROPY_HW_ENABLE_RNG),1) $(error MICROPY_HW_ENABLE_RNG must be set to 1 for secure builds) endif或者使用#if而非#ifdef进行条件编译// 在 libngu 中#ifMICROPY_HW_ENABLE_RNG1// 使用硬件RNG#else#errorHardware RNG is not enabled - this build is insecure!#endif效果从构建层面杜绝配置错误再次发生。6.5 第五层端到端测试增加测试用例验证钱包创建流程实际使用的是硬件RNG而非软件PRNG。例如在测试环境中mock硬件RNG并验证其被调用通过统计检验确认输出的随机性符合预期验证最终链接符号确保rng_get的最终链接指向预期的实现七、给开发者的八个教训1. 永远不要将硬件RNG的启用状态设为0硬件钱包的随机数生成器是其安全性的基石。任何绕过硬件RNG的配置无论出于什么原因哪怕是“编译冲突”都是一个巨大的危险信号。2. 检查宏时使用#if而非#ifdef区分“宏被定义”和“宏被启用为真值”。这个差异在安全攸关的场景下可能是致命的。3. 确保所有随机数生成路径都经过验证不能只验证“存在硬件RNG”还要验证“实际使用了硬件RNG”。钱包创建流程所用的随机数来源必须和硬件RNG是同一条路径。4. 端到端测试不可省略必须测试从种子生成到地址推导的完整流程确认熵源符合预期。Kraken CSO Nick Percoco对此评论道“审计人员可以验证设备中是否存在经批准的随机数生成器但无法确认生产固件实际使用了它”。5. 硬件TRNG需要正确的错误处理即使正确连接了硬件TRNG也必须实现完整的错误检测和恢复逻辑。否则一次硬件故障就可能让设备永久锁死。6. 构建系统应包含安全断言如果某个配置对安全性至关重要构建系统应该在编译时检查它是否正确设置而不是静默地使用不安全的备用方案。7. Fail Closed而非Fail Open在安全攸关的系统中当配置不确定时应该拒绝构建Fail Closed而不是静默回退到不安全的方案Fail Open。8. 开源不等于自动安全这个漏洞在公开代码中潜伏了五年多。开源代码需要持续的、专业的安全审计特别是针对运行时实际执行的代码路径而不仅仅是“存在哪些功能”。八、用户该怎么办如果你是Coldcard用户请注意以下几点更新固件无法修复已经生成的种子。必须在新固件上生成全新种子并迁移资金。受影响型号及固件版本Mk2/Mk3固件4.0.1 – 4.1.9Mk4/Mk5标准固件5.6.0之前 / Edge 6.6.0X之前Coldcard Q标准固件1.5.0Q之前 / Edge 6.6.0QX之前使用至少50次公平、独立且私密的骰子投掷生成的种子不受此漏洞影响。一个强且唯一的BIP-39口令可以创建独立钱包但Coinkite仍建议更换种子。Galaxy Research负责人Alex Thorn发出了一个令人不寒而栗的警告2021年3月固件漏洞之后创建的每一个单签名Coldcard地址最终都可能被清空。这次事件也凸显了一个残酷的现实即使密钥从未离开过冷钱包如果它的生成方式存在缺陷它仍然不安全。在自我托管的模式下即使用户更新了固件修复前生成的钱包也无法被补救。直到用户主动采取行动之前相关钱包可能持续暴露在攻击风险中。一行宏定义的错误五年的潜伏四十分钟的扫荡八千九百万美元的蒸发。安全不是“存在某个功能”而是“每个路径都正确执行”。