本文详细介绍了如何在智能合约中防止拒绝服务(DoS)攻击,探讨了常见的攻击模式和安全编码实践,强调了拉取模式和最低交易额的必要性,以及黑名单功能可能带来的风险。文章通过代码示例清晰展现了这些关键点,并提出了实用的预防措施。
Hans
了解如何在智能合约中防止拒绝服务(DoS)攻击。探索常见模式、现实世界实例以及 DeFi 中的安全编码实践。
好的,追求安全的小伙伴们!Hans 在此准备深入探讨“ Solodit 检查清单解读”的第一部分?
如果你是第一次参加这场盛宴,欢迎你!这个系列旨在将 Solodit 智能合约 审计检查清单中的380多个项目转化为你实际可以使用的内容。
在前言(第0部分)中,我们搭建了背景,承诺带来充满实用例子和实战故事的旅程。
今天,我们将关注那些让安全专家在审计或比赛后面临困境的问题,因为这些问题普遍存在,却在代码深处往往被轻易忽视。
我说的就是 拒绝服务(DoS) 攻击!
我们将讨论三个检查清单项目:
所以,抓一杯你喜欢的增强脑力饮品,系好安全带,让我们来提升这个表演的节奏吧!为了获得最佳体验,打开一块Solodit检查清单作为参考。
DoS 攻击难道只是传统中心化服务器的问题?这是大公司需要担心的事情,对吧?错!
事实是,在去中心化金融( DeFi)中,DoS 攻击可能是毁灭性的。想象这样一个噩梦场景:你精心打造的质押合约,旨在丰厚奖励忠实用户,突然……它停止了。
它因为某个恶意行为者正 flooding 它的交易而停滞不前。用户无法提取他们的资金,整个系统变得令人沮丧无法使用。那么你的声誉呢?好吧,下降得比一只 rug pull 后的 memecoin 还要快(我们都知道那种感觉)。
DoS 攻击是利用代码中的漏洞,使你的智能合约无法使用,要么对特定用户,要么对所有人不适用。它们就像数字路障,阻碍合法用户访问你协议的良好意图服务!在信任为重的空间中,成功的 DoS 攻击可能会造成绝对灾难。
为了说明这一点,可以这样想:你倾注心血建立了一家美丽的咖啡店。这是世界上最好的咖啡店。但接着有人不停地 点了成千上万的空咖啡杯,塞塞塞塞塞住整个系统,使得真实的客户无法获得他们的咖啡。真令人愤怒,对吧?这就是 DoS 攻击的本质!
DoS 攻击通常是关于 利用你智能合约逻辑中的设计缺陷。这不是暴力破解。它们使用微妙的操控来击败你。
下面是一般思路的细分:
逻辑利用:这完全是关于寻找聪明(或者更准确地说,是恶意)方式来触发 状态 变化,以导致 回滚 的交易或无限循环,有效地冻结合约。这种情况发生得比你可能想象的还要频繁。
资源耗尽:把它想象成公地悲剧与 gas 费用。它发生在攻击者涌进合约请求,消耗过多的 gas,使得合法用户往往承受高昂费用。记住,区块链上的每一个动作都需要 gas,而一波波交易会将这些费用抬升得天翻地覆。
现在我们简要介绍了高层视图(并且希望已经说服你 DoS 攻击确实是一个真实威胁),让我们深入探讨一些 缓解他们的具体方法。
翻译:你是否在提款时使用 “拉取”模式而不是“推送”模式?
经典错误(我在审计比赛中见得多了)是推送 ETH 给用户在提款函数中。看起来很简单,对吧?大错特错!让我们看一些代码:
// 反模式:推送 ETH
function batchWithdraw() public {
address[] memory users = getUsers(); // 一个获取用户的假想函数
for (uint i = 0; i < users.length; i++) {
uint amount = balances[users[i]];
if (amount > 0) {
balances[users[i]] = 0;
(bool success, ) = users[i].call{value: amount}(""); // 潜在的 DoS!
require(success, "转账失败"); // 如果任何转账失败,整个批处理都会回滚
}
}
}
为什么这样不好?
如果 msg.sender
是一个在接收 ETH 时会回滚的合约(无论是什么原因 - 也许它们是恶意的),整个 withdraw
函数会对每个人回滚!这造成了真正的 DoS 情况,因为合法用户因为一个失败的 地址 无法提取资金。想象一下会发生什么样的混乱!
解决方案:拉取模式
不要推送,让用户拉回他们的代币。他们从合约发起转账。这就像给他们金库的钥匙。这看起来是这样的:
// 拉取模式:更安全!
mapping(address => uint) public withdrawableBalances;
// 步骤 1:管理员标记资金为可提款,而不实际发送
function startBatchWithdrawal() public {
address[] memory users = getUsers(); // 一个获取用户的假想函数
for (uint i = 0; i < users.length; i++) {
uint amount = balances[users[i]];
if (amount > 0) {
balances[users[i]] = 0;
withdrawableBalances[users[i]] += amount;
}
}
}
// 步骤 2:每个用户分别提取自己的资金
function withdraw() public {
uint amount = withdrawableBalances[msg.sender];
require(amount > 0, "没有资金可提取");
withdrawableBalances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "转账失败");
}
现在,如果某用户的提款失败,只影响他们。合约继续为其他人正常运作。这才是正确的去中心化方式!require
语句检查转账是否成功。如果不成功,该交易仅对请求用户回滚,对正在进行的操作的影响降至最低。
例子:检查清单中的例子详细描述了一个情况,在该情况中费用在用户提取之前被转移给了所有者。如果所有者的地址意外设置为零地址,或者如果所有者是一个在代币转账时会回滚的调皮合约,用户提款将失败。哎呀。 这个调试起来很麻烦!最小示例和用 Foundry 编写的 PoC 可在这里获得。
检查清单的力量:在代码审查时只需问一句“我们是否在使用拉取模式?”可能在这段 DoS 漏洞之前就会被标记出来。预防永远比治愈更好(而且更便宜)。
翻译:你是否在防止“尘埃”交易(微不足道的、小量的)堵塞你的合约并使一切变得麻烦?
这个问题完全是关于 防止垃圾邮件,简单明了。如果你的合约允许用户以任意金额(即使是细小、微不足道的代币份额)与其互动,攻击者可以 通过无数零值或近零值交易涌入它。所有这些交易都占用了 gas,使得合法操作的成本显著增加。这种费用的增加可能会达到区块 gas 限制。如果那样发生,合法用户将无法与你的协议互动。以下是脆弱代码的样子:
// 易受尘埃攻击
struct WithdrawalRequest {
address user;
uint amount;
}
WithdrawalRequest[] public withdrawalRequests;
// 任何人都可以提交任何金额的提款请求(甚至 1 wei!)
function requestWithdrawal(uint amount) external {
require(balances[msg.sender] >= amount, "余额不足");
balances[msg.sender] -= amount;
// 加入全球提款队列 - 没有最低金额检查!
withdrawalRequests.push(WithdrawalRequest(msg.sender, amount));
}
function processWithdrawals() external onlyOwner {
for (uint256 i = 0; i < withdrawalRequests.length; i++) { // 这使得攻击加剧,因为它试图一次处理所有请求
WithdrawalRequest memory request = withdrawalRequests[i];
request.user.transfer(request.amount);
}
withdrawalRequests = new WithdrawalRequest[](0);
}
解决方案:强制最低
简单的 require
语句可以在这一点上大大改善。用一个 require
语句强制设置一个阈值,就像保安检查身份证那样。我们还需要确保能够批量处理提款请求。这里是实现方式:
uint public minimumWithdrawal = 0.1 ether;
function requestWithdrawal(uint amount) external {
require(balances[msg.sender] >= amount, "余额不足");
require(amount >= minimumWithdrawal, "低于最低提款阈值的金额"); // 保安!
balances[msg.sender] -= amount;
withdrawalRequests.push(WithdrawalRequest(msg.sender, amount));
}
function processWithdrawals(uint count) external onlyOwner { // 现在可以批量处理
for (uint256 i = 0; i < count; i++) {
WithdrawalRequest memory request = withdrawalRequests[i];
request.user.transfer(request.amount);
}
for (uint i = 0; i < withdrawalRequests.length - count; i++) {
withdrawalRequests[i] = withdrawalRequests[i + count];
}
withdrawalRequests.length = withdrawalRequests.length - count;
}
例子:
检查清单中的例子强调了一个合约,用户可以 请求字面上的任何数量的提款(甚至为零),无论多小。攻击者可以利用这个情况创建大量的零值提款请求。这样导致处理合法提款的成本变得非常高,让合法用户消耗大量 gas。最小示例和用 Foundry 编写的 PoC 可在这里获得。
检查清单的力量:简单的问题“是否强制执行最低交易金额?”迫使你停下并思考这个特定的攻击向量,并实施相当简单的保护措施。
翻译:你是否在考虑使用可 黑名单地址 的代币(看着你,USDC 及类似代币)所引发的影响?
这是一个更加微妙的问题,并且在当今逐渐演变的受规管稳定币世界中 非常 重要。某些代币(不点名,但咳咳 USDT 咳咳 USDC 咳咳)具有黑名单功能。这意味着 中央权威可以冻结或彻底阻止特定地址使用该代币。
为什么这是一个潜在的 DoS 定时炸弹?
想象一个社区质押合约,其中朋友、家人或投资伙伴将他们的代币汇聚在一个 “质押小组” 中。听起来合作高效,对吧?每个成员根据他们的贡献分配一部分奖励。
但事情危险的趣味在于:如果你的质押小组中仅有一位成员被代币合约黑名单,那么会怎么样?也许你的表弟被列入某个监管观察名单,或你的朋友的地址因为某些完全无关的交易而被标记。这对其他小组成员来说没什么大不了,是吧?错了!
当你的小组试图提取奖励时,整个交易会轰然失败!为什么?因为合约试图 在一个单个交易中为所有成员分配奖励。如果一个转账失败,整个操作就会回滚!你们小组的100 ETH综合价值的代币?完全被锁住!你的计划提款?不可能!这一切都是因为一个成员的黑名单,而这可能与你的质押活动毫无关系!
你知道更糟糕的是什么吗?通常没有办法移除黑名单中的成员或重新分配股份。小组的资金有效地被冻结,直到代币的黑名单得以更新 - 如果黑名单是出于监管原因,这种事情可能永远不会发生。
这不是一些理论担忧 - 这已经在真实合约中发生!单一故障导致多个用户同时受影响。没人要求的集体惩罚机制!可怕,不是吗?
下面是代码中的样子:
contract GroupStaking {
IERC20 public token;
struct StakingGroup {
uint256 id;
uint256 totalAmount;
address[] members;
uint256[] weights;
bool exists;
}
// 从小组 ID 到小组数据的映射
mapping(uint256 => StakingGroup) public stakingGroups;
// 当前小组 ID 计数器
uint256 public nextGroupId = 1;
constructor(IERC20 _token) {
token = _token;
}
// 创建新的质押小组
function createStakingGroup(address[] calldata _members, uint256[] calldata _weights) external returns (uint256) {
require(_members.length > 0, "空成员列表");
require(_members.length == _weights.length, "成员与权重长度不匹配");
// 验证权重总和是否为100%
uint256 totalWeight = 0;
for (uint256 i = 0; i < _weights.length; i++) {
totalWeight += _weights[i];
}
require(totalWeight == 100, "权重必须总和为100");
uint256 groupId = nextGroupId;
stakingGroups[groupId] = StakingGroup({
id: groupId,
totalAmount: 0,
members: _members,
weights: _weights,
exists: true
});
nextGroupId++;
return groupId;
}
// 向小组质押代币
function stakeToGroup(uint256 _groupId, uint256 _amount) external {
require(stakingGroups[_groupId].exists, "小组不存在");
require(token.transferFrom(msg.sender, address(this), _amount), "转账失败");
stakingGroups[_groupId].totalAmount += _amount;
}
// 根据权重从小组中提取代币,分配奖励
function withdrawFromGroup(uint256 _groupId, uint256 _amount) external {
StakingGroup storage group = stakingGroups[_groupId];
require(group.exists, "小组不存在");
require(group.totalAmount >= _amount, "小组余额不足");
// 只有小组成员可以发起提款
bool isMember = false;
for (uint256 i = 0; i < group.members.length; i++) {
if (group.members[i] == msg.sender) {
isMember = true;
break;
}
}
require(isMember, "不是小组成员");
// 更新小组的总金额
group.totalAmount -= _amount;
// 根据权重向所有成员分配提取金额
// 脆弱:如果有任何成员被黑名单,整个分配将失败
for (uint256 i = 0; i < group.members.length; i++) {
uint256 memberShare = (_amount * group.weights[i]) / 100;
if (memberShare > 0) {
token.transfer(group.members[i], memberShare);
}
}
}
// 获取小组信息
function getGroupInfo(uint256 _groupId) external view returns (
uint256 id,
uint256 totalAmount,
address[] memory members,
uint256[] memory weights
) {
StakingGroup storage group = stakingGroups[_groupId];
require(group.exists, "小组不存在");
return (
group.id,
group.totalAmount,
group.members,
group.weights
);
}
}
解决方案:考虑黑名单。取决于你的容忍水平。
不幸的是,这里没有单一的“正确”答案。每种情况都是如此具体。它真的依赖于你的协议设计、风险承受能力,甚至是与用户的法律协议。至少,要关注这种可能性并设计一个后备机制,同时确保合规性。
检查清单的力量:此检查清单项目迫使你批判性地思考 外部依赖 及其对你协议核心功能的潜在影响。它实际上是为了预期最坏的情况并提供至少一些备用方案。
最小示例和用 Foundry 编写的 PoC 可在这里获得。
拒绝服务(DoS)攻击:一种试图干扰或使智能合约或其功能对合法用户不可用的攻击。这可以通过消耗过多的 gas、导致合约回滚,或利用漏洞阻止关键操作来实现。
提款模式:一种安全的智能合约设计模式,强调提款的“拉取”模型。合约不推送Token给用户,用户发起提款过程,从而减少因接收用户转账失败而造成的 DoS 脆弱性问题。
尘埃交易:这些是极低价值的交易,通常是恶意使用的,干扰网络或智能合约,使合法交易成本更高或处理受到阻碍。
代币黑名单:某些代币合约实施的特性(例如,某些 ERC-20 实现),允许特定地址被阻止转移代币。这实际上冻结了他们的资金或阻止他们与该代币互动。
我们讨论了 拉取模式(拥抱用户控制)、最低交易金额(在接受之前进行检查)以及 代币黑名单 的潜在雷区(了解你的依赖)。
通过在开发过程中主动考虑这些检查清单项目,你不仅仅是在编写代码。你在创建一个 更安全、更可控且更值得信赖的生态系统。而这,朋友们,是真正值得追求的目标!信任和透明度是你最宝贵的资产。
敬请期待下一期!如果你觉得这篇文章有帮助,请随意分享给你的战友们。让我们一项一项地使 DeFi 空间成为一个更安全的地方!
- 原文链接: cyfrin.io/blog/solodit-c...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!