一行程式碼蒸發了¥6,447,277,680 人民幣!

CCkicker發表於2018-04-24

現在進入你還是先行者,最後觀望者進場才是韭菜。

背景

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

v2-05da24da7d69dbeb1797e15c2f385517_hd

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

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

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

整體邏輯是

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

然後你要傳送的總金額 = 傳送的人數* 傳送的金額

然後 要求你當前的餘額大於 傳送的總金額

然後扣掉你傳送的總金額

然後 給receivers 裡面的每個人傳送 指定的金額(value)

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

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

程式碼解釋

v2-05da24da7d69dbeb1797e15c2f385517_hd

這個方法會傳入兩個引數

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

0x0e823ffe018727585eaf5bc769fa80472f76c3d7

0xb4d30cac5124b46c2df0cf3e3e1be05f42119033

_value 的值是 8000000000000000000000000000000000000000000000000000000000000000

我們再檢視程式碼(如下圖)

v2-05da24da7d69dbeb1797e15c2f385517_hd

我們一行一行的來解釋

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

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次方是多少

v2-05da24da7d69dbeb1797e15c2f385517_hd

那麼假如說 設定的值超過了 取值範圍怎麼辦?這種情況稱為 溢位

舉個例子來說明

因為uint256的取值太大了,所以用uint8來 舉例。。。

從上面我們已經知道了 uint8 最小是0,最大是255

那麼當我 255 + 1 的時候,結果是啥呢?結果會變成0

那麼當我 255 + 2 的時候,結果是啥呢?結果會變成1

那麼當我 0 – 1 的時候,結果是啥呢?結果會變成255

那麼當我 0 – 2 的時候,結果是啥呢?結果會變成254

那麼 我們回到上面的程式碼中,

但是此時 _value 是16進位制的,我們把他轉成 10進位制

(python 16進位制轉10進位制)

v2-05da24da7d69dbeb1797e15c2f385517_hd

可以看到 _value = 57896044618658097711785492504343953926634992332820282019728792003956564819968

那麼amount = _value*2 = 115792089237316195423570985008687907853269984665640564039457584007913129639936

可以在檢視上面看到 uint256取值範圍最大為 115792089237316195423570985008687907853269984665640564039457584007913129639935

此時,amout已經超過了最大值,溢位 則 amount = 0

下一行程式碼 require(cnt > 0 && cnt <= 20); require 語句是表示該語句一定要是正確的,也就是 cnt 必須大於0 且 小於等於20

我們的cnt等於2,通過!

這句要求 value 大於0,我們的value是大於0 的 且,當前使用者擁有的代幣餘額大於等於 amount,因為amount等於0,所以 就算你一個代幣沒有,也是滿足的!

這句是當前使用者的餘額 – amount

當前amount 是0,所以當前使用者代幣的餘額沒有變動

這句是遍歷 _receivers中的地址, 對每個地址做以下操作

_receivers中的地址的餘額 = 原本餘額+value

所以 _receivers 中地址的餘額 則加了57896044618658097711785492504343953926634992332820282019728792003956564819968 個代幣!!!

Transfer(msg.sender, _receivers[i], _value); } 這句則只是把贈送代幣的記錄存下來!!!

總結

就一個簡單的溢位漏洞,導致BEC代幣的市值接近歸0

那麼,開發者有沒有考慮到溢位問題呢?

其實他考慮了,

v2-05da24da7d69dbeb1797e15c2f385517_hd

可以看如上截圖

除了amount的計算外, 其他的給使用者轉錢 都用了safeMath 的方法(sub,add)

那麼 為啥就偏偏這一句沒有用safeMath的方法呢。。。

這就要用寫程式碼的人了。。。

啥是safeMath

v2-05da24da7d69dbeb1797e15c2f385517_hd

safeMath 是為了計算安全 而寫的一個library

我們看看他幹了啥?為啥能保證計算安全.

如上面的乘法. 他在計算後,用assert 驗證了下結果是否正確!

如果在上面計算 amount的時候,用了 mul的話, 則 c / a == b 也就是 驗證 amount / cnt == _value

這句會執行報錯的,因為 0 / cnt 不等於 _value

所以程式會報錯!

也就不會發生溢位了…

那麼 還有一個小問題,這裡的 assertrequire 好像是乾的同一件事

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

那麼他倆有啥區別呢?

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

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

總結

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

我個人看法是

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

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

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

v2-05da24da7d69dbeb1797e15c2f385517_hd

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

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

然後發行新的token,給之前的使用者 傳送等額的代幣…

相關文章