EOS セキュリティホールがいくつ膨大な被害があったので、分かっている範囲でまとめてみます。
一部は、slowmist/eos-smart-contract-security-best-practices の意訳です。
数値オーバーフロー
数値を計算する時、境界チェックをしていないため、数値のオーバーフローが発生してしまい、想定外の数値で資産を移動してしまう
コードサンプル
void batchtransfer(symbol_name symbol, account_name from, account_names to, uint64_t balance)
{
require_auth(from);
account fromaccount;
require_recipient(from);
require_recipient(to.name0);
require_recipient(to.name1);
require_recipient(to.name2);
require_recipient(to.name3);
eosio_assert(is_balance_within_range(balance), "invalid balance");
eosio_assert(balance > 0, "must transfer positive balance");
uint64_t amount = balance * 4; // 掛け算で数値オーバーフロー
int itr = db_find_i64(_self, symbol, N(table), from);
eosio_assert(itr >= 0, "Sub-- wrong name");
db_get_i64(itr, &fromaccount, (account));
eosio_assert(fromaccount.balance >= amount, "overdrawn balance");
sub_balance(symbol, from, amount);
add_balance(symbol, to.name0, balance);
add_balance(symbol, to.name1, balance);
add_balance(symbol, to.name2, balance);
add_balance(symbol, to.name3, balance);
}
対策
数値を手動で計算するのではなく、asset
構造体のメソッドを使って計算する
asset の関数は下記をご参照ください。
権限チェック
EOS
のアカウントは、プライベートキーと分けているので、単にサインデータからアカウント名を推定できない。そのため、実行しているアクションに対して、実際の所有者であるかどうかを必ず厳密的にチェックする必要があります
コードサンプル
下記のコードは、require_auth(from)
がないので、from
でなくても、from
のトークンを送金できてしまう
void token::transfer( account_name from,
account_name to,
asset quantity,
string memo )
{
eosio_assert( from != to, "cannot transfer to self" );
eosio_assert( is_account( to ), "to account does not exist");
auto sym = quantity.symbol.name();
stats statstable( _self, sym );
const auto& st = statstable.get( sym );
require_recipient( from );
require_recipient( to );
eosio_assert( quantity.is_valid(), "invalid quantity" );
eosio_assert( quantity.amount > 0, "must transfer positive quantity" );
eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );
auto payer = has_auth( to ) ? to : from;
sub_balance( from, quantity );
add_balance( to, quantity, payer );
}
対策
操作するデータに対して、権限を持っているユーザであるかどうかをrequire_auth
を使って厳密的にチェックしましょう
apply のチェック条件
他のコントラクトからの通知を処理したい時、EOSIO_ABI
をカスタマイズして、自分のapply
関数を実装する必要があります。
そこのcode
とaction
の組み合わせを厳密的にチェックし、処理したいケースだけ通すようにする必要があります。
コードサンプル
下記コードは、if( code == self || code == N(eosio.token) || action == N(onerror) )
の開所は、
自分がtransfer
で送金する時も、このスマートコントラクトのアクションが実行されてしまう。
// extend from EOSIO_ABI
#define EOSIO_ABI_EX( TYPE, MEMBERS ) \
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
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) ) { \
TYPE thiscontract( self ); \
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \
} \
}
EOSIO_ABI_EX(eosio::charity, (hi)(transfer))
対策
if( ((code == self && action != N(transfer) ) || (code == N(eosio.token) && action == N(transfer)) || action == N(onerror)) ) { }
を使うことで。自分自身の transfer 以外
または eosio.token
の transfer
またはエラーの場合だけ実行するようにしましょう。
transfer 偽通知
require_recipient
で通知を処理する時、transfer.to
を_self
であることを確認する必要があります。
コードサンプル
// source code: https://gitlab.com/EOSBetCasino/eosbetdice_public/blob/master/EOSBetDice.cpp#L115
void transfer(uint64_t sender, uint64_t receiver) {
auto transfer_data = unpack_action_data<st_transfer>();
if (transfer_data.from == _self || transfer_data.from == N(eosbetcasino)){
return;
}
eosio_assert( transfer_data.quantity.is_valid(), "Invalid asset");
}
対策
if (transfer_data.to != _self) return;
を追加
ランダム値生成処理問題
標準ライブラリのランダム生成処理が使えないので、スマートコントラクト側で実装しないといけないですが、
予測可能なランダムアルゴリズムになってしまうのがあります。
コードサンプル
// source code: https://github.com/loveblockchain/eosdice/blob/3c6f9bac570cac236302e94b62432b73f6e74c3b/eosbocai2222.hpp#L174
uint8_t random(account_name name, uint64_t game_id)
{
auto eos_token = eosio::token(N(eosio.token));
asset pool_eos = eos_token.get_balance(_self, symbol_type(S(4, EOS)).name());
asset ram_eos = eos_token.get_balance(N(eosio.ram), symbol_type(S(4, EOS)).name());
asset betdiceadmin_eos = eos_token.get_balance(N(betdiceadmin), symbol_type(S(4, EOS)).name());
asset newdexpocket_eos = eos_token.get_balance(N(newdexpocket), symbol_type(S(4, EOS)).name());
asset chintailease_eos = eos_token.get_balance(N(chintailease), symbol_type(S(4, EOS)).name());
asset eosbiggame44_eos = eos_token.get_balance(N(eosbiggame44), symbol_type(S(4, EOS)).name());
asset total_eos = asset(0, EOS_SYMBOL);
//攻击者可通过inline_action改变余额total_eos,从而控制结果
total_eos = pool_eos + ram_eos + betdiceadmin_eos + newdexpocket_eos + chintailease_eos + eosbiggame44_eos;
auto mixd = tapos_block_prefix() * tapos_block_num() + name + game_id - current_time() + total_eos.amount;
const char *mixedChar = reinterpret_cast<const char *>(&mixd);
checksum256 result;
sha256((char *)mixedChar, sizeof(mixedChar), &result);
uint64_t random_num = *(uint64_t *)(&result.hash[0]) + *(uint64_t *)(&result.hash[8]) + *(uint64_t *)(&result.hash[16]) + *(uint64_t *)(&result.hash[24]);
return (uint8_t)(random_num % 100 + 1);
}
対策
EOS チェーンでは、本当のランダム値を生成できないので、設計する時、公式のサンプルをご参考下さい。
リプレイ攻撃
ゲームのコントラクトで、inline action
でゲームの勝敗結果をプレイヤーに通知する場合、プレイヤーが事前にアカウントにコントラクトをデプロイし、通知を受けた後結果をみて失敗だったらアクションを失敗させるようにすると、勝つまで料金を払わずに、勝つと賞金をもらえるようになってしまう。
コードサンプル
void play(name player, asset quantity) {
...
// play game
auto result = ...
require_receipt(player)
// または
action(
permission_level{get_self(), "active"_n},
get_self(),
"playend"_n,
name{result}
).send();
}
対策
遅延アクションの場合、アクションの実行が失敗してももとのアクション(トランザクション)に影響しないので、このケースを避けることができる。
やり方は EOS の inline action と deferred action をご参照ください。
まとめ
EOS はまだ新しい機能どんどん開発して行くので、セキュリティホールの対応もどんどん変わって行きます。
大変になりますが、セキュリティ対策を常に考えながらスマートコントラクトを実装してきましょう。