整数溢出 简介 solidity和其他编程语言一样,存在溢出问题。它包括加法溢出、减法溢出、乘法溢出三类
原理 在solidity中,变量支持的整数类型长度以8递增,从uint8到uint256,以及int8到int256。
EVM中储存一个数所占的位数是固定,在solidity 0.8.0之前,只有截断模式,当存储的数字长度超出最大值时会导致进位,使所有1翻转成0。比如:uint8的255(11111111),当它加1,不会变成256,而是变成0,原因是11111111===>00000000。EVM不会提示你可能溢出了。
各个uint变量的最大值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 uint8 最高为: 255 uint16 最高为: 65535 uint24 最高为: 16777215 uint32 最高为: 4294967295 uint40 最高为: 1099511627775 uint48 最高为: 281474976710655 uint56 最高为: 72057594037927935 uint64 最高为: 18446744053709551615 uint72 最高为: 4722366482869645213695 uint80 最高为: 1208925819614629174706175 uint88 最高为: 309485009821345068724781055 uint96 最高为: 79228162514264337593543950335 uint104 最高为: 20282409603651670423947251286015 uint112 最高为: 5192296858534827628530496329220095 uint120 最高为: 1329227995784915872903805060280344575 uint128 最高为: 340282366920938463463374605431768211455 uint136 最高为: 87112285931760246646623899502532662132735 uint144 最高为: 22300545198530623141535718272648361505980415 uint152 最高为: 5708990570823839524233143877797980545530986495 uint160 最高为: 1461501637330902918203684832716283019655932542975 uint168 最高为: 374144419156711147060143317175368453031918731001855 uint176 最高为: 95780971304118053647396689196894323976171195136475135 uint184 最高为: 24519928653854221733733552434404946937899825954937634815 uint192 最高为: 6277101735386680563835789423205666416102355444464034512895 uint200 最高为: 1606938044258990275541962092341162602522202993782792835301375 uint208 最高为: 411376139330301510538742295639337626245683966408394965837152255 uint216 最高为: 105312291668557186697918027683670432318895095400549111254310977535 uint224 最高为: 26959946667150639794667015087019630673637144422540572481103610249215 uint232 最高为: 6901746346790563787434755862277025452451108972170386555162524223799295 uint240 最高为: 1766847064778384329583297500542918515827483896875618958121606201292619775 uint248 最高为: 452312848583266388373324160190187140051835877600158453279131187530910662655 uint256 最高为: 115792089237316195423570985008687905853269984665640564039457584005913129639935
溢出复现 我们以加法溢出为例,对溢出原理进行深入理解
1 2 3 4 5 6 7 pragma solidity <=0.6.0; contract Test { function sub(uint a, uint b) public pure returns (uint) { uint c = a - b; return c; } }
因此,我们知道,solidity的减法操作在EVM中是使用sub操作码进行的。go-ethereum中的sub源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (z *Int) Sub(x, y *Int) *Int { var carry uint64 z[0 ], carry = bits.Sub64(x[0 ], y[0 ], 0 ) z[1 ], carry = bits.Sub64(x[1 ], y[1 ], carry) z[2 ], carry = bits.Sub64(x[2 ], y[2 ], carry) z[3 ], _ = bits.Sub64(x[3 ], y[3 ], carry) return z } type Int [4 ]uint64 func Sub64 (x, y, borrow uint64 ) (diff, borrowOut uint64 ) { diff = x - y - borrow borrowOut = ((^x & y) | (^(x ^ y) & diff)) >> 63 return }
这个Int实际上是由4个uint64串联而成的结构,而四个64位就是256位,所以我们可以把结果直接看成uint256,那么这个函数实现的就是两个uint256数的相减。关键:借位信号返回值被忽略
相关案例
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,该市场瞬间土崩瓦解。
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随即暂停了所有币种的充值提取业务。
2018年12月27日,以太坊智能合约Fountain(FNT)出现整数溢出漏洞,黑客利用其函数漏洞创造了:
2+115792089237316195423570985008687905853269984665640564039457584005913129639935的SMT币。历史的血泪教训,如今不该再次出现。让我们一起缅怀这些一夜归零的代币,吸取前人经验教训。
以BEC合约为例,合约地址为:0xC5d105E63711398aF9bbff092d4B6769C82F793D 。
在 etherscan 上的地址为:https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
存在溢出漏洞的合约代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) { uint cnt = _receivers.length; uint256 amount = uint256(cnt) * _value; //溢出点,这里存在整数溢出 require(cnt > 0 && cnt <= 20); require(_value > 0 && balances[msg.sender] >= amount); balances[msg.sender] = balances[msg.sender].sub(amount); for (uint i = 0; i < cnt; i++) { balances[_receivers[i]] = balances[_receivers[i]].add(_value); Transfer(msg.sender, _receivers[i], _value); } return true; }
黑客传入了一个极大的值(这里为2**255),通过乘法向上溢出,使得 amount(要转的总币数)溢出后变为一个很小的数字或者0(这里变成0),从而绕过 balances[msg.sender] >= amount 的检查代码,使得巨大 _value 数额的恶意转账得以成功。
实际攻击的恶意转账记录:https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
溢出漏洞攻击案例 其他有问题的代码 1
2
预防措施
使用OpenZeppelin的SafeMath库。注意:库函数可能会使合约无法使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 pragma solidity ^0.4.24; library SafeMath { function mul(uint256 a, uint256 b) internal pure returns (uint256) { if (a == 0) { return 0; } uint256 c = a * b; require(c / a == b); return c; } function div(uint256 a, uint256 b) internal pure returns (uint256) { require(b > 0); uint256 c = a / b; return c; } function sub(uint256 a, uint256 b) internal pure returns (uint256) { require(b <= a); uint256 c = a - b; return c; } function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a); return c; } function mod(uint256 a, uint256 b) internal pure returns (uint256) { require(b != 0); return a % b; } }
有效的上下文校验:使用require、revert、assert
1 2 3 4 5 function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a); return c; }
最简单的方法是使用至少 0.8 版本的 Solidity 编译器。在 Solidity 0.8 中,编译器会自动检查上溢和下溢。不会溢出
1 2 3 4 5 6 7 8 pragma solidity ^0.8.0; contract Test { uint public a = 0; function y() public{ a--; } }
如果就是想溢出呢?
可以这么做:使用unchecked,这样做的意思是让编译器不要检查(不检查模式)
1 2 3 4 5 6 7 8 9 10 pragma solidity ^0.8.0; contract Test { uint public a = 0; function y() public{ unchecked{ a--; } } }
这是使用纯python编写的solidity代码审计工具,不需要安装solc等其他环境,可一键安装。如图是BEC整数溢出漏洞检测出来的结果
其他 for循环
fori循环中,i 的类型将是 uint8,因为这是保持值 0 所需的最小类型。如果数组的元素超过 255 个,则循环不会终止。使用uint i
(256 位)可以避免这个问题。
注意:EVM 不允许无限计算,因此循环将消耗所有气体,交易将终止,但仍需向矿工支付费用。
1 2 3 for (var i = 0; i < arrayName.length; i++) { uint ikun = 0; }