欢迎订阅专栏 :10分钟智能合约:进阶实战
智能合约拒绝服务攻击(DoS):原理、类型与防御
拒绝服务攻击(Denial of Service, DoS) 在智能合约中,指攻击者通过特定手段使合约无法正常提供服务,例如使关键函数永远失败、Gas 消耗过高无法执行、或合约状态被锁定。与传统 Web 的 DoS 不同,智能合约的 DoS 不仅影响可用性,还可能导致资金被锁、业务中断等严重后果。
1. 核心原理
智能合约的 DoS 攻击主要利用 EVM 的执行规则 或 合约设计缺陷,使目标函数无法按预期完成。常见原理包括:
- Gas 消耗过高:攻击者使函数执行所需的 Gas 超过区块 Gas 上限,导致交易无法被打包。
- 外部调用失败:合约依赖的外部调用(如发送 ETH、调用其他合约)永久失败,导致主逻辑无法继续。
- 状态锁定:关键状态变量被设置为无效值,导致权限检查永远失败。
- 存储膨胀:大量占用存储,使后续操作 Gas 成本飙升。
2. 主要攻击类型与示例
2.1 Gas 耗尽型(Gas Exhaustion)
攻击者通过触发合约中的高复杂度循环 或无上限迭代,使函数执行 Gas 远超区块限制,导致交易永远无法成功。
典型场景:退款函数遍历所有参与者。
solidity
// 漏洞示例:退款时遍历所有参与者
contract VulnerableAuction {
address[] public bidders;
mapping(address => uint) public bids;
function refundAll() public {
for (uint i = 0; i < bidders.length; i++) {
address bidder = bidders[i];
uint amount = bids[bidder];
if (amount > 0) {
bids[bidder] = 0;
(bool success, ) = bidder.call{value: amount}("");
require(success, "Refund failed"); // 一旦某个地址拒绝接收,全盘回滚
}
}
}
}
攻击方式 :攻击者创建大量恶意地址(或部署拒绝接收 ETH 的合约)参与竞拍,导致 refundAll 循环 Gas 耗尽或中途失败。
2.2 外部调用失败型(External Call Failure)
合约假设外部调用必然成功,若调用失败且未妥善处理,则主逻辑无法继续。
典型场景:向用户发送 ETH 失败导致函数回滚,攻击者可故意让接收地址 revert。
solidity
// 漏洞示例:必须成功发送ETH
function claimReward() public {
require(rewarded[msg.sender] == false);
uint amount = rewards[msg.sender];
rewarded[msg.sender] = true;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed"); // 若用户拒绝,则无法领奖,且状态回滚,用户可反复尝试
}
攻击方式 :攻击者部署一个在 receive() 中 revert() 的合约,然后调用 claimReward,交易总是失败,但攻击者并未得到奖励,也无法被标记为已领取,从而阻塞其他正常用户的领奖?实际上这里攻击者无法阻止他人,但若此函数是唯一的领奖方式,且攻击者先调用导致失败,他人也无法领奖?不,这里状态未更新,攻击者可以一直尝试,但正常用户也可能被卡?其实这个例子更典型的是:如果合约需要向某地址发送 ETH 作为条件的一部分,而该地址拒绝接收,则整个交易回滚,可能导致某些业务流程永久卡住。
更典型的例子是多签名钱包:一笔交易需要多个签名者批准,其中一个签名者恶意拒绝签名,导致交易永远无法执行。
2.3 权限锁定型(Privilege Lock)
合约的关键角色(如 owner)被设置为无效地址(如零地址),导致所有受保护的函数永久不可用。
典型场景:转移所有权时未检查新地址有效性。
solidity
// 漏洞示例:owner 可被设置为零地址
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner; // 若 newOwner 为 0x0,所有 onlyOwner 函数永久失效
}
攻击方式:owner 误操作或被恶意引导,将所有权转给零地址。
2.4 存储膨胀型(Storage Bloated)
攻击者通过不断调用函数,使合约的映射或数组无限增长,导致后续操作 Gas 成本过高(例如遍历或读取存储)。
典型场景:允许任何人无成本添加数据到数组。
solidity
// 漏洞示例:无限制添加数据
contract Bloat {
uint[] public data;
function addData(uint value) public {
data.push(value); // 任何人都可无限添加
}
function processAll() public {
for (uint i = 0; i < data.length; i++) {
// 处理数据,Gas 会随数组长度增长
}
}
}
攻击方式 :攻击者通过大量交易将数组撑到极大,使 processAll 因 Gas 不足永远无法执行。
2.5 自毁导致合约不可用(Selfdestruct)
合约被 selfdestruct 后,代码和存储被清除,所有功能永久失效。若合约权限控制不当,攻击者可自毁合约。
典型案例 :2017 年 Parity 多签钱包漏洞 。攻击者利用初始化漏洞成为库合约的 owner,然后调用 selfdestruct 销毁了库合约,导致所有依赖该库的钱包无法使用(因为它们通过 delegatecall 调用已销毁的库,调用失败)。
3. 防御措施
| 攻击类型 | 防御方法 |
|---|---|
| Gas 耗尽 | - 避免无上限循环,使用 分页处理 (每次处理一小部分)。 - 采用 拉取支付(withdraw pattern),让用户自行提取,而非主动推送。 |
| 外部调用失败 | - 使用 检查返回值 并妥善处理,不要 require(success) 除非业务必须。 - 优先使用 transfer/send (但有 2300 gas 限制,已不推荐),或使用 call 但允许失败并记录 。 - 关键操作可引入 超时/替代方案。 |
| 权限锁定 | - 转移所有权时检查新地址非零 。 - 采用 两步转移 :先提名,再确认。 - 使用 多重签名或时间锁 降低单点风险。 |
| 存储膨胀 | - 对数据添加操作施加成本 (如支付 Gas 或限制调用频率)。 - 限制数据结构最大长度,或使用 映射替代数组(若无需遍历)。 |
| 自毁 | - 谨慎使用 selfdestruct,最好完全移除。 - 严格控制权限,避免未授权调用。 |
| 通用防御 | - 使用重入锁 防止某些 DoS(如重入导致状态不一致)。 - 遵循 最小权限原则 。 - 进行 全面的测试与审计,特别是复杂交互场景。 |
4. 经典案例回顾
- Parity 多签钱包事件(2017):库合约被自毁,导致约 50 万 ETH 被锁。
- GovernorAlpha 投票延迟攻击(Compound):攻击者利用提案机制,使治理系统长时间无法通过新提案。
- Akropolis 闪贷攻击(2020):攻击者通过重入和 DoS 组合,耗尽池中资金。
5. 总结
智能合约的 DoS 攻击多种多样,但其根源往往是设计时未考虑极端情况或恶意行为 。防御 DoS 需要开发者时刻保持警惕:任何外部依赖都可能是不可靠的,任何循环都可能是无限的,任何权限都可能是脆弱的。遵循安全编码模式(如拉取支付、检查-生效-交互、限制循环)并结合专业审计,是构建抗 DoS 合约的基础。