1.整数溢出攻击原理
在区块链的编程语言Solidity中,变量支持的整数类型步长以8递增,支持从uint8到uint256,以及int8到int256。一个 uint8类型 ,只能存储在范围 0到2^8-1,也就是[0,255] 的数字,一个 uint256类型 ,只能存储在范围 0到2^256-1的数字。
在以太坊虚拟机(EVM)中为整数指定固定大小的数据类型,而且是无符号的,这意味着在以太坊虚拟机中一个整型变量只能有一定范围的数字表示,不能超过这个制定的范围。
为了说明整数溢出原理,这里以 8 (uint8)位无符整型为例,8 位整型可表示的范围为 [0, 255]
,255
在内存中存储按位存储的形式为下图所示:
8 位无符整数 255 在内存中占据了 8bit 位置,若再加上 1 整体会因为进位而导致整体翻转为 0,最后导致原有的 8bit 表示的整数变为 0。
同样整数下溢也是一样,如 (uint8)0 - 1 = (uint8)255
。
2.整数溢出简单示例
pragma solidity ^0.4.25;
contract POC{
//加法溢出,uint256类型变量达到了最大值(2**256-1),再加上一个1,便变为0
function add_overflow() returns(uint256 _overflow){
uint256 max = 2**256 - 1;
return max + 1;
}
//减法溢出,uint256类型变量达到了最小值0,再减去一个1,便变成最大值
function sub_underflow() returns(uint256 _underflow){
uint256 min = 0;
return min - 1;
}
//乘法溢出,uint256类型变量超过了(2**256-1),最后会回绕为0
function mul_overflow() returns(uint256 _underflow){
uint256 mul = 2**255;
return mul * 2;
}
}
将上面的代码加载到Remix中,在Compile配置好编译器的版本:
在run中,选择好Environment为JVM。由于已经Compile,所以可以将合约POC部署在虚拟内存中:
点击Deploy按钮,在左下角的日志区会显示POC创建成功,并且在右半边的Transactions recorded中会显示出合约中可以调用的函数名:
分别点击三个函数名,即可分别调用三个函数,同时在日志区看到三次交易信息,在回执信息中可以看到上溢与下溢的结果:
![调用add,出现上溢问题](https://i-blog.csdnimg.cn/blog_migrate/3c1676efe58e060afcdec0486df880c9.png)
![](https://i-blog.csdnimg.cn/blog_migrate/72c16a836fee71335df928680a3b3d87.png)
![](https://i-blog.csdnimg.cn/blog_migrate/bf004bc4fbc698d7af96b4b598862d73.png)
3.整数溢出防护措施
对于整数溢出问题,OpenZeppelin 提供了一套智能合约函数库中的 SafeMath 。将SafeMath加入上面的示例代码中,再次通过相同操作来验证安全性。
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;
}
}
contract POC{
using SafeMath for uint256;
function add_overflow() returns(uint256 _overflow){
uint256 max = 2**256 - 1;
//return max + 1;
return max.add(1);
}
function sub_underflow() returns(uint256 _underflow){
uint256 min = 0;
//return min - 1;
return min.sub(1);
}
function mul_overflow() returns(uint256 _underflow){
uint256 mul = 2**255;
//return mul * 2;
return mul.mul(2) ;
}
}
SafeMath中对基础的算术操作都规定了函数,使得加减乘除不再是简单的符号,通过各个函数内部的assert判断机制, 来防护整数溢出问题。
通过实验可以发现,原先POC内部的三个函数都无法正常执行,相关交易也就无法发生。
4.BEC合约整数溢出复现
BEC智能合约的整数溢出问题爆发于18年4月22日,当时黑客通过合约中的整数溢出漏洞,凭空向空余额的地址转发巨额代币。
BEC合约地址为:0xC5d105E63711398aF9bbff092d4B6769C82F793D
在Etherscan上,BEC的代码地址为:https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
代码中溢出点存在于batchTransfer函数中:
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length; //cnt为转账的地址的数量
uint256 amount = uint256(cnt) * _value; //溢出点,存在整数溢出
require(cnt > 0 && cnt <= 20); //规定转账的地址数量范围
require(_value > 0 && balances[msg.sender] >= amount);
//单用户转账金额大于0,并且当前用户拥有的代币余额大于等于本次转账的总币数
balances[msg.sender] = balances[msg.sender].sub(amount);
//调用SafeMath的sub,从用户余额中减去本次转账花费的币数
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
//对转账的地址逐个执行转账操作
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
}
溢出点为 uint256 amount = uint256(cnt) * _value语句,没有使用SafeMath库的mul函数,而是直接采用乘法运算符。
每一行的代码含义已经注释,通过构造cnt和_value的值,可以绕过 require(_value > 0 && balances[msg.sender] >= amount)语句的判断,导致amount最终为0。
具体赋值为:cnt = _receivers.length = 2,_value = 2**255,这样amount = uint256(cnt) * _value = 2**255*2超过uint256表示的最大值,导致溢出,最终amount = 0。
部署好合约之后,可以在右下角看到batchTransfer函数,相关参数需要自己填写。
首先通过balanceOF查看账户的余额:
接着选取两个空余额的地址,并且验证余额的确为0:
两个地址分别为:0x14723a09acff6d2a60dcdf7aa4aff308fddc160c,0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db
然后通过调用batchTransfer函数来实现对两个地址的转账操作,转账金额均为57896044618658097711785492504343953926634992332820282019728792003956564819968(2**255)个代币,转账时只需设置好batchTranfer的两个参数即可,具体操作如图:
复制两个地址时需要注意的点是,前后双引号以及中间的逗号都需要为英文字体,否则会出错,无法解析出具体地址。
正确输入地址后,点击transact即可成功转账:
点击该交易可以看到交易具体信息,可以发现已经成功向两个地址分别转账了 2**255个代币:
与此同时,也可以通过balanceOf来查看各个地址的余额:
两个地址已经成功接收了2**255个代币,但是原地址的余额并没有发生变化,这就是利用了溢出达到了凭空转账的目的。
5.BEC合约整数溢出防护
由于已经找到了溢出点,因此只需要将乘法运算符改成SafeMath库的mul操作即可:
再次实施第四步中的操作,可以发现攻击行为已经无法实施:
6.总结
通过一个实际的攻击案例,再一次了解到整数溢出的原理和攻击方法。初步认为整数溢出的特征之一是存在简单的加减乘除运算符,因此可以将这个点作为后期检验整数溢出问题的一个方面。当时一份智能合约的代码通常上百行,而且存在简单运算符的地方可能较多,需要更多的条件来限制查找范围。于此同时,仅仅一份合约还不足以证明该特征的可信度,需要分析整合更多存在整数溢出的合约代码,争取可以分析出更多有效的信息。
参考:https://www.chainnode.com/post/293510
Remix地址:http://remix.ethereum.org/#optimize=false&version=soljson-v0.5.1+commit.c8a2cb62.js