作者:爬蟲
來源:知乎專欄“區塊鏈開發之北”

現在進入你還是先行者,最后觀望者進場才是韭菜。 美圖董事長蔡文勝曾在三點鐘群,高調的說出了這句話,隨即被大眾瘋傳。

在他發表完言論沒多久,2月美鏈(BEC)上交易所會暴漲4000%,后又暴跌。盡管他多次否認,聰明的網友早已扒出,他與BEC千絲萬縷的關系。

莊家坐莊操控幣價,美圖的股價隨之暴漲,蔡文勝順利完成了他的韭菜收割大計。

但在幣圈,割人者,人恒割之。

隨著BEC智能合約的漏洞的爆出,被黑客利用,瞬間套現拋售大額BEC,60億在瞬間歸零。

而這一切,竟然是因為一個簡單至極的程序Bug。

背景

今天有人在群里說,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

下面我來帶大家看看,黑客是如何實現的!

我們可以看到執行的方法是 batchTransfer

那這個方法是干嘛的呢?(給指定的幾個地址,發送相同數量的代幣)

整體邏輯是

你傳幾個地址給我(receivers),然后再傳給我你要給每個人多少代幣(value)

然后你要發送的總金額 = 發送的人數* 發送的金額

然后 要求你當前的余額大于 發送的總金額

然后扣掉你發送的總金額

然后 給receivers 里面的每個人發送 指定的金額(value)

從邏輯上看,這邊是沒有任何問題的,你想給別人發送代幣,那么你本身的余額一定要大于發送的總金額的!

但是這段代碼卻犯了一個很傻的錯!

代碼解釋

這個方法會傳入兩個參數

  1. _receivers
  2. _value

_receivers 的值是個列表,里面有兩個地址

0x0e823ffe018727585eaf5bc769fa80472f76c3d7

0xb4d30cac5124b46c2df0cf3e3e1be05f42119033

_value 的值是 8000000000000000000000000000000000000000000000000000000000000000

我們再查看代碼(如下圖)

我們一行一行的來解釋

uint cnt = _receivers.length;

是獲取 _receivers 里面有幾個地址,我們從上面可以看到 參數里面只有兩個地址,所以 cnt=2,也就是 給兩個地址發送代幣

uint256 amount = uint256(cnt) * _value;

uint256

首先 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

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

所以程序會報錯!

也就不會發生溢出了...

那么 還有一個小問題,這里的 assertrequire 好像是干的同一件事

都是為了驗證 某條語句是否正確!

那么他倆有啥區別呢?

用了assert的話,則程序的gas limit 會消耗完畢

而require的話,則只是消耗掉當前執行的gas

總結

那么 我們如何避免這種問題呢?

我個人看法是

  1. 只要涉及到計算,一定要用safeMath
  2. 代碼一定要測試!
  3. 代碼一定要review!
  4. 必要時,要請專門做代碼審計的公司來 測試代碼

這件事后需要如何處理呢?

目前,該方法已經暫停了(還好可以暫停)所以看過文章的朋友 不要去測試了...

不過已經發生了的事情咋辦呢?

我能想到的是,快照在漏洞之前,所有用戶的余額情況

然后發行新的token,給之前的用戶 發送等額的代幣...

寫了個爬蟲,爬取熱門ICO的源代碼

里面有沒有bug,自己找吧~

https://github.com/jin10086/ico-spider


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/582/