一行代码蒸发了¥6,447,277,680 人民币

来源:金色财经 作者:佚名 04-25 16:42

今天有人在群里说,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 个 BECfjL哈希力量 | 区块链第一门户

那笔操作记录是 0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660ffjL哈希力量 | 区块链第一门户

5f850564923142b9a3e26618d269afa1.jpegfjL哈希力量 | 区块链第一门户

下面我来带大家看看,黑客是如何实现的!fjL哈希力量 | 区块链第一门户

我们可以看到执行的方法是 batchTransferfjL哈希力量 | 区块链第一门户

那这个方法是干嘛的呢?(给指定的几个地址,发送相同数量的代币)fjL哈希力量 | 区块链第一门户

整体逻辑是fjL哈希力量 | 区块链第一门户

你传几个地址给我(receivers),然后再传给我你要给每个人多少代币(value)fjL哈希力量 | 区块链第一门户

然后你要发送的总金额 = 发送的人数* 发送的金额fjL哈希力量 | 区块链第一门户

然后要求你当前的余额大于 发送的总金额fjL哈希力量 | 区块链第一门户

然后扣掉你发送的总金额fjL哈希力量 | 区块链第一门户

然后给receivers 里面的每个人发送 指定的金额(value)fjL哈希力量 | 区块链第一门户

从逻辑上看,这边是没有任何问题的,你想给别人发送代币,那么你本身的余额一定要大于发送的总金额的!fjL哈希力量 | 区块链第一门户

但是这段代码却犯了一个很傻的错!fjL哈希力量 | 区块链第一门户

代码解释fjL哈希力量 | 区块链第一门户

这个方法会传入两个参数fjL哈希力量 | 区块链第一门户

_receiversfjL哈希力量 | 区块链第一门户

_valuefjL哈希力量 | 区块链第一门户

_receivers 的值是个列表,里面有两个地址fjL哈希力量 | 区块链第一门户

0x0e823ffe018727585eaf5bc769fa80472f76c3d7fjL哈希力量 | 区块链第一门户

0xb4d30cac5124b46c2df0cf3e3e1be05f42119033fjL哈希力量 | 区块链第一门户

_value 的值是 8000000000000000000000000000000000000000000000000000000000000000fjL哈希力量 | 区块链第一门户

我们再查看代码(如下图)fjL哈希力量 | 区块链第一门户

b04e28b3043246fcaf910e1248e4294e.jpegfjL哈希力量 | 区块链第一门户

我们一行一行的来解释fjL哈希力量 | 区块链第一门户

uint cnt = _receivers.length;fjL哈希力量 | 区块链第一门户

是获取 _receivers 里面有几个地址,我们从上面可以看到 参数里面只有两个地址,所以 cnt=2,也就是 给两个地址发送代币fjL哈希力量 | 区块链第一门户

uint256 amount = uint256(cnt) * _value;fjL哈希力量 | 区块链第一门户

uint256fjL哈希力量 | 区块链第一门户

首先 uint256(cnt) 是把cnt 转成了 uint256类型fjL哈希力量 | 区块链第一门户

那么,什么是uint256类型?或者说uint256类型的取值范围是多少…fjL哈希力量 | 区块链第一门户

uintx 类型的取值范围是 0 到 2的x次方 -1fjL哈希力量 | 区块链第一门户

也就是 假如是 uint8的话fjL哈希力量 | 区块链第一门户

则 uint8的取值范围是 0 到 2的8次方 -1fjL哈希力量 | 区块链第一门户

也就是 0 到255fjL哈希力量 | 区块链第一门户

那么uint256 的取值范围是fjL哈希力量 | 区块链第一门户

0 – 2的256次方-1 也就是 0 到115792089237316195423570985008687907853269984665640564039457584007913129639935fjL哈希力量 | 区块链第一门户

python 算 2的256次方是多少fjL哈希力量 | 区块链第一门户

那么假如说 设置的值超过了 取值范围怎么办?这种情况称为 溢出fjL哈希力量 | 区块链第一门户

举个例子来说明fjL哈希力量 | 区块链第一门户

因为uint256的取值太大了,所以用uint8来 举例。。。fjL哈希力量 | 区块链第一门户

从上面我们已经知道了 uint8 最小是0,最大是255fjL哈希力量 | 区块链第一门户

那么当我 255 + 1 的时候,结果是啥呢?结果会变成0fjL哈希力量 | 区块链第一门户

那么当我 255 + 2 的时候,结果是啥呢?结果会变成1fjL哈希力量 | 区块链第一门户

那么当我 0 – 1 的时候,结果是啥呢?结果会变成255fjL哈希力量 | 区块链第一门户

那么当我 0 – 2 的时候,结果是啥呢?结果会变成254fjL哈希力量 | 区块链第一门户

那么 我们回到上面的代码中,fjL哈希力量 | 区块链第一门户

amount = uint256(cnt) * _valuefjL哈希力量 | 区块链第一门户

则 amount = 2* _valuefjL哈希力量 | 区块链第一门户

但是此时 _value 是16进制的,我们把他转成 10进制fjL哈希力量 | 区块链第一门户

(python 16进制转10进制)fjL哈希力量 | 区块链第一门户

可以看到 _value = 57896044618658097711785492504343953926634992332820282019728792003956564819968fjL哈希力量 | 区块链第一门户

那么amount = _value*2 = 115792089237316195423570985008687907853269984665640564039457584007913129639936fjL哈希力量 | 区块链第一门户

可以在查看上面看到 uint256取值范围最大为 115792089237316195423570985008687907853269984665640564039457584007913129639935fjL哈希力量 | 区块链第一门户

此时,amout已经超过了最大值,溢出 则 amount = 0fjL哈希力量 | 区块链第一门户

下一行代码 require(cnt > 0 && cnt <= 20); require 语句是表示该语句一定要是正确的,也就是 cnt 必须大于0 且 小于等于20fjL哈希力量 | 区块链第一门户

我们的cnt等于2,通过!fjL哈希力量 | 区块链第一门户

require(_value > 0 && balances[msg.sender] >= amount);fjL哈希力量 | 区块链第一门户

这句要求 value 大于0,我们的value是大于0 的 且,当前用户拥有的代币余额大于等于 amount,因为amount等于0,所以 就算你一个代币没有,也是满足的!fjL哈希力量 | 区块链第一门户

balances[msg.sender] = balances[msg.sender].sub(amount);fjL哈希力量 | 区块链第一门户

这句是当前用户的余额 – amountfjL哈希力量 | 区块链第一门户

当前amount 是0,所以当前用户代币的余额没有变动fjL哈希力量 | 区块链第一门户

for (uint i = 0; i < cnt; i++) {fjL哈希力量 | 区块链第一门户

balances[_receivers[i]] = balances[_receivers[i]].add(_value);fjL哈希力量 | 区块链第一门户

Transfer(msg.sender, _receivers[i], _value);fjL哈希力量 | 区块链第一门户

}fjL哈希力量 | 区块链第一门户

这句是遍历 _receivers中的地址, 对每个地址做以下操作fjL哈希力量 | 区块链第一门户

balances[_receivers[i]] = balances[_receivers[i]].add(_value);fjL哈希力量 | 区块链第一门户

_receivers中的地址 的余额 = 原本余额+valuefjL哈希力量 | 区块链第一门户

所以 _receivers 中地址的余额 则加了57896044618658097711785492504343953926634992332820282019728792003956564819968 个代币!!!fjL哈希力量 | 区块链第一门户

Transfer(msg.sender, _receivers[i], _value); }fjL哈希力量 | 区块链第一门户

这句则只是把赠送代币的记录存下来!!!fjL哈希力量 | 区块链第一门户

总结fjL哈希力量 | 区块链第一门户

就一个简单的溢出漏洞,导致BEC代币的市值接近归0fjL哈希力量 | 区块链第一门户

那么,开发者有没有考虑到溢出问题呢?fjL哈希力量 | 区块链第一门户

其实他考虑了,fjL哈希力量 | 区块链第一门户

0fdb93d152e24467aab444e3f01bcc69.jpegfjL哈希力量 | 区块链第一门户

可以看如上截图fjL哈希力量 | 区块链第一门户

除了amount的计算外, 其他的给用户转钱 都用了safeMath 的方法(sub,add)fjL哈希力量 | 区块链第一门户

那么 为啥就偏偏这一句没有用safeMath的方法呢。。。fjL哈希力量 | 区块链第一门户

这就要用写代码的人了。。。fjL哈希力量 | 区块链第一门户

啥是safeMathfjL哈希力量 | 区块链第一门户

80e73a48fc6d4745829c5cd5c572edd6.jpegfjL哈希力量 | 区块链第一门户

safeMath 是为了计算安全 而写的一个libraryfjL哈希力量 | 区块链第一门户

我们看看他干了啥?为啥能保证计算安全.fjL哈希力量 | 区块链第一门户

function mul(uint256 a, uint256 b) internal constant returns (uint256) {fjL哈希力量 | 区块链第一门户

uint256 c = a * b;fjL哈希力量 | 区块链第一门户

assert(a == 0 || c / a == b);fjL哈希力量 | 区块链第一门户

return c;fjL哈希力量 | 区块链第一门户

}fjL哈希力量 | 区块链第一门户

如上面的乘法. 他在计算后,用assert 验证了下结果是否正确!fjL哈希力量 | 区块链第一门户

如果在上面计算 amount的时候,用了 mul的话, 则 c / a == b 也就是 验证 amount / cnt == _valuefjL哈希力量 | 区块链第一门户

这句会执行报错的,因为 0 / cnt 不等于 _valuefjL哈希力量 | 区块链第一门户

所以程序会报错!fjL哈希力量 | 区块链第一门户

也就不会发生溢出了…fjL哈希力量 | 区块链第一门户

那么还有一个小问题,这里的 assert 好 require 好像是干的同一件事fjL哈希力量 | 区块链第一门户

都是为了验证 某条语句是否正确!fjL哈希力量 | 区块链第一门户

那么他俩有啥区别呢?fjL哈希力量 | 区块链第一门户

用了assert的话,则程序的gas limit 会消耗完毕fjL哈希力量 | 区块链第一门户

而require的话,则只是消耗掉当前执行的gasfjL哈希力量 | 区块链第一门户

总结fjL哈希力量 | 区块链第一门户

那么 我们如何避免这种问题呢?fjL哈希力量 | 区块链第一门户

我个人看法是:fjL哈希力量 | 区块链第一门户

只要涉及到计算,一定要用safeMathfjL哈希力量 | 区块链第一门户

代码一定要测试!fjL哈希力量 | 区块链第一门户

代码一定要review!fjL哈希力量 | 区块链第一门户

必要时,要请专门做代码审计的公司来 测试代码fjL哈希力量 | 区块链第一门户

这件事后需要如何处理呢?fjL哈希力量 | 区块链第一门户

目前,该方法已经暂停了(还好可以暂停)所以看过文章的朋友 不要去测试了…fjL哈希力量 | 区块链第一门户

ea8bf58bc999473b9e7795bee6f6c658.jpegfjL哈希力量 | 区块链第一门户

不过已经发生了的事情咋办呢?fjL哈希力量 | 区块链第一门户

我能想到的是,快照在漏洞之前,所有用户的余额情况fjL哈希力量 | 区块链第一门户

然后发行新的token,给之前的用户 发送等额的代币…fjL哈希力量 | 区块链第一门户

最新技术趋势