COLDCARD 熵漏洞专业复盘:卷走 3800 万美元的随机数缺陷

onekeycn 发布于 2026-08-01 阅读 17

2026年7月30日,约594枚比特币(约3800万美元)在半小时内从近500个COLDCARD钱包中被盗。厂商Coinkite确认漏洞源于2021年固件改动中的编译配置错误,导致代码回退到MicroPython自带的软件伪随机数生成器(PRNG)。文章深入剖析了漏洞根因:libNgU迁移时宏定义检查只验证“是否存在”而非“值”,同名符号使安全实现被静默绕过;混合熵源无法补足缺失熵,Mk3有效搜索空间降至约40位,Mk4/Mk5/Q约72位,远低于128位设计目标。文章还给出了各型号的修复版本、用户迁移步骤、骰子熵补充方案,并总结了安全工程实践建议,如验证最终链接符号、fail closed、CI检查等。

图像

以下内容由 OneKey 安全实验室 @OneKey_Anzen 撰写校对完成,英文原文见评论。

在密码学里,“随机”是一个可以量化、经得起算力检验的硬指标。

一套 12 个词的助记词,背后是一串 128 位的随机数,按 BIP-39 规范编码成一组人能读写的单词。这串数字的不可预测程度,直接决定钱包能不能扛住暴力破解。128 位意味着 $2^{128}$ 种可能——把全世界的算力加在一起,穷举到宇宙消失也跑不完。

所以硬件钱包理论上应该在这件事上很严谨:必须要有专门处理随机数生成的真随机数发生器单元(TRNG),从电路噪声里取物理世界的随机,再配一颗过了 EAL 认证的安全元件(SE),最后用成熟的算法把这些随机搅匀。

2026 年 7 月 30 日,约 594 枚比特币(约 3800 万美元)在不到半小时内,从近 500 个 COLDCARD 钱包里被席卷一空。次日,厂商 Coinkite 在《Technical Deep Dive into the Entropy Issue》中确认了根源[1]:自 2021 年 3 月的一次固件改动起,一处编译配置错误让代码悄悄回退到了 MicroPython 自带的软件伪随机数生成器(PRNG)。

后果按型号不同:官方初步估计,Mk3 的有效搜索空间只剩约 40 位,Mk4、Mk5 和 Q 约 72 位,而设计目标是 128 位。40 位约等于 1.1 万亿种可能,听起来仍是天文数字,但对一个租得起 GPU 集群的攻击者来说,它已经从“物理上不可能”,降级为一个数学期望上可行的工程项目。从 128 到 40,搜索空间缩小了 $2^{88}$ 倍——三百亿亿亿倍。

这次 3800 万美元的失窃,把风险变成了既成事实。

注:OneKey 用户的资金是安全的,OneKey 的所有代码均不涉及相关问题,也不使用其相关实现和依赖。详见声明: https://x.com/OneKeyCN/status/2083104424764018990?s=20

太长不看?结论

  • 使用 COLDCARD Mk3 生成助记词并使用的用户,立刻在其他钱包重新生成助记词并逐步转移资金到新钱包
  • 使用 COLDCARD Mk4 或 Mk5 生成助记词并使用的用户,如果生成初始助记词的系统版本低于 5.6.0,立刻在其他钱包重新生成助记词并逐步转移资金到新钱包
  • 使用 COLDCARD Q 生成助记词并使用的用户,如果生成初始助记词的系统版本低于 1.5.0Q,立刻在其他钱包重新生成助记词并逐步转移资金到新钱包
  • 使用 OneKey 硬件钱包的用户,都不受同样的漏洞困扰,可以放心使用

漏洞根因:一次静默的软件回退

从硬件 RNG 接口迁移到 libNgU

2021 年的加密栈迁移将钱包种子生成从 ckcc.rng_bytes() 改为 ngu.random.bytes()[6]:

ngu.random.bytes() -> libNgU rng_get() -> 最终链接到的 rng_get 符号

风险不在函数名或输出长度,而在最后一个 rng_get() 实际由哪个目标文件提供。#ifndef 只检查“有没有定义”,不检查值。

目标板配置把 MICROPY_HW_ENABLE_RNG 定义为 0,表示 MicroPython 的硬件 RNG 路径未启用。但 libNgU 当时使用[8]:

#ifndef MICROPY_HW_ENABLE_RNG
#error ...
#endif

#ifndef 只判断宏是否存在。一个被定义为 0 的宏依然是“已定义”,所以预期用于阻止错误构建的 #error 没有触发。

同名符号让错误实现静默进入最终固件

MicroPython 在硬件 RNG 未启用时仍提供同名 rng_get(),但它是 Yasmarang 软件 PRNG fallback[9]。于是发生了完整的失败链:

  1. 宏存在但值为 0
  2. libNgU 的构建保护未触发
  3. 固件仍能成功编译
  4. 同名 rng_get 解析到 MicroPython fallback
  5. 钱包建种没有获得预期硬件 RNG 输入

这条链上每一环单独看都只是小疏忽,连起来却让整个构建系统的把关形同虚设:安全实现即使存在于源码或二进制中,也不代表安全关键调用真正到达了它。

混合熵源补不回缺失的熵

fallback 初态涉及 UID 的一部分、SysTick、RTC 时间和子秒状态,之后由 Yasmarang 确定性推进。libNgU 还将其与另一个固定初态的 Yasmarang 流混合。两个可重建或枚举的确定性流,经过异或或 SHA-256 后仍不会获得新的物理不可预测性。哈希可以调理输入,却不能把有限候选集合扩展为真正的 $2^{256}$ 未知空间。

40 位和 72 位到底意味着什么

Coinkite 使用的有效搜索空间估计,并非经过测量和认证的熵值。这两个数字描述的是攻击者需要遍历的候选状态规模,它不是 min-entropy 的实测结果,也换算不出平均搜索次数,更推不出具体的攻击时间和成本。

实际攻击成本取决于一连串前提:攻击者是否知道或能推测设备 UID,能否收窄 RTC、SysTick 和启动时间的范围,种子生成前发生过多少次 RNG 调用,手上有没有 xpub、地址或公钥可以离线验证候选,以及 BIP-39/BIP-32 派生和并行硬件要花多少钱。

所以准确的说法是:Coinkite 在当前初步攻击假设下,估计 Mk3 的有效搜索空间约为 40 位,Mk4/Q/Mk5 约为 72 位。这不等于“每个 Mk3 钱包恰好只有 40 位熵”,不等于“较新型号经过证明拥有 72 位密码学安全性”,更不等于“任何普通电脑都能在固定时间内恢复所有钱包”。

为什么 Mk4、Mk5 和 Q 也受影响

图像

Mk5 与 Q 电路板上的两颗安全元件(来源:coldcard.com)

Mk4、Mk5 和 Q 的确会从两个安全元件取得随机材料,但修复前的实现并没有把全部输出注入一个密码学 DRBG。公开源码显示,它把安全元件材料哈希后,只取四字节传给 ngu.random.reseed(),并仅替换 Yasmarang 的一个 32 位状态字。因此,在 fallback 其余状态和调用轨迹固定时:

由安全元件 reseed 区分的输出流数量 $\le 2^{32}$

这与官方“约 72 位”并不矛盾。72 位模型还计入 UID、计时器、RTC 和调用历史等状态的不确定性。Block Engineering 给出的宽松组合上界低于 $2^{73.3}$,并明确指出这不等于 73 位密码学安全认证[10]。

换句话说:Mk3 的问题最严重,因为没有安全 reseed;Mk4/Q/Mk5 的安全元件输入降低了风险,但四字节 reseed 没有恢复设计目标中的 128 位安全性。

官方公告与实际影响范围

Coinkite 当前正式警告从 Mk3 4.0.1 开始。但 v4.0.0 的不可变源码已经包含相关建熵生成路径[7],4.0.0 到 4.0.1 之间也没有对应的 RNG 修复。Block Engineering 进一步将 Mk2/Mk3 v4.0.0–v4.1.9 一并纳入源码分析[10]。

因此需要区分两层:

  • Coinkite 正式范围:Mk3 4.0.1+;
  • 源码能够支持的技术范围:Mk2/Mk3 v4.0.0–v4.1.9。

不能说 Coinkite 已经正式确认 Mk2 或 v4.0.0,但也不能因为官方没有点名就推断它们安全。

修复如何工作

Mk4/Mk5 5.6.0 和 Q 1.5.0Q 的热修复不止修改了一个条件表达式,还增加了构建级强制保证[3][4][5]:

  • 排除 MicroPython 的 fallback RNG object;
  • 确保板级对象提供全局 rng_get()
  • 使用 nm 检查目标文件的符号;
  • 如果 fallback 仍导出符号,或板级对象没有正确提供 rng_get(),构建直接失败。

用户应该如何处置

Mk4、Mk5 和 Q

  1. Mk4/Mk5 先升级到 5.6.0 或更高版本;Q 先升级到 1.5.0Q 或更高版本。
  2. 在修复后的固件中生成全新种子。
  3. 记录并完成备份恢复验证。
  4. 在设备屏幕核对钱包 fingerprint 和接收地址。
  5. 先进行小额测试交易,再迁移其余资金。
  6. 在全部迁移确认前保留旧备份,但不要继续向旧钱包收款。

Mk3

Mk3 当前没有已发布的修复固件,4.1.9 不是修复。用户不应等待未来可能出现的固件,因为任何新固件都无法增加旧种子的熵。按照 Coinkite 当前迁移指引,在可信路径生成新种子,完成备份、地址和小额交易验证后迁移资产。

两个不同的骰子阈值

关于骰子有两个数字:50 和 99。它们服务于两个不同的目标,不能混用。

原始建种时加入 50+ 次骰子。 如果种子最初生成时加入了至少 50 次公平、独立、私密、未记录或保存且未泄露的六面骰结果,Coinkite 表示不认为该种子仅因本 RNG 问题而处于风险[11]。

$$ 50 \times \log_2(6) \approx 129.25 \text{ bits} $$

这是针对本事件的约 128 位门槛,不是整个操作流程的绝对安全证明。

空白 Mk3 的 Dice Roll Import 流程。 Coinkite 还提供一个不同流程:在空白 Mk3 4.1.9 上选择 Import Existing > Dice Rolls,并输入至少 99 次公平掷骰。该专用路径直接处理骰子序列,不使用有问题的设备生成器。

$$ 99 \times \log_2(6) \approx 255.91 \text{ bits} $$

99 次是官方流程的门槛;如果按“至少 256 位”的字面标准倒推,则需要 100 次。

Passphrase 是独立屏障,不是修复

强、唯一、随机且保密的 BIP-39 passphrase 会派生一个不同钱包,可以增加独立搜索因子;设备 PIN 只控制本地访问,不参与 BIP-39 根密钥派生[12]。但 passphrase 不能补回助记词缺失的熵。短口令、名言、规律模式或复用口令仍可能被猜中。输入错误也会生成一个看似有效但完全不同的钱包,因此必须验证 fingerprint,并建立可靠、经过恢复测试的备份。

结语

这起事件暴露的核心问题,是安全关键实现的实际可达性没有被构建系统强制验证——“一个宏写错了”远不足以概括它。

硬件钱包和其他密钥生成系统至少应做到:

  • 安全 API 验证最终链接到的 object 和 symbol,而不只检查函数签名;
  • 能力检查同时验证宏的存在和值;
  • 安全关键 fallback 必须 fail closed,不能静默提供普通 PRNG;
  • CI 检查 object composition、link map、符号来源和端到端数据流;
  • 测试证明真实硬件熵进入最终种子,而不只是检查输出长度、非零或无重复;
  • 公告明确区分生成版本、当前固件、修复版本和旧种子处置。

参考链接

  1. Coinkite:Technical Deep Dive into the Entropy Issue
  2. Coinkite:Mk3 Security Advisory
  3. Coldcard 5.6.0 / 1.5.0Q 修复提交
  4. Coldcard 5.6.0 release commit
  5. Coldcard 1.5.0Q release commit
  6. 2021 libNgU 建种迁移提交
  7. 正式 v4.0.0 源码快照
  8. libNgU random.c
  9. MicroPython fallback 引入提交
  10. Block Engineering 技术分析
  11. Coldcard 骰子说明
  12. Coldcard Passphrase 说明
  13. OneKey trezorcrypto.random source 分支
  14. OneKey 建种逻辑
  15. OneKey 标准 Makefile
  16. OneKey SCons

相关文章

0 条评论