引用了慢雾科技公众号中智能合约审计入门篇-拒绝服务攻击漏洞的代码,以下是原文链接:智能合约安全审计入门篇 —— 拒绝服务
案例描述
1.2016年2月6日至8日The King of the Ether Throne“纷争时代”(Turbulent Age)期间,受到DOS攻击许多游戏中的退位君王的补偿和未接受款项无法退回用户玩家的钱包 。同年6月,连庞氏骗局GovernMental的合约也遭遇DoS攻击,当时1100以太币是通过使用250万gas交易获得,这笔交易超出了合约能负荷的gas上限,带来交易活动的暂停。
2.传统网络安全拒绝服务攻击(DoS):DoS 是 Denial of service 的简称,即拒绝服务,任何对服务的干涉,使得其可用性降低或者失去可用性均称为拒绝服务。常见的针对网络协议造成拒绝服务的攻击手段大致有以下几种:SYN Flood,IP 欺骗性攻击,UDP 洪水攻击,Ping 洪流攻击,Teardrop 攻击,Land 攻击,Smurf 攻击,Fraggle 攻击等。
智能合约拒绝服务攻击:可以导致智能合约无法正常使用的代码逻辑错误,兼容性错误或调用深度过大(区块链虚拟机的特性)的安全问题。智能合约中的拒绝服务攻击手法就相对比较简单,包括但不限于以下三种:
(1)基于代码逻辑的拒绝服务攻击:这种类型的拒绝服务攻击一般情况下是因为合约代码逻辑的不严谨造成的,最典型的就是当合约中存在对传入的映射或数组循环遍历的逻辑且没有限制传入的映射或数组的长度时攻击者可以通过传入超长的映射或者数组进行循环遍历而大量消耗 Gas 从而该笔交易的 Gas 溢出,最后使得智能合约暂时或永久不可操作。
(2)基于外部调用的拒绝服务攻击:这种拒绝服务攻击是建立在合约中对外部调用处理不当导致的。例如智能合约中存在基于外部函数执行的结来改变合约状态且没有对交易一直失败的情况做出处理,攻击者会利用这个特点故意使交易失败,智能合约则会一直重复这笔失败的交易从而造成智能合约逻辑卡在这里不能继续执行,最后使得智能合约暂时或永久不可操作。
(3)基于运营管理的拒绝服务攻击:这种拒绝服务攻击就是建立在后期运营情况下,例如在智能合约中通常会存在以 Owner 账户作为管理员角色,该角色通常会持有很高的权限,例如开启或暂停转账功能,当 Owner 角色操作失误或私钥丢失可能会受到非主观意义上的拒绝服务攻击。
案例环境
- 在线编译工具Remix
- 小狐狸钱包
账户地址1(Alice):0xdab2a6D700f171EB6207d65177457138F1354b36
账户地址2(Bob):0x9A40D57c6129b0c4266C410F71564a8aE0aa8E44
账户地址3(Eve):0xe904439348437045E8eA1c59fd3b340f4A7d1cF5
测试网络为Sepolia测试网络
合约内容
合约代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract KingOfEther {
address public king;
uint public balance;
function claimThrone() external payable {
require(msg.value > balance, "Need to pay more to become the king");
(bool sent, ) = king.call{value: balance}("");
require(sent, "Failed to send Ether");
balance = msg.value;
king = msg.sender;
}
}
上述合约的目的是选取“以太之王”,玩家可以通过 claimThrone() 合约中打入大于之前用户的任意数量的以太币来竞争“以太之王”的称号,当打入的以太币高于之前玩家时打入的以太币留在合约中并获得“以太之王”称号,之前玩家的以太币会原路退回。
我们可以看到,生成新王和退回旧王的逻辑是在同一函数内完成的,并且 claimThrone() 中还检查了退款的返回值 sent,下面我们来结合这个特点来完成攻击。
攻击复现:
-
Alice 部署 KingOfEther 合约。
-
Alice 调用 KingOfEther.claimThrone() 发送 1 个以太到 KingOfEther 合约中成为“以太之王”。
-
Bob 调用 KingOfEther.claimThrone() 发送 2 个以太到 KingOfEther 合约中成为新王。
-
Alice 收到 1 个以太币的退款。
-
Eve 使用 KingOfEther 的地址部署攻击合约 Attack。
-
Eve 调用 Attack.attack() 向 KingOfEther 合约中发送 3 个以太。
-
Attack 合约成为新王。
-
Bob 觉得不服,再次调用 KingOfEther.claimThrone() 向 KingOfEther 合约中发送了 20 个以太想要夺回“以太之王”。
-
Bob 发现自己的交易一直被 revert,无法成为新王。至此,Eve 的攻击使 KingOfEther 合约永久失效,Attack 合约成为了永远的“以太之王”。
漏洞分析
当 Bob 调用 KingOfEther.claimThrone() 发送 20 个以太到 KingOfEther 合约时会触发 KingOfEther.claimThrone() 的退款逻辑,将之前 Eve 通过 Attack.attack() 向 KingOfEther 合约中发送的 3 个以太原路退回到 Attack 合约。我们再来看 Attack 合约,该合约中没有实现 payable 的 fallback() 所以不能接收以太币,这将导致 KingOfEther.claimThrone() 的退款逻辑一直失败,退款返回值 sent 将一直为 false 无法通过 require(sent, “Failed to send Ether”) 检查一直被 revert。因为只要触发退款就会被 revert 导致 KingOfEther 合约中继 Attack 合约后无人能成为新王,Eve 成功完成了拒绝服务攻击。
`
漏洞修复
修复后的代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract KingOfEther {
address public king;
uint public KingValue;
mapping(address => uint) public balances;
function claimThrone() external payable {
balances[msg.sender] += msg.value;
require(balances[msg.sender] > balance, "Need to pay more to become the king");
KingValue = balances[msg.sender];
king = msg.sender;
}
function withdraw() public {
require(msg.sender != king, "Current king cannot withdraw");
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}
修复了合约中添加了 balances 映射,它记录了每个人向合约中打入以太的总数量相较于之前合约的优势是玩家失去王位后可以追加以太重新获得王位。修复版本的关键点是将退款逻辑作异步处理,需要玩家手动调用 withdraw() 来自助退款,就算遇到恶意玩家拒收以太也只能影响到自己,不会再造成之前的拒绝服务了。