1. 程式人生 > >EOS 官方 API 中 Asset 結構體的乘法運算溢位漏洞描述

EOS 官方 API 中 Asset 結構體的乘法運算溢位漏洞描述

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),這是一個非常重要的檢測,目的是確保兩點:

  1. 乘法結果沒有導致符號改變(如兩個正整數相乘,結果變成了負數)

  2. 乘法結果沒有溢位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官方釋出了這個漏洞的補丁:

漏洞的危害

由於asset乘法中所有的三處檢測通通無效,當合約中使用asset乘法時,將會面臨所有可能型別的溢位,包括:

  1. a > 0, b > 0, a * b < 0
  2. a > 0, b > 0, a * b < a
  3. a * b > max_amount
  4. a * b < -max_amount

響應建議

對於EOS開發者,如果您的智慧合約中使用到了asset的乘法操作,我們建議您更新對應的程式碼並重新編譯您的合約。因為像asset這樣的工具程式碼是靜態編譯進合約中的,必須重新編譯才能解決其中的安全隱患。

同時,我們也建議各位EOS開發者重視合約中的溢位問題,在編寫程式碼時提高安全意識,避免造成不必要的損失。

時間線

2018-7-26: 360 Vulcan團隊在程式碼審計中發現asset中乘法運算的溢位問題

2018-7-27: 通過Hackerone平臺將漏洞提交給EOS官方

2018-8-7: EOS官方釋出補丁修復漏洞