現在進入你還是先行者,最後觀望者進場才是韭菜。
背景
今天有人在群裡說,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 2 |
_receivers _value |
_receivers 的值是個列表,裡面有兩個地址
0x0e823ffe018727585eaf5bc769fa80472f76c3d7
0xb4d30cac5124b46c2df0cf3e3e1be05f42119033
_value 的值是 8000000000000000000000000000000000000000000000000000000000000000
我們再檢視程式碼(如下圖)
我們一行一行的來解釋
1 |
uint cnt = _receivers.length; |
是獲取 _receivers 裡面有幾個地址,我們從上面可以看到 引數裡面只有兩個地址,所以 cnt=2,也就是 給兩個地址傳送代幣
1 |
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 的時候,結果是啥呢?結果會變成254
那麼 我們回到上面的程式碼中,
1 |
amount = uint256(cnt) * _value |
則
1 |
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,通過!
1 |
require(_value > 0 && balances[msg.sender] >= amount); |
這句要求 value 大於0,我們的value是大於0 的 且,當前使用者擁有的代幣餘額大於等於 amount,因為amount等於0,所以 就算你一個代幣沒有,也是滿足的!
1 |
balances[msg.sender] = balances[msg.sender].sub(amount); |
這句是當前使用者的餘額 – amount
當前amount 是0,所以當前使用者代幣的餘額沒有變動
1 2 3 4 |
for (uint i = 0; i < cnt; i++) { balances[_receivers[i]] = balances[_receivers[i]].add(_value); Transfer(msg.sender, _receivers[i], _value); } |
這句是遍歷 _receivers中的地址, 對每個地址做以下操作
1 |
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
我們看看他幹了啥?為啥能保證計算安全.
1 2 3 4 5 |
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
總結
那麼 我們如何避免這種問題呢?
我個人看法是
- 只要涉及到計算,一定要用safeMath
- 程式碼一定要測試!
- 程式碼一定要review!
- 必要時,要請專門做程式碼審計的公司來 測試程式碼
這件事後需要如何處理呢?
目前,該方法已經暫停了(還好可以暫停)所以看過文章的朋友 不要去測試了…
不過已經發生了的事情咋辦呢?
我能想到的是,快照在漏洞之前,所有使用者的餘額情況
然後發行新的token,給之前的使用者 傳送等額的代幣…