3z/coldcard-mk3-rng-disclosure

GitHub: 3z/coldcard-mk3-rng-disclosure

通过二进制验证的逆向工程,分析并复现了 COLDCARD Mk3 硬件钱包因链接器符号解析错误导致的弱 RNG 漏洞。

Stars: 0 | Forks: 0

# COLDCARD Mk3 RNG 漏洞 — 分析与概念验证 | | | |---|---| | **受影响版本** | COLDCARD Mk3,固件 4.0.1 – 4.1.9(包含) | | **已修复版本** | 4.2.0,提交 `4543629941a83a3e2788ac06a12b208338cb8314` (`v4-legacy`) | | **类别** | 弱 RNG / 钱包种子生成中的熵不足 | | **分析构建** | 4.1.9(易受攻击,可重现构建)和 4.2.0(已修复对照组) | ## 摘要 在 Mk3 上,生成新钱包种子的代码调用了一个“随机字节”函数,但由于链接器级别的错误,该函数解析为了 MicroPython 的**确定性软件 PRNG** (Yasmarang),而不是 STM32 的硬件 RNG。 该 PRNG 仅通过设备唯一 ID 字、SysTick 相位和原始 RTC 寄存器播种一次。因此,在受影响固件上生成的每一个 Mk3 钱包种子,都是基于少量、部分已知状态的确定性函数输出,而非 256 位的硬件熵。使用 SHA-256 对输出进行哈希处理并不会增加熵,并且固件的完整性检查(重复字检查、字节多样性检查)也无法区分 PRNG 和真正的 RNG。 ## 1. 种子生成原本应如何工作 当您在 COLDCARD 上选择 **New Wallet** 时,固件会运行 `shared/seed.py`: ``` seed = random.bytes(32) # 32 bytes of "randomness" assert len(set(seed)) > 4 # sanity check: >4 distinct byte values seed = ngu.hash.sha256s(seed) # compress to the final 32-byte seed ``` 该种子会变成一个 24 个单词的 BIP39 助记词,钱包的所有密钥都由此派生。 `random.bytes` 是 `ngu.random.bytes` (libngu) 的别名,它通过重复调用全局 C 函数 **`rng_get()`** 来填充其缓冲区。 一切的核心都在于一个问题:*最终出现在二进制文件中的是哪个 `rng_get()`?* ## 2. 两个 RNG,一个符号 — 链接器决定一切 Mk3 硬件 (STM32L475) 具有真正的硬件 RNG。`stm32/COLDCARD/rng.c` 中的板级代码会读取它 —— 但它是作为一个 **`static` 函数** `rng_get_or_fault()` 存在的。Static 意味着:对链接器而言,在该文件之外它是不可见的。 与此同时,Mk3 的构建过程故意设置了 `MICROPY_HW_ENABLE_RNG = 0`。该标志告诉 MicroPython“此板卡没有硬件 RNG”,因此 MicroPython 自身的 `ports/stm32/rng.c` 很“贴心”地编译了一个**全局 `rng_get()` 回退机制** —— 一个软件 PRNG,以便 MicroPython 的 `random` 模块在没有 RNG 的板卡上依然能正常工作。 在链接阶段,结果如下: ``` libngu: calls global rng_get() ──────────────┐ ▼ board rng.c: static rng_get_or_fault() (not exported — can't be linked) micropython rng.c: global rng_get() ◄─── linker picks THIS one (deterministic Yasmarang PRNG) ``` 没有人故意调用错误的函数。板卡拥有正确的代码;是构建系统在暗中将调用者连接到了错误的实现。 这就是该漏洞能在代码审查中幸存下来的原因:**孤立地看每个文件都是正确的。** 故障只存在于链接边界处。 ## 3. 回退 PRNG 及其“熵” MicroPython 的回退机制是一个 Yasmarang PRNG,仅在首次使用时播种一次: ``` pad = *(uint32_t *)MP_HAL_UNIQUE_ID_ADDRESS ^ SysTick->VAL; // UID word ^ 1ms-phase n = RTC->TR; // raw RTC time register d = RTC->SSR; // raw RTC subsecond register ``` 拆解种子材料: - **UID 字** (`0x1fff7590`,96 位 STM32 唯一 ID 的前 32 位):这是一个设备标识符,而非秘密熵。COLDCARD 的 USB 序列号是*派生自* UID 字节的,因此它会泄露关于它们的部分信息(见第 5 节)。 - **SysTick->VAL**:一毫秒内的相位(在 Mk3 的 80 MHz / 1 kHz 配置下,计数器范围为 0–79,999)。属于时序抖动,而不是 24 位的秘密。 - **RTC->TR / RTC->SSR**:在正常的 Mk3 构建中,RTC 从未被初始化(`MICROPY_HW_ENABLE_RTC = 0`),因此这些是原始的保留/复位寄存器值 —— 通常为零,或者是之前启动时的陈旧状态。 在此基础上,libngu 将 PRNG 的输出与**第二个 Yasmarang 流(其初始状态被编译到了固件中)** 进行了 XOR 运算 —— 即一个对任何拥有该二进制文件的人来说都是已知的常量。这是确定性白化处理,零熵。 并且状态并不是静止不动的:启动时的 NVRAM 洗牌(在擦除的设备上约有 3,103 次 `rng_get()` 调用)以及薄膜键盘的行洗牌(每次按键转换 3 次调用)会在钱包创建之前推进 PRNG。因此,种子取决于初始状态**加上确切的先前调用计数**。 ## 4. 为什么安全检查没有被触发 在这个漏洞和用户之间存在两道检查,而且两者都是统计学上的,而非密码学上的: 1. `my_random_bytes()` 仅拒绝**两个连续相等的 32 位字**。 2. `make_new_wallet()` 仅拒绝具有**≤ 4 个不同字节值**的种子。 确定性的 PRNG 会产生看似完美随机的输出。两项检查每次都能通过。PoC 具体证明了这一点:重新创建的“种子”通过了固件的两项检查,并生成了一个完全有效的 24 个单词的助记词。 **教训:** 您无法通过查看输出来验证熵。熵是*来源*的属性,只有在来源处进行健康测试(或者通过构建/链接检查证明接入了哪个来源)才能验证它。 ## 5. 攻击者能做什么和不能做什么 USB 序列号将 UID 字节编码为 `(b11, b10+b2, b9, b8+b0, b7, b6)` 的 `serial = %02X%02X%02X%02X%02X%02X`(`shared/version.py:80`)。 仅凭一个序列号,攻击者就能直接获知 4 个 UID 字节以及两个字节的*和* —— 但 RNG 的种子字是 `b0..b3`(小端序),其中 `b1`、`b3` 根本不会出现在序列号中,而 `b0`、`b2` 每一个都只受到与一个未知的批次字符字节的求和限制。对于每个候选 UID 字,状态还取决于 SysTick(约 80k)、SSR (256)、TR(约 400 个 BCD 值)以及钱包生成前的 RNG 调用计数(约 4k)。总计:**每个序列号对应约 1.9×10²² 个候选状态** —— 如果没有硬件先验条件,根本无法进行枚举。 因此:受影响种子的熵受到回退机制种子状态的上限约束,而不是受限于 256 位,并且对特定设备的物理/制造层面的了解将进一步缩小此空间。本分析有意**不**发布位数说明(与供应商公告保持一致),且 PoC 有意**不包含任何枚举、钱包恢复或资金定位功能**。 ## 6. 修复方案 (4.2.0) 提交 `45436299` 直击链接边界,这正是该漏洞存在的位置: 1. 将板卡的 `rng_get()` 作为 `rng_get_or_fault()` 的全局包装器导出 —— libngu 现在链接到了**硬件** RNG。 2. 在 Mk3 构建中,使用空对象替换了 MicroPython 的回退 `rng.o`。 3. 添加了**构建时检查**:链接中不得包含任何上游 RNG 符号,且板卡对象必须定义全局 `rng_get` —— 这样一来,出现回归问题会导致构建失败,而不是被默默打包发布。 在修复后的二进制文件中,`rng_get()` 实际上只有一条指令: ``` 0803fb64 : b.w 0803faf8 ; tail-call the hardware RNG ``` 硬件 RNG 超时仍然保持故障关闭状态 (`mp_raise_OSError(MP_EFAULT)`)。 **教训:** 当两种实现可以满足同一个符号时,请让错误的解析变得不可能 —— 删除回退机制并对符号表进行断言。 ## 7. 这是如何被验证的 - 官方的 **4.1.9** 镜像是从标签 `2023-06-26T1241-v4.1.9` **可重现构建**的;Coinkite 的 ECDSA 固件签名已验证,并且除了已签名的头部之外,本地构建与已发布的载荷逐字节匹配。**4.2.0** (`v4-legacy` @ `43770339`) 也是如此。 - 两个 ELF 文件的符号表显示了完全相反的情况: | | 4.1.9(易受攻击) | 4.2.0(已修复) | |---|---|---| | `my_random_bytes` 调用 → | `rng_get` @ `0x0803df94` — **Yasmarang 回退机制** | `rng_get` @ `0x0803fb64` — **分支至硬件 RNG** | | 真正的硬件 `rng_get_or_fault` | 存在于 @ `0x0803fb84`,**未被 libngu 使用** | 在 @ `0x0803faf8` 处使用 | - 4.1.9 回退机制的反汇编结果显示了对 SysTick (`0xE000E018`)、UID 区域和 RTC 寄存器 (`0x40002800`) 的读取,随后是 Yasmarang 状态转换 —— 与 PoC 中的 Python 模型在位级别上完全匹配。 完整的详细信息、固件哈希值、时序分析和复现命令位于 **[report.md](report.md)** 中。 ## 8. 运行 PoC `poc_mk3_rng.py` 使用固定的**合成**寄存器值对易受攻击的路径进行建模:包括两台 Yasmarang 机器、XOR 混合、两项脆弱的固件检查以及最后的 SHA-256 → BIP39 步骤。 ``` python3 poc_mk3_rng.py ``` 它会输出一个有效的 24 字单词的助记词,从相同的合成状态重新派生它,并断言其一致性 —— 证明“随机”种子是种子状态的纯函数,并且两项固件完整性检查在 PRNG 输出上均能通过。 ## 9. 端到端流水线 `pipeline_mk3_rng.py` 通过完整的设备密钥链对模型进行了扩展: ``` UID word + SysTick + RTC regs → Yasmarang fallback ⊕ compiled-in whitener → 32 raw bytes → SHA-256 → BIP39 phrase → BIP39 seed (PBKDF2-HMAC-SHA512, 2048 rounds) → BIP32 → m/84'/0'/0' zpub, m/44'/0'/0' xpub, master fingerprint ``` ``` pip install -r requirements.txt python3 pipeline_mk3_rng.py # synthetic defaults (same phrase as PoC) python3 pipeline_mk3_rng.py --selfcheck # cross-validate vs hand-rolled BIP32 python3 pipeline_mk3_rng.py --space <12-hex-serial> # closed-form candidate count ``` 它还可以作为您合法持有的账户 xpub 的普通**仅监控** (watch-only) 地址派生器: ``` python3 pipeline_mk3_rng.py --xpub --addr 10 [--check] ``` `--selfcheck` 使用独立的手写 BIP32 实现(HMAC-SHA512 CKD + secp256k1 标量数学运算)重新派生所有内容,并断言其结果与 `bip_utils` 在字节上完全一致,此外还包括 BIP39 往返转换、序列号公式和地址路径的一致性。 ## 10. 仓库结构 | 文件 | 用途 | |---|---| | `poc_mk3_rng.py` | 易受攻击 RNG 路径的有界 PoC(除了内置的 `bip39.py` 外没有外部依赖) | | `pipeline_mk3_rng.py` | 完整链条:种子状态 → 助记词 → 账户 xpubs / 仅监控地址 | | `bip39.py` | 直接从 libngu(固件使用的库)原样引入的 BIP39 实现 | | `report.md` | 完整的技术报告:二进制确认、时序、种子状态数学原理 | 故意排除的内容:固件镜像、反汇编转储以及任何枚举/恢复工具。 ## 参考资料 - Coinkite 公告: - 修复提交:`4543629941a83a3e2788ac06a12b208338cb8314` (`v4-legacy` 分支, coldcard-firmware) - 易受攻击的标签:`2023-06-26T1241-v4.1.9`
标签:云资产清单, 区块链安全, 密码学, 手动系统调用, 漏洞分析, 漏洞复现, 硬件钱包, 路径探测, 逆向工具, 逆向工程