作者:古河@360 Vulcan Team
作者博客:奇虎360技術博客
綜述
asset是EOS官方頭文件中提供的用來代表貨幣資產(如官方貨幣EOS或自己發布的其它貨幣單位)的一個結構體。在使用asset進行乘法運算(operator *=
)時,由於官方代碼的bug,導致其中的溢出檢測無效化。造成的結果是,如果開發者在智能合約中使用了asset乘法運算,則存在發生溢出的風險。
漏洞細節
問題代碼存在於contracts/eosiolib/asset.hpp:
asset& operator*=( int64_t a ) {
eosio_assert( a == 0 || (amount * a) / a == amount, "multiplication overflow
or underflow" ); <== (1)
eosio_assert( -max_amount <= amount, "multiplication underflow" ); <= (2)
eosio_assert( amount <= max_amount, "multiplication overflow" ); <= (3)
amount *= a;
return *this;
}
可以看到,這裡官方代碼一共有3處檢查,用來防範溢出的發生。不幸的是,這三處檢查沒有一處能真正起到作用。
首先我們來看檢查(2)和(3),比較明顯,它們是用來檢查乘法的結果是否在合法取值範圍[-max_amouont, max_amount]
之內。這裡的問題是他們錯誤地被放置在了amouont *= a
這句代碼之前,正確的做法是將它們放到amouont *= a
之後,因為它的目的是檢測運算結果的合法性。正確的代碼順序應該是這樣:
amount *= a;
eosio_assert( -max_amount <= amount, "multiplication underflow" ); <= (2)
eosio_assert( amount <= max_amount, "multiplication overflow" ); <= (3)
下面來看檢測(1),這是一個非常重要的檢測,目的是確保兩點:
-
乘法結果沒有導致符號改變(如兩個正整數相乘,結果變成了負數)
-
乘法結果沒有溢出64位符號數(如兩個非零正整數數相乘,結果比其中任意一個都小)
eosio_assert( a == 0 || (amount * a) / a == amount, "multiplication
overflow or underflow" ); <== (1)
這裡的問題非常隱晦,直接看C++源代碼其實看不出什麼問題。但是我們要知道,EOS的智能合約最終是編譯成webassembly字節碼文件來執行的,讓我們來看看編譯后的字節碼長什麼樣子:
(call $eosio_assert
(i32.const 1) // always true
(i32.const 224) // "multiplication overflow or underflow/00")
)
上述字節碼對應於源碼中的:
eosio_assert( a == 0 || (amount * a) / a == amount, "multiplication
overflow or underflow" ); <== (1)
這個結果讓我們非常吃驚,應為很明顯,生成的字節碼代表的含義是:
eosio_assert(1, "multiplication overflow or underflow" );
相當於說這個assert的條件變成了永遠是true,這裡面的溢出檢測就這樣憑空消失了!!!
根據我們的經驗,會發生這樣的問題,很可能是編譯器優化導致的。於是我們查看了一下官方提供的編譯腳本(eosiocpp):
($PRINT_CMDS; /root/opt/wasm/bin/clang -emit-llvm -O3 --std=c++14 --target
=wasm32 -nostdinc /
可以看到它是調用clang進行編譯的,並且默認開啟了編譯器優化,優化級別是O3,比較激進的一個級別。
我們嘗試關閉編譯器優化(使用-O0),然後重新編譯相同的代碼,這次得到的對應字節碼如下:
(block $label$0
(block $label$1
(br_if $label$1
(i64.eqz
(get_local $1)
)
)
(set_local $3
(i64.eq
(i64.div_s
(i64.mul
(tee_local $1
(i64.load
(get_local $0)
)
)
(tee_local $2
(i64.load
(get_local $4)
)
)
)
(get_local $2)
)
(get_local $1)
)
)
(br $label$0)
)
(set_local $3
(i32.const 1)
)
)
(call $eosio_assert
(get_local $3) // condition based on "a == 0 || (amount * a) / a == amount"
(i32.const 192) // "multiplication overflow or underflow/00")
可以看到這次生成的字節碼中完整保留了溢出檢測的邏輯,至此我們可以確定這個問題是編譯器優化造成的。
為什麼編譯器優化會導致這樣的後果呢?這是因為在下面的語句中,amount和a的類型都是有符號整數:
eosio_assert( a == 0 || (amount * a) / a == amount, "multiplication
overflow or underflow" );
在C/C++標準中,有符號整數的溢出屬於“未定義行為(undefined behavior)”。當出現未定義行為時,程序的行為是不確定的。所以當一些編譯器(包括gcc,clang)做優化時,不會去考慮出現未定義行為的情況(因為一旦出現未定義行為,整個程序就處於為定義狀態了,所以程序員需要自己在代碼中去避免未定義行為)。簡單來講,在這個例子裡面,clang在做優化時不會去考慮以下乘法出現溢出的情況:
(amount * a)
那麼在不考慮上面乘法溢出的前提下,下面的表達式將永遠為true:
a == 0 || (amount * a) / a == amount
於是一旦打開編譯器優化,整個表達式就直接被優化掉了。
官方補丁
8月7日EOS官方發布了這個漏洞的補丁:
https://github.com/EOSIO/eos/commit/b7b34e5b794e323cdc306ca2764973e1ee0d168f
漏洞的危害
由於asset乘法中所有的三處檢測通通無效,當合約中使用asset乘法時,將會面臨所有可能類型的溢出,包括:
- a > 0, b > 0, a * b < 0
- a > 0, b > 0, a * b < a
- a * b > max_amount
- a * b < -max_amount
響應建議
對於EOS開發者,如果您的智能合約中使用到了asset的乘法操作,我們建議您更新對應的代碼並重新編譯您的合約。因為像asset這樣的工具代碼是靜態編譯進合約中的,必須重新編譯才能解決其中的安全隱患。
同時,我們也建議各位EOS開發者重視合約中的溢出問題,在編寫代碼時提高安全意識,避免造成不必要的損失。
時間線
2018-7-26: 360 Vulcan團隊在代碼審計中發現asset中乘法運算的溢出問題
2018-7-27: 通過Hackerone平台將漏洞提交給EOS官方
2018-8-7: EOS官方發布補丁修復漏洞