簡要摘要
自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(),對Python暴露為 ckcc.rng_bytes)。在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 candidate initial states
因此,窮舉搜尋最多需要 2^40.69 次嘗試,在均勻位置假設下,平均約需 2^39.69 次嘗試。這是列舉上限,而非40位元的密碼學熵值。若RTC暫存器在正常冷啟動期間為靜態,則僅 SysTick 保持不確定,將上限降至約 2^16.29。若UID、計時器和較早的RNG呼叫次數已知,則存在確切的一個串流:2^0。未知的呼叫歷史僅增加合理執行軌跡的數量,而非新的熵來源。同樣,生成八個32位元字以得到256位元種子並不會使搜尋空間倍增:每個字均由相同的初始狀態確定。
在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 fallback:
120,000 SysTick values * 86,400 RTC times * 256 subseconds
= 2^41.27 states
Libngu secure reseed:
2^32 values
Combined ceiling:
2^41.27 * 2^32 = 2^73.27 candidates
Average enumeration:
2^73.27 / 2 = 2^72.27 trials
這是「約72位元」數值的來源。這是平均攻擊工作量估計,而非安全元件提供的72位元。計時器欄位是相關的,可能被重建;若MicroPython回退狀態已知,則僅剩 2^32 個重新播種值,平均需要 2^31 次嘗試。對最終32個隨機位元組進行雜湊無法增加可能的種子數量。
受影響版本
| 裝置與軌道 | 不在此迴歸範圍內 | 受影響的種子生成韌體 | 有效位元安全性 | 首個修復版本 |
|---|---|---|---|---|
| Mk1 | 至v3.0.6 | 無 | - | 不適用 |
| Mk2/Mk3 | 至v3.2.2 | v4.0.0-v4.1.9(官方公告自v4.0.1起) | 受影響時約40位元 | v4.2.0 |
| Mk4/Mk5 Standard | 不適用 | v5.6.0之前 | 修復前約72位元;修復後至少128位元 | v5.6.0 |
| Q Standard | 不適用 | v1.5.0Q之前 | 修復前約72位元;修復後至少128位元 | v1.5.0Q |
| Mk4/Mk5 Edge | 不適用 | v6.6.0X之前 | 修復前約72位元;修復後至少128位元 | v6.6.0X |
| Q Edge | 不適用 | v6.6.0QX之前 | 修復前約72位元;修復後至少128位元 | v6.6.0QX |
相關版本是生成種子時的韌體版本,而非當前安裝的韌體版本。在修復版本當日或之後生成的新種子使用已修正的路徑,但更新並不能修復現有種子。Mk2/Mk3的官方受影響範圍自 v4.0.1 起 [5],而源碼層級分析亦涵蓋 v4.0.0 [4]。獨立骰子熵值可提高種子的安全性,而強大的BIP-39密語可在不修復種子本身的情況下添加獨立屏障 [6]。
Web3最佳安全審計機構
在上線前驗證設計、程式碼及業務邏輯
受害者與資金追蹤
此次事件沒有可追蹤的鏈上利用交易;恢復工作必然是離線進行的。針對受影響韌體生成的種子,攻擊者可以約束並列舉上述候選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。
這可在登入前觸及使用者介面。數字鍵盤的 mempad._start_scan() 和Q keyboard._start_scan() 都從按鍵中斷中隨機排列其掃描順序。RNG異常在那裡可能阻礙PIN輸入和正常韌體升級選單,直至該硬體工作階段結束。
程式碼層級的失敗路徑是可信的,但更強烈的「永久磚機攻擊」聲稱尚未確立。RNG控制和狀態位元在硬體重置時歸零,因此單一的暫時性錯誤不應永久損壞外設;跨完整電源循環的持久性尚未被證明。也沒有已展示的遠端或可靠控制方式來觸發種子健康測試失敗。一篇X貼文 [8] 聲稱磚機已確認,但其指向的PR——社群提交的PR #692 [9]——其作者自述他們使用暫存器模擬分析了該故障,未在實際Mk4/Q硬體上重現,也未獨立確認現場報告。因此,最有支撐的分類是潛在的登入前服務拒絕和可靠性迴歸,而非已確認的永久磚機攻擊。
維護者自己的修復,PR #693 [12],檢查種子錯誤標誌,添加有界恢復和重試,拒絕可疑樣本,並僅捕獲預期的鍵盤錯誤;它於2026年8月5日合併,取代社群PR #692 [9],後者於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恢復及鍵盤錯誤處理



