コンテンツにスキップ

QBridgeエクスプロイトの技術分析

これは、オンチェーン証拠、コントラクトソースコード、および攻撃時に記録されたスクリーンショット証拠に基づくエクスプロイトの完全な技術的解説です。被害者コミュニティによる中国語の原文分析を翻訳し、抽出されたすべてのデータとともに拡充しました。


役割アドレス
攻撃者0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7
QBridgeコントラクト(Ethereum)0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6
QBridgeハンドラー(BSC)0x4d8ae68fcae98bf93299548545933c0d273ba23a
ETH resourceID0x0000000000000000000000002f422fe9ea622049d6f73f81a906b9b8cff03b7f01

ステップ1 — BSCは最初の犯行現場ではなかった

Section titled “ステップ1 — BSCは最初の犯行現場ではなかった”

調査者が最初にBSC上の攻撃者のアドレスを確認したとき、何かが明らかにおかしかった:

BSCScanに表示されたQubitFin Exploiterの借入トランザクション — 準備段階なし

攻撃者はいきなりborrow()を実行していた — 準備もなく、フラッシュローンもなく、コントラクトのデプロイもなかった。これはBSCが攻撃の起点ではないことを意味していた。攻撃者はレンディングプロトコルに触れる前に、すでにqXETHトークンを保有していたのだ。

これらのqXETHトークンを遡ると、ブリッジリレイヤーによってミントされたことが判明した — つまり起点はEthereumだった。


ステップ2 — BSC上のvoteProposalトランザクション

Section titled “ステップ2 — BSC上のvoteProposalトランザクション”

qXETHミントトランザクションの一つ:

トランザクション: 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc

BSCScan voteProposalトランザクション — ヌルアドレスから攻撃者へxETHがミントされている

このトランザクションの主な観察事項:

  • 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上の攻撃者のデポジット”

Etherscanに表示された攻撃者からQBridgeへの複数のゼロ値Deposit呼び出し

攻撃者はEthereum QBridgeコントラクト(0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6)に対して複数回のdeposit()呼び出しを行った:

トランザクションハッシュブロック
0x3dfa33b5...140902340 ETH
0x58020654...140902300 ETH
0x501f8541...140902300 ETH
0xa6282e60...140902230 ETH
0x94031569...140902160 ETH
0xbdedb13d...140902100 ETH
0xeb9f622e...140902010 ETH

すべてのトランザクションのETH値は0だった。メソッド: deposit。各トランザクションはDepositイベントをトリガーし、リレイヤーはそれを忠実に処理した。

これらのトランザクションの一つの詳細:

deposit()呼び出しの詳細 — 0 Ether、ETH resourceIDで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つのデポジット関数があった:

QBridgeコントラクトコード — deposit()とdepositETH()が同じDepositイベントを発行している

  • depositETH() — ETHの正しいパス、msg.value > 0が必須
  • deposit() — ERC-20トークン用に設計されており、ETHは不要

**どちらもまったく同じDepositイベント型を発行していた。**リレイヤーはDepositイベントをリッスンしており、どちらの関数がトリガーしたかを区別できなかった。


ステップ5 — ハンドラーコードとホワイトリスト

Section titled “ステップ5 — ハンドラーコードとホワイトリスト”

QBridgeHandlerコード — deposit()関数、128行目にホワイトリストチェック、135行目にsafeTransferFrom

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にマッピングされている

ETH resourceID0x0000000000000000000000000000000000000000 — ゼロアドレス — にマッピングされていた。これは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のEOAトリック

このまったく同じトリックは2019年の0x Protocolセキュリティアップデートで公に文書化されていた。Mound Inc.は2022年にこれを考慮せずブリッジをデプロイした。


ステップ8 — Borrow関数は正しかったが、すでに手遅れだった

Section titled “ステップ8 — Borrow関数は正しかったが、すでに手遅れだった”

Qore borrow()関数 — Compound型、正しいborrowAllowedチェック

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 — 決定的証拠:攻撃前のパラメータ変更”

これはいまだ説明されていない詳細である。

Ethereumトランザクション — 2021年12月1日にdeposit()関数でWETHが入金されている — resourceIDが変更される前

攻撃前に、誰かがdeposit()関数の履歴を調査した。彼らが発見したのは:

2021年12月1日 — 正当なトランザクション(0xc85df3fbfee7a8541e9fd354e98df78dca21ac6be40b929ff5da52ea8eab80de):

  • ブロック: 13719888
  • From: 0xbee3971293374d0b4db7bf1654936951e5bdfe5a6
  • To: QBridgeコントラクト 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6
  • 転送されたトークン: 0.1 WETH (当時$241.81)
  • 関数: 同じresourceIDを使用したdeposit()

2021年12月時点では、そのresourceIDdeposit()を呼び出すと実際のWETHが転送されていた — resourceIDが実際のWETHコントラクトアドレスにマッピングされていたからだ。

2021年12月から2022年1月28日の間のどこかで — オーナー限定関数が呼び出され、resourceIDのマッピングがWETHのコントラクトアドレスからゼロアドレスに再割り当てされた。

resourceIDパラメータがオーナー関数によって変更されたことを確認するコード

コントラクトオーナーのみがこの変更を行うことができた。タイムロックは不要だった。アナウンスもなかった。この単一の不可視なパラメータ変更こそが、動作するブリッジを悪用可能なものに変えたのだ。


障害影響
deposit()depositETH()が同じイベントを発行リレイヤーが実際のデポジットと偽のデポジットを区別できない
EOAのサイレント成功が考慮されていないゼロアドレスへのsafeTransferFromが転送なしで通過する
ゼロアドレスがホワイトリストに登録EOA呼び出しがホワイトリストチェックを通過する
未監査の本番デプロイ外部レビュアーが上記のいずれも検出しなかった
オーナー関数にタイムロックなしresourceIDがサイレントに、即座に、不可視に再マッピングされた
攻撃前のresourceID再マッピングエクスプロイトを可能にした変更
イベントを盲目的に信頼するリレイヤーオフチェーンサービスがオンチェーンの価値を検証せずにトークンをミントする


決定的証拠トランザクション — 2021年12月13日に呼び出されたsetResource()

Section titled “決定的証拠トランザクション — 2021年12月13日に呼び出されたsetResource()”

これが最も重要な証拠である。

640_7.jpg — 2021年12月13日のsetResource()オーナー呼び出し、ETH resourceIDをゼロアドレスに再マッピング

トランザクション: 0xe3da555d506638bd7b697c0bdf7920be8defc9a175cd35bf72fb10bc77167b66

フィールド
ステータス✅ 成功
ブロック13797391
タイムスタンプ2021年12月13日 14:31:21 UTC
From0xbee3971293374d0b4db7bf1654936951e5bdfe5a6
To0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6(QBridge、Ethereum)
0 ETH
関数setResource(address handlerAddress, bytes32 resourceID, address tokenAddress)

デコードされた入力引数:

#パラメータ
0handlerAddress0x17B7163cf1Dbb6286262ddc68b553D899B893f526
1resourceID0x0000000000000000000000002f422fe9ea622049d6e73f81a906b9b8cff03b7f01
2tokenAddress0x0000000000000000000000000000000000000000

**setResource()onlyOwner関数である。**Mound Inc.チームのみが呼び出すことができた。

2021年12月13日 — ハッキングの45日前 — コントラクトオーナーはETH resourceIDtokenAddressマッピングを意図的にゼロアドレスに変更した。この呼び出しの前は、同じresourceIDはWETHトークンコントラクトを指していた。この呼び出しの後は、何も指さなくなった — EOAサイレント成功エクスプロイトを可能にしたのだ。

voteProposalコードにおけるリレイヤーのみが呼び出し可能であることの証拠:

640_1.jpg — voteProposal関数: onlyRelayersモディファイアがハイライトされている

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()コード:

640_7.jpg — setResource() onlyOwner関数

function setResource(address handlerAddress, bytes32 resourceID, address tokenAddress) external onlyOwner {
resourceIDToHandlerAddress[resourceID] = handlerAddress;
IQBridgeHandler(handlerAddress).setResource(resourceID, tokenAddress);
}

qXETHミントシーケンス(BSCトークン転送)

Section titled “qXETHミントシーケンス(BSCトークン転送)”

640_8.jpg — QubitFin Exploiterが複数ラウンドにわたりヌルアドレスからミントされたqXETHを受け取っている

攻撃者はヌルアドレスから直接ミントされたqXETHを複数ラウンドにわたり受け取った:

トランザクションハッシュ数量トークンソース
0xd8bba15555...999qXETHヌルアドレス → Exploiter
0xf6008ab482...499qXETHヌルアドレス → Exploiter
0xcfa4379af6...140ETH (BEP-20)0xb4b778… → Exploiter
0x61ca8bc28f...190qXETHヌルアドレス → Exploiter
0x881a68c9c9...0.1qXETHヌルアドレス → Exploiter
0x8c5877d1b6...0.1qXETHヌルアドレス → Exploiter

各「ヌルアドレス → Exploiter」のqXETHミントは、Ethereum上の偽のdeposit()呼び出しに対応している。