背景python
今天有人在群里说,Beauty Chain 美蜜 代码里面有bug,已经有人利用该bug得到了 57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968 个 BEC安全
那笔操做记录是 0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f测试
下面我来带你们看看,黑客是如何实现的!ui
咱们能够看到执行的方法是 batchTransfer
3d
那这个方法是干吗的呢?(给指定的几个地址,发送相同数量的代币)code
你传几个地址给我(receivers),而后再传给我你要给每一个人多少代币(value)orm
而后你要发送的总金额 = 发送的人数* 发送的金额blog
而后 要求你当前的余额大于 发送的总金额token
而后扣掉你发送的总金额开发
而后 给receivers 里面的每一个人发送 指定的金额(value)
从逻辑上看,这边是没有任何问题的,你想给别人发送代币,那么你自己的余额必定要大于发送的总金额的!
可是这段代码却犯了一个很傻的错!
这个方法会传入两个参数
_receivers 的值是个列表,里面有两个地址
0x0e823ffe018727585eaf5bc769fa80472f76c3d7
0xb4d30cac5124b46c2df0cf3e3e1be05f42119033
_value 的值是 8000000000000000000000000000000000000000000000000000000000000000
咱们再查看代码(以下图)
咱们一行一行的来解释
uint
cnt
=
_receivers.length;
是获取 _receivers 里面有几个地址,咱们从上面能够看到 参数里面只有两个地址,因此 cnt=2,也就是 给两个地址发送代币
uint256 amount
=
uint256(cnt)
*
_value;
首先 uint256(cnt)
是把cnt 转成了 uint256类型
那么,什么是uint256类型?或者说uint256类型的取值范围是多少...
uintx 类型的取值范围是 0 到 2的x次方 -1
也就是 假如是 uint8的话
则 uint8的取值范围是 0 到 2的8次方 -1
也就是 0 到255
那么uint256 的取值范围是
0 - 2的256次方-1 也就是 0
到115792089237316195423570985008687907853269984665640564039457584007913129639935
python 算 2的256次方是多少
那么假如说 设置的值超过了 取值范围怎么办?这种状况称为 溢出
举个例子来讲明
由于uint256的取值太大了,因此用uint8来 举例。。。
从上面咱们已经知道了 uint8 最小是0,最大是255
那么当我 255 + 1 的时候,结果是啥呢?结果会变成0
那么当我 255 + 2 的时候,结果是啥呢?结果会变成1
那么当我 0 - 1 的时候,结果是啥呢?结果会变成255
那么当我 0 - 2 的时候,结果是啥呢?结果会变成255
那么 咱们回到上面的代码中,
amount
=
uint256(cnt)
*
_value
则 amount = 2* _value
可是此时 _value 是16进制的,咱们把他转成 10进制
(python 16进制转10进制)
能够看到 _value = 57896044618658097711785492504343953926634992332820282019728792003956564819968
那么amount = _value*2 = 115792089237316195423570985008687907853269984665640564039457584007913129639936
能够在查看上面看到 uint256取值范围最大为 115792089237316195423570985008687907853269984665640564039457584007913129639935
此时,amout已经超过了最大值,溢出 则 amount
=
0
下一行代码 require(cnt
>
0
&&
cnt
<=
20);
require 语句是表示该语句必定要是正确的,也就是 cnt 必须大于0 且 小于等于20
咱们的cnt等于2,经过!
require(_value
>
0
&&
balances[msg.sender]
>=
amount);
这句要求 value 大于0,咱们的value是大于0 的 且,当前用户拥有的代币余额大于等于 amount,由于amount等于0,因此 就算你一个代币没有,也是知足的!
balances[msg.sender]
=
balances[msg.sender].sub(amount);
这句是当前用户的余额 - amount
当前amount 是0,因此当前用户代币的余额没有变更
for
(uint
i
=
0;
i
<
cnt;
i++)
{
balances[_receivers[i]]
=
balances[_receivers[i]].add(_value);
Transfer(msg.sender,
_receivers[i],
_value);
}
这句是遍历 _receivers中的地址, 对每一个地址作如下操做
balances[_receivers[i]]
=
balances[_receivers[i]].add(_value);
_receivers中的地址 的余额 = 本来余额+value
因此 _receivers 中地址的余额 则加了57896044618658097711785492504343953926634992332820282019728792003956564819968 个代币!!!
Transfer(msg.sender,
_receivers[i],
_value);
}
这句则只是把赠送代币的记录存下来!!!
就一个简单的溢出漏洞,致使BEC代币的市值接近归0
那么,开发者有没有考虑到溢出问题呢?
其实他考虑了,
能够看如上截图
除了amount的计算外, 其余的给用户转钱 都用了safeMath 的方法(sub,add)
那么 为啥就恰恰这一句没有用safeMath的方法呢。。。
这就要用写代码的人了。。。
safeMath 是为了计算安全 而写的一个library
咱们看看他干了啥?为啥能保证计算安全.
function
mul(uint256 a,
uint256 b)
internal
constant returns
(uint256)
{
uint256 c
=
a
*
b;
assert(a
==
0
||
c
/
a
==
b);
return
c;
}
如上面的乘法. 他在计算后,用assert 验证了下结果是否正确!
若是在上面计算 amount的时候,用了 mul的话, 则 c
/
a
==
b
也就是 验证 amount / cnt == _value
这句会执行报错的,由于 0 / cnt 不等于 _value
因此程序会报错!
也就不会发生溢出了...
那么 还有一个小问题,这里的 assert
好 require
好像是干的同一件事
都是为了验证 某条语句是否正确!
那么他俩有啥区别呢?
用了assert的话,则程序的gas limit 会消耗完毕
而require的话,则只是消耗掉当前执行的gas
那么 咱们如何避免这种问题呢?
我我的见解是
这件过后须要如何处理呢?
目前,该方法已经暂停了(还好能够暂停)因此看过文章的朋友 不要去测试了...
不过已经发生了的事情咋办呢?
我能想到的是,快照在漏洞以前,全部用户的余额状况
而后发行新的token,给以前的用户 发送等额的代币...