CPS公告·溢出攻击测试报告
2018-04-27 10:31:09

尊敬的CHIPS用户:


在4月22日,关于“以太坊智能合约溢出漏洞事件”,引发币圈市场抛售狂潮,导致BEC(美链)64亿市值一夜归零。然而时隔三天,SMT(SmartMesh)的智能合约又爆出漏洞,SMT在火币pro交易所的价格下跌近20%。一时间,无论是先行者还是准“韭菜”都惨遭收割,一行代码蒸发了无数市值,令人着实痛心。

 

简单的说,BEC和SMT的某一段代码忘记使用SafeMath方法,导致系统产生了整数溢出漏洞,利用该漏洞,黑客可以通过转账手段生成大量原本合约中不存在的代币,并将这些“无中生有”的代币在市场进行抛售。

 

除此之外,我们通过代码检测出现了许多项目方其智能合约存在相同漏洞,这些项目方无一例外的需要重新发行新的代币,或者接受市值归零的可能性。近些天来,我们通过对各大社区的关注,发现有许多项目方纷纷发行了新的代币,一时间整个以太坊生态恐慌。也发现还有许多项目方存在相同漏洞,但是项目方自己都尚未察觉,这些都是非常危险的行为。


因此,为了让CHIPS用户安心,我们进行了溢出攻击测试,并公告测试结果:我们的TOKEN在上线之前就已经对各种攻击的可能性做过反复测试,在成功部署后也进行了多次线上回归测试。并且,我们的CPS和CPST是使用了ERC223协议(兼容ERC20协议),这是一种更先进的智能合约,因此溢出攻击对我们是无效的,我们不需要重新发行新的代币。并且受此次事件影响,我们谨慎的对智能合约代码进行了多次复测,经最终测试结果得出:请用户放心使用CPS和CPST代币,目前已知的所有智能合约漏洞攻击对我们都是无效的。通过此次事件的惨态,足以令所有的项目方和用户反思的是:基础性的技术工作远大于商业价值口号的宣传。CHIPS也一直坚信,做好区块链的基础技术工作是根本,一步一个脚印才能走得稳,走的远。



以下是CHIPS公司程序员通过社区收集,发现的各种智能合约攻击方式,为了资产安全,号召所有项目方必须知晓并在写智能合约时避开这些漏洞。


可重入性(Reentrancy)一般可以理解为一个函数在同时多次调用,例如操作系统在进程调度过程中,或者单片机、处理器等的中断的时候会发生重入的现象。


这个漏洞第一种可能出现的情况是:在调用其他函数的操作完成之前,这个被调的函数可能会多次执行。这可能会导致智能合约中的几个函数以破坏性的方式进行交互。


1.png


因为用户的余额一直没有被置0,直到函数执行的结束。第二次(之后一次)调用其他函数的操作仍会成功,并且会一次一次地取消对账户余额的置0操作。The DAO事件中以太坊被盗就是因为攻击者执行了这样的操作。


解决方案,在给出的示例中,为了避免碰到这个漏洞,我们的解决方案是:使用函数send()而不是函数call.value()(),这将阻止任何外部代码的执行。


但是如果无法避免要调用外部函数时,防止这种攻击的下一个简便方法就是确保在你调用外部函数时已完成所有要执行的内部操作。


2.png


请注意,如果你有另一个函数也调用了withdrawBalance(),那么它也可能会受到相同的攻击,因此你必须将这种调用不可信合约的函数视为不可信函数,接下来我会进一步讨论潜在的解决方案。


漏洞二:跨函数的竞态条件


攻击者也可以对共享相同状态的两个不同函数进行类似的攻击。


3.png


在这种情况下,攻击者可以在代码执行到调用withdrawBalance()时调用transfer() 函数,由于他们的余额在此时还未被置0,所以即使他们已经收到退款,他们也还能转移通证,这个漏洞也被用在了The DAO事件中。


同样的原理,同样的注意事项。注意在这个例子中,这两个函数都是同一个智能合约的组成部分,同样的,当多个合约共享同一状态时,这几个合约之间也可能会出现这个漏洞。


由于竞态条件可能发生在多个函数之间,甚至是多个智能合约之间,所以旨在防止重入现象的解决方案都是明显不够的。


解决方案,这儿有两种解决方案:


  • 一是我们建议先完成所有的内部工作,然后再调用外部函数;

  • 二是使用互斥锁。


1.首先第一种解决方案,先完成所有的内部工作,然后再调用外部函数。


如果你在编写智能合约时仔细地遵循这个规则,那么就可以避免出现竞态条件。但是,你不仅需要注意避免过早地调用外部函数,还要注意这个外部函数调用的外部函数,例如,下面的操作就是不安全的。


4.jpg


尽管函数getFirstWithdrawalBonus()不直接调用外部的合约,但在函数withdraw()中的调用足以使其进入竞态条件之中。因此,你需要将函数withdraw()视为不可信函数。


43.jpg


除了修复漏洞使这种重入现象变得不可能外,还要标记出不可信的函数。这种标记要注意一次次的调用关系,因为函数

untrustedGetFirstWithdrawalBonus()


调用了不可信函数

untrustedWithdraw()


这意味着调用了一个外部的合约,因此你必须将函数

untrustedGetFirstWithdrawalBonus()


也列为不可信函数。


2.第二中解决方案是使用互斥锁。


即让你“锁定”某些状态,后期只能由锁的所有者对这些状态进行更改,如下所示,这是一个简单的例子:


5.jpg


如果用户在第一次调用结束前尝试再次调用withdraw() 函数,那么这个锁定会阻止这个操作,从而使运行结果不受影响。这可能是一种有效的解决方案,但是当你要同时运行多个合约时,这种方案也会变得很棘手,以下是一个不安全的例子:


6.png


这种情况下攻击者可以调用函数getLock()锁定合约,然后不再调用函数releaseLock()解锁合约。如果他们这样做,那么合约将被永久锁定,并且永远不能做出进一步的更改。


如果你使用互斥锁来防止竞态条件,你需要确保不会出现这种声明了锁定但永远没有解锁的情况。在编写智能合约时使用互斥锁还有很多其他的潜在风险,例如死锁或活锁。如果你决定采用这种方式,一定要大量阅读关于互斥锁的文献,避免“踩雷”。


有些人可能会反对使用竞态条件这个术语,因为以太坊并没有真正地实现并行性。然而,逻辑上不同的进程争夺资源的基本特征仍然存在,所以同样的漏洞和潜在的解决方案也同样适用。




交易顺序依赖与非法预先交易导致的漏洞


交易顺序依赖(Transaction-Ordering Dependence,TOD);

非法预先交易(Front Running)非法预先交易是经纪人从客户交易中获利的一种不道德做法。在手中持有客户交易委托的情况下抢先为自己的账户进行交易。


以下是区块链固有的不同类型的竞态条件:在区块内部,交易本身的顺序很容易受到人为操控。


由于在矿工挖矿时,每笔交易都会在内存池中待一段时间,因此可以想象到交易被打包进区块前会发生什么。对于去中心化的市场,可更改的交易顺序会带来很多的麻烦。比如市场上常见的买入某些代币的交易。而防范这一点十分地困难,因为它会涉及到合约中具体的实现细节。


例如,在去中心化市场中,由于可以防止高频交易,故批量拍卖的效果更好。另一种解决方法就是采用预先提交方案的机制,别着急,后面我会详细介绍这个机制的细节。


漏洞三:时间戳依赖


请注意,区块的时间戳可被矿工人为操纵,所以要留意时间戳的所有直接和间接使用。


还有很多与时间戳相关的注意事项,编程前一定要认真学习。




整数的上溢和下溢导致的漏洞


想象一个很简单的转移通证的场景:


7.jpg


如果你的账户余额达到了以太坊中最大的无符号整型值(2^256),那么你的余额再增加就无法表示了。因为平时遇到这种现象进位就可以了,但在这里无符号整型值只有256位,进位的第257位是不显示的,所以你没有猜错,当你进位后你的余额就会回到0。在计算机科学中这种现象就叫做整数的上溢。


当然了,这种现象也不太常见,因为它需要同时保证你真的有这么多余额,你的智能合约中还没考虑到上溢问题。考虑一下这个无符号整型值是否有机会达到这么大一个数字,再考虑一下这个无符号整型值如果改变当前数值,以及谁有权做出这样的改变。


如果智能合约中任何用户都可以调用函数来更新这个无符号整型值,那么这个智能合约就会很容易受到攻击。如果只有管理员可以做出更改,那么它才可能是安全的。如果合约中规定用户的账户余额每次只能增加1,那么这个合约可能也很安全,因为现在还没有可行的方法让你短时间内达到这个限制。


账户余额达到最大时再增加就会被清零,你会瞬间从最富有的人变成最穷的人。不知你有没有想到可以从最穷的人变成最富有的人?没错,下溢也是这个道理,如果这个无符号整型值小于0,那么它需要向前借位,而借的那一位并不显示,所以你的余额就会下溢达到最大值。


看到这里,你一定要小心使用像8位,16位和24位的无符号整型值,因为8位无符号整型值最大仅可以表示255,所以相比之下它们更容易达到最大值而发生溢出现象。


对待溢出现象请千万小心,之前有程序员整理了20个智能合约中上溢和下溢的场景。


漏洞四:存储操作中的深度下溢


Doug Hoyte在2017年的以太坊黑客比赛中提出了这个漏洞,这也让他获得了比赛中的荣誉。这个想法很有意思,因为它引起了人们对C类语言下溢如何影响以太坊编程语言Solidity的担忧。这是一个简化了的版本:


8.jpg


一般来说,如果不经过keccak256哈希计算(当然,这是不现实的),变量manipulateMe的存储位置就不会被影响。但由于动态数组是按顺序存储的,如果攻击者想要改变manipulateMe这个变量,他只需要这样做:


  1. 调用函数popBonusCode()来实现下溢。(请注意,以太坊编程语言Solidity并没有内置的pop函数。)

  2. 计算变量manipulateMe的存储位置。

  3. 使用函数modifyBonusCode()修改和更新变量manipulateMe的值。


实际上,人们都知道这种数组存在的漏洞。但如果这样的数组被掩埋在更复杂的智能合约架构之下,谁又能轻易发现呢?这样它就可以任意地对变量进行恶意篡改。


解决方案,在考虑使用动态数组时,使用一个容器式的数据结构是一种不错的选择。Solidity CRUD的第1部分和第2部分文章详细介绍了这个漏洞。




意外恢复导致的漏洞


漏洞五:利用交易失败,促使意外恢复


考虑一个简单的拍卖合同:


9.png


当智能合约准备给商品原主人付款时,如果付款失败,它将恢复。这意味着一个恶意的投标人可以在拍下商品的同时确保给商品原主人的付款总是付款失败。这样他们可以阻止其他人调用bid()函数,成为商品的新主人。如前所述,为了资金安全,建议拍卖时建立一个预授权方式的付款合约。


另一个例子是当智能合约通过数组的迭代向用户付款时,例如给众筹合约的支持者退款。通常要确保每笔付款都成功,如果哪一笔付款失败了,则会恢复,重新付款。问题是如果一笔付款失败了,那么你要恢复整个付款系统。这意味着如果哪一笔付款卡住了,这次迭代付款永远都不会完成,因为一个地址出错,所有人都拿不到这笔钱。


10.png


解决方案,这里我们的建议是使用预授权方式付款。




区块燃料限制导致的漏洞


漏洞六:利用区块燃料上限引发漏洞


你可能已经注意到了前一个例子中的另一个问题:如果要一次性地支付给所有人,你可能会遇到达到区块中燃料上限的情况。每个以太坊的区块都只能处理一定的最大计算量,如果你试图超过这个限制,那么你的交易将会失败。


即使没有黑客故意攻击你,这都是一个问题。如果攻击者能够操控你所需的燃料,情况就会变得更加糟糕。在前面的例子中,攻击者可以添加一堆地址,每个地址都需要很少量的退款。因此,加上给攻击者地址退款使用的燃料,可能会导致超过区块燃料上限,从而阻止退款交易的发生。


解决方案,我们推荐使用预授权方式付款来解决这个漏洞。


如果你绝对需要遍历未知大小的数组,那么你应该规划一下应该把它们分到多少个区块中,每个区块需要多少笔交易。这样你只需要留意现在进行到哪个区块中的交易了,出错后仅需从当前区块开始恢复,如下所示:


11.png


你需要确保在等待payOut()函数的下一次迭代时处理的其他交易不出现错误。所以只有在绝对必要的时候再使用这种模式。




强行给智能合约中加入以太币导致的漏洞


漏洞七:强行给智能合约中加入以太币,引发程序逻辑漏洞


原则上,我们可以将以太币强制发送到智能合约中而不触发回退函数。当给回退函数加入重要功能或计算智能合约的收支平衡时,这是一个重要的考虑因素。请看下面这个例子:


12.png


这个智能合约的逻辑似乎不允许对智能合约付款,以防发生一些“不好的事情”。但是还是存在一些方法可以强制将以太币送到合约中,使智能合约的余额大于0。


智能合约中的自毁方法允许用户向指定的受益人发送任意数量的以太币,而这个自毁方法并不会触及合约的回退功能。


在部署一个智能合约之前,可以预先算出合约的地址并将以太币发送到该地址。


解决方案,智能合约的开发者应该意识到以太币可以被强制送到智能合约中,并应该相应地设计智能合约逻辑。一般情况下,需要假设无法限制智能合约的资金来源。




对已被弃用的协议进行攻击导致的漏洞


漏洞八:利用已被弃用的协议进行攻击


这些攻击由于以太坊协议的改变或以太坊编程语言solidity的改进而不能使用。在这里记录仅供参考,不做过多说明。