首先要说明,整型溢出漏洞在Solidity的0.8.0版本以前是一直存在的,并且有不少因此漏洞被攻击的案例。但在0.8.0版本之后,官方团队修复了此漏洞,再出现溢出情况时会直接revert掉。此篇讲述的是0.8.0版本之前的整型溢出漏洞案例。
整型溢出漏洞描述
在以太坊虚拟机(EVM)中为整数指定固定大小的数据类型,而且是无符号的。这意味着一个整型变量只能有一定范围的数字表示。例如,一个 uint8 ,只能存储范围 [0,255] 的数字,试图在这个uint8变量中存储 256,将会导致这个变量变为0。不加注意的话,只要没有检查用户输入又执行计算,导致数字超出存储它们的数据类型允许的范围,Solidity 中的变量就可以被用来组织攻击。
整型溢出攻击案例
2018年4月22日,黑客对BEC智能合约发起攻击,凭空取出巨量BEC代币并在市场上进行抛售,BEC随即急剧贬值,价值几乎为0,该市场瞬间土崩瓦解。
漏洞代码:
js
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;
}
在batchTransfer函数中,出问题的地方在下面这行:
uint256 amount = uint256(cnt) * _value
其中变量cnt为转账的地址数量,可以通过外界的用户输入_receivers进行控制,_value为单地址转账金额,也可以直接进行控制。外界可以控制_receivers和_value的数值,那么我们就可以控制amount变量的值,让其产生非预期的值,导致向上溢出。如cnt = _receivers.length = 2,_value = 2255,这样amount = uint256(cnt) * _value = 2255*2超过uint256表示的最大值,导致溢出,最终amount = 0。 紧接着下面有一句对amount进行条件检查的代码require(_value > 0 && balances[msg.sender] >= amount);其中balances[msg.sender]代表当前用户的余额,amount代表要转的总币数。代码意思为确保单用户转账金额大于0,并且当前用户拥有的代币余额大于等于本次转账的总币数才进行后续转账操作。因为amount溢出后可以为一个很小的数字或者0(这里变成0),很容易绕过balances[msg.sender] >= amount的检查代码。从而产生巨大_value数额(这里为2**255)的恶意转账。
规避方法
- 现在可以直接升级Solidity到0.8.0版本以上规避此问题
- 在0.8.0版本以前,有一种选择是通过使用Open Zepplin 的 SafeMath库中定义的算术运算防止溢出。
在上述例子中,把问题行:
uint256 amount = uint256(cnt) * _value
改为:
uint256 amount = uint256(cnt).mul(_value)
SafeMath库中基本运算的方法如下所示:
js
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}