簽章都正確,交易為何還是錯的:Bybit 與 Safe 的簽署供應鏈
2025 年 2 月 21 日,Bybit 的 ETH 冷錢包在例行轉帳時簽出一筆惡意交易。這筆交易沒有直接轉走資產,它把錢包的邏輯換掉;約 40 萬 ETH,以及 stETH、cmETH、mETH,是換完之後另外轉走的,那時錢包已經不再檢查原本的 3 個簽章。換邏輯的那筆湊滿 3 個有效簽章,Safe 合約照程式執行,公開分析指出簽署者看到的內容都來自同一個被竄改的前端。簽章驗得過,代表這把金鑰簽了這組欄位,欄位和簽署者看到的是不是同一筆,要另外核對。
這筆交易用 delegatecall 執行攻擊者的合約,這種呼叫借別人的程式碼來跑,讀寫的卻是自己的儲存空間。Safe 錢包是 proxy 合約,呼叫都交給 implementation 合約,其位址存在 slot 0(儲存空間第一格),攻擊者把它改成自己的合約,錢包邏輯就換了。
公開調查確認了哪些步驟
- 開發者工作站與雲端存取:Cointelegraph 轉述 Safe 與 Mandiant 在 2025 年 3 月公布的調查:Safe 的 AWS 設定每 12 小時要重新驗證,攻擊者幾次嘗試註冊自己的 MFA 裝置都沒成功。之後他們入侵一名 Safe 開發者的 macOS 電腦,文中寫可能透過惡意程式,再沿用還有效的 AWS session token,不必再過 MFA。
- 部署產物:BleepingComputer 轉述 Bybit 執行長引用的 Sygnia 調查,惡意 JavaScript 由 Safe{Wallet} 的 AWS S3 bucket 提供,檔案在攻擊前兩天被改過。SlowMist 轉述的 Bybit 聲明寫的是 2 月 19 日。SlowMist 依 urlscan 紀錄指出檔案是
_app-52c9031bfa03da47.js,Wayback Machine 有 2 月 19 日 17:29 UTC 的快照。 - 觸發:NCC Group 分析快照後認為,惡意碼只在 Safe 地址符合清單、原交易是一般 call(
operation == 0)時才替換欄位。 - 鏈上執行:Verichains 記錄交易時間為 2 月 21 日 14:13:35 UTC,
to是0x9622…7242、operation是1、data是transfer(address,uint256)的 calldata。 - 前端換回:Sygnia 說交易執行後約兩分鐘,S3 的 JavaScript 換回沒有惡意碼的版本。
- 排除範圍:Sygnia 在 Bybit 自家基礎設施沒發現入侵證據,Safe 說外部審查沒在智慧合約、前端與服務的原始碼找到漏洞。
FBI 在 2025 年 2 月 26 日的公告將攻擊歸於北韓,稱相關活動為 TraderTraitor。

三種可能的失效,修法不一樣
本案的公開分析沒有指出攻擊利用了 Safe 合約漏洞,換邏輯時仍須湊齊 3 個有效簽章,門檻檢查有執行。這不能證明合約沒有其他漏洞。
部署產物被竄改,要查發布流程。即使原始碼沒問題,瀏覽器仍可能載到被換掉的 S3 檔案,因此要管 bucket 寫入權限與告警,上線的 JS 要能對回某個 commit 的建置。
顯示和實際簽的內容是否一致,也要在簽署端核對。BlockSec 引 Bybit 執行長 Ben Zhou 的說法:簽署者在 Safe 看到普通轉帳,送到 Ledger 的是另一組內容。依 NCC Group 貼出的惡意碼,簽完之後整份交易欄位(to、value、data、operation、safeTxGas)會換回原值,再送出 proposal,簽章仍對應換過的那份。proposal 是 Safe 後端先收下、等其他人補簽的待簽交易。NCC Group 據此推論,網頁上會一直顯示原本那筆。NCC Group 的結論是簽署者盲簽(blind signing):信任 Safe 網頁,沒在裝置上仔細核對內容。Safe 事後暫時移除原生 Ledger 整合,稱那是這次的簽署方式,並加強 transaction hash、data 與簽章驗證。
3 個簽署者看的是同一份 JS
這次 3 個簽署者各有金鑰,判斷時看的卻是同一份 JS,資料被換掉就可能一起簽錯。若新增的簽署者也用同一個前端,提高門檻仍不會多出查核來源。
除了金鑰數量,還要看簽署者各用什麼軟體查核、有沒有共用 CDN 或雲端帳號,硬體錢包能不能自己解出欄位。
拿畫面上的欄位重算 hash
SafeTx hash 是 Safe 交易的摘要,由 to、value、data、operation、safeTxGas 等欄位和 Safe 地址算出,也是 EIP-712 簽章簽的值。簽署前用獨立工具從畫面欄位重算,和硬體錢包要簽的 hash 比對,欄位被換過就對不上。
範例照 Safe 1.1.1 算,公開分析指出 Bybit 的 implementation 是 1.1.1,domain 只有 verifyingContract,沒有 chainId。畫面交易是虛構轉帳;送簽交易的 to、data、operation、safeTxGas 取自公開分析,nonce 虛構,hash 不會對應鏈上那筆。
from Crypto.Hash import keccak
def k(b: bytes) -> bytes:
return keccak.new(digest_bits=256, data=b).digest()
def word(x) -> bytes:
if isinstance(x, str):
x = int(x, 16)
return x.to_bytes(32, "big")
SAFE_TX_TYPEHASH = k(b"SafeTx(address to,uint256 value,bytes data,uint8 operation,"
b"uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,"
b"address gasToken,address refundReceiver,uint256 nonce)")
DOMAIN_TYPEHASH_V111 = k(b"EIP712Domain(address verifyingContract)") # Safe 1.1.1 沒有 chainId
def safe_tx_hash(safe: str, tx: dict) -> str:
domain = k(DOMAIN_TYPEHASH_V111 + word(safe))
body = k(SAFE_TX_TYPEHASH + word(tx["to"]) + word(tx["value"]) + k(bytes.fromhex(tx["data"]))
+ word(tx["operation"]) + word(tx["safeTxGas"]) + word(0) + word(0)
+ word(0) + word(0) + word(tx["nonce"]))
return "0x" + k(b"\x19\x01" + domain + body).hex()
SAFE = "0x1db92e2eebc8e0c075a02bea49a2935bcd2dfcf4"
# 畫面上顯示的交易:假設性的例行轉帳,收款地址是虛構的
shown = {"to": "0x00000000000000000000000000000000000000b0", "value": 10**18,
"data": "", "operation": 0, "safeTxGas": 0, "nonce": 0}
# 送到硬體錢包簽的交易:to、data、operation、safeTxGas 取自公開分析,nonce 是虛構的
signed = {"to": "0x96221423681a6d52e184d440a8efcebb105c7242", "value": 0,
"data": "a9059cbb" + "bdd077f651ebe7f7b3ce16fe5f2b025be2969516".rjust(64, "0") + "0" * 64,
"operation": 1, "safeTxGas": 45746, "nonce": 0}
DELEGATECALL_ALLOWLIST = {"0x00000000000000000000000000000000000000c1"} # 虛構:自己審過的 MultiSend 部署位址
def review(tx: dict) -> list[str]:
findings = []
if tx["operation"] == 1 and tx["to"].lower() not in DELEGATECALL_ALLOWLIST:
findings.append("delegatecall 目標不在允許清單:%s" % tx["to"])
if tx["data"][:8] == "a9059cbb" and tx["value"] == 0 and tx["operation"] == 1:
findings.append("用 delegatecall 呼叫 transfer(address,uint256),函式名稱不代表行為")
return findings
h_shown, h_signed = safe_tx_hash(SAFE, shown), safe_tx_hash(SAFE, signed)
print("domain type", "0x" + DOMAIN_TYPEHASH_V111.hex())
print("safetx type", "0x" + SAFE_TX_TYPEHASH.hex())
print("shown hash ", h_shown)
print("signed hash", h_signed)
print("match ", h_shown == h_signed)
for f in review(signed):
print("finding ", f)
domain type 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749
safetx type 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8
shown hash 0xb1e3a4fa5d96c2c924777da4cf354a710ddc318f0f6688cb5df071e43f85e718
signed hash 0x20eb91008f8bcae8517c47d4311fc4016fef61ed72406eb2eee404dd9ca2369c
match False
finding delegatecall 目標不在允許清單:0x96221423681a6d52e184d440a8efcebb105c7242
finding 用 delegatecall 呼叫 transfer(address,uint256),函式名稱不代表行為
兩個 typehash 和 Safe 1.1.1 原始碼的常數一致,整段算法還要拿已知鏈上交易驗過(需驗證)。比對工具須使用獨立於受害前端的資料,發現 hash 不符就拒簽。只有在剩餘簽署者無法湊足門檻時,拒簽才會擋下交易。
review 的兩項檢查都針對 operation = 1 的 delegatecall。Verichains 與 NCC Group 的反組譯顯示,攻擊合約的 transfer 沒做 ERC-20 轉帳,只把第一個參數寫進 slot 0。所以 to 是被 delegatecall 執行的程式,寫進 slot 0、成為新 implementation 的是 calldata 裡另一個地址。解碼器只看選擇器 0xa9059cbb 會顯示轉 0 個代幣,簽署者就可能漏掉錢包邏輯被換掉。
畫面如實顯示惡意交易但簽署者沒看懂時,hash 比對幫不上忙,得靠 delegatecall 允許清單這類規則,在簽署者之外強制執行。
解碼和模擬工具也要有可信來源
解碼器或模擬器若和受害前端共用雲端帳號或 API,結果不能當獨立佐證。查 calldata、目的地址、delegatecall 和 implementation 變更前,先確認工具來源。
- 解碼與 hash 計算用離線或本機工具,原始碼固定版本、自己建置,不從網頁即時載入。
- 硬體錢包優先用能顯示 EIP-712 欄位的 clear signing,至少要在裝置上看到 SafeTx hash 供比對。BlockSec 的說法是,硬體錢包看得到實際要簽的內容,Ledger 畫面上的細節應該會和網頁不同。多簽原始資料很難在小螢幕上讀,不能只靠裝置畫面。
- 簽署者可分用官方網頁、CLI 或自架前端,部署權限和資料來源也要分開才算隔離。
- 模擬結果只當參考,模擬服務也得驗證。
Mastra:發布權限和安裝腳本
2026 年 6 月的 Mastra 事件說明套件安裝怎麼讓開發機失陷,但機制和 Bybit 不同,解釋不了那台工作站最初怎麼被入侵。
Socket 調查指出,2026-06-17 大約 01:15 到 02:36 UTC,141 個 @mastra/* 套件透過 ehindero 發布惡意 patch 版本,Socket 說這是一個有正式發布權限的 npm 使用者帳號。新增的依賴 easy-day-js 仿冒 dayjs,1.11.22 版用 postinstall(套件裝完後自動執行的腳本)執行 setup.cjs,環境允許 install script 就會在 npm install 時跑,不必等應用程式 import。
Socket 說惡意程式會讀 Chrome、Edge、Brave 瀏覽紀錄,比對 166 個錢包擴充套件 ID,偵察主機並設定登入自動啟動。分析樣本只列出錢包擴充套件,沒複製它們的儲存資料,後續模組可能有更多能力。
Socket 沒寫帳號是被盜用還是持有者本人發布的,持有者與權限狀態需驗證;Mastra 或 npm 說明前,發布紀錄不足以認定帳號被盜,也不足以歸責個人或認定權限管理疏失。
可以在隔離環境驗收的防護
驗收在本機 fork 或測試網做,用測試 Safe 和假資產,每項假設某層已失陷,看後面的防護是否仍有效。
| 防護 | 驗收條件 |
|---|---|
| 簽署前用獨立工具重算 SafeTx hash | 前端被換成會改 to 與 operation 的版本時,至少一個簽署者比對失敗而拒簽,剩餘簽署者湊不到門檻 |
delegatecall 允許清單在簽署者之外強制執行 | 送一筆 operation = 1、目標不在清單的交易,guard 或簽署政策拒絕,不靠簽署者肉眼判斷。guard 是掛在 execTransaction 上的合約檢查,Safe 1.1.1 沒有,要用 guard 得換到支援的版本(版本需驗證) |
| proposal 送出時伺服器檢查 hash 與簽章一致 | 簽章對應的 hash 和 proposal 欄位算出的 hash 不同時,伺服器拒收 |
| 前端產物可追溯 | 正式環境載入的每個 JS 檔,雜湊值都能對回某次 CI 建置;手動上傳到 bucket 會觸發告警 |
| 開發機的雲端 session 權限分離 | 用開發者的一般 session 寫正式 bucket 會失敗,必須另走需要第二人核准的部署流程 |
| CI 與開發機安裝套件時不執行 install script | 在測試 repo 引入一個帶 postinstall 的套件,安裝後腳本沒有執行 |
和 2026 年幣安事件的暫時比較
2026 年 10 月 29 日的幣安熱錢包事件同樣有有效簽章把資產轉走,目前只能當待驗證對照。EIP-7702 讓一般地址簽一份授權,授權上鏈生效後,對這個地址的呼叫才改跑指定合約的程式。TRM Labs 觀察到 Ethereum 一筆交易含 50 個由幣安熱錢包簽的 EIP-7702 授權,另外九條鏈有不經委派的簽章轉帳,時間不一,包含較晚的 Avalanche、Polygon 轉出。TRM 評估證據「指向」熱錢包簽署基礎設施遭入侵,這仍不足以確定入侵點。
幣安共同執行長 Richard Teng 在 10 月 30 日說,攻擊者用 EIP-7702 繞過正常核准程序控制熱錢包,調查也找到原先懷疑的員工筆電以外的入侵點,完整經過仍在調查。兩案入口、錢包與簽署流程是否相同,須等正式鑑識報告(需驗證)。
尚未釐清的事
- Safe 開發者工作站最初如何遭入侵,Cointelegraph 只寫可能透過惡意程式,BlockSec 另提到社交工程。這兩種描述可以同時成立,但還不足以還原初始入侵過程,仍須讀 Safe 原始貼文與 Mandiant 報告。
- 惡意 JS 觸發清單的其他地址與用途,NCC Group 未完整解釋。
- Bybit 簽署者使用的裝置型號、韌體與實際畫面,須以 Bybit 或 Safe 說明為準;NCC Group 的 Trezor 截圖是研究示範。
補充筆記
- 簽章都有效,問題在簽署者看到的和實際簽的對不上。
- 金鑰獨立,簽署者共用前端時資訊來源仍只有一個。
- 用不依賴前端的工具重算 SafeTx hash 可抓出欄位替換;
delegatecall仍要另設允許清單在簽署者之外執行。 - Mastra 案中,有發布權限的帳號引入含 postinstall 的依賴,若環境允許安裝腳本執行,
npm install時就會執行惡意程式。帳號是否遭盜用,Socket 沒有說明。這個手法解釋不了 Safe 工作站最初怎麼被入侵。
延伸閱讀
- BleepingComputer〈Lazarus hacked Bybit via breached Safe{Wallet} developer machine〉:2025-02-26,轉述 Sygnia、Verichains 與 Safe,涵蓋 S3 竄改與復原、Bybit 鑑識及 Ledger 整合;原始報告未直接讀取。
- Cointelegraph〈Safe Wallet releases Bybit hack post-mortem〉:2025-03-06,轉述 Safe 與 Mandiant 對工作站、AWS session token 與 MFA 的調查。
- NCC Group〈In-Depth Technical Analysis of the Bybit Hack〉:2025-03-10,研究者分析惡意 JS 的觸發、欄位替換與簽完換回,以及盲簽的判斷。
- Verichains〈Technical Analysis of the Bybit Hot Wallet Exploit〉:2025-02-25,交易時間、地址與
transfer寫 slot 0 的反組譯;文中稱 hot wallet,Bybit 與 Sygnia 稱冷錢包。 - SlowMist〈Bybit’s $1.5 Billion Theft Unveiled〉:檔名與 Wayback 快照時間。
- BlockSec〈Bybit Incident: A Web2 Breach Enables the Largest Crypto Hack in History〉:Safe proxy、門檻、交易列表、換邏輯後不驗簽章的 sweep 函式,以及硬體錢包小螢幕難讀的問題。
- Safe 原始碼〈GnosisSafe.sol v1.1.1〉:範例核對的
DOMAIN_SEPARATOR_TYPEHASH與SAFE_TX_TYPEHASH。 - Socket〈Mastra npm packages compromised〉:套件數、時間、
easy-day-js與postinstall的原始調查。 - TRM Labs〈North Korea steals USD 6.36 billion from Binance〉:2026-10-29,鏈上觀察與 TRM 評估;截至該文發布時,幣安尚未公布根因。
- CoinDesk〈Binance discovers multiple breach points in $6.3 billion attack〉:2026-10-30,Richard Teng 說法與調查未決事項。
- FBI/IC3〈North Korea Responsible for $1.5 Billion Bybit Hack〉:2025-02-26,北韓歸因與 TraderTraitor 名稱。
- Wikipedia〈Bybit〉:日期、金額與 FBI 歸因的引用索引,不作根因證據。
想法與技術判斷出自 Sheng,和 Claude 一起起草 · 題目由 Sheng 選定,內文、案例整理與範例由 Claude 撰寫,範例在 Linux 上以 Python 3.13.18 與 pycryptodome 4.2.0 實測。