看个图:
这是个计算汽车行驶距离的里程表。 范围是000000 到999999。说明啥,车开了100万公里后,里程表直接回到000000。
细思极恐。
回看下2000年的千年虫(Y2K)。 Y2K是一类计算机漏洞,堪称假想中的大浩劫。
千禧危机、千年虫、千年问题 。千年问题可以追溯到二十世纪六十年代。当时计算器内存非常宝贵,故而编程人员一直借助使用 MM/DD/YY 或 DD/MM/YY 即月月/日日/年年或日日/月月/年年的方式来显示年份,但是当年序来到公元2000年的1月1日,系统却无法自动辨识00/01/01究竟代表1900年的1月1日,还是2000年的1月1日,所有的软硬件都可能因为日期的混淆而产生资料流失、系统死机、程序紊乱、控制失灵等问题,如此所造成的损失以及灾难是无法估计想像的。
上面的两个例子叫‘整数溢出’,属于一类错误。今天,我们就来探讨下溢出和下溢攻击对智能合约造成的影响。
溢出错误
数字增长超过其最大值时发生溢出。 好比声明一个uint8变量(8位的无符号变量)。 意思是,变量的数值范围0到28-1(255)。
看下边:
uint 8 a = 255;
a++;
发生溢出,因为a的最大值是255。
Solidity最大能处理256位数字,最大值为2256-1,加1会导致归 0,发生溢出:
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+ 0x000000000000000000000000000000000001
—————————————-
= 0x000000000000000000000000000000000000
用简单的token转账合约看看溢出错误(代码取自GitHub):
mapping (address => uint256) public balanceOf;
// INSECURE
function transfer(address _to, uint256_value) {
/* Check if sender has balance */
require(balanceOf[msg.sender] >= _value);
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
// SECURE
function transfer(address _to, uint256_value) {
/* Check if sender has balance and for overflows */
require(balanceOf[msg.sender] >= _value && balanceOf[_to] +_value >= balanceOf[_to]);
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
什么意思?
上面程序中有两个“转账”函数(安全、不安全)。 一个有检查溢出,另一个没有。 安全转账函数有检查余额是否到达最大值。
注意一点,特别在上面的例子中,安全函数不一定必须被包含。 就是说作为开发者,对余额达到上限的可能性自己得有个判断,免得花无谓的gas。
下溢错误
好了,看看另个极端,也就是下溢错误。Over跟under是反义词。这俩错误也是反着来的。
uint8只能取0到255之间的值,还记得吧? 那么,考虑以下代码。
unint8 a = 0;
a–;
这就是下溢,后果是a的最大可能值是255。
出现在Solidity智能合约中,就是这:
0x000000000000000000000000000000000000
– 0x000000000000000000000000000000000001
—————————————-
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
意思是,造成严重的数据错误表示。
还是用合约代码解释下,下溢的严重后果(代码取自GitHub):
mapping (address => uint256) public balanceOf;
// INSECURE
function transfer(address _to, uint256_value) {
/* Check if sender has balance */
require(balanceOf[msg.sender] >= _value);
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
// SECURE
function transfer(address _to, uint256_value) {
/* Check if sender has balance and for overflows */
require(balanceOf[msg.sender] >= _value && balanceOf[_to] +_value >= balanceOf[_to]);
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
这个例子中,因为动态数组是以顺序方式存储的,黑客可以利用manipulateMe,然后这么做:
调用popBonusCode产生下溢
计算manipulateMe的存储位置
使用modifyBonusCode修改并更新manipulateMe的值
当然了,单独函数中能一眼找出错误。 试想,有个超复杂的智能合约,几千行代码,代码检查也能一眼找出错误么?
溢出攻击、下溢攻击的危害
下溢错误发生的几率高于溢出错误,因为某人为引发溢出获取所需数量token(最大值就是)的可能性相当低。
假设Bob手里有X个token,但是不妨碍他想花掉X+1个。
若程序不检查一下Bob的余额,就有可能什么呢,Bob最后还真花掉了X+1个token。
看看下面的例子(取自nethemba):
pragma solidity ^0.4.22;
contract Token {
mapping(address => uint) balances;
function transfer(address _to, uint _value)public {
require(balances[msg.sender] – _value >=0);
balances[msg.sender] -= _value;
balances[_to] += _value;
}
}
代码中,“转账”函数中的require条件乍一看好像没问题,注意交易双方之间的操作产生的是单位值。
意思是,不管条件为何,balances[msg.sender]– _value >= 0的值永远为真。
这种情形下,黑客可以最大化自己的余额。
比方说, 黑客有100个token,但是他想要101个。最后会得到100 - 101个token,因为下溢,一共得来2 256-1个token。
注释:
关于无符号数减去无符号数的用法错误:
if ( i - j >=0) 假如i,j为无符号数,这样写可能会引发错误,即当i小于j的时候,这个式子仍然成立,因为无符号数始终是大于等于零的。例: if ( strlen( a ) >= 10) 与 if (strlen ( b ) -10 >= 0) 这两条语句是不相等的 ,因为strlen函数返回的是无符号数类型。
意思是:
无符号数相减,如果被减数小于减数,那么结果会是一个非常大的无符号数,而不是一个想象中的有符号数。
所以对于无符号数相减之前需要进行判断,最好做比较的时候使用 if ( strlen( a ) >=
10) 这种方式,而不要使用if (strlen ( b ) -10 >= 0) 这种方式。因为无符号数进行计算的结果还是无符号数;另外无符号数和有符号数计算时,有符号数会被强制转提升无符号数。
再然后,系统可能就瘫了。
解决方法是总是检查代码中的下溢或上溢。这里有安全库协助检查,例如SafeMath或者OpenZeepelin。
现实生活中的下溢问题
有个准庞氏币叫POWH(Proof of Weak Hands Coin)。然后买的人还不少,筹了100多万刀。
但是,POWH开发者对操作的安全性不太上心,无法妥善应溢出或下溢攻击。然后,有个黑客瞄准这个弱点,直接卷了2000个ETH,差不多230万刀的样子。
这里想说的是,作为开发者,真是要加强对这种攻击是防御;作为赏金猎人的话,也是不应该对这种攻击放松警惕的。
Overflow and Underflow Attacks on Smart Contracts (Blockgeeks Guide)blockgeeks.com
网友评论