綜述
asset是EOS官方頭文件中提供的用來代表貨幣資產(如官方貨幣EOS或自己發(fā)布的其它貨幣單位)的一個結構體。在使用asset進行乘法運算(operator *=)時,由于官方代碼的bug肿轨,導致其中的溢出檢測無效化寿冕。造成的結果是,如果開發(fā)者在智能合約中使用了asset乘法運算椒袍,則存在發(fā)生溢出的風險驼唱。
漏洞細節(jié)
問題代碼存在于contracts/eosiolib/asset.hpp:
可以看到,這里官方代碼一共有3處檢查驹暑,用來防范溢出的發(fā)生玫恳。不幸的是,這三處檢查沒有一處能真正起到作用优俘。
首先我們來看檢查(2)和(3)京办,比較明顯,它們是用來檢查乘法的結果是否在合法取值范圍[-max_amouont, max_amount]之內帆焕。這里的問題是他們錯誤地被放置在了amouont *= a這句代碼之前惭婿,正確的做法是將它們放到amouont *= a之后,因為它的目的是檢測運算結果的合法性叶雹。正確的代碼順序應該是這樣:
下面來看檢測(1)财饥,這是一個非常重要的檢測,目的是確保兩點:
1.乘法結果沒有導致符號改變(如兩個正整數相乘折晦,結果變成了負數)
2.乘法結果沒有溢出64位符號數(如兩個非零正整數數相乘钥星,結果比其中任意一個都小)
這里的問題非常隱晦满着,直接看C++源代碼其實看不出什么問題谦炒。但是我們要知道,EOS的智能合約最終是編譯成webassembly字節(jié)碼文件來執(zhí)行的风喇,讓我們來看看編譯后的字節(jié)碼長什么樣子:
上述字節(jié)碼對應于源碼中的:
這個結果讓我們非常吃驚宁改,應為很明顯,生成的字節(jié)碼代表的含義是:
相當于說這個assert的條件變成了永遠是true响驴,這里面的溢出檢測就這樣憑空消失了M盖摇!豁鲤!
根據我們的經驗秽誊,會發(fā)生這樣的問題,很可能是編譯器優(yōu)化導致的琳骡。于是我們查看了一下官方提供的編譯腳本(eosiocpp):
可以看到它是調用clang進行編譯的锅论,并且默認開啟了編譯器優(yōu)化,優(yōu)化級別是O3楣号,比較激進的一個級別最易。
我們嘗試關閉編譯器優(yōu)化(使用-O0)怒坯,然后重新編譯相同的代碼,這次得到的對應字節(jié)碼如下:
可以看到這次生成的字節(jié)碼中完整保留了溢出檢測的邏輯藻懒,至此我們可以確定這個問題是編譯器優(yōu)化造成的剔猿。
為什么編譯器優(yōu)化會導致這樣的后果呢?這是因為在下面的語句中嬉荆,amount和a的類型都是有符號整數:
在C/C++標準中归敬,有符號整數的溢出屬于“未定義行為(undefined behavior)”。當出現未定義行為時鄙早,程序的行為是不確定的汪茧。所以當一些編譯器(包括gcc,clang)做優(yōu)化時限番,不會去考慮出現未定義行為的情況(因為一旦出現未定義行為舱污,整個程序就處于為定義狀態(tài)了,所以程序員需要自己在代碼中去避免未定義行為)弥虐。簡單來講扩灯,在這個例子里面,clang在做優(yōu)化時不會去考慮以下乘法出現溢出的情況:
那么在不考慮上面乘法溢出的前提下躯舔,下面的表達式將永遠為true:
于是一旦打開編譯器優(yōu)化驴剔,整個表達式就直接被優(yōu)化掉了。
漏洞的危害
由于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開發(fā)者惜互,如果您的智能合約中使用到了asset的乘法操作,我們建議您更新對應的代碼并重新編譯您的合約琳拭。因為像asset這樣的工具代碼是靜態(tài)編譯進合約中的训堆,必須重新編譯才能解決其中的安全隱患。
同時白嘁,我們也建議各位EOS開發(fā)者重視合約中的溢出問題坑鱼,在編寫代碼時提高安全意識,避免造成不必要的損失絮缅。
本文轉載自《Asset乘法運算溢出漏洞》鲁沥,已獲得原作者授權