OneSwap系列八之 魔鬼的名字叫重入

在智能合约的开发过程中,一个需要考虑的重要问题即是合约是否有可能遭受重入攻击。一个最经典的重入攻击案例即为2016年的DAO项目所经历的攻击,最终造成约360万个以太币被盗窃,并直接导致了Ethereum从Ethereum Classic的硬分叉。

重入攻击的原理很简单:以太坊上的智能合约彼此之间可以相互调用。假设在一个合约A执行过程中发生了一次外部的合约B调用,并且合约B是由黑客所控制的,合约B的调用过程中可以重新进入合约A的调用。如果合约A在执行外部合约调用之前并未完成自己的内部状态更新,则有可能会被合约B利用从而盗取资产。

以下面的合约C为例:

contract C{
    function deposit() external{
        ....
    }
  function withdraw() external {
      uint256 amount = balances[msg.sender];
      require(msg.sender.call.value(amount)());
      balances[msg.sender] = 0;
  }
}

合约C提供了以太币的质押和提取接口,用户可以将一定数量的以太币质押到该合约,以使用合约提供的其他功能,如获取利息、投票等。在需要退出时,可以调用提取接口将自己原先质押的资产取回。

可以看到,合约C的withdraw方法首先读取了当前交易者质押的资金总量,随后调用call方法将该数量的以太币转给交易者账户。但由于msg.sender有可能是一个合约账户,合约的fallback方法在收到以太币后被触发,并且在fallback方法中再次调用了合约C的withdraw方法。当再次进入合约C的withdraw方法时,再次读取交易者质押的资金总量,与上次完全一样。因此,恶意合约再次收到了该数量的以太币,再次进入合约C的withdraw方法,直到合约C的余额耗尽。

另外,还有一种是跨函数的重入攻击。即在一个合约的两个方法之间共享了某些内部状态,从一个方法中调用其他合约,其他合约再次冲入到该合约的另外一个方法,最终造成类似的效果。

防止重入攻击的方法也很简单,一个是利用锁机制,另一个是编码时遵循Checks-effects-interactions模式。

锁机制在合约内部加了一个状态变量,该状态变量用来标识合约是否被重入。在合约的关键方法被调用时,首先会判断是否已经处于锁定状态,如果是则回退所有的状态更改。如果否则将锁锁上,在执行完当前方法所有逻辑后再将锁打开。因此当外部合约再次重入到合约的内部时,合约已经被锁定,攻击无法继续。

仍然以合约C为例,采用锁机制的解决方法如下:在合约中增加了unlocked变量,合约构造时需要将该变量初始化为true。并且为withdraw方法都增加了修饰符onlyUnlocked,仅在合约当前处于非重入的状态下才会真正执行资产的提取操作。

contract C{
    bool unlocked;
    modifier onlyUnlocked{
            require(unlocked,"contract is already locked");
        unlocked = false;
        _;
        unlocked = true;
    }
    function deposit() external{
        ....
    }
  function withdraw() external onlyUnlocked{
      uint256 amount = balances[msg.sender];
      require(msg.sender.call.value(amount)());
      balances[msg.sender] = 0;  
  }
}


值得注意的是,上述机制定义了锁变量unlocked来标识当前状态是否是解锁状态,同样也可以定义一个locked变量来实现同样的功能。两者的区别在于unlocked变量在构造时需要被设置为true,而locked变量则节省了这一步操作。

Checks-effects-interactions模式指的是在开发人员在编码时应遵循先检查,再更改内部状态,最后与外部合约进行交互的规范。以合约C为例,采用该模式的合约写法如下:

contract C{
    function deposit() external{
        ....
    }
  function withdraw() external {
      uint256 amount = balances[msg.sender];
      require(amount != 0,"account balance is zero");
      balances[msg.sender] = 0;
      require(msg.sender.call.value(amount)());
  }
}

在进行资产提取时,首先判断账户余额是否为0。如果是则回退状态更改,如果否则先将账户余额设置为0,最后调用call方法进行外部转账。此时,恶意合约的fallback方法再次调用withdraw方法时,由于账户余额已经被清零,攻击者无法提取更多的资产。

另外,还有一种情况下无需特别考虑重入攻击问题,即合约没有保存任何内部状态。此时,即使当前合约被重入,也不存在前述的状态部分更改的问题,因此不会带来之前的严重后果。Oneswap项目中的Router合约即属于这种类别,Router合约本身仅负责对一些计算工作和对 Pair合约的调用转发,即使该合约调用过程中被重入也仅仅是重复进行一些计算和调用,因此相比于Router合约而言,Pair合约如何防止重入攻击则显得更加重要。

相比内部账户而言,对外部账户进行转账充满了各种风险,主要的原因在于被调用的外部合约代码是不可控的。看到这里,或许你会想到可以通过extcodesize指令将外部账户和内部账户区分开来,检查一下接收方账户的合约字节码是否为0,如果是则说明这是一个内部账户,反之则为外部账户。但事实上,即使extcodesize指令返回值为0也无法确认这并非一个合约账户。原因在于,extcodesize指令获取的是当前调用时相应账户的合约代码大小,而合约在初始化构造时也没有可用的运行时代码,因此此时extcodesize指令返回值同样为0。这就说明,如果extcodesize指令返回值不为0,那么该账户一定是一个外部账户,反之并不说明该账户就是一个内部账户。

总结

本文介绍了智能合约可能面临的重入攻击的原理、防御措施,并以Oneswap项目的Router合约为例说明了重入攻击可能不会造成严重破坏的情况,尽管如此,开发人员需时刻保持警惕,在编码过程中尽量通过使用锁机制或Checks-effects-interactions模式来杜绝重入攻击的一切可能入口,杜绝后患。

原文:《OneSwap Series 8 - The Evil Has a Name: Re-entrancy》
链接:https://oneswap.medium.com/oneswap-series-8-the-evil-has-a-name-re-entrancy-b293429f2d0c

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