Ethernaut(16-20)详解
字数 1891 2025-08-30 06:50:27

Ethernaut 16-20关详细教学文档

第16关 Preservation

关键知识点

  1. delegatecall机制

    • 允许合约执行另一个合约的代码,但保持调用合约的存储、msg.sender和msg.value不变
    • 语法:(bool success, bytes memory data) = target.delegatecall(abi.encodeWithSignature("functionName(uint256)", arg));
    • 与call的区别:
      • 代码执行:都使用目标合约的代码
      • 存储:call使用目标合约的存储,delegatecall使用调用者合约的存储
      • this值:call中是被调用者合约地址,delegatecall中是当前合约地址
  2. 漏洞分析

    • 合约使用delegatecall但未考虑存储布局兼容性
    • LibraryContract的setTime函数会修改slot 0,而Preservation合约的slot 0存储的是timeZone1Library地址

攻击步骤

  1. 部署攻击合约,其setTime函数可以修改owner
  2. 调用Preservation的setFirstTime,将timeZone1Library地址覆盖为攻击合约地址
  3. 再次调用setFirstTime,此时会执行攻击合约的代码修改owner

防御措施

  1. 使用library类型而非普通合约
  2. 避免对用户可控地址做delegatecall
  3. 使用OpenZeppelin的UpgradeableProxy模板

第17关 Recovery

关键知识点

  1. 合约地址计算

    • 常规创建:keccak256(RLP_encode(address, nonce))
    • CREATE2创建:keccak256(0xff ++ address(this) ++ salt ++ keccak256(init_code))[12:]
    • 合约的nonce从1开始(创建后变为1)
  2. RLP编码规则

    • 单字节值(0x00-0x7f):直接使用
    • 短字符串(0-55字节):前缀0x80+长度
    • 长字符串(>55字节):前缀0xb7+长度长度+长度+内容
    • 短列表(总长度≤55):前缀0xc0+总长度
    • 长列表(总长度>55):前缀0xf7+长度长度+长度+内容

解题方法

方法一:区块链浏览器查询

  1. 在etherscan上追踪合约创建交易
  2. 找到SimpleToken合约地址
  3. 调用其destroy函数

方法二:地址预计算

  1. 计算创建者合约地址和nonce(1)的RLP编码
  2. 计算keccak256哈希
  3. 取最后20字节作为合约地址

第18关 MagicNumber

关键知识点

  1. EVM字节码要求

    • 合约必须返回42(0x2a)
    • 代码大小限制为10字节
  2. EVM操作码

    • PUSH1: 0x60
    • MSTORE: 0x52
    • RETURN: 0xF3
  3. 内存布局

    • 0x00-0x3f: 哈希暂存空间
    • 0x40-0x5f: 空闲内存指针
    • 0x60-0x7f: 零槽
    • 0x80: 可用内存起始位置

解决方案

运行时字节码

60 2A    // PUSH1 0x2A (值42)
60 80    // PUSH1 0x80 (存储位置)
52       // MSTORE (存储)
60 20    // PUSH1 0x20 (返回长度32字节)
60 80    // PUSH1 0x80 (返回位置)
F3       // RETURN

部署方式

  1. 直接部署:仅包含运行时代码
  2. 完整部署:包含初始化逻辑和运行时代码

第19关 AlienCodex

关键知识点

  1. 动态数组存储

    • 数组长度存储在声明的slot(本例为slot 1)
    • 数组元素存储在:keccak256(slot) + index
    • 整个存储空间可视为一个环形缓冲区
  2. 下溢漏洞

    • Solidity 0.6.0以下版本无自动溢出检查
    • codex.length--可能导致下溢,使length变为2^256-1

攻击步骤

  1. 调用makeContact()设置contact为true
  2. 调用retract()使codex.length下溢
  3. 计算覆盖slot 0所需的index:2^256 - keccak256(1)
  4. 调用revise()修改"owner"

第20关 Denial

关键知识点

  1. call调用特性

    • 默认转发所有剩余gas
    • 失败不会导致整个交易回滚
    • 无gas限制可能导致DoS
  2. 攻击原理

    • 通过成为partner合约
    • 在接收ETH时耗尽所有gas(无限循环或复杂计算)
    • 导致withdraw()调用因gas不足失败

防御措施

  1. 设置gas限制:partner.call{value:amountToSend, gas:10000}("")
  2. 使用transfer或send(有固定gas限制)
  3. 采用pull支付模式而非push模式
Ethernaut 16-20关详细教学文档 第16关 Preservation 关键知识点 delegatecall机制 : 允许合约执行另一个合约的代码,但保持调用合约的存储、msg.sender和msg.value不变 语法: (bool success, bytes memory data) = target.delegatecall(abi.encodeWithSignature("functionName(uint256)", arg)); 与call的区别: 代码执行:都使用目标合约的代码 存储:call使用目标合约的存储,delegatecall使用调用者合约的存储 this值:call中是被调用者合约地址,delegatecall中是当前合约地址 漏洞分析 : 合约使用delegatecall但未考虑存储布局兼容性 LibraryContract的 setTime 函数会修改slot 0,而Preservation合约的slot 0存储的是timeZone1Library地址 攻击步骤 部署攻击合约,其 setTime 函数可以修改owner 调用Preservation的 setFirstTime ,将timeZone1Library地址覆盖为攻击合约地址 再次调用 setFirstTime ,此时会执行攻击合约的代码修改owner 防御措施 使用library类型而非普通合约 避免对用户可控地址做delegatecall 使用OpenZeppelin的UpgradeableProxy模板 第17关 Recovery 关键知识点 合约地址计算 : 常规创建: keccak256(RLP_encode(address, nonce)) CREATE2创建: keccak256(0xff ++ address(this) ++ salt ++ keccak256(init_code))[12:] 合约的nonce从1开始(创建后变为1) RLP编码规则 : 单字节值(0x00-0x7f):直接使用 短字符串(0-55字节):前缀0x80+长度 长字符串(>55字节):前缀0xb7+长度长度+长度+内容 短列表(总长度≤55):前缀0xc0+总长度 长列表(总长度>55):前缀0xf7+长度长度+长度+内容 解题方法 方法一:区块链浏览器查询 在etherscan上追踪合约创建交易 找到SimpleToken合约地址 调用其destroy函数 方法二:地址预计算 计算创建者合约地址和nonce(1)的RLP编码 计算keccak256哈希 取最后20字节作为合约地址 第18关 MagicNumber 关键知识点 EVM字节码要求 : 合约必须返回42(0x2a) 代码大小限制为10字节 EVM操作码 : PUSH1: 0x60 MSTORE: 0x52 RETURN: 0xF3 内存布局 : 0x00-0x3f: 哈希暂存空间 0x40-0x5f: 空闲内存指针 0x60-0x7f: 零槽 0x80: 可用内存起始位置 解决方案 运行时字节码 : 部署方式 : 直接部署:仅包含运行时代码 完整部署:包含初始化逻辑和运行时代码 第19关 AlienCodex 关键知识点 动态数组存储 : 数组长度存储在声明的slot(本例为slot 1) 数组元素存储在: keccak256(slot) + index 整个存储空间可视为一个环形缓冲区 下溢漏洞 : Solidity 0.6.0以下版本无自动溢出检查 codex.length-- 可能导致下溢,使length变为2^256-1 攻击步骤 调用makeContact()设置contact为true 调用retract()使codex.length下溢 计算覆盖slot 0所需的index: 2^256 - keccak256(1) 调用revise()修改"owner" 第20关 Denial 关键知识点 call调用特性 : 默认转发所有剩余gas 失败不会导致整个交易回滚 无gas限制可能导致DoS 攻击原理 : 通过成为partner合约 在接收ETH时耗尽所有gas(无限循环或复杂计算) 导致withdraw()调用因gas不足失败 防御措施 设置gas限制: partner.call{value:amountToSend, gas:10000}("") 使用transfer或send(有固定gas限制) 采用pull支付模式而非push模式