黑客如何用”种子彩票”工具扫荡3800万美元——Coldcard RNG漏洞深度分析

导语:这不是一场精准打击,而是一次”彩票开奖”。攻击者根本不需要知道具体是哪个钱包——他们只是遍历所有可能的种子,生成地址,然后检查区块链上有没有钱。匹配上了,钱就转走。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_RNG0,编译的是软件回退路径:

#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^40beacon302的估算上限
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的手续费

这个手续费率非常低(远低于当时的网络中位数),但有三个巧妙之处:

  1. 降低链上可见性:低费率使交易看起来像常规的钱包整理
  2. 不急需确认:攻击者不需要这笔交易快速确认,只需要最终确认
  3. 成本最小化:扫荡数百个地址时,手续费累积显著,低费率节省大量成本

五、受影响范围与固件版本对照

设备受影响固件有效熵风险等级
Mk1所有版本128比特安全(无法安装受影响固件)
Mk2/Mk33.2.2及更早128比特安全(直接使用硬件TRNG)
Mk2/Mk3v4.0.1 ~ v4.1.9~40比特极高
Mk3v5.0.1-mk3 ~ v5.0.3-mk3~40比特极高
Mk4/Q/Mk5v5.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状态字

问题:

  1. 只注入32比特:reseed()只接受一个整数,覆盖Yasmarang的pad一个状态字
  2. 不重置其他状态:n、d、dat三个状态字保持原值
  3. 不重置MicroPython回退:MicroPython自己的Yasmarang状态不受影响
  4. 不建立新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 关键难点

  1. UID获取:需要知道目标设备的UID_low32,可通过攻击者自有同型号设备建立分布模型
  2. SysTick约束:需要大量测量建立分布模型,缩小搜索空间
  3. skip变体:固件在uniform()重试时会额外调用mixer推进状态,需要处理多种skip场景

八、防御与修复建议

8.1 立即行动(受影响用户)

  1. 不要慌张操作:Coinkite警告,操作失误的损失比漏洞本身更大
  2. 安装修复固件:Mk3升级至v4.2.0,Mk4/Mk5升级至v5.6.0,Q升级至v1.5.0Q
  3. 生成新种子:使用修复后的固件在设备上生成全新种子
  4. 验证新地址:在新设备屏幕上验证接收地址
  5. 执行测试转账:先转小额确认新钱包可用
  6. 转移全部余额:确认后转移所有资金

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月)

这次攻击最值得深思的地方在于:攻击者不需要任何零日漏洞,不需要物理接触设备,不需要诱骗用户泄露种子。他们只是在链上”买彩票”——枚举所有可能的种子,生成所有可能的地址,然后等待区块链告诉他们哪些”中了奖”。硬件钱包的物理隔离在这种方式面前毫无作用,因为私钥从来就不在设备里——它在枚举完成的那一刻就已经被知道了。

版权声明:本文由华盟网原创发布,保留所有权利。配图由华盟网授权使用。

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容