Back to Blog

COLDCARD事件:當錢包的「隨機」助記詞並不隨機

Code Auditing
August 7, 2026
16 min read
Key Insights
  • COLDCARD 的漏洞源於單一的建置設定錯誤——一個巨集防護檢查的是「是否存在」(#ifndef),而非「值為何」(#if),導致種子生成在無聲無息中被導向確定性軟體亂數產生器的備援路徑。

  • 此攻擊完全離線進行:無需任何鏈上的漏洞利用交易,僅透過種子列舉(Mk2/Mk3 約 40 位元,Mk4/Q/Mk5 約 72 位元)與公開錢包資料進行比對即可。

  • 更新韌體並無法修復已生成的種子——受影響的資金必須轉移至在已修補版本上重新建立的新種子。

  • 此案例說明了為何加密熵路徑必須採取「預設失敗關閉」的設計,並在出廠韌體中進行端對端驗證,而非僅依賴編譯時期的檢查。

簡要摘要

自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發布了其方法論,我們以其說明識別過程——一個鏈下報告與鏈上分析之間的反饋迴路:

  1. 鏈下錨點。 受害者和研究人員提供了公開地址或交易ID,以及可用時的裝置和種子生成背景。每份報告均被視為線索並在鏈上核查;不需要助記詞、私鑰或xpub。
  2. 鏈上擴展。 從已確認的錨點出發,掃描器在相關區塊中搜尋相同的掃出特徵:錢包在沒有找零的情況下被清空、相似的輸入類型、緊密的時間、重複的手續費率、共同目標地址,或後來的聯合花費。
  3. 鏈下交叉核對。 新的受害者報告、研究人員資料集和服務歸因用於確認或否定候選波次。已驗證的群集和啟發式候選保持分開。

比特幣識別地址,而非人員或錢包型號。一個錢包可能控制多個地址,因此地址數量不等於受害者數量。首個被廣泛報道的主要波次 960188594.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 BTC2,055 BTC(以相同價格計,約合 $133M)[3]。差異反映了證據門檻、發現時間,以及各追蹤者依賴的確認管道。

隨後,資金被追蹤經過三個觀察到的層次:掃出的來源地址、直接掃出目標(holding)以及後續整合目標(vault)。下表顯示每個層次的不同地址數量:

模式 示例路由 追蹤後果
多次掃出至一或兩個持有地址,然後至一個金庫 960183: 204 -> 2 -> 1960188: 500 -> 1 -> 1 目標收斂使群集比較強健且易於追蹤
掃出在持有地址停止,沒有後續整合 960352: 352 -> 1 -> 0960668: 795 -> 1 -> 0 持有地址仍可追蹤,但沒有後來的聯合花費來強化歸因
每次掃出使用新目標地址,有時後跟獨立金庫 960359: 13 -> 13 -> 0960395: 1,918 -> 294 -> 293 共享收集器偵測器失效;分組依賴於時間、手續費率、交易範本和鏈下佐證

當被追蹤的輸出移動時,分析會跟蹤分割、合併和剝皮鏈,同時在扣除手續費後保存價值,並將歸因上限設為掃出金額。進入交易所或其他混合服務的資金會降低信心;該服務不會被添加到攻擊者群集中。

探索MetaSleuth調查

追蹤資金流向並為調查建立證據

立即免費試用

需要再次修復的補丁:潛在的磚機迴歸?

與熵值故障無關,解決此問題的熱修復引入了一個獨立的韌體迴歸 [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]。

參考文獻

Sign up for the latest updates
~$88M損失:COLDCARD與LULA漏洞利用事件 | BlockSec週報
Security Insights

~$88M損失:COLDCARD與LULA漏洞利用事件 | BlockSec週報

在2026年7月27日至8月2日當週,兩起重大安全事件導致比特幣和BNB鏈共損失約8800萬美元。COLDCARD硬體錢包韌體因亂數產生器配置檢查邏輯缺陷,將種子生成導向確定性軟體回退,致使攻擊者恢復受影響種子並盜走至少1,370 BTC(約8800萬美元)。BNB鏈上的LULA代幣因業務邏輯漏洞損失約57.8萬美元,攻擊者觸發特權`recycle()`函數,從PancakeSwap V2流動池抽取LULA並操控儲備餘額,耗盡其流動性。

通訊報 - 2026年7月
Security Insights

通訊報 - 2026年7月

2026年7月三起最大DeFi事件在Arbitrum和Solana造成約6790萬美元損失。AFX Trade因供應鏈攻擊導致驗證者簽名權限遭竊,損失約2415萬美元。Ostium的OLP金庫因預言機基礎設施遭入侵,攻擊者操控價格,損失約2375萬美元。BonkDAO攻擊者花費440萬美元取得足夠投票權,通過惡意國庫轉移提案,損失約2000萬美元。三起事件均顯示協議安全邊界遠超智能合約代碼本身。

~$3950萬損失:Allbridge、Wanchain等|BlockSec週報
Security Audits

~$3950萬損失:Allbridge、Wanchain等|BlockSec週報

2026年7月20-26日當週,8起重大安全事件導致Solana、Ethereum、BNB Chain、Arbitrum、Zilliqa及Cardano合計損失約3,950萬美元。重點事件Allbridge Core(約165萬美元)揭露Solana輸入驗證漏洞,同一Pool帳戶被同時接受於兩個兌換角色,分析完全從已部署程式二進位重建。其他事件包括Wanchain(約50萬美元,Cardano橋接驗證器訊息編碼缺陷)、Zilliqa(約40萬美元,Ledger應用自2019年存在的隨機數生成缺陷)及Lien Finance(約54.2萬美元,債券交易驗證邏輯缺陷)。

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit