Ethernaut_WP(11-15)
字数 1969 2025-08-29 22:41:24

Ethernaut 关卡11-15详细解析与教学文档

第11关:Elevator - 多次外部调用引发逻辑不一致

漏洞分析

该关卡展示了一个典型的"多次外部调用引发逻辑不一致"问题。合约通过接口多次调用外部合约的方法,但两次调用之间返回值可能不同,导致逻辑错误。

关键点

  • 合约实现了一个接口,定义了isLastFloor函数签名但未实现
  • 攻击者可控制外部合约,在两次调用间返回不同值
  • 主合约未缓存第一次调用结果,直接进行第二次调用

攻击流程

  1. 攻击者部署恶意合约,实现isLastFloor接口
  2. 第一次调用返回false,第二次返回true
  3. 绕过building函数的楼层检查

修复建议

修复方法 描述
缓存第一次返回值 将第一次调用的结果存储在本地变量中
限制外部调用次数 避免一个逻辑流程中多次调用外部合约
检查返回值一致性 手动检查多次调用结果是否一致
使用可信源 避免依赖用户传入的地址做接口调用

第12关:Privacy - 存储布局与类型转换

存储布局分析

Solidity存储使用2²⁵⁶个32字节的槽位。关键概念:

静态变量存储

  • 小于32字节的变量会被打包到同一槽位
  • 例如:booluint8可共享一个槽位

动态变量存储

  • 使用标记槽的keccak256哈希作为指针
  • 动态数组的标记槽存储数组长度

漏洞利用

  • 目标:获取私有变量data[2]的值
  • 通过存储槽计算定位数据:
    • data数组从槽4开始(前3个槽被其他变量占用)
    • data[2]位于槽6
  • 类型转换漏洞:bytes32bytes16会截断高位数据

修复建议

  1. 不要将敏感信息保存在链上(即使是private)
  2. 使用加密哈希验证而非明文存储

第13关:Gatekeeper One - 多条件绕过

三个关卡分析

Gate One:

  • 要求:msg.sender != tx.origin
  • 绕过方法:通过中间合约调用

Gate Two:

  • 要求:gasleft() % 8191 == 0
  • 解决方法:
    • 使用循环尝试0-300偏移量
    • 基准点设为8191 * 3再微调

Gate Three:

  • 涉及类型转换:uint32uint16uint64
  • 绕过方法:
    • 使用tx.origin推导合适值
    • 确保转换后满足0x11111111 == 0x1111

攻击脚本关键点

for (uint256 i = 0; i < 300; i++) {
    try attacker.attack{gas: 8191 * 3 + i}(...) {
        break;
    } catch {}
}

修复建议

问题 修复建议
Gate One 使用授权逻辑替代单纯合约检查
Gate Two 避免依赖gas相关条件
Gate Three 使用明确身份验证,避免危险的类型转换

第14关:Gatekeeper Two - 汇编与构造器利用

三个关卡分析

Gate One:

  • 同13关:msg.sender != tx.origin

Gate Two:

  • 使用内联汇编检查extcodesize
  • 绕过方法:在构造器中调用(此时代码未完全部署)

Gate Three:

  • 要求:keccak64(sender) ^ gateKey == type(uint64).max
  • 数学推导:gateKey = keccak64(sender) ^ 0xFFFFFFFFFFFFFFFF

攻击合约示例

contract Attacker {
    constructor(GatekeeperTwo target) {
        // Gate Two绕过:构造器中extcodesize为0
        bytes8 gateKey = bytes8(keccak64(msg.sender)) ^ type(uint64).max;
        target.enter(bytes8(gateKey));
    }
    
    function keccak64(address addr) internal pure returns (uint64) {
        return uint64(bytes8(keccak256(abi.encodePacked(addr))));
    }
}

修复建议

问题 修复建议
Gate Two 改为检查msg.sender.code.length > 0
Gate Three 加入动态元素如block.timestamp增加预测难度

第15关:Naught Coin - ERC20转账限制绕过

漏洞分析

  • 合约继承自OpenZeppelin ERC20实现
  • 重写了transfer函数添加限制
  • 但未重写transferFrom函数,留下绕过途径

攻击步骤

  1. 攻击者先调用approve授权自己(或攻击合约)可支配代币
  2. 然后使用transferFrom转移代币
  3. 绕过transfer的时间锁限制

修复建议

  • 同样对transferFrom函数添加限制修饰符
  • 关键修复代码:
function transferFrom(address sender, address recipient, uint256 amount) 
    public override lockTokens returns (bool) {
    return super.transferFrom(sender, recipient, amount);
}

总结

这五个关卡涵盖了智能合约开发中的多个重要安全概念:

  1. 外部调用的一致性问题
  2. 存储布局与类型安全
  3. 多条件验证的绕过技巧
  4. 汇编代码与构造器的特殊行为
  5. ERC20标准实现的安全考虑

开发者应特别注意:

  • 外部调用的不可预测性
  • 私有数据的实际可见性
  • 类型转换的数据丢失风险
  • 合约在构造期间的独特状态
  • 标准接口的完整实现要求
Ethernaut 关卡11-15详细解析与教学文档 第11关:Elevator - 多次外部调用引发逻辑不一致 漏洞分析 该关卡展示了一个典型的"多次外部调用引发逻辑不一致"问题。合约通过接口多次调用外部合约的方法,但两次调用之间返回值可能不同,导致逻辑错误。 关键点 : 合约实现了一个接口,定义了 isLastFloor 函数签名但未实现 攻击者可控制外部合约,在两次调用间返回不同值 主合约未缓存第一次调用结果,直接进行第二次调用 攻击流程 攻击者部署恶意合约,实现 isLastFloor 接口 第一次调用返回 false ,第二次返回 true 绕过 building 函数的楼层检查 修复建议 | 修复方法 | 描述 | |---------|------| | 缓存第一次返回值 | 将第一次调用的结果存储在本地变量中 | | 限制外部调用次数 | 避免一个逻辑流程中多次调用外部合约 | | 检查返回值一致性 | 手动检查多次调用结果是否一致 | | 使用可信源 | 避免依赖用户传入的地址做接口调用 | 第12关:Privacy - 存储布局与类型转换 存储布局分析 Solidity存储使用2²⁵⁶个32字节的槽位。关键概念: 静态变量存储 : 小于32字节的变量会被打包到同一槽位 例如: bool 和 uint8 可共享一个槽位 动态变量存储 : 使用标记槽的keccak256哈希作为指针 动态数组的标记槽存储数组长度 漏洞利用 目标:获取私有变量 data[2] 的值 通过存储槽计算定位数据: data 数组从槽4开始(前3个槽被其他变量占用) data[2] 位于槽6 类型转换漏洞: bytes32 转 bytes16 会截断高位数据 修复建议 不要将敏感信息保存在链上(即使是private) 使用加密哈希验证而非明文存储 第13关:Gatekeeper One - 多条件绕过 三个关卡分析 Gate One : 要求: msg.sender != tx.origin 绕过方法:通过中间合约调用 Gate Two : 要求: gasleft() % 8191 == 0 解决方法: 使用循环尝试0-300偏移量 基准点设为 8191 * 3 再微调 Gate Three : 涉及类型转换: uint32 → uint16 → uint64 绕过方法: 使用 tx.origin 推导合适值 确保转换后满足 0x11111111 == 0x1111 攻击脚本关键点 修复建议 | 问题 | 修复建议 | |------|----------| | Gate One | 使用授权逻辑替代单纯合约检查 | | Gate Two | 避免依赖gas相关条件 | | Gate Three | 使用明确身份验证,避免危险的类型转换 | 第14关:Gatekeeper Two - 汇编与构造器利用 三个关卡分析 Gate One : 同13关: msg.sender != tx.origin Gate Two : 使用内联汇编检查 extcodesize 绕过方法:在构造器中调用(此时代码未完全部署) Gate Three : 要求: keccak64(sender) ^ gateKey == type(uint64).max 数学推导: gateKey = keccak64(sender) ^ 0xFFFFFFFFFFFFFFFF 攻击合约示例 修复建议 | 问题 | 修复建议 | |------|----------| | Gate Two | 改为检查 msg.sender.code.length > 0 | | Gate Three | 加入动态元素如 block.timestamp 增加预测难度 | 第15关:Naught Coin - ERC20转账限制绕过 漏洞分析 合约继承自OpenZeppelin ERC20实现 重写了 transfer 函数添加限制 但未重写 transferFrom 函数,留下绕过途径 攻击步骤 攻击者先调用 approve 授权自己(或攻击合约)可支配代币 然后使用 transferFrom 转移代币 绕过 transfer 的时间锁限制 修复建议 同样对 transferFrom 函数添加限制修饰符 关键修复代码: 总结 这五个关卡涵盖了智能合约开发中的多个重要安全概念: 外部调用的一致性问题 存储布局与类型安全 多条件验证的绕过技巧 汇编代码与构造器的特殊行为 ERC20标准实现的安全考虑 开发者应特别注意: 外部调用的不可预测性 私有数据的实际可见性 类型转换的数据丢失风险 合约在构造期间的独特状态 标准接口的完整实现要求