本文探讨了智能合约中的拒绝服务(DoS)攻击,并提供了一个 KingOfEther 合约的漏洞示例,解释了攻击原理,并给出了修复建议。文章还从开发者和审计员的角度,分析了如何避免和检测此类漏洞,包括代码逻辑错误、外部调用处理不当以及权限管理不当等问题。
背景
拒绝服务(DoS)是智能合约安全中的一个问题,类似于传统的网络安全。
先决知识
传统网络安全拒绝服务(DoS):DoS 是拒绝服务的缩写。当对服务的干扰降低或消除其可用性时,就会发生拒绝服务。以下是针对网络协议的常见拒绝服务攻击的示例:SYN Flood、IP Spoofing、UDP Flood、Ping Flood、Teardrop Attack、LAND attack、Smurf attack、Fraggle attack 等。
智能合约拒绝服务攻击:一种安全问题,可能导致代码逻辑错误、兼容性问题或过多的调用深度(区块链虚拟机的特性),从而导致智能合约无法正常运行。智能合约拒绝服务攻击方法相对简单,包括但不限于以下三种:
漏洞示例
在我看来,由于常见的背景知识,每个人都熟悉拒绝服务攻击的概念。外部调用拒绝服务攻击是三种 DOS 攻击中最常见的一种。为了提供详尽的介绍,我们将在下面引导你完成一个典型的代码示例:
// 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() 合约允许用户通过输入任何大于前一个用户的以太币数量来竞争“以太之王”的称号。如果币值高于前一个玩家的 ETH,则 ETH 将保留在合约中,新玩家将被加冕为“以太之王”,而旧玩家的 ETH 将以相同的方式返还给他们。
我们可以看到,生成新国王和返回旧国王的逻辑在同一个函数中完成,并且在 claimThrone() 中也检查了退款返回值。让我们结合这些特性来完成攻击。
攻击合约
注意:以下攻击情景和合约代码逻辑仅为示例,仅用于演示目的。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Attack {
KingOfEther kingOfEther;
constructor(KingOfEther _kingOfEther) {
kingOfEther = KingOfEther(_kingOfEther);
}
function attack() public payable {
kingOfEther.claimThrone{value: msg.value}();
}
}
首先让我们分析一下攻击过程:
富有而有魅力的 Bob 感到沮丧。如果他如此富有,为什么不能成为国王呢?
免费加入 Medium,以获取此作者的更新。
让我们看看为什么。
当 Bob 调用 KingOfEther.claimThrone() 向 KingOfEther 合约发送 20 个以太币时,将触发 KingOfEther.claimThrone() 的退款逻辑,Eve 的 3 个以太币将返回到 Attack 合约。让我们再次检查 Attack 合约。该合约未实现 payable 的 fallback() 方法,因此无法接收以太币。因此,KingOfEther.claimThrone() 的退款逻辑将始终失败,并且其返回值将始终为 false。(sent, “Failed to send Ether”) 检查会始终反转。只要触发退款,在 KingOfEther 合约中继 Attack 合约之后,任何人都无法成为新的国王。因此,Eve 成功地执行了拒绝服务攻击。
修复建议
作为开发者:
以下是先前提到的易受攻击合约的修复示例:
// SPDX-License-Identifier: MITpragma 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 map 已添加到修复合约中,该合约记录了每个人放入合约中的以太币总数。与之前的合约相比,玩家现在可以增加以太币来夺回王位。修复版本的关键点在于,存在一种异步处理退款逻辑的方法。要获得退款,玩家必须手动调用 withdraw()。即使恶意玩家拒绝接受以太币,这也没有任何影响,也不会导致先前提到的拒绝服务。
作为审计员:
在审计期间,必须检查和确认所有函数方法的可见性和访问权限。有必要结合项目方提供的设计文档,以确认审计期间的权限是否符合设计文档的描述。如果确定存在过多的授权或权限划分不明确,则必须与项目团队沟通,以改进其流程和方法,以确保合约生效时防止行政和运营错误。
- 原文链接: slowmist.medium.com/intr...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!