智能合约安全审计 | 整数溢出

溢出攻击事件

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 + 115792089237316195423570985008687907853269984665640564039457584007913129639935

的SMT币

让我们一起以沉痛的心情缅怀 以上一夜归零的代币。

整数溢出简介

整数溢出原理

通常来说,在编程语言里由算数问题导致的整数溢出漏洞屡见不鲜,在区块链的世界里,智能合约的Solidity语言中也存在整数溢出问题,整数溢出一般分为又分为上溢和下溢,在智能合约中出现整数溢出的类型包括三种:

  • 乘法 溢出

  • 加法 溢出

  • 减法 溢出

在Solidity 语言中,变量支持的整数类型步长以8递增,支持从uint8到uint256,以 及int8到int256。例如,一个 uint8类型 ,只能存储在范围 0到2^8-1,也就是[0,255] 的数字,一个 uint256类型 ,只能存储在范围 0到2^256-1的数字。

在以太坊 虚拟机(EVM)中为整数指定固定大小的数据类型,而且是无符号的,这意味着在以太坊虚拟机中一个整型变量只能有一定范围的数字表示,不能超过这个制定的范围。

如果试图存储 256这个数字 到一个 uint8类型中,这个256数字最终将变成 0,所以整数 溢出的原理其实很简单,为了说明整数溢出原理,这里以 8 (uint8)位无符整型为例,8 位整型可表示的范围为 [0, 255],255 在内存中存储按位存储的形式为下图所示:


8 位无符整数 255 在内存中占据了 8bit 位置,若再加上 1 整体会因为进位而导致整体翻转为 0,最后导致原有的 8bit 表示的整数变为 0

数上溢的原理,同样整数下溢也是一样,如 (uint8)0 - 1 = (uint8)255。

溢出简单实例演示,这里以uint256类型演示:

pragma solidity ^0.4.25;


contract POC{

//加法溢出

//如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0

function add_overflow() returns (uint256 _overflow) {

uint256 max = 2**256 - 1;

return max + 1;

}



//减法溢出

//如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)

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;

}

}

将上述代码在编辑器上编译,部署。这里为了方便起见使用: http://remix.ethereum.org/#optimize=false&version=soljson-v0.4.25+commit.59dbf8f1.js 编辑器。

加法溢出

然后在给max变量加上1,导致上溢,最终结果max的值输出为0。

减法溢出

这里将uint256 类型的变量min设置为它的最小值(0),如果在减去一个1,导致下溢,最后min的值便会变成一个很大的值,即2**256-1,也就是uin256类型的最大值。


乘法溢出

这里将uint256 类型的mul变量设置为 2**255 ,然后在给mul变量乘以2,变成 2**256 ,超过最大值导致上溢,最终结果mul的值输出为0。

通过例子我们可以看到uint256当取最大整数值,上溢之后直接返回值为0,uint256当取0下溢之后直接返回值为2^256-1。这是solidity中整数溢出场景的常规情况,其他类型如uint8等也是一样的原理。

整数溢出防护

为了防止整数溢出的发生,一方面可以在算术逻辑前后进行验证,另一方面可以直接使用 OpenZeppelin 维护的一套智能合约函数库中的 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;

//加法溢出

//如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0

function add_overflow() returns (uint256 _overflow) {

uint256 max = 2**256 - 1;

return max.add(1);

}



//减法溢出

//如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)

function sub_underflow() returns (uint256 _underflow) {

uint256 min = 0;

return min.sub(1);

}

//乘法溢出

//如果uint256 类型的变量超过了它的最大值(2**256 - 1),最后它的值就会回绕变成0

function mul_overflow() returns (uint256 _underflow) {

uint256 mul = 2**255;

return mul.mul(2);

}

}

使用Safemath 方法后,不再产生溢出漏洞:

通过使用Safemath封装的加法,减法,乘法接口,使用assert方法进行判断,可以避免产生溢出漏洞。

案例分析

上面的例子介绍了原理,下面我们将通过实际案例介绍溢出漏洞是如何在生产环境中进行恶意攻击的。

案例一 BEC

漏洞原理分析

BEC合约地址:0xC5d105E63711398aF9bbff0 92d4B6769C82F793D

在etherscan上的 地址为: https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code

存在 溢出漏洞的合约代码如下:

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 ,没有使用safemath library,直接使用乘法运算符,产生整数溢出的地方就在合理。

其中变量 cnt 为转账的地址数量,可以通过外界的用户输入 _receivers 进行控制, _value 为单地址转账金额,也可以直接进行控制。

外界可以控制 _receivers _value 的数值,那么我们就可以控制 amount 变量的值,让其产生非预期的值,导致向上溢出。如 cnt = _receivers.length = 2 _value = 2**255 这样 amount = uint256(cnt) * _value = 2**255*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 )的 恶意转账。

实际攻击的恶意转账记 录: https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

从转账记录中可以看到分别给两个账户转了很大一 笔代币。

攻击模拟演示

部署成 功后,可以在Remix IDE的console窗口看到我们在测试网的交易hash,以及我们的合约。

为了真实模拟黑客攻击过程,下面进行演示。

实战操作视频链接:

https://v.qq.com/x/page/w082246wiuo.html 下面我们将BEC合约的代码在remix编辑器编译,部署,然后调用batchTransfer函数。

转入两 个需要转账的账户: ["0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e92 9c4e5364d2db"]

然后转入单 用户转账金额 578960446186580977117854925043439539266349923328202820197287920039565648199 68 2**255

最后转账后 ,转出账 户:0xca35b7d915458ef540ade6068dfe2f44e8fa733c代币不变,转入的两个账户余额都变为很大的值。

漏洞修复

在代码中可以看到,在语句 balances[msg.sender] = balances[msg.sender].sub(amount) balances[_receivers[i]] = balances[_receivers[i]].add(_value) 中,调用Safemath库中的安全函数来完成加减操作,所以这里在进行乘法运算时也需要使用Safemath来运算,将代码 uint256 amount = uint256(cnt) * _value 改为 uint256 amount = uint256(cnt).mul(_value); ,如图所示,使用Safemath库之后,攻击的转账失败。

案例二 SMT

BEC合约地址:0x55F93985431 Fc9304077687a35A1BA103dC1e081

在eth erscan上的地址为: https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code

存在溢出漏洞的合约 代码如下:

function transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt,

uint8 _v,bytes32 _r, bytes32 _s) public transferAllowed(_from) returns (bool){


if(balances[_from] < _feeSmt + _value) revert(); //溢出点,这里存在整数溢出


uint256 nonce = nonces[_from];

bytes32 h = keccak256(_from,_to,_value,_feeSmt,nonce);

if(_from != ecrecover(h,_v,_r,_s)) revert();


if(balances[_to] + _value < balances[_to]

|| balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();

balances[_to] += _value;

Transfer(_from, _to, _value);


balances[msg.sender] += _feeSmt;

Transfer(_from, msg.sender, _feeSmt);


balances[_from] -= _value + _feeSmt;

nonces[_from] = nonce + 1;

return true;

}

从代码中可 以看到这里的加、减、乘都没有使用安全处理,直接进行算数运算,而且_from, _to, _value, _feeSmt都可控,导致在if(balances[_from] < _feeSmt + _value) revert();这里, 将_value, _feeSmt构造为相加刚好溢出为0的值

如上面的value和feeSmt相 加然后去掉最高位,结果为0,小于balances[_frome],使得if条件不成立,绕过判断。

通过攻击的交易信息中可 以看到,黑客构造的内容:

上图可以看出,_value和_feeSmt相加刚好溢出为0,绕过if条件判断,从而给to账户和msg.sender账户分别转入value和feeSmt的代币。

案例三 FNT

FNT合约地址:0x82Cf44bE0768A3600c4BDe A58607783A3A7c51AE

在etherscan上的地 址为: https://etherscan.io/address/0x82cf44be0768a3600c4bdea58607783a3a7c51ae#code

此漏洞跟案例一的BEC漏洞,如出一辙。存在溢 出漏洞的合约代码如下:

function batchTransfers (address[] receivers, uint256[] amounts) public whenRunning returns (bool) {

uint receiveLength = receivers.length;

require(receiveLength == amounts.length);


uint receiverCount = 0;

uint256 totalAmount = 0;

uint i;

address r;

for (i = 0; i < receiveLength; i ++) {

r = receivers[i];

if (r == address(0) || r == owner) continue;

receiverCount ++;

totalAmount += amounts[i]; ////溢出点,这里存在整数溢出,计算需要转出的全部数额

}

require(totalAmount > 0);

require(canPay(msg.sender, totalAmount)); //这里判断msg.sender的余额大于totalAmount


wallets[msg.sender] -= totalAmount;

uint256 amount;

for (i = 0; i < receiveLength; i++) {

r = receivers[i];

if (r == address(0) || r == owner) continue;

amount = amounts[i];

if (amount == 0) continue;

wallets[r] = wallets[r].add(amount);

emit Transfer(msg.sender, r, amount); //逐一转账

}

return true;

}

从上述代码中可以看到变量receivers和amounts可控,首先计算需要转出的全部数额totalAmount,然后判断total Amount大于0,并且msg.senders的余额大于totalAmount的数值,最后从receivers和amounts中一一转账。

这里的溢出点在计算转账总额的地方totalAmount += amounts[i];,当amount的元素相加产生 溢出时,可以构造溢出时为一个很小的值比如1或者2,构造的amounts如下:

所以通 过上面的构造,我们成功绕过了判断,最后给两个账户分别转了2个代币和 1157920892373161954235709850 08687907853269984665640564039457584007913129639935个(2*256-1)的代币。

通过攻击的交易信息可以看 到结果跟我们分析的一致:

总结

我们在开发智能合约时,开发人员如果不加注意的话,只要没有检查用户输入内容,而且最终将输入带入执行计算,导致计算结果数字超出存储它们的数据类型允许的范围,那么此智能合约的输入内容就可以被用来组织攻击,导致安全漏洞。

所以为了防止整数溢出的发生,一方面可以在算术逻辑前后进行验证,另一方面可以直接使用 OpenZeppelin 维护的一套智能合约函数库中的 SafeMath 来处理算术逻辑。

很多项目在合约中已经导入了SafeMath库,但是因为开发者的粗心大意导致部分运算忘记添加本来已经using的safemath库,出现溢出漏洞。所以除了开发者自己提高安全开发意识之外,找专业的安全团队对合约进行全面的审计也是非常必要的。

(浏览原文 请点下方“阅读原文“——零时科技官方博客)

· E N D ·

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章