返回论坛

Solidity 安全深度解析(五):未检查的CALL返回值与竞争条件攻击

查找币 漏洞披露 安全研究 Web3安全 区块链安全

查找币安全研究院

钱包恢复评估 | 链上取证分析 | Web3 事件响应
以合法授权、证据保全、隐私保护和可复核流程为前提,不要求用户在线提交完整私钥或助记词。

查看研究院 研究报告中心
**查找币安全团队 | 技术研究系列** --- ## 一、未检查的 CALL 返回值:隐形的交易炸弹 在Solidity智能合约开发中,外部调用是连接链上世界的桥梁,但这座桥梁也可能成为攻击者的突破口。本文将深入剖析两个典型的安全漏洞模式:未检查的CALL返回值以及竞争条件攻击,帮助开发者构建更安全的智能合约。 ### 1.1 漏洞原理 Solidity提供了多种执行外部调用的方式。向外部账户发送Ether通常使用`transfer()`方法,但开发者也可能使用`send()`,甚至直接调用`CALL`操作码。关键问题在于:`call()`和`send()`函数返回一个布尔值指示调用成功与否,**但不会自动回滚交易**。 ```solidity contract Lotto { bool public payedOut = false; address public winner; uint public winAmount; function sendToWinner() public { require(!payedOut); winner.send(winAmount); // 漏洞点:未检查返回值 payedOut = true; } function withdrawLeftOver() public { require(payedOut); msg.sender.send(this.balance); } } ``` 在上述示例中,第11行的`send()`调用存在严重隐患。如果winner账户的交易失败(Gas耗尽、回退函数抛出异常或调用栈深度攻击),`send()`仅返回`false`,但`payedOut`仍被设置为`true`。攻击者随后可通过`withdrawLeftOver()`提取本应属于winner的奖金。 ### 1.2 攻击向量分析 攻击者可利用以下方式触发漏洞: - **Gas限制攻击**:使目标合约的回退函数消耗超过2300 Gas - **调用栈深度攻击**:将调用栈深度推至1024,使外部调用失败 - **恶意回退函数**:在目标合约中编写抛出异常的回退函数 ### 1.3 防御策略 **首选方案**:使用`transfer()`替代`send()` ```solidity winner.transfer(winAmount); // 自动回滚 ``` **最佳实践**:采用取出(Withdrawal)模式 ```solidity contract SecureLotto { mapping(address => uint) public balances; function withdraw() public { uint amount = balances[msg.sender]; require(amount > 0); balances[msg.sender] = 0; msg.sender.transfer(amount); } } ``` **强制检查**:若必须使用`send()`,务必验证返回值 ```solidity bool success = winner.send(winAmount); require(success, "Transfer failed"); ``` ### 1.4 真实案例:Etherpot 彩票合约 Etherpot是一个典型的受此漏洞影响的智能合约。在其`cash()`函数中(lotto.sol第80行),未检查外部调用返回值,结合区块哈希使用不当,导致攻击者可操纵彩票结果。具体分析显示: - 仅最后256个区块的哈希值可用 - 未验证外部调用成功与否 - 攻击者可构造交易使奖金发送失败,但状态被错误更新 --- ## 二、竞争条件攻击:抢先交易的博弈 ### 2.1 漏洞本质 区块链的公开透明性使交易在进入区块前对所有人可见。攻击者可监测待处理交易池,支付更高Gas费使自己的交易优先执行,从而利用时间差获利。 ### 2.2 ERC20 approve() 函数漏洞 ERC20标准中的`approve()`函数存在经典竞争条件: ```solidity function approve(address _spender, uint256 _value) returns (bool success) ``` **攻击场景**: 1. Alice批准Bob花费100个代币 2. Alice决定将额度改为50个代币,提交交易 3. Bob监测到该交易,立即提交花费100个代币的交易(设置更高Gas费) 4. Bob的交易先被执行,花费100个代币 5. Alice的交易执行,将Bob额度设为50个代币 6. Bob实际可花费150个代币(100+50) ### 2.3 防御方案 **推荐方法**:使用`increaseApproval()`和`decreaseApproval()`替代直接调用`approve()` ```solidity function increaseApproval(address _spender, uint _addedValue) public returns (bool) { allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue); Approval(msg.sender, _spender, allowed[msg.sender][_spender]); return true; } ``` ### 2.4 真实案例:Bancor 价格操纵攻击 Ivan Bogatty团队曾记录针对Bancor初始实现的攻击: - 代币价格基于交易价值动态计算 - 攻击者监测交易池,识别大额交易 - 抢先提交交易,利用价格差异套利 - Bancor团队已修复此漏洞 --- ## 三、综合防护建议 1. **始终检查外部调用返回值**:使用`transfer()`或显式验证`send()`/`call()`返回值 2. **采用取出模式**:将资金提取逻辑与核心业务逻辑分离 3. **使用安全数学库**:防止整数溢出/下溢 4. **实现防抢跑机制**:使用commit-reveal方案或限制交易顺序 5. **定期审计**:使用Slither、MythX等工具进行自动化分析 ## 四、总结 未检查的CALL返回值和竞争条件攻击是Solidity开发中最常见但最危险的安全漏洞。通过理解其原理、掌握防御技术,并参考真实案例教训,开发者能够显著提升智能合约的安全性。记住:**在区块链世界,每一个未检查的返回值都可能成为攻击者的突破口**。 --- *本文由查找币安全团队整理发布* **安全预警**:建议所有Solidity开发者将本文提到的漏洞模式纳入安全审查清单,并在部署前进行全面的第三方审计。 --- **查找币安全团队联系方式**: - Telegram:https://t.me/czbofficial - Twitter:@czb_team - 知识星球:https://t.zsxq.com/Q3zNvvF
在论坛中查看和回复