拒绝服务攻击进阶
基础模式(无界循环 Gas 耗尽、外部转账失败阻断流程、Push 与 Pull 支付模式、King of the Ether 案例)参见 DoS 攻击。EVM 底层 Gas 机制与内存扩展成本参见 EVM 与内部状态。
EIP-150 63/64 规则与 Gas Griefing 攻击
63/64 规则的 EVM 机制
在 Tangerine Whistle 硬分叉(EIP-150)之前,EVM 存在因调用栈深度耗尽(Call Stack Depth Attack,深度上限 1024)导致的攻击。EIP-150 引入了 63/64 规则:主调合约发起子调用(CALL / STATICCALL / DELEGATECALL)时,最多只能将当前剩余 Gas 的 63/64 传递给子调用帧,剩余的 1/64 强制保留给当前调用帧处理善后逻辑。
$$ \text{Gas}_{\text{forwarded}} = \min\left(\text{gas_requested},; \text{gasleft}() - \left\lfloor \frac{\text{gasleft}()}{64} \right\rfloor\right) $$
Gas Griefing 攻击模型
尽管 1/64 的 Gas 留给了主调合约,若主调合约在子调用返回后需要执行持久化写入(如 SSTORE,开销可能达 5000 至 20000 Gas),留存的 1/64 Gas 可能不足以完成后续操作。
恶意接收合约可以在其回退函数中故意消耗绝大部分转发给它的 Gas,然后以成功状态(STOP / RETURN)正常退出,而非触发 Revert。此时主调合约误认为子调用成功,但在继续执行后续记账时遭遇 Out of Gas(OOG),导致整笔顶层事务意外回滚。
// 恶意 Gas 消耗合约
contract GasGriefer {
fallback() external payable {
// 动态消耗绝大部分 Gas,仅保留微小余量成功返回
while (gasleft() > 2000) {}
}
}
防范策略
- 显式指定子调用 Gas 上限:
在发起外部交互时,必须显式限制子调用可用 Gas,确保主调合约保留足够的执行预算:
(bool success, ) = recipient.call{value: amount, gas: 10000}(""); - 异步结账模式(Pull Payment): 关键业务状态的流转严禁同步依赖外部未知地址的调用结果。若转账失败或 Gas 耗尽,应将资产记入待提取映射,由接收方主动发起提取操作。
Return Bomb(返回数据内存炸弹)
EVM 内存扩展的二次方成本模型
EVM 的内存(Memory)使用按线性加二次方的复合函数计费。当合约分配的内存字数(Word,32 字节)由 \(a\) 扩展到 \(a^\prime\) 时,消耗的 Gas 增量为:
$$ C(a) = 3a + \left\lfloor \frac{a^2}{512} \right\rfloor $$
当分配的内存规模显著增大时,二次方项 \(\frac{a^2}{512}\) 会导致 Gas 成本呈指数级飙升。
攻击原理
子调用合约可以通过汇编指令 return(offset, size) 返回任意超大规模的数据段。若主调合约采用高级语法解码或在底层调用中显式接收 bytes memory data:
// 易受 Return Bomb 影响的调用方式
(bool success, bytes memory returnData) = target.call(payload);
Solidity 编译器会在底层自动执行 returndatacopy,将返回的全部字节流复制至主调合约的内存空间中。攻击合约若返回长达数兆字节的伪造数据,将瞬间强制触发主调合约的内存二次方扩展,直接引发 OOG 异常导致交易失败。
contract ReturnBomber {
fallback() external payable {
assembly {
// 返回超大虚假长度数据,直接引发主调方内存扩充爆炸
return(0, 0x100000) // 1 MB 数据
}
}
}
防御范式
- 忽略不必要的返回数据:对不可信外部交互,若无需读取返回值,直接省略接收参数:
(bool success, ) = target.call{gas: CALL_GAS_CAP}(payload); - 基于汇编的受控数据复制:若必须解析返回值,应在内联汇编中先通过
returndatasize()获取返回长度,施加安全阈值截断后再执行returndatacopy:function safeCallWithLimit(address target, bytes memory data, uint256 maxCopy) internal returns (bool success) { assembly { success := call(gas(), target, 0, add(data, 0x20), mload(data), 0, 0) let size := returndatasize() if gt(size, maxCopy) { size := maxCopy } // 按限定上限截断读取至内存 } }
强制注资对系统状态不变式的破坏
在很多智能合约中,开发者错误地假设合约持有的资产余额只能通过合约自身定义的充值(deposit)或转账接口产生。
强制向合约注入资产的物理途径
合约逻辑无法拒绝以下途径的原生代币(ETH)注入:
SELFDESTRUCT强制转账: 在 Cancun 升级之前,任意合约执行selfdestruct(victimAddress)均可无条件将其全部余额清空并转入目标地址,绕过目标地址的receive()与fallback()逻辑。Cancun(EIP-6780)实施后,虽然同一交易外部署的旧合约不再支持就地擦除代码,但强制向指定地址转移代币资产的底层行为依然保留。- 验证者/出块奖励与交易费用定向(Coinbase Beneficiary): 区块打包者或验证者可将 Coinbase 地址直接指定为目标合约地址,导致出块奖励直接入账。
- 预先计算部署地址的预先转账:
利用
CREATE或CREATE2可预测合约未来部署地址,在合约代码正式部署前向该地址预先转账。
破坏不变式导致的业务瘫痪
若合约将 address(this).balance 作为确定性系统状态不变式(Invariant)的一部分:
// 危险假设:内部状态记账严格等于实际物理余额
require(address(this).balance == totalDeposited, "State mismatch");
攻击者仅需向合约强制转账 1 wei 的 ETH,即可使上述条件永久无法达成,直接导致协议的清算、取款或状态推进接口完全瘫痪。类似地,对于 ERC-20 代币,通胀代币、通缩代币或直接向合约地址执行 transfer 同样会打破 balanceOf(this) == internalTotal 的强假设。
设计准则:协议内部记账逻辑必须基于持久化状态变量(如 totalShares、totalDeposited)维护确定性状态机,严禁将动态物理余额作为严格相等比较的前提。
生产环境 DoS 检查清单
- 外部交互 Gas 隔离:外部调用必须配置合理的 Gas 上限,避免子调用恶意截留耗尽计算资源。
- 防御 Return Bomb 内存扩张:对不可信外部交互避免使用高级语法接收任意长度的
bytes memory返回值。 - 消除严格物理余额相等断言:禁止使用
assert(address(this).balance == totalShares)作为核心前置校验条件。 - 数据结构分页与解耦:管理逻辑避免遍历可由外部无许可递增的动态数组,强制执行分页提取机制。
- 灾备与降级出口:确保系统在依赖的外部预言机宕机或治理多签失联时,提供基于时间锁的降级保护或用户直接紧急赎回接口。