Coldcardハードウェアウォレットに脆弱性、2026年の攻撃で1,719BTCが流出
AI マーケットサマリー
報告されたColdcardのファームウェアにおけるエントロピー欠陥により、デフォルト設定のデバイスについて、オフラインの総当たりでシードを復元できる状態となり、数千のアドレスにわたり少なくとも1,719 BTCの盗難が確認された。この事案は、"コールドストレージ"のリスクをネットワーク侵害ではなく鍵生成の品質に結び付けて捉え直すものとなり得るため重要であり、ハードウェアウォレットへの信頼に圧力をかけ、緊急のファームウェア更新を促し、短期的にはセキュリティ要因による資金移動と、BTCのカストディ慣行全般にわたる精査を増加させる可能性がある。
影響度
● 高い
影響を受ける資産
BTC/USDT-1.73%
AI インサイト · BTC/USDTAI インサイト
▼ 弱気
今すぐ取引
⚠️ AI によって生成されたインサイトはニュースコンテンツに基づくものであり、情報提供のみを目的としています。投資助言を構成するものではなく、BingX の見解を示すものでもありません。投資にはリスクが伴います。責任ある取引を心がけてください。
執筆:Johan & Lisa 編集:77
【概要】
2026年7月30日、ブロックチェーン上で複数のアドレスから資金が連続的に移動し始め、41分の間に単一署名(single-signature)の1,196アドレスが空になった。消失したビットコインは約1,082BTC。これは第一波に過ぎず、8月上旬までに被害は少なくとも1,719BTC(約1億1,100万ドル)に拡大、関与アドレスは5,200超。攻撃は3〜4回の波状で進んだと確認されている。
注目すべき点は、被害ウォレットの大半が数カ月から数年にわたり動きのない「コールド」状態だったことだ。問題はネットワーク侵入ではなく秘密鍵そのものにあった。流出したウォレットはすべてColdcardハードウェアウォレットで生成された秘密鍵を使用し、所有者は追加のダイス由来エントロピーを加えず、BIP39パスフレーズも有効化していなかった。いずれも最も単純なデフォルト設定で運用されていた。
影響範囲はファームウェア系統で異なる。Mk2/Mk3のファームウェア4.0.1〜4.1.9では実効エントロピーが約40ビットに落ち込み、最も深刻。Mk4/Mk5/Qでも特定ファームウェアでは約72ビット程度にとどまり、オフライン総当たり攻撃の射程に入ると分析されている。
Coinkiteは7月30日に警告を出し、翌日に修正版ファームウェアを公開した。更新要件は、Mk2/Mk3が4.2.0以上、Mk4/Mk5が5.6.0以上、Qが1.5.0Q以上。脆弱バージョンの在庫はすべて廃棄し、出荷も停止した。
同社によれば、今回の事件はリモート侵入やサプライチェーン汚染とは無関係で、攻撃者が端末に触れた形跡はない。攻撃者はオフラインでシード探索空間を系統的に試し、秘密鍵を総当たりで復元した。脆弱性は長年埋もれていたが、外部研究者が総当たりツールを開発したことで顕在化し、その後Muanが攻撃チェーン全体の再現に成功したとされる。
【脆弱性の根本原因】
ColdcardはSTM32L475チップ内蔵のハードウェアTRNG(真性乱数生成器)でシードを生成する設計だが、ファームウェア上の2つの不備が重なり、事実上ソフトウェアPRNG(疑似乱数)に置き換わった。しかも、そのPRNG(Yasmarang)の状態が世界中の端末でほぼ同型になり得るため、Mk3では実効エントロピーが約40ビットまで低下した。
■ミス1:ハードウェアRNGが明示的に無効化されていた
ビルド設定(mpconfigboard.h)でMicroPythonのハードウェアRNGサポートを無効化していた。
// stm32/COLDCARD/mpconfigboard.h
// The team has implemented their own version of ckcc.rng_bytes, so disable MicroPython\u0027s builtin version
#define MICROPY_HW_ENABLE_RNG (0)
一見すると、Coinkite独自のckcc.rng_bytesがSTM32のTRNGを直接呼ぶため問題がないように見える。ところがlibnguのmy_random_bytes()はCHIP_TRNG_32()を介して乱数を取得し、展開後はMicroPython STM32ポート層のrng_get()を「ハードウェアTRNG」として扱う。MICROPY_HW_ENABLE_RNGが0の場合、rng_get()は黙ってソフトウェアPRNGへフォールバックし、次のミスにつながる。
■ミス2:rng_get()のフォールバックをTRNGとして扱っていた
libnguのrandom.cを追うと、CHIP_TRNG_32()はrng_get()を呼ぶ。my_random_bytes()は取得値が直前値と同一ならエラー扱いし、さらにグローバル定数ストリームのYasmarang出力(pad=0x0a8ce26f)とXORして混ぜる実装になっている。
// external/libngu/ngu/random.c
#ifdef MICROPY_PY_STM
// ports/stm32/rng.c
extern uint32_t rng_get(void);
# define CHIP_TRNG_SETUP()
# define CHIP_TRNG_32() rng_get()
#endif
...
void my_random_bytes(uint8_t *dest, uint32_t count) {
uint32_t chip = CHIP_TRNG_32();
// Assumes reading from hardware TRNG, but may actually receive Yasmarang output
if (chip == last) ...
chip ^= my_yasmarang();
...
}
rng_get()はports/stm32/rng.cでMICROPY_HW_ENABLE_RNGを条件分岐し、非ゼロならRNG>DR(ハードウェアTRNG)を読み取る。ゼロならソフトウェアPRNGを返す分岐がコンパイルされる。Coldcardの設定は0であり、出荷状態のMk3は全台この経路に入る。
// external/micropython/ports/stm32/rng.c
#if MICROPY_HW_ENABLE_RNG
uint32_t rng_get(void) { ... read RNG>DR hardware TRNG ... }
#else
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; // pad = UID ^ SysTick
n = RTC>TR;
d = RTC>SSR;
}
...
}
#endif
初回のシードは主にUIDとSysTick>VALから作られる。SysTickは80MHz動作でリロード値80,000のため0〜79,999を取り得て、約17ビット相当。RTC>TRとRTC>SSRは電源投入直後でアプリが初期化する前のRTCレジスタ値で、探索空間は小さい。検証済みのオンチェーン命中ケースでは両者とも0だったという。結果として、UIDが約14ビット、SysTickが約17ビット、RTC由来が0ビット、ボタン操作が約5ビットで、シード生成全体の真のエントロピーは36〜37ビット程度にとどまる。Coinkiteが示した「約40ビット」という説明は、コード解析で得られる探索空間と整合する。この規模はGPUクラスタで数日あれば総当たり可能とされる。
【攻撃の流れ(Mk3初回起動を中心に)】
攻撃者の主目的は、初回起動時に乱数が消費されるパターンをモデル化し、PRNGの全ステップを正確に再現することにある。Mk3では典型的に3つの消費プロファイルが存在する。
フェーズ1:初期状態
端末起動直後に、(1) libngu側のYasmarangが定数(pad=0x0a8ce26f、n=69、d=233、dat=0)で開始され、(2) rng_get()が最初に呼ばれた際にUID^SysTick、RTC>TR、RTC>SSRで種が入る。ここで「乱数性」は32ビットpadと小さな列挙次元へ圧縮され、全体として探索可能な箱に収まる。
フェーズ2:ボタン操作による状態進行
初期セットアップではPIN設定、規約のOK、メニュー遷移などの操作が発生する。キー押下のたびにshared/mempad.pyの_start_scan()が走り、shuffle(self.scan_order)を通じて_rand_below()が複数回呼ばれる。scan_orderの長さはNUM_ROWS=4。shuffleはFisher-Yatesで、randbelowはlibnguのngu.random.uniformに直結している。
# shared/random.py
import ngu
randbelow = ngu.random.uniform
bytes = ngu.random.bytes
def shuffle(lst):
for i in reversed(range(1, len(lst))):
j = randbelow(i + 1)
lst[i], lst[j] = lst[j], lst[i]
この消費の仕方が複数あり、解析では以下の3プロファイルが確認された。
・プロファイルA(一般的な初回セットアップ)
accept_terms前に2回押下(kpad_a=2)、settings.save()が32スロット中の空きを探索(my_pos除外で31候補→_rand_belowを30回呼ぶ)、PIN入力とメニュー操作(kpad_bは4〜34の範囲で列挙)、最後にrandom_bytes(32)でエントロピー取得。
・プロファイルB(フラッシュ/ワイプ後、NVRAM空の初回起動)
nvstoreがshuffle(32)を1回実行後、3スロット×16ブロック×256バイト=3,072ステップを出力フィードバックなしで消費し状態だけを進める。空NVRAMのオンボーディングではさらにshuffle(32)を追加、その後にキー操作、最後にmy_random_bytes(32)。
・プロファイルC(ペーパーウォレット)
既存ウォレット作成後にPaper Walletsメニューへ入り直すと、ボタン操作が8〜25回程度発生し、その後my_random_bytes(32)が直接秘密鍵として使われる。BIP39の単語列を介さない最短経路になる。
フェーズ3:シード構築とアドレス導出
Mk3はrandom_bytes(32)から最終アドレスまで決定論的パイプラインで進む。
raw_bytes = random_bytes(32)
entropy = ngu.hash.sha256s(raw_bytes)(SHA256は1回、sha256dではない)
mnemonic = BIP39(英語ワードリスト、24語)
seed = PBKDF2-HMAC-SHA512(\u0022mnemonic\u0022、2048回)
master = HMAC-SHA512(\u0022Bitcoin seed\u0022、seed)
child = m/{44,49,84}\u0027/0\u0027/0\u0027/0/0
default address = bech32(hash160(compressed_pubkey))(BIP84、受取先1番目)
各段階は再現可能な関数であり、padが当たれば攻撃者はオフラインで同じ鍵・アドレスを再計算できる。
フェーズ4:GPUでの総当たりと照合
手順は大きく4段階。
(1) 候補pad集合を構築:ウェハ座標X/Yを0〜72、UID空間約5,300、SysTickを0〜79,999で直積し、約4億2,400万の候補padを作る。数日規模でGPU処理可能。
(2) 各padについてキー押下回数を列挙(kpad_bは4〜34)。検証済みヒットではRTC_TR/RTC_SSRが0のため、これらは列挙ループに置く。
(3) GPUカーネル内で全パイプライン(SHA256、BIP39、PBKDF2 2048回、BIP32、Hash160、Bech32)を実行。libnguの定数ストリームは事前計算して参照し、各スレッドの進行コストを削る。
(4) Bloomフィルタまたはhash160のソート配列で照合(O(log n))し、ネットワーク上の単一署名P2WPKHアドレス集合に突き合わせる。
計算コストの目安として、Apple M1の単体GPUで「72×72のUID、SysTick 80,000変動、キー押下31通り」を回し、約148億候補を探索するのに約8.6日。データセンター向けA100クラスタなら同等探索を数時間へ短縮できるという。
Mk4/Mk5/Qの約72ビット空間はMk3より2^32倍大きいが、脆弱性の起点は同じで、プロファイリングとモデリング手法も共通。単体マシンからクラスタへ計算資源が移るだけで、日単位が週単位に伸びる程度とされ、攻撃者が負担可能なコストに収まるため被害リストに同様に現れる、というのが分析の結論だ。