QBridge 漏洞技術分析
這是根據鏈上證據、合約原始碼及攻擊發生時所擷取的截圖證據,所進行的完整技術拆解。原文為受害者社群撰寫的中文分析;此處翻譯並擴充,附上所有已提取的數據。
| 角色 | 地址 |
|---|---|
| 攻擊者 | 0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7 |
| QBridge 合約(Ethereum) | 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 |
| QBridge Handler(BSC) | 0x4d8ae68fcae98bf93299548545933c0d273ba23a |
| ETH resourceID | 0x0000000000000000000000002f422fe9ea622049d6f73f81a906b9b8cff03b7f01 |
步驟 1 — BSC 並非第一現場
Section titled “步驟 1 — BSC 並非第一現場”當調查人員首次在 BSC 上查看攻擊者的地址時,立即發現異常:

攻擊者直接呼叫了 borrow()——沒有準備、沒有閃電貸、沒有合約部署。這意味著 BSC 並非攻擊的起點。攻擊者在觸及借貸協議之前就已經持有 qXETH 代幣。
追溯這些 qXETH 代幣,發現它們是由跨鏈橋中繼器鑄造的——這代表攻擊源頭在 Ethereum。
步驟 2 — BSC 上的 voteProposal 交易
Section titled “步驟 2 — BSC 上的 voteProposal 交易”其中一筆 qXETH 鑄造交易:
交易: 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc

此交易的關鍵觀察:
- 在 QBridge BSC 合約上呼叫了
voteProposal(uint8 chainID, uint64 depositNonce, bytes32 resourceID, bytes data) - 發生了 3 筆代幣轉移:
- 空地址 →
0xa6b1bb...— 59,900 Cross-Chain xETH(鑄造) 0xa6b1bb...→0xfd7a55...— 59,900 xETH(跨鏈轉移)- 空地址 → QubitFin Exploiter — 59,900 qXETH(作為抵押品鑄造)
- 空地址 →
- 價值:0 BNB — 沒有真正的 ETH 被存入任何地方
voteProposal 函數僅能由中繼器呼叫。中繼器呼叫它是因為在 Ethereum 上偵測到了 Deposit 事件。那個事件是偽造的。
步驟 3 — 攻擊者在 Ethereum 上的存款
Section titled “步驟 3 — 攻擊者在 Ethereum 上的存款”
攻擊者對 Ethereum QBridge 合約(0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6)發起了多次 deposit() 呼叫:
| 交易雜湊 | 區塊 | 價值 |
|---|---|---|
0x3dfa33b5... | 14090234 | 0 ETH |
0x58020654... | 14090230 | 0 ETH |
0x501f8541... | 14090230 | 0 ETH |
0xa6282e60... | 14090223 | 0 ETH |
0x94031569... | 14090216 | 0 ETH |
0xbdedb13d... | 14090210 | 0 ETH |
0xeb9f622e... | 14090201 | 0 ETH |
每一筆交易的 ETH 價值都是 0。方法:deposit。每一筆都觸發了一個 Deposit 事件,中繼器忠實地處理了這些事件。
其中一筆交易的詳情:

- 區塊: 14090216
- 時間戳: 2022 年 1 月 27 日 21:45:32 UTC
- 發送方:
0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7(攻擊者) - 接收方:
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6(QBridge Ethereum) - 價值: 0 Ether($0.00)
- 呼叫函數:
deposit(uint8 destinationDomainID, bytes32 resourceID, bytes data) - 攻擊時 ETH 價格: $2,425.83
步驟 4 — 兩個函數,同一個事件
Section titled “步驟 4 — 兩個函數,同一個事件”QBridge 合約有兩個存款函數:

depositETH()— ETH 的正確路徑,要求msg.value > 0deposit()— 設計用於 ERC-20 代幣,不需要 ETH
兩者發出的 Deposit 事件類型完全相同。 中繼器監聽 Deposit 事件,無法區分是哪個函數觸發的。
步驟 5 — Handler 程式碼與白名單
Section titled “步驟 5 — Handler 程式碼與白名單”
QBridgeHandler.deposit() 函數:
function deposit( bytes32 resourceID, address depositer, bytes calldata data) external override { address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; // get token address require(_contractWhitelist[tokenAddress], "not whitelisted"); // line 128: whitelist check // ... ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount); // line 135}對於 ETH 的 resourceID,合約回傳 tokenAddress = 0x0000000000000000000000000000000000000000(零地址)。
步驟 6 — 零地址在白名單上
Section titled “步驟 6 — 零地址在白名單上”
調查人員查詢合約:零地址是否在白名單中?
是的。 確實在。
這是必要的,因為 depositETH() 也使用相同的白名單機制,以零地址作為 ETH 的佔位符。但這造成了一個致命的副作用:零地址在 deposit() 中也通過了白名單檢查。

ETH resourceID 被映射到 0x0000000000000000000000000000000000000000——零地址——這是透過查詢 resourceIDToTokenContractAddress 確認的。
步驟 7 — 呼叫 EOA 靜默成功
Section titled “步驟 7 — 呼叫 EOA 靜默成功”當 tokenAddress = 0x0000...0000 時,handler 執行了:
ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount);零地址是一個 EOA——外部擁有帳戶,沒有合約程式碼。
在 EVM 中:對一個沒有合約程式碼的地址呼叫任何函數,都會靜默成功——不會回滾、不會報錯、不會實際執行。
safeTransferFrom 呼叫「成功了」。什麼都沒有移動。沒有 ETH,沒有代幣。但程式碼繼續執行,彷彿一切正常——並發出了一個合法的 Deposit 事件。

這個技巧早在 0x Protocol 2019 年的安全更新中就已公開記錄。Mound Inc. 在 2022 年部署其跨鏈橋時並未考慮到這一點。
步驟 8 — 借貸函數本身是正確的,但為時已晚
Section titled “步驟 8 — 借貸函數本身是正確的,但為時已晚”
function borrow(address qToken, uint amount) external override onlyListedMarket(qToken) nonReentrant { _enterMarket(qToken, msg.sender); require(IQValidator(qValidator).borrowAllowed(qToken, msg.sender, amount), "Qore: cannot borrow");
IQToken(payable(qToken)).borrow(msg.sender, amount); qDistributor.notifyBorrowUpdated(qToken, msg.sender);}借貸合約本身是正確的——borrowAllowed() 正確地檢查了抵押品價值。但 qXETH 代幣在鏈上看起來是合法的。詐騙已經在跨鏈橋層面發生。當 borrow() 執行時,攻擊者已持有看似真實但實際上毫無支撐的抵押品。
步驟 9 — 關鍵證據:攻擊前的參數變更
Section titled “步驟 9 — 關鍵證據:攻擊前的參數變更”這是從未被解釋的細節。

在攻擊發生之前,有人調查了 deposit() 函數的歷史。他們發現:
2021 年 12 月 1 日 — 一筆合法交易(0xc85df3fbfee7a8541e9fd354e98df78dca21ac6be40b929ff5da52ea8eab80de):
- 區塊:13719888
- 發送方:
0xbee3971293374d0b4db7bf1654936951e5bdfe5a6 - 接收方:QBridge 合約
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 - 轉移的代幣:0.1 WETH(當時價值 $241.81)
- 函數:使用相同
resourceID的deposit()
在 2021 年 12 月,使用該 resourceID 的 deposit() 轉移了真正的 WETH——因為 resourceID 映射到了真正的 WETH 合約地址。
在 2021 年 12 月到 2022 年 1 月 28 日之間——有人呼叫了一個僅限擁有者的函數,將 resourceID 的映射從 WETH 合約地址重新指向了零地址。

只有合約擁有者才能進行此更改。不需要時間鎖。沒有公告。這一個不可見的參數變更,就是將一座正常運作的跨鏈橋變成可被利用的關鍵。
總結:七個同時發生的缺陷
Section titled “總結:七個同時發生的缺陷”| 缺陷 | 影響 |
|---|---|
deposit() 和 depositETH() 發出相同的事件 | 中繼器無法區分真實與偽造的存款 |
| 未考慮 EOA 靜默成功的特性 | 對零地址的 safeTransferFrom 在無轉移的情況下通過 |
| 零地址被列入白名單 | EOA 呼叫通過了白名單檢查 |
| 未經審計的生產環境部署 | 沒有外部審查員發現上述任何問題 |
| 擁有者函數無時間鎖 | resourceID 被靜默地、即時地、不可見地重新映射 |
攻擊前 resourceID 被重新映射 | 使漏洞利用成為可能的關鍵變更 |
| 盲目信任事件的中繼器 | 鏈下服務在未驗證鏈上價值的情況下鑄造代幣 |
外部參考資料
Section titled “外部參考資料”- 攻擊交易(BSC):
0x8c5877d1... - 攻擊者 Etherscan 頁面:
0xd01ae1... - QBridge(Ethereum):
0x20e5e3... - EOA 零地址技巧(0x Protocol,2019): blog.0xproject.com
- 原始中文分析: qbt.wiki
關鍵證據交易 — 2021 年 12 月 13 日呼叫的 setResource()
Section titled “關鍵證據交易 — 2021 年 12 月 13 日呼叫的 setResource()”這是最關鍵的證據。

交易: 0xe3da555d506638bd7b697c0bdf7920be8defc9a175cd35bf72fb10bc77167b66
| 欄位 | 值 |
|---|---|
| 狀態 | ✅ 成功 |
| 區塊 | 13797391 |
| 時間戳 | 2021 年 12 月 13 日 14:31:21 UTC |
| 發送方 | 0xbee3971293374d0b4db7bf1654936951e5bdfe5a6 |
| 接收方 | 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6(QBridge,Ethereum) |
| 價值 | 0 ETH |
| 函數 | setResource(address handlerAddress, bytes32 resourceID, address tokenAddress) |
解碼後的輸入參數:
| # | 參數 | 值 |
|---|---|---|
| 0 | handlerAddress | 0x17B7163cf1Dbb6286262ddc68b553D899B893f526 |
| 1 | resourceID | 0x0000000000000000000000002f422fe9ea622049d6e73f81a906b9b8cff03b7f01 |
| 2 | tokenAddress | 0x0000000000000000000000000000000000000000 |
setResource() 是一個 onlyOwner 函數。 只有 Mound Inc. 團隊才能呼叫。
在 2021 年 12 月 13 日——駭客事件發生前 45 天——合約擁有者刻意將 ETH resourceID 的 tokenAddress 映射更改為零地址。在此呼叫之前,同一 resourceID 指向 WETH 代幣合約。在此呼叫之後,它指向了虛無——從而啟用了 EOA 靜默成功的漏洞利用。
先前的 voteProposal 程式碼顯示只有中繼器才能呼叫:

function voteProposal(uint8 originDomainID, uint64 depositNonce, bytes32 resourceID, bytes calldata data) external onlyRelayers notPaused { address handlerAddress = resourceIDToHandlerAddress[resourceID]; require(handlerAddress != address(0), "QBridge: invalid handler"); // ... if (proposal._status == ProposalStatus.Passed) { executeProposal(originDomainID, depositNonce, resourceID, data, true); return; } // ... if (proposal._status == ProposalStatus.Inactive) { proposal = Proposal({ status: ProposalStatus.Active, _yesVotes: 0, _yesVotesTotal: 0, _proposedBlock: uint40(block.number) });以及改變一切的 setResource() 程式碼:

function setResource(address handlerAddress, bytes32 resourceID, address tokenAddress) external onlyOwner { resourceIDToHandlerAddress[resourceID] = handlerAddress; IQBridgeHandler(handlerAddress).setResource(resourceID, tokenAddress);}qXETH 鑄造序列(BSC 代幣轉移)
Section titled “qXETH 鑄造序列(BSC 代幣轉移)”
攻擊者接收了多輪直接從空地址鑄造的 qXETH:
| 交易雜湊 | 數量 | 代幣 | 來源 |
|---|---|---|---|
0xd8bba15555... | 999 | qXETH | 空地址 → 攻擊者 |
0xf6008ab482... | 499 | qXETH | 空地址 → 攻擊者 |
0xcfa4379af6... | 140 | ETH (BEP-20) | 0xb4b778… → 攻擊者 |
0x61ca8bc28f... | 190 | qXETH | 空地址 → 攻擊者 |
0x881a68c9c9... | 0.1 | qXETH | 空地址 → 攻擊者 |
0x8c5877d1b6... | 0.1 | qXETH | 空地址 → 攻擊者 |
每一筆「空地址 → 攻擊者」的 qXETH 鑄造,都對應著 Ethereum 上的一筆偽造 deposit() 呼叫。