文章总结: 本文深度解析智能合约拒绝服务攻击,归纳UnexpectedRevert、BlockGasLimit等6大类核心手法。结合Nomad与PlumeNetwork真实案例,揭示了资金锁死、功能瘫痪的漏洞原理,并提出采用Pull模式、限制循环次数、部署多签时间锁等防御策略。文章还提供了Slither与Foundry自动化检测工具链的实战应用,指导审计人员有效识别并修复风险,防止业务停摆。 综合评分: 92 文章分类: 代码审计,漏洞分析,区块链安全,解决方案,安全工具
合约审计中的拒绝服务攻击:从“意外中止”到“永久锁死”的深度狩猎指南
原创
梦到什么写什么 梦到什么写什么
逍遥子讲安全
2026年2月25日 00:30 广东
一个合约功能的永久瘫痪,往往只因为一个被忽略的require——资金被锁,用户无法操作,而攻击者甚至不需要获利,只为证明“我能让你停下”。
2023年,某DeFi协议因拒绝服务漏洞导致价值1200万美元的用户资金被锁定长达6个月,最终只能通过合约升级强制解冻——而这原本可以通过一行require避免。更夸张的案例来自跨链桥Nomad:攻击者利用逻辑缺陷,仅用0.1 ETH就阻塞了整个跨链通道数小时,造成数百万美元的交易延迟。
拒绝服务(Denial of Service,DoS)攻击在合约审计中被称为“沉默的杀手”——它不直接窃取资金,但能让核心功能瘫痪、资金锁死、业务停摆。本文将完整拆解合约DoS攻击的6大类核心手法、9个真实审计案例以及自动化检测工具链,全是干到拧不出水的干货。
第一章 重新认知拒绝服务攻击:为什么它是合约审计的“必查项”
1.1 DoS攻击的本质
拒绝服务攻击的核心目标是使智能合约无法正常执行预期功能。在区块链环境下,这意味着:
- 用户无法提取资金
- 核心业务函数执行失败
- 合约进入永久不可用状态
- 需要特权干预甚至重新部署才能恢复
核心认知:DoS攻击不一定要“赚钱”,它往往是组合攻击的前置步骤,或是直接破坏业务的“精确打击”。
1.2 合约DoS攻击的6大类型
| 类型 | 攻击手法 | 典型案例 | 危害等级 | | — | — | — | — | | Unexpected Revert | 利用外部调用失败阻塞关键流程 | 拍卖合约退款失败 | ⭐⭐⭐⭐ | | Block Gas Limit | 数组循环导致Gas耗尽 | 批量退款、遍历数组 | ⭐⭐⭐⭐⭐ | | Owner Action | 中心化权限滥用或丢失 | 管理员私钥丢失 | ⭐⭐⭐ | | Gas Griefing | 低代价耗尽对手Gas | 垃圾交易攻击 | ⭐⭐⭐ | | Block Stuffing | 填充区块阻止合法交易 | DEX抢跑攻击 | ⭐⭐⭐⭐ | | Cross-Batch Inconsistency | 跨批次状态不一致 | 收益分配冻结 | ⭐⭐⭐⭐⭐ |
第二章 Unexpected Revert:最经典的DoS攻击手法
2.1 漏洞原理
当合约执行外部调用(如转账、回调)时,如果调用失败且未正确处理,整个交易可能回滚,导致关键功能被阻塞。
核心问题:
- 使用
send()或transfer()且未检查返回值 - 外部调用的成功与否直接影响主流程
- 攻击者可以构造恶意合约主动拒绝接收
2.2 实战案例:拍卖合约的永久瘫痪
漏洞合约(存在DoS风险):
contract AuctionHouse { address currentBidder; uint256 highestBidAmount;
function placeBid() public payable { require(msg.value > highestBidAmount, "Bid must be higher");
// 关键问题:将退款和主流程耦合 require(payable(currentBidder).send(highestBidAmount), "Failed to send Ether to previous bidder");
currentBidder = msg.sender; highestBidAmount = msg.value; }}
攻击合约:
soliditycontract MaliciousBidder { AuctionHouse auctionHouse; constructor(AuctionHouse _auctionHouse) { auctionHouse = _auctionHouse; } // 没有 receive/fallback 函数,故意拒绝接收ETH function placeMaliciousBid() public payable { auctionHouse.placeBid{value: msg.value}(); }}
攻击流程:
- 正常用户A出价3 ETH成为最高出价者
- 正常用户B出价5 ETH,A收到退款,B成为新最高者
- 攻击者用
MaliciousBidder出价7 ETH,成为最高者,B收到退款(正常) - 后续任何用户出价时,合约尝试退款给攻击者合约 → 失败 → 交易回滚
- 结果:拍卖合约永久瘫痪,无法接受新出价
2.3 漏洞变种:多轮退款场景
solidity// 更危险的变种:批量退款function refundAll() external onlyOwner { for(uint i = 0; i < refundRecipients.length; i++) { // 任意一个退款失败,整个交易回滚,所有用户都被卡住 require(refundRecipients[i].send(refundAmounts[i])); }}
问题:只要有一个用户恶意拒绝接收(如合约无fallback),所有用户的退款都被阻塞。
2.4 审计检测技巧
静态分析重点:
- 搜索
send()、transfer()、call.value()等外部调用 - 检查返回值是否被验证
- 识别退款操作是否与主流程耦合
动态测试思路:
solidity// 测试恶意合约接收ETH失败的情况contract Attacker { // 故意不实现receive() function attack() external payable { // 触发目标合约的退款操作 }}
第三章 Block Gas Limit:循环遍历的“隐形杀手”
3.1 漏洞原理
每个以太坊区块有Gas上限(目前约3000万)。如果一个函数执行的Gas消耗超过区块Gas限制,交易将失败。
高危场景:
- 遍历动态数组(用户数量增长后,Gas超限)
- 循环内执行复杂操作(如多次外部调用)
- 依赖外部状态变化的批量操作
3.2 实战案例:批量退款函数的陷阱
漏洞代码:
solidityaddress[] private investors;mapping(address => uint) public refundAmounts;function distributeRefunds() external onlyOwner { // 问题:遍历长度未知的数组 for(uint i = 0; i < investors.length; i++) { // 更糟:每次迭代都进行外部调用 (bool success, ) = investors[i].call{value: refundAmounts[investors[i]]}(""); require(success, "refund failed"); }}
攻击向量:
- 项目早期只有10个投资者,
distributeRefunds正常运行 - 项目发展后投资者增加到1000人
distributeRefunds的Gas消耗超过区块限制- 结果:退款功能永久失效,资金被锁
3.3 Immunefi真实案例:Plume Network收益分配DoS
漏洞详情:ArcToken合约的distributeYieldWithLimit函数在处理收益分配时,需要遍历所有持有人重新计算effectiveTotalSupply。
solidity// 漏洞代码片段for (uint256 i = 0; i < totalHolders; i++) { address holder = $.holders.at(i); if (_isYieldAllowed(holder)) { effectiveTotalSupply += balanceOf(holder); }}
问题:
- 遍历所有持有人(可能数百人)
- 每次遍历都进行外部状态检查
_isYieldAllowed - 随着持有人增长,Gas消耗线性增加
实际影响:某次测试中,当持有人达到200人时,该函数的Gas消耗已接近区块上限。当攻击者通过空投增加大量“垃圾持有人”后,收益分配功能彻底瘫痪。
3.4 安全变体:Pull模式解决方案
solidity// 安全模式:用户自己提取(Pull模式)mapping(address => uint) public pendingRefunds;function claimRefund() external { uint amount = pendingRefunds[msg.sender]; require(amount > 0, "no refund"); pendingRefunds[msg.sender] = 0; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "transfer failed");}
优势:
- Gas成本由用户承担
- 单个用户失败不影响整体
- 可组合防重入设计
第四章 Owner Action:中心化权限的“单点故障”
4.1 漏洞原理
当合约的关键功能依赖于特定地址(owner、admin)的操作时,该地址的故障或恶意行为将导致服务中断。
高危场景:
- 只有owner可以触发的关键操作(提现、暂停、升级)
- owner私钥丢失或被盗
- owner地址被合约控制且合约逻辑错误
4.2 实战案例:单点权限导致的系统瘫痪
漏洞合约:
soliditycontract SinglePointOfFailure { address public owner; bool public paused = false;
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
function emergencyPause() external onlyOwner { paused = true; }
function emergencyResume() external onlyOwner { paused = false; }
function withdrawFunds() external onlyOwner { payable(owner).transfer(address(this).balance); }}
攻击场景:
- owner私钥在钓鱼攻击中被盗
- 攻击者调用
emergencyPause()使合约暂停 - 攻击者调用
withdrawFunds()窃取资金 - owner恢复后无法重新获得控制权(无转移所有权机制)
- 结果:合约永久停摆,资金被盗
4.3 防御方案:多签与时间锁
soliditycontract MultiSigWithTimelock { address[] public owners; uint public required; mapping(bytes32 => bool) public executed; uint public constant DELAY = 2 days;
mapping(bytes32 => uint) public queuedTransactions;
function queueTransaction(address target, uint value, bytes memory data) external onlyOwner returns (bytes32) { bytes32 txHash = keccak256(abi.encode(target, value, data)); queuedTransactions[txHash] = block.timestamp + DELAY; return txHash; }
function executeTransaction(address target, uint value, bytes memory data) external onlyOwner returns (bytes memory) { bytes32 txHash = keccak256(abi.encode(target, value, data)); require(queuedTransactions[txHash] > 0, "tx not queued"); require(block.timestamp >= queuedTransactions[txHash], "too early"); require(block.timestamp <= queuedTransactions[txHash] + DELAY, "tx expired");
(bool success, bytes memory result) = target.call{value: value}(data); require(success, "tx failed"); return result; }}
第五章 Gas Griefing与Block Stuffing:资源耗尽型攻击
5.1 Gas Griefing原理
攻击者通过少量操作,迫使受害者消耗大量Gas,使攻击在经济上不可行。
典型场景:跨链桥、荷兰拍卖、MEV竞争
5.2 TrailOfBits发现:荷兰拍卖中的Gas Griefing
漏洞描述:Reserve Protocol的荷兰拍卖机制中,攻击者可以通过发起极小额的“部分成交”来阻塞整个拍卖流程。
solidity// 漏洞代码简化function swapActive() public view returns (bool) { if (block.number != blockInitialized) { return false; } uint256 sellTokenBalance = sellToken.balanceOf(address(this)); if (sellTokenBalance >= sellAmount) { return false; } uint256 minimumExpectedIn = (sellAmount - sellTokenBalance) * price / D27; return minimumExpectedIn > buyToken.balanceOf(address(this));}
攻击流程:
- 攻击者在区块开始时创建trusted fill
- 执行1 wei的极小部分成交,使
swapActive()返回true - 整个区块内所有其他操作(包括其他用户的出价)都会被阻塞
- 拍卖价格随时间下降,攻击者可等待价格更低时再出价
- 经济模型缺陷:阻塞1个区块的成本(Gas费)可能低于价格下降带来的收益
5.3 Block Stuffing:填充区块阻止合法交易
攻击原理:攻击者发送大量高Gas费交易,填满区块Gas上限,使其他合法交易无法被打包。
真实案例:某DEX在IDO期间,攻击者连续发送高Gas交易填充5个区块,导致超过2000名用户的参与交易失败,攻击者趁机抢购大量代币。
5.4 防御策略
- 动态Gas定价:根据网络拥堵调整关键操作的Gas要求
- 最小交易金额:设置合理的最小参与门槛
- 时间加权机制:避免瞬时价格剧烈波动
- MEV保护:使用私有内存池或暗池
第六章 跨批次状态不一致:新型DoS攻击手法
6.1 漏洞原理
当合约在处理批量操作时,如果中间状态发生变化,可能导致后续批次计算错误,最终因资金不足而回滚。
6.2 Immunefi审计案例:Plume Network收益分配DoS
漏洞ID:#52468,2025年8月披露
合约逻辑:
- 收益分配分多个批次进行,每次处理部分用户
- 总收益金额
totalAmount在第一个批次固定 - 每批次遍历所有持有人重新计算
effectiveTotalSupply - 用户份额 =
(totalAmount * holderBalance) / effectiveTotalSupply
漏洞触发:
- 第一批次:处理用户A和B,
effectiveTotalSupply=1000,分配840 USDC - 中间状态变化:管理员将用户B加入黑名单(不再享受收益)
- 第二批次:处理用户C和D,
effectiveTotalSupply变为700(少了B的200) - 但
totalAmount仍然是1200 - 计算结果:C和D应得份额超过剩余资金
ERC20InsufficientBalance回滚,收益分配永久停止
PoC关键代码:
solidityfunction testDoSVulnerability() public { // Step 1: 第一批次处理A和B arcToken.distributeYieldWithLimit(1200, 0, 2);
// Step 2: 管理员将B加入黑名单(状态变化) yieldRestrictions.addToBlacklist(bob);
// Step 3: 第二批次处理C和D → 预期回滚 vm.expectRevert(); // ERC20InsufficientBalance arcToken.distributeYieldWithLimit(1200, 2, 2);}
6.3 整数溢出/下溢导致的永久DoS
Immunefi案例:#48998,Algorand智能合约
python# 漏洞代码(PyTeal)def remove_item(self, item): # 问题:当items为空时,items.length - 1下溢 last_idx = items.length - 1 # 0 - 1 = 2^64-1 # 导致AVM运行时错误,函数永久不可用
果:
- 集合为空后,
remove_item函数永久失效 - 所有依赖该集合的功能全部瘫痪
- 需重新部署合约才能恢复
第七章 自动化检测与审计工具链
7.1 静态分析工具
| 工具 | 检测能力 | 集成方式 |
| — | — | — |
| Slither | 循环检测、外部调用分析 | slither . --print human-summary |
| Mythril | 符号执行、Gas耗尽检测 | mythril analyze contract.sol |
| Securify | 数据流分析、权限检查 | 在线版本 |
自定义检测规则示例(Slither):
python# detect_dos_loop.pyfrom slither.detectors.abstract_detector import AbstractDetector, DetectorClassificationclass DoSLoopDetector(AbstractDetector): ARGUMENT = 'dos-loop' HELP = 'Detect potential DoS due to unbounded loops' IMPACT = DetectorClassification.HIGH CONFIDENCE = DetectorClassification.MEDIUM
def _detect(self): results = [] for contract in self.contracts: for function in contract.functions: for node in function.nodes: if node.type == 'LOOP': # 检查循环是否依赖动态数组长度 if self._depends_on_dynamic_array(node): info = [f"Potential DoS in {function.canonical_name}\n"] results.append(self.generate_result(info)) return results
7.2 动态分析框架
Foundry测试模板(检测Gas Griefing):
solidity// test/DosTest.t.solcontract DosTest is Test { function testGasGriefing() public { // 模拟1000次调用 for(uint i = 0; i < 1000; i++) { vm.startPrank(address(uint160(i))); targetContract.claim(); // 可能消耗Gas的操作 vm.stopPrank(); }
// 检查Gas消耗是否超限 uint gasUsed = vm.snapshotGasLastCall(); assertLt(gasUsed, 30_000_000, "Gas exceeds block limit"); }}
7.3 GasGaugeAI:AI驱动的DoS漏洞修复
滑铁卢大学的研究团队开发了GasGaugeAI——一个基于多LLM框架的自动化DoS漏洞检测与修复工具。
核心能力:
- 漏洞分类:识别Gas依赖型漏洞(循环、外部调用等)
- 测试生成:自动生成Foundry测试用例验证DoS风险
- 函数级修复:插入Guard条件防止Gas耗尽
- 迭代验证:修复后重新测试确保安全性
研究成果:在数百个真实合约的测试中,GasGaugeAI成功修复了89%的Gas耗尽型漏洞,平均减少人工审计时间70%。
第八章 防御体系:构建抗DoS的智能合约
8.1 设计原则
| 原则 | 具体措施 | 解决的问题 | | — | — | — | | Pull模式 | 用户主动提取,避免批量操作 | 批量退款、批量转账 | | Pausable | 紧急暂停机制(多签控制) | Owner权限滥用 | | 限制循环规模 | 分页处理、最大长度限制 | Gas耗尽 | | 检查返回值 | 所有外部调用验证 | Unexpected Revert | | 经济模型验证 | 攻击成本 > 潜在收益 | Gas Griefing | | 状态一致性 | 跨批次操作锁定中间状态 | 跨批次不一致 |
8.2 代码级防御示例
solidity// 安全模式:分页处理 + 推送+拉取分离contract SecureAuction { struct Bid { address bidder; uint amount; }
Bid[] public bids; uint constant MAX_BATCH_SIZE = 50; mapping(address => uint) public pendingRefunds;
function placeBid() external payable { // 新逻辑:不立即退款,记录待退款金额 if (bids.length > 0) { Bid memory lastBid = bids[bids.length - 1]; require(msg.value > lastBid.amount, "bid too low"); pendingRefunds[lastBid.bidder] += lastBid.amount; } bids.push(Bid(msg.sender, msg.value)); }
function claimRefund() external { uint amount = pendingRefunds[msg.sender]; require(amount > 0, "no refund"); pendingRefunds[msg.sender] = 0; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "transfer failed"); }
// 分页处理批量退款 function processBidsBatch(uint start, uint count) external onlyOwner { require(count <= MAX_BATCH_SIZE, "batch too large"); uint end = start + count; require(end <= bids.length, "out of bounds");
for(uint i = start; i < end; i++) { // 处理逻辑 } }}
8.3 审计检查清单
- 是否存在无限制的循环?(数组遍历、动态长度)
- 外部调用的返回值是否被检查?
- 关键函数是否只有单一权限控制?
- 是否存在批量退款的逻辑?
- 跨批次操作是否可能被中间状态破坏?
- 攻击者的阻塞成本是否远大于潜在收益?
- 是否有紧急暂停和恢复机制?
- 权限地址是否为多签或时间锁控制?
第九章 结语:拒绝服务的终点是信任崩塌
拒绝服务攻击的精髓在于:它不偷你的钱,但让你的钱永远拿不出来。
从Unexpected Revert到跨批次状态不一致,从Gas耗尽到Block Stuffing——这些攻击手法的共同点是利用合约设计中的“耦合点”:退款与主流程耦合、循环与用户数耦合、状态与计算耦合。
在审计合约时,问自己三个问题:
- 如果某个外部调用失败,合约还能继续吗?
- 如果用户数量增长10倍,这个函数还能执行吗?
- 如果攻击者刻意制造中间状态变化,会破坏最终结果吗?
答案,往往决定了一个合约是稳健运行还是永久停摆。
而你的审计价值,也在这三个问题的延长线上。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:逍遥子讲安全 梦到什么写什么 梦到什么写什么《合约审计中的拒绝服务攻击:从“意外中止”到“永久锁死”的深度狩猎指南》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。





![[视频]DLL侧载技术详解](/images/random/titlepic/9.jpg)





评论