超越安全儲存的第 1 部分探討了支付系統自身簽署能力遭到濫用的情況。這第二部分探討另一條取得未經授權簽章的途徑:密碼學實作缺陷讓攻擊者得以還原私鑰並自行簽署。問題是:攻擊者能否在不讀取受保護的金鑰副本的情況下重建金鑰?
第 1 節說明金鑰生成、儲存與簽署的安全要求。第 2 節和第 3 節探討四起金鑰生成事件與兩起簽署事件,說明其中的實作錯誤及其後果。第 4 節從這些案例歸納設計與實作原則,再說明如何驗證必要的安全特性。
1. 金鑰生命週期各階段的安全特性
金鑰必須經過生成、儲存,並用於簽署。如圖所示,每個階段都有不同的安全要求。

在使用 BIP-39 助記詞與 BIP-32 金鑰衍生方式的分層確定性錢包中,具備密碼學安全性的隨機資料會編碼成助記詞,再轉換為種子。該種子用於衍生私鑰、公鑰和錢包地址 [1, 2]。
種子和私鑰生成後,需透過加密或硬體隔離加以保護。簽署時則使用私鑰產生可公開驗證的簽章。視所用方案而定,簽署也可能需要安全生成的隨機臨時值,或從秘密資料以確定性方式衍生的臨時值。
這些階段的安全要求各不相同:
- 建立時的不可預測性。 攻擊者不得能夠預測或在可行範圍內窮舉控制錢包的秘密。
- 儲存時的機密性。 未經授權者不得透過軟體、實體介面、備份、日誌或遠端服務讀取秘密資料。
- 使用時不洩露秘密。 即使有許多可公開觀察的簽章,簽署過程也不得洩露足以重建秘密的資訊。
硬體錢包、安全元件和可信執行環境(TEE)可以加強機密性,但光是具備這些機制,並不能確保另外兩項安全特性。可預測的金鑰放入安全儲存環境後仍然可預測;即使私鑰位元組與主機隔離,簽章所暴露的數學資訊仍然可供利用。因此,攻擊者可能無須讀取受影響金鑰的儲存副本,就能還原金鑰並存取其控制的資產。
接下來兩節探討實作如何違反這些要求:生成缺陷可能使金鑰變得可預測,簽署缺陷則可能使金鑰能從公開簽章中被還原。這些案例是第 4 節實作原則與驗證目標的基礎。
2. 金鑰生成缺陷:可預測的私鑰
金鑰生成的每個步驟都必須維持不可預測性。熵來源必須適合密碼學用途,其狀態不得在實務上可被窮舉,而將隨機位元組轉換為種子或私鑰的過程,必須保留預期的不確定性。128 位元的安全目標要求實作缺陷不得將秘密的可能範圍縮減到可行的搜尋範圍。
下表概述生成過程可能因四種方式失去這項特性:函式庫中的弱隨機數生成器、錯誤的隨機數生成器(RNG)綁定、造成資訊流失的輸入轉換,以及初始狀態有限的生成器。影響數字來自涵蓋不同期間的報告,並非在同一日期統計的總額。
| 事件 | 相關期間 | 通報影響(約略) | 下文探討的失效模式 |
|---|---|---|---|
| Ill Bloom | 2026 年 5 月至 7 月 | 截至 7 月 13 日,下限約為 500萬 USD | 助記詞生成過程中的弱隨機性 |
| Coldcard | 自 2026年7月30日 起;於 8 月 14 日統計 | 1.1270億 USD | 韌體中錯誤的 RNG 路徑 |
| XRPH Wallet | 2026年9月3日 | 45萬 USD | 可預測且受到嚴重限制的錢包生成輸入 |
| Qubic Wallet | 2026年10月17日–2026年10月18日 | 130萬 USD | 使用 Math.random() 以不安全的方式生成種子 |
2.1 Ill Bloom:錢包相依套件中的弱隨機性
在 Ill Bloom 事件中,部分軟體錢包使用有漏洞的 crypto-js 版本生成 BIP-39 復原詞組。該函式庫的 CryptoJS.lib.WordArray.random() 並未從具備密碼學安全性的來源取得熵,而是使用以 Math.random() 初始化的自訂 Multiply-With-Carry(MWC)生成器。將此函式用作安全關鍵熵來源的錢包,會承襲該生成器的弱點,即使後續助記詞與金鑰衍生步驟都正確遵循各自標準也一樣 [3]。
有漏洞的 crypto-js v3.1.9-1 實作 [4] 包含以下具代表性的邏輯:
random: function (nBytes) {
var words = [];
var r = (function (m_w) {
var m_w = m_w;
var m_z = 0x3ade68b1;
var mask = 0xffffffff;
return function () {
m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask;
m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask;
var result = ((m_z << 0x10) + m_w) & mask;
result /= 0x100000000;
result += 0.5;
return result * (Math.random() > .5 ? 1 : -1);
}
});
for (var i = 0, rcache; i < nBytes; i += 4) {
var _r = r((rcache || Math.random()) * 0x100000000);
rcache = _r() * 0x3ade67b7;
words.push((_r() * 0x100000000) | 0);
}
return new WordArray.init(words, nBytes);
}
其中,r() 是以自訂 MWC 為基礎的生成器,而 Math.random() 有助於初始化或影響其狀態。這個偽隨機數生成器(PRNG)產生的位元組看似一般的熵,但該實作並未提供密碼學上的不可預測性。對於標稱 128 位元的輸出,有效候選空間約為 2 USD^{39} [3]。
這些較小的空間,使重建候選復原詞組、衍生對應的錢包地址,再與公開區塊鏈活動比對成為可行之事。因此,攻擊者無須從受害者的裝置擷取任何資料,就可能還原秘密。遵循助記詞標準,無法修正從舊版相依套件承襲的弱隨機性。
2.2 Coldcard:程式碼未使用的硬體隨機數產生器
Coldcard 顯示,即使具備安全的隨機數來源,只要應用程式呼叫的是另一個產生器,就仍然不足以確保安全。雖然裝置配備硬體隨機數產生器,但建置時的整合錯誤,使種子產生流程中的 rng_get() 實際指向 MicroPython 的軟體偽隨機數產生器(PRNG)Yasmarang。這個非密碼學用途的 PRNG 使用裝置固定的唯一 ID 和有限的時間資訊作為種子,令其初始狀態具有足夠的可預測性,大幅降低所產生錢包種子的有效熵 [5]。

種子產生程序還混入第二個 Yasmarang 產生器的輸出。在較舊的 Mk2/Mk3 裝置上,第二個產生器以公開常數作為初始值,因此未增加任何熵。在較新的型號上,雖然引入了安全元件提供的隨機性,但重新設定種子時只保留 32 位元。因此,儘管產生的是 256 位元的錢包種子,有效搜尋空間仍大幅縮小。依據其所述的攻擊假設,Coinkite 初步估計 Mk2/Mk3 約有 2 USD^{40}$ 個候選種子,而 Mk4/Q/Mk5 約有 2 USD^{72}$ 個 [6, 7]。
這項弱點使種子復原變成離線搜尋:攻擊者可以從候選種子推導地址,再與公開區塊鏈資料比對。實際成本取決於裝置型號和攻擊假設;2 USD^{72}$ 的估計值,不代表其可行性與 2 USD^{40}$ 的情況相同。一旦成功找到相符結果,對應的私鑰就會暴露。
2.3 XRPH Wallet:縮小金鑰空間的輸入格式錯誤
產生流程也包括將隨機輸出轉換成錢包 API 預期的輸入格式。在 XRPH Wallet 中,這項轉換捨棄了大部分產生的輸入。Android 應用程式以 JavaScript 的 Math.random() 產生十進位數字區塊,將它們串接成文字字串,再把字串傳給 Wallet.fromEntropy()。接收端程式碼將字元解讀為數值較小的整數位元組值,且只保留前 16 個位置。由於其中兩個位置是固定的空格分隔符,最終只有 14 個可變的十進位數字為產生的錢包提供熵 [8]。
padlock = { A, B, C, D, E, F, G, H }; // each = 6 digits from the line above
r6 = A.join('') + ' ' + B.join('') + ' ' + C.join('') + ' ' + D.join('')
+ ' ' + E.join('') + ' ' + F.join('') + ' ' + G.join('') + ' ' + H.join('');
r5 = Wallet.fromEntropy;
r3 = r5.bind(Wallet)(r6); // a 55-char STRING is passed; fromEntropy wants 16 bytes
// compute entropy be like
const input = "528875 921568 528239 ..."; // decimal blocks
const entropy = Uint8Array.from(input).slice(0, 16);
// Result: [5,2,8,8,7,5,0,9,2,1,5,6,8,0,5,2]
經過這項轉換後,可產生的錢包輸入約有 10 USD^{14}\approx 2^{46}$ 種;原本預期從均勻取樣的 16 位元組熵所得到的空間,則約有 2 USD^{128}$ 種。前者描述的是在觀察到的轉換方式下可產生的候選輸入數量,而非對 Math.random() 統計熵所作的密碼學證明。
兩項缺失相互加劇:使用不適當的隨機數來源,以及 API 邊界上的表示方式/編碼錯誤。兩者都可能通過功能測試,因為錢包仍會產生看似正常的地址,並簽署有效交易。公開調查證實金鑰空間可供搜尋並復原金鑰;但調查本身並未證明攻擊者使用了哪一種取得金鑰的途徑。
2.4 Qubic Wallet:55 字元種子背後的弱隨機性
無論輸出字串看起來有多長,產生器的初始狀態也可能限制可能產生的種子。Qubic 說明了這個問題:據報其錢包遭入侵事件涉及使用 JavaScript 非密碼學用途的 Math.random() 產生的種子。受影響的環境包括 Apple 瀏覽器,以及部分以 JavaScriptCore 為基礎的舊版 iOS 應用程式版本 [9, 10]。
有漏洞的產生邏輯及其修正方式可概述如下 [11]:
// Vulnerable seed generation (abridged)
seed += letters[Math.floor(Math.random() * 26)];
// Corrected approach (schematic excerpt of the fix)
crypto.getRandomValues(bytes);
if (byte < 234) seed += letters[byte % 26];
呼叫 Math.random() 55 次,並不會提供 55 個彼此獨立的熵來源。在兩次重新設定種子之間,每次呼叫 PRNG 都會以確定性的方式推進其內部狀態。對於固定的呼叫位置,若初始種子為 ,整個 55 字元的錢包種子就是確定性函數 的結果。只要知道 和該呼叫位置,就足以重現全部 55 個字元;多呼叫幾次 PRNG 並不會產生額外的熵。
可能的初始狀態範圍取決於執行階段的初始化方式。於 2026年10月29日 取得的 JavaScriptCore 原始碼快照顯示,其 PRNG 使用 32 位元種子初始化,因此在固定呼叫位置,最多會產生 2 USD^{32}$ 種序列。此處檢視的 V8 原始碼使用 64 位元種子,因此此類序列最多有 2 USD^{64}$ 種。這些上限適用於所檢視的初始化流程及固定的呼叫位置。這些快照無法確定事件中使用的瀏覽器確切版本,也無法確定利用這些版本進行攻擊的成本。修正方式則改用 crypto.getRandomValues() 取得具密碼學安全性的隨機數,並以拒絕取樣(byte < 234)避免將位元組對應至 26 個字母時產生偏差 [12, 13, 14]。
3. 有缺陷的簽署實作:從公開簽章復原私鑰
金鑰安全產生後,在用於簽署時也必須保持機密。每份簽章都會向觀察者提供資料,而實作缺陷可能使這些公開資料足以用來復原私鑰。ECDSA 和 Schnorr 都使用每份簽章專屬的秘密純量,稱為 nonce,來遮蔽私鑰對公開簽章的影響。
nonce 必須保持機密、避免在同一把金鑰簽署不同訊息時重複使用,並符合該簽章方案的分布要求。以確定性方式推導 nonce 可以符合這些要求:僅知道公鑰和已簽署訊息,不應足以預測 nonce。
以簡化的 Schnorr 關係式為例:
其中, 是私鑰, 是 nonce, 是利用公開資訊(例如公鑰和訊息)計算的公開挑戰值, 是公開的簽章純量, 是子群的階。若已知 nonce,且 可逆,就能立即透過 得出 。
此外,即使無法完全得知 nonce,金鑰仍可能洩漏。多個 nonce 的限制條件或偏差,可能提供足夠的資訊,讓攻擊者透過格基方法復原金鑰。可行性取決於暴露的資訊及可取得的簽章。
表格區分了本節探討的兩種 nonce 失效情況:Zilliqa 的 nonce 受到部分限制,而 SecondFi 的 nonce 則變得可由公開資料計算。所報告的影響分別採用不同的歷史統計期間。
| 事件 | 相關期間 | 所報告的影響(約) | 下文探討的失效模式 |
|---|---|---|---|
| Zilliqa Ledger Wallet | 2026 年 3 月至 7 月 | 683,130,969.66 ZIL |
有偏差的 EC-Schnorr 簽章 nonce |
| SecondFi | 2026 年 6 月 | 無法根據所引用的程式碼證據確定 | 有缺陷的 Ed25519 nonce 推導 |
3.1 Zilliqa Ledger Wallet:每個 Schnorr nonce 都缺少 64 位元
Zilliqa Ledger Wallet 事件涉及用於傳統 Zilliqa EC-Schnorr 交易的應用程式中的一項缺陷 [15]。該應用程式產生了 nonce 資料,卻將錯誤的部分複製到最終簽章緩衝區。它保留了八個位元組的零填充,卻捨棄了八個位元組的實際隨機資料。因此,每個受影響的 nonce 都滿足 0 USD \le k < 2^{192},$,而不是適當地分布在約 256 位元的 secp256k1 群階範圍內。
簽章方程式說明了為何限制 nonce 會造成影響:
此處, 是群的生成元。將方程式移項可得
因此,每個簽章都提供了一個關於未知數 的模數關係,其中 nonce 的上界為 2 USD^{192}$。結合數個獨立關係後,便形成一個隱藏數問題(Hidden Number Problem,HNP)實例,可透過格基約化處理。官方事後檢討報告指出,利用四個或更多受影響的簽章即可復原私鑰。
安全元件完全不需要洩露私鑰位元組。存在偏差的 nonce 使該私鑰可從已發布至鏈上的簽章中復原。
3.2 SecondFi:可由公開資料計算的簽章 nonce
在 SecondFi 事件中,錯誤的 Ed25519 nonce 推導造成的問題不只是部分限制 nonce:它讓 nonce 可由公開資料計算。這使攻擊者能從單一公開可取得的交易簽章中復原簽章私鑰 [16]。
對於 Ed25519 類型的簽章,預期的建構方式會從祕密金鑰資料推導出祕密純量 和私密 nonce 前綴。此處, 是公鑰, 是 Ed25519 基點, 是被簽署的訊息, 是子群的階,而 表示 SHA-512。其示意形式如下:
有缺陷的實作卻將 設為 。任何知道被簽署訊息的人都能推導出 。由於 、 和 都是公開的,觀察者接著便能計算
前提是 在模 下不為零。
不同於 Zilliqa 的 nonce 部分資訊洩露,一個受影響的簽章就足以復原簽章純量。這並不自動證明能復原原始助記詞,或分層錢包中的每一把同層金鑰。失效發生在金鑰推導與簽章之間的介面:所需的祕密資料雖然可用,卻從未傳給 nonce 推導常式。
4. 安全實作原則與驗證
綜合這些案例可知,除了隨機數來源和簽章演算法,連接兩者的程式碼也必須接受審查。不可預測的來源必須產生不可預測的金鑰,而這些金鑰在簽章期間也必須保持機密。轉換常式或函式庫介面的錯誤都可能破壞任一要求。
第 4.1 節和第 4.2 節說明金鑰產生與簽章的設計及實作原則。第 4.3 節則說明,為評估已部署系統是否符合這些原則,審查與測試應提供哪些證據。
4.1 在金鑰產生過程中維持安全性
金鑰產生相關案例指出三項實作要求:
-
使用密碼學安全的隨機性,並在失敗時停止。 種子和私鑰需要適當的密碼學安全來源,例如密碼學安全的偽隨機數產生器(CSPRNG)。Ill Bloom 和 Qubic 顯示了為何通用型產生器無法擔任此角色。如果預定使用的來源無法取得或發生故障,金鑰產生作業就必須停止,而非悄悄改用較弱的替代來源。
-
在資料轉換過程中保留熵。 安全來源的輸出在編碼、取樣或介面間傳遞時,必須保有所需的不確定性。XRPH Wallet 案例顯示,輸出看起來有效仍不足以證明安全。型別、長度和編碼方面的限制必須防止非預期的截斷或有偏差的映射縮小有效金鑰空間。
-
確保已部署的程式碼使用預定來源。 Coldcard 顯示,安全隨機數來源的存在,並不能證明應用程式實際呼叫的是哪個來源。來源選擇必須在整合、編譯和部署過程中維持正確,包含錯誤處理及備援路徑。對 RNG 的設計假設,必須在目標平台上執行的程式中成立。
4.2 在簽章過程中維持機密性
金鑰產生方面的要求保護金鑰的建立過程。要在使用期間維持金鑰機密性,還需要正確處理 nonce。具體要求取決於所用方案;除案例探討的 Schnorr 和 Ed25519 外,下列清單也納入 ECDSA。
-
ECDSA: 維持 nonce 的機密性和不可預測性,並防止在同一把金鑰下,對不同訊息重複使用 nonce。遵循 RFC 6979 [17] 進行確定性產生,可避免每次簽章都依賴新的外部隨機資料,但實作仍須防範旁通道洩漏和故障攻擊。
-
EdDSA: 保留 nonce 推導所需的所有祕密輸入。SecondFi 展示了省略私密前綴、僅使用公開資料的後果。包裝程式和金鑰適配器必須傳遞 RFC 8032 [18] 所要求的完整祕密輸入,而不是把能產生有效簽章視為輸入正確的證據。
-
Schnorr: 維持方案要求的 nonce 分布與表示方式。如 Zilliqa 案例所示,即使避免了 nonce 重複或完全可預測,只要遺失部分隨機資料仍然不足。資料處理和純量轉換不得引入可遭利用的限制或偏差。
經過充分審查的函式庫可降低實作風險,但無法消除整合錯誤。如果呼叫端提供的輸入不完整或已損毀,即使簽章基本運算本身正確,仍然不安全。
4.3 測試安全性質
若要評估這些要求是否得到滿足,審查必須追蹤輸入如何經過實際執行的實作。有效的輸出可能掩蓋整合錯誤,因此驗證也需要涵蓋一般功能測試未觸及的路徑。下表將金鑰產生與簽章方面的要求轉化為具體審查目標:
| 審查領域 | 驗證目標 |
|---|---|
| 熵的不可預測性 | 確認來源、初始化程序和有效種子空間是否能抵禦實際可行的預測或枚舉 |
| 熵的保留 | 驗證轉換和取樣是否保有所需的不確定性,且不會發生非預期的截斷或偏差 |
| RNG 整合 | 確認已部署的執行檔使用預定來源,包括錯誤處理和備援路徑 |
| nonce 的機密性 | 驗證 nonce 的產生或推導是否維持機密性、納入所有必要的祕密輸入,且無法僅憑公開資料重現 |
| nonce 的正確性 | 對照方案要求檢查 nonce 的處理方式,並在適用時使用已知答案測試及獨立實作 |
| 相依套件安全性 | 審查密碼學相依套件的版本與整合方式,並確認更新後仍維持所需行為 |
評估這些目標需要結合多種方法。程式碼審查會追蹤祕密與隨機輸入如何進入已部署的金鑰產生和簽章常式。負向路徑測試與故障注入可檢查一般假設不成立時的行為;已知答案測試及與獨立實作的比較則可揭露偏離方案要求之處。統計測試可以偵測明顯偏差,但僅憑成功產生地址、驗證簽章或通過統計測試,無法確立密碼學安全性。應依照來源、輸入處理和方案要求,評估各項證據的整體結果。
第三方安全團隊或專家可透過程式碼與整合稽核,補充內部驗證,尤其是在隨機數來源、金鑰推導與簽章函式庫交會之處。稽核範圍應涵蓋相依套件版本、建置設定和實際執行的程式碼路徑。稽核發現應指出受影響的安全性質,並提供可重現的證據及回歸檢查。實作或其相依套件變更後,仍須進行驗證。
Web3 最佳安全稽核機構
上線前驗證設計、程式碼和業務邏輯
5. 結論
這六起事件揭示了實作錯誤破壞受保護金鑰安全性的兩種方式:在產生過程中縮小可能的金鑰範圍,或透過簽章洩露現有金鑰的資訊。本文分析從這些失效情況歸納出金鑰產生與簽章方面的要求,以及針對部署於支付基礎設施的程式碼所訂的審查與測試目標。
對支付機構而言,安全儲存能保護已儲存的祕密資料,卻無法補救可預測的金鑰產生方式,或簽章過程洩露的資訊。所需的安全性質必須在相依套件、適配器和部署過程中持續成立。審查與測試需要檢視完整實作,而非從有效的地址和簽章推斷其安全性。
這些實作要求與《Beyond Secure Storage》第 1 部分探討的工作流程控管相互補充。第 1 部分探討工作流程風險、預防性檢查,以及這些檢查未能阻止未經授權操作時如何控制損失。第 2 部分則討論可能使攻擊者取得獨立簽章能力的實作缺陷。密碼學運作正確,不代表業務操作已獲授權;業務審批有效,也不代表簽章金鑰不可預測或無法復原。安全的支付與簽章系統,必須同時設計並驗證授權工作流程及底層密碼學實作。
參考資料
-
[1] BIP-39,用於產生確定性金鑰的助記詞碼。
-
[2] BIP-32,分層確定性錢包。
-
[3] CryptoJS,安全公告 GHSA-rg76-677x-56q9。
-
[4] CryptoJS,core.js 原始碼,版本 3.1.9-1。
-
[5] Coldcard,韌體公告與版本說明。
-
[6] MicroPython,從 MicroPython 的角度檢討 Coldcard 事件。
-
[7] Coinkite,熵問題的深入技術分析。
-
[8] XRPL.to,XRPH Wallet 遭駭事件:4011 個錢包遭清空,且每把金鑰都可被猜出。
-
[9] Qubic,事件聲明與受影響平台指引。
-
[10] Qubicmap,Qubic 事件時間軸與錢包摘要。
-
[11] Qubic Wallet,安全種子產生修正 09dd288。
-
[12] WebKit,JSGlobalObject.cpp,m_weakRandom 初始化。
-
[13] WebKit,WeakRandom.h,種子與 PRNG 實作。
-
[14] V8,math-random.cc,PRNG 狀態初始化。
-
[15] Zilliqa,事後檢討:傳統 Schnorr nonce 偏差。
-
[16] KarpelesLab,SecondFi 實作分析與重現。
-
[17] RFC 6979,DSA 與 ECDSA 的確定性用法。
-
[18] RFC 8032,愛德華曲線數位簽章演算法(EdDSA)。



