重入攻击概述

说到智能合约安全,重入攻击绝对是个绕不开的话题。我入行那会儿,第一个深入研究的漏洞就是它。说白了,重入攻击就像是你去银行取钱,ATM机还没扣完你的余额,你又插了一次卡——结果钱吐了两遍,账户只扣了一次。

嗯,这个比喻虽然粗糙,但核心逻辑就是这么回事。

什么是重入攻击

重入攻击,指的是攻击者利用合约执行过程中的控制权转移,在原始调用尚未完成之前,再次调用同一个函数。这种递归调用会导致合约状态被反复操作,最终让攻击者获利。

你想想看,智能合约的执行是顺序的。如果合约A调用了合约B,合约B在返回之前又调用了合约A,这就形成了一个循环。如果合约A在第一次调用时没有更新自己的状态,那第二次调用时它还以为一切正常——这就是漏洞所在。

核心要点:重入攻击的本质是「状态更新滞后于外部调用」。合约先转账,后更新余额,给了攻击者可乘之机。

我在审计一个DeFi项目时遇到过类似情况。那个合约的提现函数是先发ETH,再改余额。我当时就跟团队说:这顺序反了。改完之后,攻击路径就堵死了。

攻击原理图解

光说理论不够直观。我画了一张流程图,帮你理清攻击的完整链路。

重入攻击原理流程图 攻击者合约 受害者合约 ① withdraw() 检查余额(通过) 发送 ETH(call) 更新余额(未执行) ETH 发送 fallback() 再次调用 withdraw() ② 重入调用 检查余额(仍通过) 再次发送 ETH 循环直到 gas 耗尽或合约余额归零

这张图展示的是最经典的重入攻击流程。注意看受害者合约里的「更新余额」那一步——它被虚线框起来了,意思是还没执行到。攻击者就是抓住了这个时间差。

我的经验:审计时我习惯先看「外部调用」的位置。如果外部调用出现在状态更新之前,基本可以判定有重入风险。这个检查点,百试百灵。

历史著名案例:The DAO 事件

说到重入攻击,就不得不提 The DAO。这是以太坊历史上最著名的事件,没有之一。2016年,一个叫 The DAO 的去中心化自治组织被攻击,损失了约360万枚ETH——按当时价格算,超过6000万美元。

为什么会这样?

The DAO 的合约里有一个提现函数,逻辑大概是这样的:

function withdraw(uint _amount) public {
    if (balances[msg.sender] >= _amount) {
        // 先转账
        msg.sender.call.value(_amount)();
        // 后更新余额
        balances[msg.sender] -= _amount;
    }
}

你看,问题就出在call.value()上。这个调用会把控制权交给接收方。如果接收方是个合约,它的fallback函数里又调用了withdraw——好,重入攻击就开始了。

我记得当时有个朋友跟我说,攻击者一共提现了200多次,每次都能成功。因为余额一直没更新,合约以为他还有钱。

⚠️ 关键教训:永远遵循「先检查-后更新-再交互」的顺序。也就是 Checks-Effects-Interactions 模式。先改状态,再发外部调用。顺序错了,钱就没了。

The DAO 事件最终导致了以太坊的硬分叉,分裂成了 ETH 和 ETC 两条链。你说一个漏洞能改变区块链的历史走向,这影响力够大吧?

从那以后,整个行业对重入攻击的警惕性提高了不少。但说实话,直到今天,我审计的合约里偶尔还能看到类似的问题。有些开发者觉得「我用的是 transfer,不是 call,应该安全吧?」——嗯,transfer 确实限制了 gas,但也不是万能的。后面我会详细讲。

攻击的典型特征

总结一下,重入攻击有几个明显的特征,你在审计时可以重点留意:

  • 外部调用出现在状态更新之前——这是最明显的信号
  • 使用 call.value() 或 call.gas() 发送 ETH——transfer 和 send 有 gas 限制,相对安全一些
  • 合约的 fallback 函数包含业务逻辑——攻击者往往在这里做文章
  • 没有使用重入锁——OpenZeppelin 的 ReentrancyGuard 就是干这个的

避坑指南:我曾经审计过一个项目,他们用了重入锁,但锁只加在了提现函数上。攻击者绕了个弯,通过另一个函数间接调用了提现函数——锁形同虚设。所以,重入锁要加在所有可能被重入的函数上,别留死角。

攻击的变种

重入攻击不只有「转账触发」这一种形式。我见过一些变种,比如:

变种类型 触发方式 典型场景
跨函数重入 通过不同函数进入 一个函数减余额,另一个函数加余额
跨合约重入 通过多个合约协作 A调用B,B回调A的另一个函数
只读重入 利用 view 函数 攻击者读取未更新的状态做决策

你想想看,攻击者为了绕开防护,什么招都能想出来。所以防御不能只靠一个锁,得从架构层面去设计。

好了,这一章我们聊了重入攻击的基本概念、攻击原理,还有那个改变历史的 The DAO 事件。下一章我会带你手写一个攻击合约,再写一个防御合约,咱们实战走一遍。

一句话记住:先改状态,再发调用。顺序对了,重入就进不来。


公众号:蓝海资料掘金营,微信deep3321