# 簽章都正確，交易為何還是錯的：Bybit 與 Safe 的簽署供應鏈

> 2025 年 Bybit 被盜時簽章都有效，問題出在 Safe 前端被竄改，簽署者看到的交易和實際簽的不同。本文整理公開調查，示範用畫面欄位重算 SafeTx hash，並列出能在測試環境驗收的防護。

原文：https://sheng.page/posts/bybit-safe-signing-supply-chain/ · 發布：2026-11-01 · 作者：Sheng

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。

![Bybit 事件的公開調查路徑：攻擊者註冊 MFA 失敗後入侵 Safe 開發者的 macOS 工作站，沿用還有效的 AWS session token，S3 上的前端 JS 在 2 月 19 日被改，簽署者打開 app.safe.global 看到例行轉帳，硬體錢包收到 operation = 1 的交易，3 個有效簽章在 2 月 21 日執行，delegatecall 改寫 slot 0 後資產被轉出，約兩分鐘後 S3 換回乾淨的 JS](/images/bybit-safe-signing-supply-chain/attack-path.zh.png)

## 三種可能的失效，修法不一樣

本案的公開分析沒有指出攻擊利用了 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 不會對應鏈上那筆。

```python
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)
```

```text
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](https://www.bleepingcomputer.com/news/security/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](https://cointelegraph.com/news/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](https://www.nccgroup.com/research/in-depth-technical-analysis-of-the-bybit-hack/)〉：2025-03-10，研究者分析惡意 JS 的觸發、欄位替換與簽完換回，以及盲簽的判斷。
- Verichains〈[Technical Analysis of the Bybit Hot Wallet Exploit](https://blog.verichains.io/p/technical-analysis-of-the-bybit-hot)〉：2025-02-25，交易時間、地址與 `transfer` 寫 slot 0 的反組譯；文中稱 hot wallet，Bybit 與 Sygnia 稱冷錢包。
- SlowMist〈[Bybit's $1.5 Billion Theft Unveiled](https://slowmist.medium.com/bybits-1-5-billion-theft-unveiled-safe-wallet-front-end-code-tampered-84b78f0fa9c2)〉：檔名與 Wayback 快照時間。
- BlockSec〈[Bybit Incident: A Web2 Breach Enables the Largest Crypto Hack in History](https://blocksec.com/blog/bybit-incident-a-web2-breach-enables-the-largest-crypto-hack-in-history)〉：Safe proxy、門檻、交易列表、換邏輯後不驗簽章的 sweep 函式，以及硬體錢包小螢幕難讀的問題。
- Safe 原始碼〈[GnosisSafe.sol v1.1.1](https://github.com/safe-global/safe-smart-account/blob/v1.1.1/contracts/GnosisSafe.sol)〉：範例核對的 `DOMAIN_SEPARATOR_TYPEHASH` 與 `SAFE_TX_TYPEHASH`。
- Socket〈[Mastra npm packages compromised](https://socket.dev/blog/mastra-npm-packages-compromised)〉：套件數、時間、`easy-day-js` 與 `postinstall` 的原始調查。
- TRM Labs〈[North Korea steals USD 6.36 billion from Binance](https://www.trmlabs.com/resources/blog/north-korea-steals-usd-6-36-billion-from-binance-in-the-largest-hack-in-history)〉：2026-10-29，鏈上觀察與 TRM 評估；截至該文發布時，幣安尚未公布根因。
- CoinDesk〈[Binance discovers multiple breach points in $6.3 billion attack](https://www.coindesk.com/tech/2026/10/30/binance-uncovers-wider-breach-in-usd6-3-billion-exploit-targets-sunday-withdrawal-restart)〉：2026-10-30，Richard Teng 說法與調查未決事項。
- FBI／IC3〈[North Korea Responsible for $1.5 Billion Bybit Hack](https://www.ic3.gov/PSA/2025/PSA250226)〉：2025-02-26，北韓歸因與 TraderTraitor 名稱。
- Wikipedia〈[Bybit](https://en.wikipedia.org/wiki/Bybit)〉：日期、金額與 FBI 歸因的引用索引，不作根因證據。

> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 題目由 Sheng 選定，內文、案例整理與範例由 Claude 撰寫，範例在 Linux 上以 Python 3.13.18 與 pycryptodome 4.2.0 實測。