05.整数溢出_1
2023-06-23 20:22:31 # 00.security

整数溢出

简介

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
}//我们需要注意的是,有两个返回值:一个是diff,一个是借位信号borrowOut

这个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

image-20221210213433458

2

image-20221210213514729

预防措施

  • 使用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;
}
Prev
2023-06-23 20:22:31 # 00.security
Next