简要摘要
自2026年7月30日起,多批资金从COLDCARD比特币硬件钱包中被清空,此后数日内持续有新的清空事件发生。此次事件既无单笔链上攻击交易,也不涉及可利用的智能合约;损失源于一个离线种子恢复问题,随后引发链上清空操作。截至2026年8月7日,链上追踪已核实约 1,405 BTC(按8月7日价格 $64,700 计算约合 $91M)从约 4,925 个地址中被提取[1],波次级别归因追踪到十个波次共约 1,433 BTC[2],而通过私信渠道与受害者核对后,数字高达 2,055 BTC(约合 $133M)[3]。
根本原因是钱包熵值缺陷:2021年的一次固件迁移将种子生成路由到 ngu.random.bytes(),该函数回退到确定性软件生成器,而非预期的STM32硬件随机数生成器(RNG)。对于受影响的设备,这使得种子恢复从密码学上不可行的问题变为一个可离线搜索的问题,使攻击者得以枚举候选种子并将其与公开钱包数据进行匹配,从而恢复私钥。在我们早期对该事件的每周分析基础上,本次深入分析量化了每个受影响设备代次的剩余熵值,追踪了受害者和被盗资金在链上的识别过程,并审查了一个在热修复固件后出现的独立回归问题——该问题可能在登录前导致服务拒绝。
背景
COLDCARD是一款比特币硬件钱包。与其他自托管钱包一样,其安全性最终取决于钱包设置期间生成种子短语的不可预测性。BIP-39种子短语是人类可读的,但其底层安全属性仍取决于用于创建它的随机字节的熵值。如果种子生成的输出是可预测的,则无论种子此后保存得多么谨慎,钱包的安全性都将崩溃。
COLDCARD涵盖Mk1至Mk5硬件版本以及Q型号。Mk2和Mk3使用传统固件线,Mk4、Mk5和Q使用独立的Standard和Edge发布轨道。
大多数COLDCARD应用逻辑是运行在MicroPython上的Python代码,由原生C模块提供硬件和密码学操作。一个安全的种子生成路径应从STM32硬件RNG读取32字节,若外设停滞或重复某个值则报错,对结果进行哈希处理,然后将其编码为BIP-39词汇。在v3.2.2版本中,make_new_wallet()遵循此路径:
make_new_wallet()
|- ckcc.rng_bytes(seed)
| `- random_buffer()
| |- read STM32 RNG->DR
| `- fail on timeout or repeated output
|- SHA-256(seed)
`- BIP-39 seed words
漏洞分析
根本原因是围绕 MICROPY_HW_ENABLE_RNG 的构建和集成错误。COLDCARD的生产板配置将此宏设置为 0,因为固件使用其自身的板级本地硬件RNG封装,而非MicroPython的硬件RNG实现。然而,钱包生成路径已被迁移至 ngu.random.bytes(32),而libngu的STM32路径最终依赖于全局 rng_get() 符号。
受影响的路径进入libngu的 my_random_bytes()。对于每个输出字,它将 CHIP_TRNG_32() 返回的值与自身的Yasmarang输出进行XOR运算:
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: public initial state
| `- Mk4/Q/Mk5: 32-bit pad reseed
|
`- output = Yasmarang A XOR Yasmarang B
Yasmarang A是MicroPython在 rng_get() 背后的回退,由设备和定时器状态初始化。Yasmarang B属于libngu:在Mk2/Mk3上使用公开初始值,而Mk4/Q/Mk5仅用安全元件派生数据替换其32位 pad。板级本地STM32 RNG仍然存在,但 ngu.random.bytes() 并不调用它。
这一不匹配在构建配置中直接可见。Mk4的 mpconfigboard.h 禁用了MicroPython的硬件RNG分支:
// We have our own version of this code.
#define MICROPY_HW_ENABLE_RNG (0)
Libngu的 CHIP_TRNG_32() 仍将该宏视为存在硬件RNG的充分证明,因为它只检查宏是否存在,然后调用 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
该保护遗漏了危险情况:定义为 0 的宏仍然是已定义的。因此构建成功,rng_get() 解析到MicroPython的STM32 RNG模块,而非COLDCARD独立的板级本地封装(random32() / random_buffer(),以 ckcc.rng_bytes 形式暴露给Python)。在MicroPython的 rng_get() 选择逻辑中,MICROPY_HW_ENABLE_RNG == 0 选择了软件回退分支:
#if MICROPY_HW_ENABLE_RNG
// STM32 hardware RNG
#else
// Yasmarang software fallback
#endif
Mk2/Mk3:约40位
已编译的 pyb_rng_yasmarang() 回退函数按如下方式初始化并推进Yasmarang:
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);
}
这些变量占用104位,但状态大小并不等于熵。dat 从零开始,其他值是固定元数据或相关的定时器读数,而非独立的秘密值。在用于估算约40位数字的宽松Mk2/Mk3模型下:
| 输入 | 候选值数量 | 枚举代价 |
|---|---|---|
已知 UID_low32 |
1 |
2^0 |
SysTick->VAL |
80,000 |
2^16.29 |
RTC->TR 时间(一天中) |
86,400 |
2^16.40 |
RTC->SSR 亚秒 |
256 |
2^8 |
将所有定时器字段视为独立,可得到一个有意偏大的上限:
80,000 * 86,400 * 256
= 1,769,472,000,000
= 2^40.69 候选初始状态
因此穷举搜索最多需要 2^40.69 次试验,在均匀位置假设下平均约需 2^39.69 次试验。这是枚举上限,而非40位密码学熵。如果在正常冷启动期间RTC寄存器是静态的,则只剩 SysTick,将上限降低至约 2^16.29。如果已知UID、定时器及之前RNG调用次数,则恰好只有一个流:2^0。未知的调用历史仅增加了合理执行路径的数量,而非引入新的熵源。同样,为生成256位种子而生成八个32位字并不会成倍扩大搜索空间:每个字都由相同的初始状态决定。
在libngu层,my_random_bytes() 将上述MicroPython回退与libngu独立的Yasmarang生成器混合。源码中字面上并不包含 chip = rng_get():CHIP_TRNG_32() 展开为 rng_get()。在Mk2/Mk3上,第二个生成器从公开常量开始:
static uint32_t yasmarang_pad = 0x0a8ce26f, yasmarang_n = 69, yasmarang_d = 233;
static uint8_t yasmarang_dat = 0;
uint32_t chip = CHIP_TRNG_32();
// ... adjacent-output health check ...
chip ^= my_yasmarang();
因此,一旦知道MicroPython回退状态和调用历史,两个流都可以重现。XOR改变了输出值,但没有增加熵。即使 UID_low32 未知,UID_low32 ^ SysTick 仍会将两个输入折叠为一个32位 pad;它们名义上的位数不能相加。
Mk4/Q/Mk5:约72位
较新型号保留了相同的双生成器结构,但 rng_seeding() 在启动期间向libngu的生成器添加了安全元件材料:
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)
尽管有40字节进入哈希,但只有其前四字节到达 reseed()[4]。random_reseed() 实现随后仅替换了libngu的32位 pad 字:
STATIC mp_obj_t random_reseed(mp_obj_t arg)
{
yasmarang_pad = mp_obj_get_int_truncated(arg);
return mp_const_none;
}
其他libngu状态字保留其公开值,MicroPython回退未被重新播种。约72位的数字结合了32位libngu重播种与较新型号定时器状态的宽松上限:
MicroPython 回退:
120,000 个 SysTick 值 * 86,400 个 RTC 时间 * 256 个亚秒
= 2^41.27 个状态
Libngu 安全重播种:
2^32 个值
组合上限:
2^41.27 * 2^32 = 2^73.27 个候选值
平均枚举:
2^73.27 / 2 = 2^72.27 次试验
这就是"约72位"数字的来源。这是平均攻击工作量估计,而非安全元件提供的72位。定时器字段存在相关性,可能被重建;如果MicroPython回退状态已知,则只剩 2^32 个重播种值,平均需要 2^31 次试验。对最终32个随机字节进行哈希不能增加可能的种子数量。
受影响版本
| 设备与轨道 | 不受此回归影响的版本 | 受影响的种子生成固件 | 有效位安全性 | 首个修复版本 |
|---|---|---|---|---|
| Mk1 | 至v3.0.6 | 无 | - | N/A |
| Mk2/Mk3 | 至v3.2.2 | v4.0.0-v4.1.9(官方公告从v4.0.1起) | 受影响时约40位 | v4.2.0 |
| Mk4/Mk5 Standard | N/A | v5.6.0之前 | 修复前约72位;修复后至少128位 | v5.6.0 |
| Q Standard | N/A | v1.5.0Q之前 | 修复前约72位;修复后至少128位 | v1.5.0Q |
| Mk4/Mk5 Edge | N/A | v6.6.0X之前 | 修复前约72位;修复后至少128位 | v6.6.0X |
| Q Edge | N/A | v6.6.0QX之前 | 修复前约72位;修复后至少128位 | v6.6.0QX |
相关版本是生成种子时的固件版本,而非当前安装的固件版本。在修复版本发布后生成的新种子使用了经过修正的路径,但升级固件并不能修复已有的种子。官方对Mk2/Mk3受影响范围的描述始于 v4.0.1[5],而源码级分析还包括 v4.0.0[4]。独立的骰子熵可以提高种子的安全性,而强BIP-39密码短语在不修复种子本身的情况下增加了单独的保护屏障[6]。
Best Security Auditor for Web3
Validate design, code, and business logic before launch
受害者与资金追踪
没有链上利用交易可供追踪;恢复工作必然是离线进行的。针对受影响固件生成的种子,攻击者可以约束并枚举上述候选RNG状态,重放每个候选的种子生成流,推导出对应的钱包密钥,并将其与公开钱包数据进行匹配,然后在链上清空任何匹配的有资金的钱包。这仅能触及仅从种子可推导的钱包:强且唯一的BIP-39密码短语通过PBKDF2将独立的、用户提供的熵(RNG缺陷从未触及)混入密钥推导过程,使此类钱包置于纯种子枚举之外。被清空的钱包必然是那些没有此类保护的钱包[6]。由于盗窃仅以链上清空的形式浮出水面,而非可追踪的利用行为,识别受害者和追踪资金流向成为链上取证的工作。
多个独立力量追踪了被盗资金:公开追踪网站(Coldcard Sweep Watch[1]、coldcard.rip[2]和Coldcard Hack Tracker[7])以及Galaxy Research的私信渠道核对[3],其报告总额比较如下。由于Coldcard Sweep Watch公布了其方法论,我们以此为例说明识别过程——一个链下报告与链上分析之间的反馈循环:
- 链下锚点。 受害者和研究人员提供了公开地址或交易ID,以及可获取的设备和种子生成背景信息。每条报告均被视为线索并在链上核实;无需种子短语、私钥或xpub。
- 链上扩展。 从已确认的锚点出发,扫描器在相关区块中搜索相同的清空特征:无找零地址清空的钱包、相似的输入类型、紧密的时间节点、重复的手续费率、共同目标地址,或后续的共同花费。
- 链下交叉核实。 利用新的受害者报告、研究人员数据集和服务归因来确认或排除候选波次。已验证的集群和启发式候选保持独立。
比特币标识地址,而非人或钱包型号。一个钱包可能控制多个地址,因此地址数量并不等于受害者数量。首个被广泛报道的重大波次 960188 占 594.48 BTC;持续的扫描和报告使总数不断增加。截至2026年8月7日,Coldcard Sweep Watch报告的已核实底限为约 1,405.07 BTC(按8月7日价格 $64,700 计算约合 $91M),来自约 4,925 个地址[1];而8月3日的coldcard.rip快照将十个波次中共 5,477 个地址的毛流出归因为最高 1,433.13 BTC,扣除手续费后目标地址收到 1,432.48 BTC[2]。Galaxy Research通过与受害者通信进行的独立私信渠道核对将数字进一步提高,从约 1,596 BTC 到最高 2,055 BTC(按同一价格约合 $133M)[3]。差异反映了证据门槛、发现时间以及各追踪者所依赖的确认渠道的不同。
随后,资金被追踪经过三个观察到的层次:被清空的源地址、直接清空目标地址(holding)和后续整合目标地址(vault)。下表显示每层的不同地址数量:
| 模式 | 示例路由 | 追踪后果 |
|---|---|---|
| 多次清空至一两个持仓地址,然后归入一个保险库 | 960183: 204 -> 2 -> 1;960188: 500 -> 1 -> 1 |
目标汇聚使集群相对强健且易于追踪 |
| 清空停在持仓地址而无后续整合 | 960352: 352 -> 1 -> 0;960668: 795 -> 1 -> 0 |
持仓地址仍可追踪,但无后续共同花费来强化归因 |
| 每次清空使用全新目标地址,有时随后有独立保险库 | 960359: 13 -> 13 -> 0;960395: 1,918 -> 294 -> 293 |
共享收集器检测器失效;分组依赖时间、手续费率、交易模板和链下佐证 |
当被追踪的输出移动时,分析跟随拆分、合并和剥皮链,同时在扣除手续费后保值并将归因上限限定为被清空金额。进入交易所或其他混同服务的资金会降低置信度;该服务不会被添加到攻击者集群中。
一个需要再次修复的补丁:潜在的变砖回归问题?
与熵值缺陷独立地,解决该问题的热修复引入了一个不同的固件回归[8, 9]。在恢复硬件RNG路径的过程中,该修复遗留了一个未处理的硬件种子错误条件,这可能在登录前导致服务拒绝,并引发设备被永久变砖的说法。这一更强的说法是否成立,取决于寄存器级别的细节。
STM32硬件RNG提供一个控制寄存器(RNG_CR)、一个状态寄存器(RNG_SR)和一个32位数据寄存器(RNG_DR)。相关状态如下:
| 位 | 作用 |
|---|---|
RNGEN |
启用RNG及其模拟噪声源 |
DRDY |
指示 RNG_DR 中有数据就绪;软件仍须拒绝零值 |
SECS / SEIS |
当前种子健康测试失败 / 锁存的种子错误状态 |
CECS / CEIS |
当前RNG时钟故障 / 锁存的时钟错误状态 |
当前状态与锁存状态的区别至关重要。SECS 描述当前噪声源条件,而 SEIS 记录种子错误发生直到软件清除为止。在Mk4/Q系列STM32L4S上,种子错误会停止新的随机数生成;在Mk3 STM32L4上,数据可能仍然可用但不可信任。时钟错误是独立的,不会引发此种子错误锁定。
所需的恢复序列取决于STM32代次:
| 设备系列 | 文档记录的种子错误恢复方法 |
|---|---|
| Mk3 STM32L4(RM0351,RNG错误管理[10]) | 清除 SEIS,然后清除并设置 RNGEN |
| Mk4/Q系列 STM32L4S(RM0432,RNG错误管理[11]) | 清除 SEIS,读取并丢弃12个 RNG_DR 字,然后确认 SEIS 保持清除状态 |
7月31日的熵值热修复正确地使 rng_get() 解析到硬件TRNG,但Mk4/Q系列的 rng_get_or_fault() 未实现种子错误恢复。rng_init() 仅在 RNGEN 清零时起作用,而读取循环仅检查 DRDY。如果种子错误使 RNGEN 保持启用但抑制 DRDY,则初始化变为空操作;每次读取等待10毫秒并抛出 OSError(EFAULT),而不清除 SEIS。
这一问题可在登录前到达UI。数字键盘 mempad._start_scan() 和Q型 keyboard._start_scan() 均从按键中断中打乱扫描顺序。RNG异常可能在该硬件会话剩余时间内阻塞PIN输入和正常固件升级菜单。
代码级的失效路径是可信的,但更强的"永久变砖攻击"说法尚未得到证实。RNG控制和状态位在硬件复位时重置为零,因此单次瞬态错误不应永久损坏外设;跨完整断电重启的持久性尚未得到证明。也没有经过验证的远程或可靠控制的方式来触发种子健康测试失败。一篇X帖子[8]声称变砖已得到确认,但其指向的PR——社区提交的PR #692[9]——作者本人表示他们使用寄存器模拟分析了该故障,未在真实Mk4/Q硬件上复现,也未独立确认现场报告。因此,最有支撑的分类是潜在的登录前拒绝服务和可靠性回归,而非经证实的永久变砖攻击。
维护者自己的修复PR #693[12]检查了种子错误标志,添加了有界恢复和重试机制,拒绝可疑样本,并仅捕获预期的键盘错误;该PR于2026年8月5日合并,取代了社区PR #692[9](该PR于2026年8月4日被关闭,未合并)。
结论
此次事件是一个钱包熵值缺陷,将受影响设备和工作流程的种子恢复从密码学上不可行的问题转变为可离线搜索的问题。关键的工程失败在于:已发布的固件没有证明安全关键的种子生成API实际到达了预期的硬件RNG。构建保护必须同时检查宏的存在性和宏的值,密码学熵的回退必须以安全失败的方式关闭,CI应在最终固件镜像中验证符号来源和端到端熵流。对于受影响的用户,补救措施并非固件更新:更新不能修复在有缺陷的路径下已经生成的种子,事后添加密码短语也不能保护已存放于该种子地址中的资金。这些资金必须转移到基于修复版本生成的新种子的钱包中;只有已经处于强且唯一密码短语保护下的资金,才置于纯种子枚举之外[6]。
参考资料
- [1] Coldcard Sweep Watch — 已核实的被提取地址集合与损失追踪器
- [2] coldcard.rip — 事件账本、路由与归因
- [3] Galaxy Research — 来自受害者报告的COLDCARD损失估计
- [4] Block Engineering — COLDCARD固件中的可预测RNG回退与32位重播种
- [5] Coinkite — COLDCARD安全公告及受影响固件范围
- [6] Coinkite — 熵值问题技术深度解析
- [7] Coldcard Hack Tracker — 实时逐波清空总额
- [8] 声称热修复后RNG故障可能永久变砖设备的X帖子
- [9] Coldcard固件PR #692 — 社区TRNG故障恢复提案(已关闭,未合并)
- [10] STMicroelectronics RM0351 — STM32L4 RNG寄存器与错误管理
- [11] STMicroelectronics RM0432 — STM32L4S RNG寄存器与错误管理
- [12] Coldcard固件PR #693 — 已合并的有界重试TRNG恢复与键盘错误处理



