区块链整数溢出漏洞
文章首发于-区块链整数溢出漏洞 - 先知社区 (aliyun.com)
0x01 溢出攻击事件
2018年4月22日,黑客对BEC智能合约发起攻击,凭空取出
1 | 57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968 |
个BEC代币并在市场上进行抛售,BEC随即急剧贬值,价值几乎为0,该市场瞬间土崩瓦解。
2018年4月25日,SMT项目方发现其交易存在异常,黑客利用其函数漏洞创造了
1 | 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随即暂停了所有币种的充值提取业务。
2018年12月27日,以太坊智能合约Fountain(FNT)出现整数溢出漏洞,黑客利用其函数漏洞创造了
1 | 2 + 115792089237316195423570985008687907853269984665640564039457584007913129639935 |
的SMT币
0x02 漏洞简介
在编程语言里面,因为算术运算导致的整数溢出漏洞屡见不鲜
在solidity语言中,变量支持的类型从uint8-uint256,int8-int256。每一个整型变量只能存储固定大小数值范围内的数。uint表示无符号数。比如uint8只能储存0->2^8-1范围内的数字。当一个uint8类型的变量值为255时,在进行加一,就会发生进位,导致整体翻转为零。
简单来说,就是Solidity整形变量被赋值高于或者低于可以表示的范围时 值会发生改变 一般会溢出为2的uint类型次方 -1 或者 0
- 上溢:会溢出为0
- 下溢:会溢出为2^n-1
根据运算形式又可以分为
- 加法溢出
- 乘法溢出
- 减法溢出
0x03 简单演示
0x01加法溢出
在本地JavaScript VM 部署之后可以查看max与_overflow的值
如果uint8 类型的变量达到了它的最大值(2^8 - 1),如果在加上一个大于0的值便会变成0
可以看到max+1=256.超出了uint8能表示的范围,导致发生了加法上溢。
0x02乘法溢出
在本地JavaScript VM 部署之后可以查看max与_overflow的值
如果uint8 类型的变量超过了它的最大值(2^8 - 1),最后它的值就会回绕变成0
可以看到max*2=256.超出了uint8能表示的范围,导致发生了乘法上溢。
0x03减法溢出
在本地JavaScript VM 部署之后可以查看min与_overflow的值
如果uint8 类型的变量达到了它的最小值(0),如果在减去一个大于0的值便会变成2^8-1(uin8类型的最大值)
可以看到min-1=-1.超出了uint8能表示的范围,导致发生了减法下溢。
0x04 案例分析
下面将分别从三个案例分别分析加法,乘法,减法的整数溢出
0x01 SMT
在etherscan上的地址为:https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code
攻击记录:https://etherscan.io/tx/0x1abab4c8db9a30e703114528e31dee129a3a758f7f8abc3b6494aad3d304e43f
存在溢出漏洞的合约代码如下:
1 | function transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt, |
函数分析:
实现可以签名的转账功能
判断发送者的balances是否小于fee与value的和,如果小于就revert,就是如果发送者的balances大fee与value的和就继续往下执行。这里因为_value,_feeSmt两个参数可控,可以构造两个大叔造成加法溢出
- ecrecover是对函数的签名进行验证
- 判断_value与_feeSmt是否为零
- 执行转账,同时改变转账账户和接收账户的余额,交易次数加一
分析交易详情
1 | Function: transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt, uint8 _v, bytes32 _r, bytes32 _s) |
可以明显的看到_value, _feeSmt两个都很大,相加起来等于
1 | 0x10000000000000000000000000000000000000000000000000000000000000000 |
超出了uint256能储存的范围,产生了上溢,导致_feeSmt + _value变成了0,从而绕过了余额的检查,导致了恶意转账的发生
1 | 0x8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff= |
发生的转账
当时的市价约为0.7元…..然后那哥们对着UGT又玩了一次。
0x02 EBC
在etherscan上的地址为:https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
攻击记录:https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
存在溢出漏洞的合约代码如下:
1 | function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) { |
函数分析:
- 实现的是批量转账功能,接收的参数是地址数组和转账金额
- _value由用户控制,可以实现cnt * _value > 2^256 - 1使得 amount置零
- 使用require语句对地址数组和账户余额进行判断
- 通过上述判断之后,对地址数组里面的地址转账
分析交易详情
1 | Function: batchTransfer(address[] _receivers, uint256 _value)MethodID: 0x83f12fec[0]: 0000000000000000000000000000000000000000000000000000000000000040[1]: 8000000000000000000000000000000000000000000000000000000000000000[2]: 0000000000000000000000000000000000000000000000000000000000000002[3]: 000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033[4]: 0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7 |
这里涉及到区块链的参数编码,可以参考PIKACHU师傅的文章
可以看到此时的
1 | _receivers.length=2_value=8000000000000000000000000000000000000000000000000000000000000000 |
两者相乘得到2^256,超出uint256能储存的范围,产生了上溢,导致amount变成了零,从而绕过了第二个语句对账户余额的判断。
发生的转账
0x03 BTCR
在etherscan上的地址为:https://etherscan.io/address/0x6aac8cb9861e42bf8259f5abdc6ae3ae89909e11#code
存在溢出漏洞的合约代码如下:
1 | function distributeBTR(address[] addresses) onlyOwner { for (uint i = 0; i < addresses.length; i++) { balances[owner] -= 2000 * 10**8; balances[addresses[i]] += 2000 * 10**8; Transfer(owner, addresses[i], 2000 * 10**8); }} |
函数分析:
- 实现代币的批量分配,但是只能转账固定的数额
- 每分配一次,就减去相应的数值
攻击分析:
由于合约部署的时候将onlyOwner设置为合约人的账户地址,所以该漏洞只有Owner可以利用
- 因为没有判断Owner的账户是否有足够的余额,所以导致了减法的整型下溢出
- 在部署时,balances[owner] = 21000000 * 10^8,也就是说最多执行10500次Transfer()就会产生下溢出
- 这样会导致改代币严重的供给关系失衡,导致代币市值严重下跌
0x05 防御方式
0x01 算术运算前后验证
- 加法运算的和一定大于加数和被加数
- 乘法运算的积一定大于乘数和被乘数
- 减法运算的差一定小于两者的和,或者至少小于其中一个
0x02 SafeMath
SafeMath时OpenZeppelin 维护的一套智能合约函数库中用来处理算术逻辑的函数库
1 | pragma solidity ^0.4.25;library SafeMath { function mul(uint256 a, uint256 b) internal constant returns (uint256) { uint256 c = a * b; assert(a == 0 || c / a == b); return c; } function div(uint256 a, uint256 b) internal constant returns (uint256) { uint256 c = a / b; return c; } function sub(uint256 a, uint256 b) internal constant returns (uint256) { assert(b <= a); return a - b; } function add(uint256 a, uint256 b) internal constant returns (uint256) { uint256 c = a + b; assert(c >= a); return c; }} |
再次演示,为了方便展示结果,使用uint8
1 | contract OverFlow { using SafeMath for uint8; //加法溢出 function add_overflow() returns (uint8 _overflow) { uint8 max =2**8 - 1; return max.add(1); } //乘法溢出 function mul_overflow() returns (uint8 _underflow) { uint8 mul = 2**7; return mul.mul(2); } //减法溢出 function sub_underflow() returns (uint8 _underflow) { uint8 min = 0; return min.sub(1); }} |
部署成功后,此时不论调用哪个函数都会报错。
0x06 总结
开发智能合约时,如果不严格检查用户的输入的话,会将用户的输入带入执行计算,这就有可能带来安全风险。同时在前面案例分析中,SMT和EBC合约源码中有使用SafeMth,但是在运算忘记添加,导致了漏洞的发生。由于区块链的不可篡改性质,一旦部署上链的合约无法进行漏洞修复。虽然可以通过拉黑ETH地址的方式阻止攻击者提现,但是是在攻击发现后,依旧无法阻止在未发现攻击行为之前攻击者短时间的大额提现。