4月发生的BEC事件以及SMT事件已经沉淀一段时间了,具体的情况也被多方媒体所报道,相关的漏洞根源问题也有很多大神团队的分析和指正,成都链安科技团队将各种已经发生或可能发生的类似溢出漏洞原理进行整理,再次将全方位的原理分析与大家分享。
事件回顾
2018年4月22日,黑客对BEC智能合约发起攻击,凭空取出57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968 个BEC代币并在市场上进行抛售,BEC随即急剧贬值,价值几乎为0,该市场瞬间土崩瓦解[1]。
2018年4月25日,SMT项目方发现其交易存在异常,黑客利用其函数漏洞创造了65,133,050,195,990,400,000,000,000,000,000,000,000,000,000,000,000,000,000,000
+
50,659,039,041,325,800,000,000,000,000,000,000,000,000,000,000,000,000,000,000
的SMT币,火币Pro随即暂停了所有币种的充值提取业务[2]。
让我们一起以沉痛的心情缅怀以上一夜归零的代币。
仅仅在BEC事件过去后的12小时内,就有多达12个项目的智能合约存在类似的整数溢出类型的漏洞,黑客可以利用这一漏洞转账“无中生有”的巨大数量代币,也就是我们所说的“增发”。
代币增发为什么就会贬值
人们常说“物以稀为贵”,意思是事物因稀少而且有益,从而显得珍贵[3]。
黄金被选为最早时期全世界公认的货币的原因就是因为它稀缺,地球上储量有限,易分割,属性稳定等特点[4]。
中国人首先发明了“交子”,来代替不便交易的黄金。
所以就出现了金本位的概念,很多国家发行自己货币的时候都是按照其黄金储量来发行,并不是想发多少就发多少。
试想一下如果凭空出现了一颗黄金做的大陨石,撞击地球后黄金撒了一地,黄金还会值钱吗?
所以当一个代币的数量在有了额定发行总量之后,突然肆意的增加,必然会导致代币的贬值,甚至失去在市场上流通的意义。这就是“代币增发”的不良后果。
整数溢出漏洞分析
前面提到,黑客是利用整数溢出漏洞绕过了转账数额的相关规则,进而增发代币。
那什么是整数溢出呢?为什么能用整数溢出来实现增发来秀一波空手套白狼的操作?
我来举个例子:
比如有一个国家的人不会复杂的数学运算,只会从0数到9,每次数到9之后又从0开始数,最后以数到的数作为结果。大家都这样平安无事的生活着,但有一天,有一个从别的国家来的小黑,他发现了这个问题,于是他去金库拿金锭,拿出来的个数超过了9个,于是金库管理员帮他数,数到9之后又从0开始了,最后结算发现他取出来的结果是0个金锭,但实际上他已经把金库里的金锭几乎都取完了。
黑客利用类似的机制凭空向一个账户中转账了超级大数额的代币,而合约中的逻辑只要求他花费很小的代价。
成都链安科技审计组对过往代币增发事件漏洞类型进行整理,概括出整型溢出类型漏洞的全面分析。
以太坊虚拟机(EVM)为整数指定固定大小的数据类型。这意味着一个整型变量只能有一定范围的数字表示。例如,一个 uint8 ,只能存储在范围 [0,255] 的数字。试图存储 256 到一个 uint8 将变成 0。不加注意的话,只要没有检查用户输入又执行计算,导致数字超出存储它们的数据类型允许的范围,Solidity 中的变量就可以被用来组织攻击。
整数溢出的类型包括乘法溢出,加法溢出,减法溢出三种。
链安科技团队对BEC事件进行分析后,发现可将其漏洞归类于乘法溢出,原理如下:
乘法溢出
• 案例(CVE-2018-10299)
上述合约代码中,存在漏洞的代码为uint256 amount = uint256(cnt) * _value;,计算转出总额度amount未使用SafeMath也未对溢出进行检查,直接将转账地址数量乘以转账额度,如果输入极大的_value,那么amount计算结果就可能产生溢出,导致代币增发。
在Remix-ide中测试如下:
1、部署合约;
2、调用batchTransfer函数,向batchTransfer函数传入地址数组["0xb4D30Cac5124b46C2Df0CF3e3e1Be05f42119033","0x0e823fFE018727585EaF5Bc769Fa80472F76C3d7"],以及_value"0x8000000000000000000000000000000000000000000000000000000000000000"即2*255,使得amount=2\*255 * 2,超出uint256类型的范围[0,2**256-1],溢出为0,发送者账户余额不减少,并且,本例中,发送者的代币可以为零,实现"无中生有"。
3、查看余额:
而针对SMT事件进行分析后,发现其漏洞属于加法溢出类型,其原理如下:
加法溢出
• 案例
上述合约代码中,mintToken函数的功能是owner向指定账户增发mintedAmount数量的代币,但是在对balanceOf[target]与totalSupply进行加法操作未做溢出检查,导致其可能存在溢出,并且,通过溢出,恶意owner可以任意增减target账户的余额,或者增发实际远远超过totalSupply的代币。
在Remix-ide中测试如下:
1、部署合约;
2、向target预先转一部分代币,模拟目标账户中已有的代币:调用transfer函数,传入target地址:
0x14723a09acff6d2a60dcdf7aa4aff308fddc160c,以及转账额度,比如2000000000000000000(2 * 10**uint256(decimals));
3、如果owner想控制target的余额减半,那么,他只需要向target增发2*256-balanceOf[target]+10\*18=0xfffffffffffffffffffffffffffffffffffffffffffffffff21f494c589c0000,现在调用mintToken函数,向target地址转入上述数量的代币:
另外还有减法溢出的操作,虽然未出现相关的漏洞攻击事件,我们也提供了相关的原理分析:
减法溢出
• 案例
上述合约代码中,distribute函数的功能是从owner账户向指定的地址列表转入2000 * 10**8代币,但是在对balances[owner]的计算中未使用SafeMath,也未判断owner账户是否有足够的代币,当转出代币总量大于owner账户余额的时候,balances[owner]产生减法溢出,变成一个极大值。
在Remix-ide中测试如下:
1、部署合约
2、调用distribute函数,传入地址数组:
["0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"]
owner分别向这两个地址发送2000 * 10**8代币,超过owner余额,balances[owner]产生减法溢出;
3、查询owner账户余额,等于2**256-2000*10**8:
黑客虽然无孔不入,但链安科技团队对具有此类漏洞的合约进行综合性调研后,提出了“亡羊补牢”的修复措施:
漏洞修复
OpenZeppelin提供了一套很好的SafeMath库,使用SafeMath库函数能够有效避免溢出漏洞,SafeMath库源码如下:
子曰:吾日三省吾身
我们从这些惨痛的教训中能总结出:
1. 基于ERC20协议编写的这些智能合约给予开发者的权力过大了,这些溢出问题本应该放在底层检测。
2. 编写合约的开发者没有以严谨敬业的精神去遵守开发规范,使用SafeMath去做相关功能,或者进行溢出测试,在这里提出批评。
3. 没有严谨的逻辑,单单凭借创造力并不是都能出成果,还是要稳中求胜,能使用库就使用库。
所以,链安科技团队建议,为了避免程序结果中产生溢出,破坏智能合约执行逻辑,建议开发者在所有数学运算中都使用SafeMath(敲黑板)。
引用:
[1]: http://bc.jrj.com.cn/2018/04/26132824455908.shtml
[2]: https://zhuanlan.zhihu.com/p/36116810
[3]: https://baike.baidu.com/item/%E7%89%A9%E4%BB%A5%E7%A8%80%E4%B8%BA%E8%B4%B5
[4]: http://www.woshipm.com/blockchain/945232.html
领取专属 10元无门槛券
私享最新 技术干货