首页
/ 6年仍未绝迹的亿元级漏洞:WTF-Solidity重入攻击深度剖析与防御

6年仍未绝迹的亿元级漏洞:WTF-Solidity重入攻击深度剖析与防御

2026-02-04 04:53:29作者:舒璇辛Bertina

你还在忽视智能合约安全?从The DAO被盗360万ETH到Fei Protocol损失8000万美元,重入攻击始终是区块链安全的"头号杀手"。本文将通过WTF-Solidity项目的实战案例,用10分钟带你掌握重入攻击的原理、复现过程和防御方案,让你的合约从此远离亿元级损失。

读完本文你将获得:

  • 重入攻击的底层原理与经典案例库
  • 漏洞合约的关键代码识别技巧
  • 两种经过实战验证的防御方案
  • 完整攻击复现与防御部署指南

重入攻击:区块链世界的"幽灵提款机"

重入攻击(Reentrancy Attack)是指攻击者利用合约在外部调用完成前未更新状态变量的漏洞,通过递归调用实现资产窃取的攻击方式。这种攻击模式自2016年The DAO事件后,仍在持续造成巨额损失:

  • 2016年:The DAO被重入攻击损失360万ETH,导致以太坊分叉
  • 2020年:Lendf.me被盗2500万美元
  • 2022年:Fei Protocol损失8000万美元

重入攻击原理示意图

WTF-Solidity项目的S01_ReentrancyAttack模块通过银行抢劫的故事生动展示了攻击逻辑:当银行机器人先转账后更新余额时,攻击者可在转账回调中反复发起提款请求,形成"提款-回调-再提款"的无限循环。

漏洞合约深度解析:从代码到原理

危险的银行合约实现

WTF-Solidity提供的漏洞合约ReentrancyAttack.sol中,Bank合约的withdraw()函数存在致命缺陷:

function withdraw() external {
    uint256 balance = balanceOf[msg.sender]; // 获取余额
    require(balance > 0, "Insufficient balance");
    // 致命漏洞:先转账后更新状态
    (bool success, ) = msg.sender.call{value: balance}(""); 
    require(success, "Failed to send Ether");
    // 状态更新在外部调用之后
    balanceOf[msg.sender] = 0; 
}

这段代码违反了智能合约开发的"检查-影响-交互"原则,在ETH转账(外部交互)后才更新用户余额(状态影响),为攻击者提供了可乘之机。

攻击合约的精妙设计

攻击合约通过重写receive()回调函数实现循环调用:

receive() external payable {
    // 当银行合约还有余额时继续攻击
    if (address(bank).balance >= 1 ether) {
        bank.withdraw(); 
    }
}

function attack() external payable {
    require(msg.value == 1 ether, "Require 1 Ether to attack");
    bank.deposit{value: 1 ether}(); // 存入初始资金获取提款权
    bank.withdraw(); // 触发第一次提款
}

Bank合约执行call{value: balance}("")转账时,会自动触发攻击合约的receive()函数,该函数立即再次调用withdraw(),形成递归提款循环。

完整攻击复现:从部署到资产窃取

在Remix环境中复现攻击仅需5步:

  1. 部署Bank合约并存入20 ETH作为攻击目标
  2. 部署Attack合约并传入Bank合约地址
  3. 调用Attack.attack{value:1 ether}()发起攻击
  4. 检查Bank.getBalance()发现余额已被清空
  5. 查看Attack.getBalance()显示余额变为21 ETH(初始1 ETH+盗取20 ETH)

这种攻击模式不仅适用于ETH转账,在ERC721/ERC1155的safeTransfer、ERC777的回调函数中同样可能发生,开发者需保持高度警惕。

防御方案:从补丁到最佳实践

方案一:检查-影响-交互模式重构

ReentrancyAttack.sol中的GoodBank合约展示了修复方案:

function withdraw() external {
    uint256 balance = balanceOf[msg.sender];
    require(balance > 0, "Insufficient balance");
    // 关键修复:先更新状态再执行外部调用
    balanceOf[msg.sender] = 0; 
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success, "Failed to send Ether");
}

将状态更新(balanceOf[msg.sender] = 0)提前到外部调用之前,从根本上杜绝了递归调用的可能性。

方案二:重入锁机制实现

更通用的防御手段是使用重入锁(Reentrancy Guard),如ProtectedBank合约所示:

uint256 private _status; // 重入锁状态变量

modifier nonReentrant() {
    require(_status == 0, "ReentrancyGuard: reentrant call");
    _status = 1; // 锁定
    _; 
    _status = 0; // 解锁
}

function withdraw() external nonReentrant {
    // 函数逻辑保持不变
}

通过nonReentrant修饰器确保函数在执行期间无法被递归调用,OpenZeppelin的ReentrancyGuard库实现了更完善的重入保护机制。

防御部署与安全开发指南

实战防御 checklist

  1. 状态更新优先:所有状态变量修改必须在外部调用前完成
  2. 重入锁全覆盖:为所有外部状态修改函数添加重入锁
  3. 使用安全库:优先采用OpenZeppelin的ReentrancyGuard而非自定义实现
  4. 避免危险调用:谨慎使用calldelegatecall等低级调用
  5. 全面测试:使用Foundry或Truffle进行重入攻击专项测试

WTF-Solidity项目的安全最佳实践章节提供了更多合约安全开发资源,建议开发人员系统学习。

总结与展望

重入攻击作为区块链安全的"常青树"漏洞,其防御核心在于遵循"检查-影响-交互"原则和使用重入锁双重保障。WTF-Solidity项目通过S01_ReentrancyAttack模块提供的攻防案例,为开发者提供了直观的学习材料。

随着智能合约复杂性增加,重入攻击也在演化出跨合约重入、NFT重入等新型变种。掌握本文介绍的防御思想,将帮助你构建更安全的Web3应用。

安全提示:所有资金相关合约上线前必须经过专业审计,推荐使用CertiK、OpenZeppelin等机构的审计服务。

延伸学习资源

如果本文对你有帮助,请点赞、收藏并关注WTF-Solidity项目,下期我们将解析"整数溢出攻击"的防御策略。

登录后查看全文
热门项目推荐
相关项目推荐