当 random.bytes() 运行却失效时

INSIDER 发布于 2026-08-02 阅读 36

本文由Core-Lightning开发者ddustin撰写,深入分析了近期COLDCARD固件漏洞的根因。通过检查提交历史,作者发现引入漏洞的提交信息仅为“runs”,但代码改动巨大,且禁用了硬件随机数生成器(RNG),导致系统回退到极弱的Yasmarang随机数算法。文章详细推断了开发者的错误过程:试图覆盖函数却遭遇编译冲突,错误地通过关闭硬件RNG来规避错误,最终使钱包种子生成变得可预测。作者以此案例强调,比特币开发者必须深刻理解所提交代码,并保持高质量的提交信息与代码审查。

bitcoin++ 是一个国际性的 bitcoin 开发者会议系列。“Insider Edition” 是我们的新闻编辑室,报道 bitcoin++ 世界内外正在发生的事情。

OpEd

当 random.bytes() 运行了却没有生效

一条 commit message 能告诉我们的关于最近 COLDCARD bug 的内容

本文来自知名 Core-Lightning 开发者 ddustin ,他深入研究了 Coldcard 固件的 commit 历史,以揭示发生了什么以及代码为何失败。

引言

我开始调查 Coldcard 黑客事件,并立即感到震惊。我需要解释为什么。

当我们开发者处理代码时,我们会把代码变更组织成我们称之为 “commits” 的变更集。这样做的目的是为了清晰地展示代码被修改的历史,包括为什么和怎么修改的。

这恰恰是为了此类情况而做的:当看起来 Bitcoiners 的资金正被大规模盗取时,我们可以调查并确切理解它是如何发生的。

优秀的开发者会编写清晰的 commit message,即伴随代码更改的文字说明,解释具体更改在实现什么。

要写出清晰的 commit message,通常你会希望该 commit 代表一次较小的代码更改,这样需要注释的内容就较少。

作为开发者,一个好的目标是让 commit message 与代码更改的比值保持较高。你更改的代码行数越多,就需要越多的注释来解释你为什么要改这些代码。每个 commit 包含更多的说明和更少的代码更改通常是个好主意。

这里有一个例子,随机选自我自己的一些工作。

这条 commit message 有 235 个字符,而这个 commit 更改了 15 行代码。比值为 235/15 = ~16。

在 Coldcard 中,引入低熵 bug 的 commit 在这里。

这条 commit message 只有 5 个字符,就是单词 “runs”。该 commit 更改了 1534 行代码,比值为 5/1534 = ~0.003。

这是一个糟糕透顶的 comment 与代码更改比值。

在少数情况下,低 comment 比值是可以接受的——但更改代码中最重要的部分绝不在此列!

涉及项目安全关键函数的代码,需要更高的 comments 与更改比值,以及更严格的审查。

导致 Coldcard 弱熵问题的第二个 commit 在这里。

这条 commit message 只有 1 个字符:就是字符 “x”。该 commit 更改了约 1000 行代码,比值为 1/1000 = ~0.001。

问题

标题为 “runs” 的 commit(比值:~0.003)中,看起来他们正在导入并配置 C 代码,以使自定义 micropython 代码能在 STM32 上运行,STM32 是所有 Coldcards 运行所用的板子。

STM32 是此类小型设备最常见的 CPU,而这个 commit 引入的那种配置也很常见。

在 ‘runs’ commit 中,硬件 RNG(Random Number Generation,随机数生成)通过以下代码行被禁用。

#define MICROPY_HW_ENABLE_RNG (0)

这就是导致 bug 的原因。将此值设为零会告诉默认的 micropython rng 代码不要使用硬件 RNG 设备,而是应该使用 Yasmarang RNG。

开发者添加了一个内联注释来“解释”这一更改

// We have our own version of this code.

COLDCARD 对这个代码的版本似乎指的是 rng.h 和 rng.c 中添加的函数。

rng.h

MP_DECLARE_CONST_FUN_OBJ_0(pyb_rng_get_obj);

MP_DECLARE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj);

这些看起来是尝试覆盖 stm32 rng 库的 pyb_rng_getobj 函数。这种方法遇到了问题。stm32 的 rng.c 文件已经定义了 pyb_rng_et_obj 变量,并将其值设置为 pyb_mg_get。你不能让同一个变量有两个定义,还要指望它能编译通过。

为了更清楚地理解,这个宏展开后的结果如下。

MP_DEFINE_CONST_FUN_OBJ_0(pyb_rng_get_obj, pyb_rng_get);

展开宏并从逻辑上思考,它就是这个伪代码:

var pyb_rng_get_obj = pyb_rng_get

Commit 37e4af5 添加了一个自定义的 rng.c 文件,他在这里复制粘贴了相同的宏定义

MP_DEFINE_CONST_FUN_OBJ_0(pyb_rng_get_obj, pyb_rng_get);

现在这段代码根本不可能编译通过。看起来开发者天真地想通过创建变量的重复版本来覆盖 pyb_rng_get_obj 变量。C 不是这样工作的。这个错误会给他一个编译错误:“duplicate symbol pyb_rng_get_obj”,因为它现在有两个定义位置:stm32 库和新建的 rng.c 文件。

从这里开始,我猜测他在一阵沮丧中将 MICROPY_HW_ENABLE_RNG 设为 0,这解决了编译错误。

有时开发者在挣扎中会乱试一些随机的东西,看看是否有用。把 MICROPY_HW_ENABLE_RNG 设为 0 确实会让编译错误消失,但因为错误的原因

这掩盖了两个冲突定义引发的编译错误。

// 我们有自己的这个代码的版本。
#define MICROPY_HW_ENABLE_RNG       (0)

MICROPY_HW_ENABLE_RNG 设为零会完全移除使用硬件随机数生成器的代码,即 stm32 的 rng.c 文件中的第 31 到 80 行。这产生了移除 pyb_rng_get_obj 第二个定义的副作用,从而“修复”了编译错误。

那个编译错误是在恳求程序员重新考虑他的逻辑。但这一切没有发生,编译错误只是被压制了。编译器给了开发者最后一次机会去重新考虑他即将要做的事,但这个警报被无视并压制了。

现在一切都对上了。看起来开发者试图覆盖这个变量,却遇到了冲突定义。当面对“duplicate symbol”错误时,他试图修改一些随机的东西来让代码**运行**。

他发现把 MICROPY_HW_ENABLE_RNG 改为 0 可以编译通过。他可能不知道原因,但提出了一个理论:我们不再需要那段代码了,因为我们自己有一个版本。

他对 pyb_rng_get 的定义已经就位,而他本来不想运行的代码已经被关闭了。

令人困惑的调用链

Coldcard 的固件是用 C 编写的。告诉硬件应该做什么的应用层是用 Python 编写的。Python 代码调用 C 代码。开发者覆盖了 pyb_rng_get 函数。

对许多人来说不幸的是,他的代码覆盖的 pyb_rng_get 函数并不是 Python 代码实际调用的函数。Coldcard v4.0.0 中的 Python 代码在其 make_new_wallet() 函数中调用 random.bytes()

async def make_new_wallet():
    await ux_dramatic_pause('Generating...', 4)
    seed = random.bytes(32) # 糟糕
    assert len(set(seed)) > 4
    seed = ngu.hash.sha256s(seed)
    await approve_word_list(seed)

MICROPY_HW_ENABLE_RNG 设为 0 关闭了 stm32 库提供的硬件代码,但允许开发者设置 pyb_rng_get_obj。问题是,pyb_rng_get_obj 是 Python 可见的 pyb.rng() 可调用对象。

这并不是开发者在钱包函数里调用的东西。相反,他们使用的是 random.bytes(32),它完全绕过了 pyb_rng_get 调用路径,而是调用 rng_get,由于 MICROPY_HW_ENABLE_RNG 被设为零,它使用了 micropython stm32 rng.c 中第 L112 行的定义,而该定义调用的是不安全的 Yasmarang——非硬件钱包熵。

uint32_t rng_get(void) {
    return pyb_rng_yasmarang();
}

简而言之:固件更改覆盖了在创建新钱包时不会用到的函数,同时,作为包含 Python 方法覆盖的副作用,它在所有情况下都关闭了硬件 RNG 的使用,转而使用一个非常弱的随机数生成器。

最后的证据就是 commit message 本身。它就是简单的 “runs” 这个词。

开发者们,如果你们曾处于这种情况下,请停下来。不管你拿到多少报酬,都不值得你可能对他人造成的毁灭性影响。

不要发布你不理解的代码。

对 micropython 的诅咒

从核心来看,这似乎是过多复杂性层次导致的后果。有 micropython 库、C 代码与 Python 应用程序之间的绑定,还有 COLDCARD 正在添加的新函数。做这个补丁的开发者实际上是不是一个被迫编写和处理 C 代码的 Python 开发者?

Micropython 制造了一种错觉:嵌入式开发者不需要理解 C、他们的 CPU 或其它高级概念就能进行嵌入式编程。

那是谎言。

而这场灾难正是相信这个谎言的结果。

你必须理解你发布的代码。到此为止。没有任何借口。层层误导让它更难以理解。

如果你做了更改,请确保你验证过它们确实在做你打算让它们做的事。

这里的 commit messages 讲述了真实的故事。用这里展现出的这种缺乏勤勉和理解的态度对待你代码中最关键的部分,是不可原谅的。

作为一个在 Bitcoin 上工作的开发者,你需要花时间理解你的更改,清楚地记录它们,并验证它们确实如你所想。

当人们的生活被毁时,他们不会也不应该同情你。你需要认识到你所置于风险中的资金。作为 bitcoin 开发者,你的任务至关重要。我自己过去也犯过错误,但当代码如此关键时,无法理解你发布的内容是不可原谅的。

我希望我们作为一个行业,能够从中吸取教训,相互依靠,作为一个开发者社区发布安全的代码。

  • 原文链接: insider.btcpp.dev/p/when...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论