重入攻击 Reentrancy
Excavation initiation!
今日讲讲入门漏洞–重入攻击Reentrancy
这是智能合约世界中最经典也最致命的漏洞之一。其曾让 The DAO 损失超过 6000 万美元,甚至导致以太坊社区 硬分叉 自救,是血的教训。
漏洞原理
重入攻击的本质是:在合约向外部账户转账时,外部账户可以“反过来”重新调用原合约的函数,绕过状态更新,从而多次执行本应只能执行一次的操作。
在 Solidity 中,当合约通过 .call 向某个地址发送 ETH,该地址对应的合约的 fallback() 或 receive() 函数会立即执行。如果此时原合约的状态还没更新完,攻击者就可以利用这个窗口“反复”进入函数,造成严重破坏。
补充关于 fallback() 和 receive() 函数
在 Solidity 中,当一个合约收到 ETH 时,会触发它的某个特殊函数,这两个函数分别是:
receive() 函数
自 Solidity 0.6.0 起,receive() 被引入,用于区分普通的转账和未知函数调用。
1 | receive() external payable { |
- 专门处理纯粹的 ETH 转账(比如
send、transfer); - 没有 calldata(即调用的数据为空)时触发;
- 只能有一个,没有
function关键字。
fallback() 函数
1 | fallback() external payable { |
- 当合约接收到 ETH 且找不到对应的函数名时触发;
- 通常用于处理未知调用、代理合约、或攻击场景;
- 可以接收 ETH,也可以执行逻辑。
注意:在攻击中我们常用的是 fallback(),因为它能接住 .call{value: ...}("") 这类非明确函数调用的转账,然后再反向重入发起攻击。
示例合约
1 | contract EtherStore{ |
这里的流程是:
- 检查
msg.sender的余额是否足够; call函数给msg.sender转账;- 转账成功后,再更新
msg.sender的余额。
关键点:转账发生在状态更新之前。
如果 msg.sender 是一个合约,并且它的 fallback() 函数中再次调用了 withdraw(),就可以在余额还未减少前重复调用自己,从而反复提币,直到合约被掏空
攻击合约
1 | contract exploit { |
攻击逻辑:
- 向
attack()转入 1 ETH,调用deposit()储存进去; - 紧接着调用
withdraw()提取 1 ETH; - 合约转账时触发
fallback()函数; - 在
fallback()中再次调用withdraw(),进入循环; - 只要
EtherStore中还有钱,攻击就不停; - 最终攻击者合约中余额越来越多,而
EtherStore被清空。
如何防御?
方法很简单,第一个思路是先更新状态,再转账
第二个思路是使用锁:
如果你写过并发编程或者了解过操作系统,这里就很容易理解
为了防止防止多个线程同时访问临界资源:
Linux 资源锁
- 实现是
pthread_mutex_lock - 进入条件是
if (mutex_available) - 锁定是
mutex_lock() - 释放是
mutex_unlock()
为了防止函数再执行时被重入:
Solidity 重入锁
- 实现是
bool locked+require+ 修饰器 - 进入条件是
require(!locked) - 锁定是
locked = true - 释放是
locked = false
针对上述合约的重入锁的具体代码如下
1 | bool internal locked; |
这样就能确保在函数执行期间不能再次进入。
重入攻击虽是老生常谈,但其变体和演化依然层出不穷,比如跨合约重入、DEFI 闪电贷结合重入等,后续也会继续讲这类高级用法。
- Title: 重入攻击 Reentrancy
- Author: Chiu
- Created at : 2025-04-20 21:47:24
- Updated at : 2025-07-15 13:01:54
- Link: https://github.com/Idealist17/github.io/2025/04/20/Reentrancy/
- License: This work is licensed under CC BY-NC-SA 4.0.