☰
以太坊 CREATE2 操作码原理与 CTF 利用:在同一地址反复部署不同合约的攻击技巧(ctf-wiki)
2026/9/25 7:47:33 网站建设 项目流程
  • 文档
  • 网络安全
  • 教程

【免费下载链接】ctf-wiki

Come and join us, we need you!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载

CREATE2是 EIP-1014 引入的以太坊合约创建操作码,它用0xff ++ address ++ salt ++ keccak256(init_code)的确定性哈希取代了依赖nonce的CREATE地址推导,使合约地址变得完全可控、可预先计算。在 CTF 区块链题目中,这一特性最常见的利用方式是:保持init_code不变、让构造函数返回的运行时字节码可变,从而在同一个地址上先后部署字节码完全不同的合约,绕过各类基于地址或字节码的校验。阅读本篇后,你将掌握CREATE与CREATE2的地址推导原理、二者差异、地址预计算脚本,以及 2019 Balsn CTF Creativity 题目的完整 Solidity PoC 复现思路。

背景:为何 CREATE2 会在以太坊与 CTF 中被关注

在 CTF-Wiki 的区块链板块中,以太坊(Ethereum)安全是占比最高、也是最常出现的主题(见 区块链介绍)。智能合约代码一旦上链便难以篡改,因此合约部署地址的生成规则直接决定了攻击者能在多大程度上"预测"或"重放"合约。以太坊虚拟机(EVM)为开发者提供了两种创建合约的操作码:

  • CREATE:最传统的合约创建方式,地址由创建者地址与账户nonce决定;
  • CREATE2:在"君士坦丁堡"(Constantinople)硬分叉中引入的新操作码,地址由创建者地址、盐值(salt)与创建代码(init_code)共同决定,完全不再依赖nonce。

两者都输出一个新合约的地址,但CREATE2让地址生成具有"确定性 + 可预计算"的能力,这正是其衍生出大量攻击技巧的根源。

CREATE 的地址推导:nonce 驱动的确定性

在理解CREATE2之前,需要先理解旧的CREATE操作码。无论使用外部账户(EOA)部署,还是由合约通过CREATE创建子合约,新合约的地址都由创建者的账户地址以及该账户的nonce共同决定。

以太坊中每个账户都维护着一个nonce字段(Ethereum Basics 将其定义为"已执行交易总数,用来标示该账户发出的交易数量"):

  • 对外部账户而言,每发送一笔交易,nonce就会+1;
  • 对合约账户而言,每通过CREATE创建一个子合约,nonce就会+1。

新合约地址的计算公式为:

keccak256(rlp.encode(address, nonce))[12:]

即:将创建者地址与当前nonce做 RLP 编码,取keccak256哈希结果的后 20 字节(160 位)作为新合约地址。

由于nonce会随交易或子合约创建次数而递增,同一账户两次创建合约得到的地址必然不同,而且地址无法在创建之前被精确预知(至少不直观)。这种"不可控性"正是CREATE2要解决的痛点。

CREATE2 的地址推导:EIP-1014 的确定性公式

CREATE2摒弃了对nonce的依赖,改为对以下三个参数做哈希运算:

参数含义可控性
address合约创建者的地址(部署方)部署方确定,可控
salt由调用方显式传入的 32 字节混淆值完全可控
init_code合约的创建代码(用于部署合约的字节码)可控,是整个技巧的关键

其地址计算公式为:

keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:]

公式中0xff是一个固定的单字节前缀,用于与CREATE的地址空间(以及普通交易地址推导)作区分;随后拼接创建者地址、salt,再拼接init_code本身的keccak256哈希;最终对整个结果再取一次keccak256,并截取低 160 位(后 20 字节)作为新合约地址。

一个容易忽略的关键细节:init_code 与运行时字节码

计算地址时使用的最后一个参数并非合约最终存储的运行时字节码(runtime code),而是创建代码(init code)。创建代码是"用来创建合约的那段代码",它在合约创建交易执行期间运行,执行完毕后通过RETURN(0xf3)返回一段数据,这段数据才是写入链上的运行时字节码(参见 Ethereum Opcodes 中RETURN的语义:return memory[offset:offset+length])。

这一细节带来了极具攻击性的推论:只要init_code保持不变,无论构造函数最终返回什么运行时字节码,计算出的地址都完全一致。于是攻击者可以控制构造函数返回任意字节码,同时保持init_code固定,从而在同一个地址上反复部署、覆盖出完全不同行为的合约。

CREATE2允许合约在部署后被"重新更改"的特性天然存在潜在安全问题,这也是 EIP-1014 社区讨论(Potential Security Implications of CREATE2)所关注的重点。在 CTF 中,该技巧通常被用作"在同一地址部署不同合约,以绕过不同校验"的通用手段。

实战案例:2019 Balsn CTF Creativity 的 CREATE2 巧用

下面以 2019 Balsn CTF 的 Creativity 题目(基于其官方 Write-up 提供的 PoC)为例,完整演示如何借助CREATE2在同一个地址上先后部署两个完全不同的合约。

核心 PoC:Deployer 与 Dumper 两个合约

PoC 使用 Solidity^0.5.10,包含两个合约:Deployer负责执行CREATE2部署,Dumper则充当"始终不变的init_code",它唯一的职责就是在构造函数阶段把调用者传入的任意字节码"倾倒"(return)出来,作为最终部署的运行时字节码。

pragma solidity ^0.5.10; contract Deployer { bytes public deployBytecode; address public deployedAddr; function deploy(bytes memory code) public { deployBytecode = code; address a; // Compile Dumper to get this bytecode bytes memory dumperBytecode = hex'6080604052348015600f57600080fd5b50600033905060608173ffffffffffffffffffffffffffffffffffffffff166331d191666040518163ffffffff1660e01b815260040160006040518083038186803b158015605c57600080fd5b505afa158015606f573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052506020811015609857600080fd5b81019080805164010000000081111560af57600080fd5b8281019050602081018481111560c457600080fd5b815185600182028301116401000000008211171560e057600080fd5b50509291905050509050805160208201f3fe'; assembly { a := create2(callvalue, add(0x20, dumperBytecode), mload(dumperBytecode), 0x9453) } deployedAddr = a; } } contract Dumper { constructor() public { Deployer dp = Deployer(msg.sender); bytes memory bytecode = dp.deployBytecode(); assembly { return (add(bytecode, 0x20), mload(bytecode)) } } }

逐段拆解这个 PoC 的攻击链路:

  1. deploy(code)是唯一的对外入口:调用方把希望部署的任意合约字节码作为code参数传入。Deployer先将code存入公共状态变量deployBytecode(后面Dumper会读取它),再通过内联汇编执行CREATE2。
  2. init_code恒定:dumperBytecode硬编码了编译后的Dumper合约字节码,也就是说,无论传入什么code,CREATE2使用的init_code始终是同一段字节码,salt固定为0x9453,部署方地址固定为Deployer合约地址。三者不变 ⇒ 计算出的目标地址不变。
  3. Dumper构造函数"偷梁换柱":Dumper的构造函数在创建阶段执行,它会拿到msg.sender(即Deployer合约),调用deployBytecode()读出刚才传入的code,然后用内联汇编的return(add(bytecode, 0x20), mload(bytecode))把这整段code作为运行时字节码返回给 EVM。

需要注意汇编参数的内存布局:mload(bytecode)读取的是bytecode指针处的长度字段,add(bytecode, 0x20)则跳过长度字段指向数据区起始位置,二者恰好构成RETURN所需的(offset, length)参数——这也与 Ethereum Opcodes 中RETURN的栈语义完全一致。

效果:同一个地址,任意合约

对调用者而言,效果是惊人的:每次调用deploy(code),实际参与地址计算的init_code都是同一个dumperBytecode,加上固定的部署方地址与salt,因此deploy(code)部署出来的合约最终总是落在同一个地址上;而该地址上最终存储的运行时字节码,则是本次调用传入的code。

也就是说:

  • 第一次deploy(codeA)→ 地址 X 上部署的是codeA;
  • 第二次deploy(codeB)→地址 X 上部署的变成了codeB;
  • 无论传入什么合约代码,都能"精确投递"到同一个地址。

这正好实现了本文开头所说的"在同一次 CTF 挑战中,用同一个地址绕过不同校验"的攻击模式:题目校验某地址上的合约行为时,攻击者可以先在该地址部署满足校验 A 的合约,再重新部署满足校验 B 的合约。

地址预计算:getAddress 函数

由于CREATE2的地址完全确定,攻击者在部署前就能算出目标地址。题目给出的已知条件是Deployer合约地址为0x99Ed0b4646a5F4Ee0877B8341E9629e4BF30c281,配合固定的dumperBytecode与salt,可以预先算出实际部署合约的地址为0x4315DBef1aC19251d54b075d29Bcc4E81F1e3C73。

地址预计算的 Solidity 实现如下,其计算逻辑与keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:]一一对应:

function getAddress(address addr, bytes memory bytecode, uint salt) public view returns (address) { bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), addr, salt, keccak256(bytecode) ) ); // NOTE: cast last 20 bytes of hash to address return address(uint160(uint256(hash))); }

实现细节解读:

  • abi.encodePacked将0xff、创建者地址、salt与keccak256(init_code)紧凑拼接,保证不引入任何填充字节,与 EVM 内部计算逻辑完全一致;
  • bytes1(0xff)对应公式中的固定前缀;
  • 最后通过uint160(uint256(hash))截取哈希的低 20 字节并转换为address类型。

实际复现时,可以先用getAddress验证预计算地址0x4315DBef1aC19251d54b075d29Bcc4E81F1e3C73,再分别在deploy中传入两段不同的字节码,观察同一地址上先后部署出两个不同合约。

部署过程实测截图

下面的两张图来自本题的复现过程,直观展示了"同一个地址上先后部署两个不同合约"的完整链路。

两张图中,部署方(DEPLOYER)地址均为0x99Ed0b4646a5F4Ee0877B8341E9629e4BF30c281,但传入的待部署字节码参数不同(第一张为0x33ff,第二张为完整合约字节码),两次部署计算出的合约地址却都是0x4315DBef1aC19251d54b075d29Bcc4E81F1e3C73——这正是CREATE2地址确定性在实战中的直接体现。

实战中的攻击套路与防御视角

为什么攻击者偏爱 CREATE2

从 CTF 出题与解题两个角度看,CREATE2的核心价值在于:

  • 地址可预计算:部署前即可获知目标地址,便于配合其他合约预先设置白名单、授权等依赖地址的逻辑;
  • 地址可复用:同一地址可以承载完全不同的代码逻辑,从而绕过"地址 A 已经存在/不可修改"之类的假设;
  • init_code与运行时字节码解耦:只要控制好构造函数返回的数据,就能让地址计算与最终代码"脱钩"。

对应的防御注意事项

  • 合约逻辑中若把"某地址上已部署的代码"视为不可变信任根,需要意识到该地址可能被CREATE2重新部署出恶意代码(即所谓的"合约可以自我替换"风险);
  • 审计中应特别关注项目方是否使用了固定salt+ 可变init_code的模式,以及构造函数是否会返回来自外部输入的字节码;
  • 在编写依赖合约地址做权限控制的系统时,应显式验证部署方身份与部署时间,而不是只依赖地址本身。

相关 CTF 题目

下列题目均与CREATE2技巧相关,适合作为练习(题目附件可参考 CTF-Wiki 的 challenges 仓库):

赛事题目名称
Balsn 2019Creativity
QWB 2020EasyAssembly

其中 Balsn 2019 Creativity 正是上文 PoC 的出处;QWB 2020 EasyAssembly 则将CREATE2与汇编/字节码构造技巧结合,进一步考验对 EVM 执行细节的把控。

参考

  • EIP-1014: Skinny CREATE2:CREATE2操作码的正式规范,定义了地址计算公式与引入背景;
  • 充分利用 CREATE2:社区对CREATE2各类用法的系统梳理;
  • Balsn CTF 2019 - Creativity:本题的官方 Write-up 与 PoC 原始出处。

如需回顾相关基础知识,可继续阅读 Ethereum Basics(账户、nonce、交易与合约创建机制)以及 Ethereum Opcodes(RETURN、CALL等关键指令的栈语义)。

  • 文档
  • 网络安全
  • 教程

【免费下载链接】ctf-wiki

Come and join us, we need you!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载

相关推荐

上一篇:推荐开源项目:Zulip Mobile - 跨平台的高效聊天客户端
下一篇:nginx-rtmp-win32性能优化:提升Windows平台流媒体服务效率

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询