QBridgeエクスプロイトの技術分析
これは、オンチェーン証拠、コントラクトソースコード、および攻撃時に記録されたスクリーンショット証拠に基づくエクスプロイトの完全な技術的解説です。被害者コミュニティによる中国語の原文分析を翻訳し、抽出されたすべてのデータとともに拡充しました。
主要アドレス
Section titled “主要アドレス”| 役割 | アドレス |
|---|---|
| 攻撃者 | 0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7 |
| QBridgeコントラクト(Ethereum) | 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 |
| QBridgeハンドラー(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
- From:
0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7(攻撃者) - To:
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6(QBridge Ethereum) - 値: 0 Ether ($0.00)
- 呼び出された関数:
deposit(uint8 destinationDomainID, bytes32 resourceID, bytes data) - 攻撃時のETH価格: $2,425.83
ステップ4 — 2つの関数、1つのイベント
Section titled “ステップ4 — 2つの関数、1つのイベント”QBridgeコントラクトには2つのデポジット関数があった:

depositETH()— ETHの正しいパス、msg.value > 0が必須deposit()— ERC-20トークン用に設計されており、ETHは不要
**どちらもまったく同じDepositイベント型を発行していた。**リレイヤーはDepositイベントをリッスンしており、どちらの関数がトリガーしたかを区別できなかった。
ステップ5 — ハンドラーコードとホワイトリスト
Section titled “ステップ5 — ハンドラーコードとホワイトリスト”
QBridgeHandler.deposit()関数:
function deposit( bytes32 resourceID, address depositer, bytes calldata data) external override { address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; // トークンアドレスを取得 require(_contractWhitelist[tokenAddress], "not whitelisted"); // 128行目: ホワイトリストチェック // ... ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount); // 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の状態で、ハンドラーは以下を実行した:
ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount);ゼロアドレスはEOA — コントラクトコードを持たない外部所有アカウントである。
EVMでは:コントラクトコードを持たないアドレスの任意の関数を呼び出すと、サイレントに成功する — リバートもエラーも実際の実行もない。
safeTransferFromの呼び出しは「成功」した。何も移動しなかった。ETHもトークンも。しかしコードはすべてが正常であるかのように続行し — 正当なDepositイベントを発行した。

このまったく同じトリックは2019年の0x Protocolセキュリティアップデートで公に文書化されていた。Mound Inc.は2022年にこれを考慮せずブリッジをデプロイした。
ステップ8 — Borrow関数は正しかったが、すでに手遅れだった
Section titled “ステップ8 — Borrow関数は正しかったが、すでに手遅れだった”
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
- From:
0xbee3971293374d0b4db7bf1654936951e5bdfe5a6 - To: QBridgeコントラクト
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 - 転送されたトークン: 0.1 WETH (当時$241.81)
- 関数: 同じ
resourceIDを使用したdeposit()
2021年12月時点では、そのresourceIDでdeposit()を呼び出すと実際のWETHが転送されていた — resourceIDが実際のWETHコントラクトアドレスにマッピングされていたからだ。
2021年12月から2022年1月28日の間のどこかで — オーナー限定関数が呼び出され、resourceIDのマッピングがWETHのコントラクトアドレスからゼロアドレスに再割り当てされた。

コントラクトオーナーのみがこの変更を行うことができた。タイムロックは不要だった。アナウンスもなかった。この単一の不可視なパラメータ変更こそが、動作するブリッジを悪用可能なものに変えたのだ。
要約:7つの同時障害
Section titled “要約:7つの同時障害”| 障害 | 影響 |
|---|---|
deposit()とdepositETH()が同じイベントを発行 | リレイヤーが実際のデポジットと偽のデポジットを区別できない |
| EOAのサイレント成功が考慮されていない | ゼロアドレスへのsafeTransferFromが転送なしで通過する |
| ゼロアドレスがホワイトリストに登録 | EOA呼び出しがホワイトリストチェックを通過する |
| 未監査の本番デプロイ | 外部レビュアーが上記のいずれも検出しなかった |
| オーナー関数にタイムロックなし | resourceIDがサイレントに、即座に、不可視に再マッピングされた |
攻撃前のresourceID再マッピング | エクスプロイトを可能にした変更 |
| イベントを盲目的に信頼するリレイヤー | オフチェーンサービスがオンチェーンの価値を検証せずにトークンをミントする |
- 攻撃トランザクション(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 |
| From | 0xbee3971293374d0b4db7bf1654936951e5bdfe5a6 |
| To | 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 | ヌルアドレス → Exploiter |
0xf6008ab482... | 499 | qXETH | ヌルアドレス → Exploiter |
0xcfa4379af6... | 140 | ETH (BEP-20) | 0xb4b778… → Exploiter |
0x61ca8bc28f... | 190 | qXETH | ヌルアドレス → Exploiter |
0x881a68c9c9... | 0.1 | qXETH | ヌルアドレス → Exploiter |
0x8c5877d1b6... | 0.1 | qXETH | ヌルアドレス → Exploiter |
各「ヌルアドレス → Exploiter」のqXETHミントは、Ethereum上の偽のdeposit()呼び出しに対応している。