导语:这不是一场精准打击,而是一次”彩票开奖”。攻击者根本不需要知道具体是哪个钱包——他们只是遍历所有可能的种子,生成地址,然后检查区块链上有没有钱。匹配上了,钱就转走。25分钟,594枚比特币,约3800万美元。漏洞本身很简单:一个宏比较用了
#ifndef而非#if,硬件TRNG五年没人调用过。
一、事件概述:一场自动化的”扫奖”行动
2026年7月30日,一个攻击者在链上展开了一场精准而高效的”扫奖”行动。整个过程持续约25分钟(后期The Hacker News引用Galaxy Research数据修正为41分钟),从约500个钱包中扫走约594枚比特币。后期Block和Galaxy Research的联合分析将总量修正为1,082.65枚BTC(约7,000万美元),涉及1,196个地址。
关键行为特征(来自Galaxy Research链上分析):
| 特征 | 值 |
|---|---|
| 手续费率 | 约30 sat/vB(极低,用于隐藏混入常规交易) |
| 交易结构 | 单输出、无找零、P2WPKH输入 |
| 攻击指纹 | 与正常钱包转账模式高度相似,难以被传统监控系统识别 |
| 被盗地址类型 | 1,183个Native SegWit + 7个SegWit + 6个Legacy |
| 收款地址 | bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0 |
这个交易结构特征至关重要——攻击者刻意压低手续费(约30 sat/vB)并保持单输出结构,使这笔大规模资金转移在链上看起来像普通的钱包整理交易,混淆视觉警报。
IntCyberDigest为此制作了一个可视化动画,展示了攻击者如何在区块窗口内对多个目标地址执行连续扫荡。
攻击者身份:截至本文发布,攻击者身份未被确认。Galaxy Research指出,这一交易模式可以被识别,但无法直接确认归属——因为从链上行为看,一次”扫奖”和钱包所有者自主整理资金的模式完全一致。
二、漏洞根因:一条配置宏引发的密码学雪崩
2.1 问题出在哪一层
Coldcard固件基于MicroPython,密码学操作委托给libngu库。正常情况下,种子生成应该走这条路径:
用户触发"新建钱包"
→ generate_seed()
→ ngu.random.bytes(32)
→ my_random_bytes()
→ CHIP_TRNG_32() ← 应当调用硬件TRNG
→ STM32 RNG外设 ← 真随机数
但实际走的是:
用户触发"新建钱包"
→ generate_seed()
→ ngu.random.bytes(32)
→ my_random_bytes()
→ CHIP_TRNG_32() ← 实际调用了MicroPython的rng_get()
→ pyb_rng_yasmarang() ← 软件PRNG
→ 固定芯片ID + SysTick定时器 ← 可预测
2.2 第一层错误:Coinkite的宏定义
Coinkite为了禁用MicroPython内置的硬件RNG,在board配置中写入:
// stm32/COLDCARD/mpconfigboard.h
// We have our own version of this code.
#define MICROPY_HW_ENABLE_RNG (0)
这个操作本身是正确的——Coinkite确实有自己的硬件RNG封装,不希望MicroPython的多余代码干扰。
2.3 第二层错误:libngu的编译检查形同虚设
问题出在libngu的编译时守卫:
// libngu/ngu/random.c
#ifndef MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz"
#endif
#ifndef只检查宏是否被定义过,不检查其值是否非零。由于宏被定义为0(数值上为假),#ifndef MICROPY_HW_ENABLE_RNG的结果是假,检查通过,编译继续。正确写法应该是#if !MICROPY_HW_ENABLE_RNG或#ifndef MICROPY_HW_ENABLE_RNG == 0。
就这么一个字符的差异,五年里每一版固件编译都安静地通过了。
2.4 第三层错误:MicroPython的替换路径
MicroPython看到MICROPY_HW_ENABLE_RNG为0,编译的是软件回退路径:
#if MICROPY_HW_ENABLE_RNG
uint32_t rng_get(void) { return RNG->DR; } // 真随机数,读取STM32外设
#else
// For MCUs that don't 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
uint32_t rng_get(void) { return pyb_rng_yasmarang(); } // 软件PRNG
#endif
注释说得很清楚:这个回退是给”没有TRNG的芯片”准备的。Coldcard有TRNG,而且工作正常——只是从来没人调用它。
2.5 Yasmarang的种子:固定ID加一个定时器
Yasmarang是一个非密码学安全的线性同余软件PRNG。它在初始化时将三个输入混合为一个32位的pad值:
pad = UID_low32 ^ SysTick->VAL; // 芯片唯一ID低32位 XOR 系统定时器当前值
n = RTC->TR; // 实时时钟时间寄存器
d = RTC->SSR; // 实时时钟亚秒寄存器
这三个输入的问题:
| 输入 | 固定/可测? | 安全影响 |
|---|---|---|
| UID_low32 | 固定,可通过USB序列号或芯片手册查询 | 不是秘密,可枚举 |
| SysTick | 冷启动时0~80,000(Mk2/Mk3),可统计分析约束 | 主要不确定性来源 |
| RTC寄存器 | 冷启动时可能静态为0(Mk3 RTC振荡器被禁用) | 实际贡献接近零 |
对于Mk3,SysTick在80,000个可能值之间(约2^16.3),加上可识别的芯片UID,实际搜索空间约2^32个可能的pad值。更进一步,如果攻击者使用自有Mk3设备建立时序模型,可以将SysTick约束到更小的范围。
2.6 双重PRNG的XOR:确定性加确定性还是确定性
libngu在调用Yasmarang输出后,再与另一个也用公开常量初始化的Yasmarang实例做XOR:
static uint32_t yasmarang_pad = 0x0a8ce26f; // 公开常量,每个设备相同
static uint32_t yasmarang_n = 69;
static uint32_t yasmarang_d = 233;
chip = rng_get(); // MicroPython Yasmarang(确定性,取决于设备状态)
chip ^= my_yasmarang(); // libngu Yasmarang(确定性,公开常量)
两个输出都是确定性的,XOR结果同样是确定性的。健康检查(拒绝相邻重复输出)被任何非退化的PRNG轻易通过。
2.7 钱包哈希:不能从无到有创造熵
生成钱包时执行:
seed = ngu.random.bytes(32) # 32字节,但熵只有约40比特
assert len(set(seed)) > 4 # 仅检测明显硬件故障
return ngu.hash.sha256d(seed) # SHA256d
如果输入熵只有2^40种可能,SHA256d的输出也只有2^40种可能。密码学哈希不能从无到有创造熵。BIP-39校验和同样如此。
三、Mk3的熵空间:从128比特到40比特
3.1 理论搜索空间
| 条件 | 搜索空间 | 备注 |
|---|---|---|
| 最佳条件(UID和RTC已知,SysTick未知) | ~2^16.3(80,000种SysTick) | 极低成本 |
| 合理约束(UID已知,SysTick范围约2^20) | ~2^20 | 中等成本 |
| 上界(UID未知,RTC未知) | ~2^40 | beacon302的估算上限 |
| BIP-39安全标准 | 2^128 | 差了约88比特 |
对于有准备的攻击者,使用自有同型号设备建立时序模型后,实际搜索空间可以压缩到2^16~2^20范围。
3.2 回归时间线
| 日期 | 事件 |
|---|---|
| 2021年1月28日 | 存在漏洞的libngu STM32守卫被引入 |
| 2021年3月1日 | commit b18723dd:种子生成从ckcc.rng_bytes(硬件)切换到ngu.random.bytes(新路径) |
| 2021年3月17日 | 固件v4.0.0发布,包含漏洞路径 |
| 2022年3月11日 | Mk4引入安全元素32位重新注入(reseed),但只注入4字节 |
| 2026年7月30日 | 攻击发生,1,082.65枚BTC在41分钟内被盗 |
| 2026年7月31日 | Coinkite发布v4.2.0(Mk3)、v5.6.0(Mk4/Mk5)、v1.5.0Q(Q)热修复 |
四、黑客工具分析:”种子彩票”是怎么玩的
4.1 工具的核心逻辑
beacon302将这种攻击方式称为”seed lottery”(种子彩票)——不是因为它有什么彩票的随机性,而是因为攻击者对整个种子空间进行枚举,对每个生成的地址检查区块链余额,中了就有钱赚。
工具的四个模块:
┌─────────────────────────────────────────────────────┐
│ 模块1:空间枚举引擎 │
│ 遍历 pad = UID_low32 ^ SysTick候选值 │
│ 约束条件:SysTick分布模型(来自自有设备测量) │
│ 输出:候选Yasmarang初始状态 │
├─────────────────────────────────────────────────────┤
│ 模块2:种子生成器 │
│ 用Yasmarang生成32字节 │
│ 调用sha256d()得到BIP-39种子 │
│ 对每个种子尝试BIP44/49/84路径 │
├─────────────────────────────────────────────────────┤
│ 模块3:地址匹配Oracle │
│ 从目标地址列表生成待查地址集合 │
│ 派生地址与集合比对(布隆过滤器加速) │
│ 命中则触发下一步 │
├─────────────────────────────────────────────────────┤
│ 模块4:资金转移(自动化) │
│ 从命中种子派生私钥 │
│ 构建交易(单输出,无找零,约30 sat/vB) │
│ 广播至网络 │
└─────────────────────────────────────────────────────┘
4.2 目标地址获取
攻击者不需要知道具体哪个钱包地址——他们从链上获取所有由有缺陷Coldcard钱包资助的地址列表:
- 来源:bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0的501笔入金交易
- 识别特征:491个P2WPKH + 5个P2PKH + 5个P2SH-P2WPKH(与攻击者收款地址类型分布一致)
- 间接推断:攻击者通过分析资金流向,锁定 Consolidation Address(聚合地址),再反向追踪其所有funding inputs
这501个地址对应约500个独立钱包,与报告的”约500个钱包被清空”完全吻合。
4.3 性能数据
beacon302的实测数据:
| 指标 | 数值 |
|---|---|
| 单卡算力 | 0.14 Mcand/s(RTX PRO 6000) |
| 全空间枚举(2^32 × 8 skip变体) | ~34小时/卡 |
| 20卡集群 | ~1.7小时 |
| 50卡集群 | ~40分钟 |
这个成本对于数千万美元的收益来说,相当于零。攻击者在漏洞公开后数小时内就完成了枚举——很可能在Coinkite发布修复之前就已经完成。
4.4 为什么选30 sat/vB的手续费
这个手续费率非常低(远低于当时的网络中位数),但有三个巧妙之处:
- 降低链上可见性:低费率使交易看起来像常规的钱包整理
- 不急需确认:攻击者不需要这笔交易快速确认,只需要最终确认
- 成本最小化:扫荡数百个地址时,手续费累积显著,低费率节省大量成本
五、受影响范围与固件版本对照
| 设备 | 受影响固件 | 有效熵 | 风险等级 |
|---|---|---|---|
| Mk1 | 所有版本 | 128比特 | 安全(无法安装受影响固件) |
| Mk2/Mk3 | 3.2.2及更早 | 128比特 | 安全(直接使用硬件TRNG) |
| Mk2/Mk3 | v4.0.1 ~ v4.1.9 | ~40比特 | 极高 |
| Mk3 | v5.0.1-mk3 ~ v5.0.3-mk3 | ~40比特 | 极高 |
| Mk4/Q/Mk5 | v5.0.0 ~ v5.5.x | ~72比特 | 高 |
| 所有型号 | v4.2.0 / v5.6.0 / v1.5.0Q+ | 128比特 | 已修复 |
判断规则:风险取决于钱包创建时的固件版本,而非当前版本。升级固件不修复已生成的种子。
dice rolls例外:如果在创建钱包时使用了50次以上公平、独立、私密的骰子掷骰,则骰子输入通过SHA-256直接作为种子熵,不经过有缺陷的RNG路径,种子安全。
六、Mk4/Q/Mk5的”部分修复”为何仍不安全
2022年3月,Mk4引入了安全元素重新注入:
a = callgate.read_rng(1) # SE1读取32字节(认证随机数)
b = callgate.read_rng(2) # SE2读取8字节(ROM选项页)
n = ngu.hash.sha256d(a + b)
n, = ustruct.unpack('I', n[0:4]) # 只取前4字节 = 32比特
ngu.random.reseed(n) # 仅替换Yasmarang的pad状态字
问题:
- 只注入32比特:reseed()只接受一个整数,覆盖Yasmarang的pad一个状态字
- 不重置其他状态:n、d、dat三个状态字保持原值
- 不重置MicroPython回退:MicroPython自己的Yasmarang状态不受影响
- 不建立新DRBG:没有初始化密码学安全的随机生成器
Coinkite估算Mk4熵约72比特,但Block工程团队的结论更严峻:如果攻击者知道设备UID和启动时序,搜索空间只剩2^32。2^32在现代GPU上约小时级可枚举。
七、技术复现思路(仅供学习研究)
免责声明:以下内容仅用于安全研究和学习。攻击有缺陷的固件钱包仍可能触犯法律。Coinkite已于2026年7月31日发布修复固件,漏洞已被修复。
7.1 复现环境准备
硬件:Coldcard Mk3(受影响固件v4.0.1-v4.1.9)
软件:libngu(commit 537519a)+ MicroPython(Coldcard fork)
工具:Python 3.x + bip39 + coincurve
7.2 核心复现步骤
第一步:提取目标地址列表
# 从链上获取已知有缺陷钱包的地址
# 可通过blockchain.com API获取特定地址的输入脚本
target_addresses = set()
# 攻击者收款地址的funding inputs即为受害者地址
第二步:建立时序模型
# 用自有Mk3设备多次测量冷启动到第一次RNG调用的SysTick分布
import statistics
sysTick_samples = []
for _ in range(1000):
boot_and_measure() # 测量SysTick->VAL
sysTick_samples.append(value)
mean, std = statistics.mean(sysTick_samples), statistics.stdev(sysTick_samples)
# 假设SysTick服从正态分布,约95%值在mean±2*std范围内
第三步:枚举Yasmarang初始状态
from yasmarang import Yasmarang
uid_low32 = 0xXXXXXXXX # 从自有设备测量或从芯片手册查询
for sysTick_candidate in range(max(0, int(mean - 2*std)),
int(mean + 2*std)):
pad = uid_low32 ^ sysTick_candidate
rng = Yasmarang(pad=pad, n=0, d=0) # 初始化Yasmarang
# 生成32字节种子
seed_bytes = rng.random_bytes(32)
seed = sha256d(seed_bytes)
# 派生地址
for path in [BIP44, BIP49, BIP84]:
addr = derive_address(seed, path)
if addr in target_addresses:
print(f"命中!种子: {seed_bytes.hex()}")
print(f"路径: {path}, 地址: {addr}")
break
第四步:验证并转移
# 验证私钥
private_key = derive_private_key(seed_bytes)
public_key = private_key.public_key
assert public_key.address() == target_addr
# 构建交易(需要签名)
tx = build_transaction(inputs=[], outputs=[(attacker_addr, amount)])
signed_tx = tx.sign([private_key])
broadcast(signed_tx)
7.3 关键难点
- UID获取:需要知道目标设备的UID_low32,可通过攻击者自有同型号设备建立分布模型
- SysTick约束:需要大量测量建立分布模型,缩小搜索空间
- skip变体:固件在uniform()重试时会额外调用mixer推进状态,需要处理多种skip场景
八、防御与修复建议
8.1 立即行动(受影响用户)
- 不要慌张操作:Coinkite警告,操作失误的损失比漏洞本身更大
- 安装修复固件:Mk3升级至v4.2.0,Mk4/Mk5升级至v5.6.0,Q升级至v1.5.0Q
- 生成新种子:使用修复后的固件在设备上生成全新种子
- 验证新地址:在新设备屏幕上验证接收地址
- 执行测试转账:先转小额确认新钱包可用
- 转移全部余额:确认后转移所有资金
8.2 临时缓解措施
BIP-39 passphrase:如果当前种子使用了强且唯一的passphrase,攻击者必须同时破解passphrase才能动用资金。PBKDF2-HMAC-SHA512的2,048次迭代对单次验证成本很低,但对批量攻击仍构成一定阻碍。
多签配置:如果多签仲裁中包含足够数量的未受影响设备,则攻击者需要同时破解所有相关密钥才能转移资金。
九、为什么这个漏洞存在了五年没人发现
这不是一个隐藏极深的逻辑陷阱,而是一个简单的宏比较错误:
- Coinkite以为
#define MICROPY_HW_ENABLE_RNG (0)会禁用MicroPython的硬件RNG路径 - libngu的作者用
#ifndef(存在性检查)而非#if(真值检查)来保护构建 - 固件完全开源,五年间经手多人审查
- BIP-39合规性测试不验证种子熵的密码学强度
五年间每一次代码审查、每一次固件发布,这个错误都安静地通过了。硬件钱包最核心的安全功能——私钥生成——依赖于一条从未有人端到端验证过的代码路径。
十、总结
Coldcard Mk3的RNG漏洞是一个教科书级别的固件集成错误:
| 维度 | 实际情况 |
|---|---|
| 漏洞根因 | #ifndef vs #if——一个字符的差异 |
| 硬件TRNG | 完好无损,只是从未被调用 |
| 熵降级 | 从128比特降至Mk3约40比特、Mk4约72比特 |
| 攻击成本 | 2^32~2^40空间枚举,GPU小时级 |
| 损失规模 | 1,082枚BTC(7,000万美元),41分钟 |
| 漏洞存续 | 五年(2021年3月至2026年7月) |
这次攻击最值得深思的地方在于:攻击者不需要任何零日漏洞,不需要物理接触设备,不需要诱骗用户泄露种子。他们只是在链上”买彩票”——枚举所有可能的种子,生成所有可能的地址,然后等待区块链告诉他们哪些”中了奖”。硬件钱包的物理隔离在这种方式面前毫无作用,因为私钥从来就不在设备里——它在枚举完成的那一刻就已经被知道了。
版权声明:本文由华盟网原创发布,保留所有权利。配图由华盟网授权使用。














暂无评论内容