区块链安全—循环Dos安全分析(二)
字数 1875 2025-08-22 12:22:15
区块链安全分析:循环DoS攻击与合约设计缺陷
一、概述
本文详细分析了一种基于以太坊智能合约的循环DoS(拒绝服务)攻击漏洞,通过真实合约案例展示其危害性,并提供解决方案。这种漏洞源于合约设计中对循环操作和Gas消耗的考虑不足,导致合约功能最终无法正常执行。
二、漏洞合约分析
2.1 合约结构
分析的目标合约Pandemica具有以下核心结构:
contract Pandemica {
struct _Tx {
address txuser;
uint txvalue;
}
_Tx[] public Tx; // 存储用户地址和转账金额
uint public counter; // 记录交易数量
address owner; // 合约所有者
// 仅允许owner调用的修饰器
modifier onlyowner {
if (msg.sender == owner) _
}
// 构造函数
function Pandemica() {
owner = msg.sender;
}
// 回调函数
function() {
Sort();
if (msg.sender == owner) {
Count();
}
}
// 内部排序函数
function Sort() internal {
uint feecounter;
feecounter += msg.value/5;
owner.send(feecounter);
feecounter = 0;
uint txcounter = Tx.length;
counter = Tx.length;
Tx.length++;
Tx[txcounter].txuser = msg.sender;
Tx[txcounter].txvalue = msg.value;
}
// 奖励分发函数
function Count() onlyowner {
while (counter > 0) {
Tx[counter].txuser.send((Tx[counter].txvalue/100)*3);
counter -= 1;
}
}
}
2.2 合约功能逻辑
-
用户存款:任何用户向合约转账时,触发
Sort()函数- 收取20%的手续费(msg.value/5)发送给owner
- 记录用户地址和转账金额到Tx数组
- 更新counter计数器
-
奖励分发:当owner向合约转账时,触发
Count()函数- 遍历所有存款记录(while循环)
- 向每个用户返还存款金额的3%作为奖励
- 递减counter直到为0
三、漏洞原理分析
3.1 Gas消耗问题
以太坊合约执行需要消耗Gas,每个操作都有固定的Gas成本。当合约操作过于复杂或数据量过大时,可能超出区块Gas限制(目前约800万Gas),导致交易失败。
在Count()函数中:
while (counter > 0) {
Tx[counter].txuser.send((Tx[counter].txvalue/100)*3);
counter -= 1;
}
随着用户数量(counter)增加:
- 循环次数线性增长
- 每次循环包含:数组访问、计算、转账操作
- 总Gas消耗 = 单次循环Gas × 循环次数
3.2 实际影响
- 合约功能瘫痪:当用户数量达到一定规模后,
Count()函数将因Gas不足无法执行 - 资金冻结:奖励无法分发,用户资金被永久锁定在合约中
- 攻击放大:恶意用户可通过小额存款占用数组位置,加速Gas耗尽
3.3 历史案例
-
原始合约地址:0xD8a1Db7AA1E0ec45E77B0108006dc311cD9D00e7
- 冻结资金:5万美元以上ETH
- 交易记录显示多次高Gas(最高790万)尝试失败
-
模仿合约案例:0x4208a616fb79828ebff99f17fc472a2ad6374c72
- 类似结构,奖励比例提高到5%
- 同样存在循环DoS风险
四、漏洞复现与验证
4.1 测试环境部署
- 部署合约,owner为部署账户
- 初始状态:
- Tx[0]:全零(无用户)
- counter:0
4.2 模拟用户存款
-
账户A转账0.1 ETH:
- 触发Sort()
- Tx[0]记录A地址和0.1 ETH
- owner收到0.02 ETH手续费
- counter更新为1
-
重复多个账户存款:
- 构建测试数组(如6个用户)
- 每次存款更新Tx数组和counter
4.3 模拟奖励分发
-
owner账户调用Count():
- 遍历Tx数组
- 向每个用户发送3%奖励(0.003 ETH)
- 验证余额变化
-
小规模测试成功:
- 少量用户时函数正常执行
- Gas消耗约8万(6用户)
-
大规模测试失败:
- 用户数量增加导致Gas线性增长
- 超过区块Gas限制时交易失败
五、解决方案与最佳实践
5.1 避免大规模循环
- 分批次处理:
function Count() onlyowner {
uint iterations = 0;
while (iterations < 100 && counter > 0) {
Tx[counter].txuser.send((Tx[counter].txvalue/100)*3);
counter -= 1;
iterations += 1;
}
}
- 每次最多处理100个用户
- 需要owner多次调用完成全部分发
- 提取模式(Pull over Push):
mapping(address => uint) public rewards;
function Count() onlyowner {
for (uint i = 0; i < counter; i++) {
rewards[Tx[i].txuser] += (Tx[i].txvalue/100)*3;
}
counter = 0;
delete Tx;
}
function withdrawReward() public {
uint amount = rewards[msg.sender];
require(amount > 0);
rewards[msg.sender] = 0;
msg.sender.send(amount);
}
- 集中计算奖励到mapping
- 用户自主提取(pull)奖励
- 避免合约主动发送(push)
5.2 Gas优化技巧
- 使用storage变量缓存数组长度
- 减少循环中的冗余计算
- 优先使用mapping而非数组存储用户数据
5.3 合约设计原则
- 避免无界循环:任何循环应有明确上限
- 分离读写操作:将计算与转账分离
- 状态最小化:只存储必要数据,定期清理
- 费用预估:提前计算最大Gas消耗
六、相关资源
- 原始合约:https://etherscan.io/address/0xD8a1Db7AA1E0ec45E77B0108006dc311cD9D00e7#code
- 模仿合约:https://etherscan.io/address/0x4208a616fb79828ebff99f17fc472a2ad6374c72#code
- 失败交易示例:https://etherscan.io/tx/0xc6ea76e1250dffc706e0040ab75b84a78a8ba295dc6def69ea250d3a500f6c4a
- 安全报告:https://bcsec.org/index/detail/id/260/tag/2
七、总结
循环DoS漏洞是智能合约常见设计缺陷,通过合理控制循环规模、采用提取模式替代批量发送、优化数据结构等方法可有效防范。合约开发者应充分考虑以太坊Gas机制限制,避免因操作复杂度导致合约功能瘫痪。