A guide to EOS smart contract security best practices
Branch: master
Clone or download
ever4cys and keywolf add Korean translations (#5)
* add Korean translations

* add backslash for new line
Latest commit 3486a7b Feb 4, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
LICENSE Initial commit Sep 17, 2018
README.md add Korean translations (#5) Feb 4, 2019
README_EN.md add Korean translations (#5) Feb 4, 2019
README_KR.md add Korean translations (#5) Feb 4, 2019

README.md

EOS 智能合约最佳安全开发指南

English Version: check here
한국어 버전: 여기를 클릭

这篇文档旨在为 EOS 智能合约开发人员提供一些智能合约的安全准则已知漏洞分析。我们邀请社区对该文档提出修改或完善建议,欢迎各种合并请求(Pull Request)。若有相关的文章或博客的发表,也请将其加入到参考文献中。

目录

安全准则

EOS 处于早期阶段并且有很强的实验性质。因此,随着新的 bug 和安全漏洞被发现,新的功能不断被开发出来,其面临的安全威胁也是不断变化的。这篇文章对于开发人员编写安全的智能合约来说只是个开始。

开发智能合约需要一个全新的工程思维,它不同于我们以往项目的开发。因为它犯错的代价是巨大的,很难像中心化类型的软件那样,打上补丁就可以弥补损失。就像直接给硬件编程或金融服务类软件开发,相比于 Web 开发和移动开发都有更大的挑战。因此,仅仅防范已知的漏洞是不够的,还需要学习新的开发理念:

  • 对可能的错误有所准备。任何有意义的智能合约或多或少都存在错误,因此你的代码必须能够正确的处理出现的 bug 和漏洞。需始终保证以下规则:
    • 当智能合约出现错误时,停止合约
    • 管理账户的资金风险,如限制(转账)速率、最大(转账)额度
    • 有效的途径来进行 bug 修复和功能提升
  • 谨慎发布智能合约。 尽量在正式发布智能合约之前发现并修复可能的 bug。
    • 对智能合约进行彻底的测试,并在任何新的攻击手法被发现后及时的测试(包括已经发布的合约)
    • 从 alpha 版本在麒麟测试网(CryptoKylin-Testnet)上发布开始便邀请专业安全审计机构进行审计,并提供漏洞赏金计划(Bug Bounty)
    • 阶段性发布,每个阶段都提供足够的测试
  • 保持智能合约的简洁。复杂会增加出错的风险。
    • 确保智能合约逻辑简洁
    • 确保合约和函数模块化
    • 使用已经被广泛使用的合约或工具(比如,不要自己写一个随机数生成器)
    • 条件允许的话,清晰明了比性能更重要
    • 只在你系统的去中心化部分使用区块链
  • 保持更新。通过公开资源来确保获取到最新的安全进展。
    • 在任何新的漏洞被发现时检查你的智能合约
    • 尽可能快的将使用到的库或者工具更新到最新
    • 使用最新的安全技术
  • 清楚区块链的特性。尽管你先前所拥有的编程经验同样适用于智能合约开发,但这里仍然有些陷阱你需要留意:
    • require_recipient(account_name name) 可触发通知,如果账户name下有合约,会调用name合约中的同名函数,官方文档

已知漏洞

数值溢出

在进行算术运算时,未进行边界检查可能导致数值上下溢,引起智能合约用户资产受损。

漏洞示例

存在缺陷的代码:batchtransfer 批量转账

typedef struct acnts {
    account_name name0;
    account_name name1;
    account_name name2;
    account_name name3;
} account_names;

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 结构体进行运算,而不是把 balance 提取出来进行运算。

真实案例

权限校验

在进行相关操作时,应严格判断函数入参和实际调用者是否一致,使用require_auth进行校验。

漏洞示例

存在缺陷的代码:transfer 转账

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( from )校验资产转出账户与调用账户是否一致。

真实案例

暂无

apply 校验

在处理合约调用时,应确保每个 action 与 code 均满足关联要求。

漏洞示例

存在缺陷的代码:

// 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)) ) { }

绑定每个关键 action 与 code 是否满足要求,避免异常调用。

真实案例

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链上不能生成真随机数,在设计随机类应用时建议参考官方的示例

真实案例

回滚攻击

  • 手法1:在事务中探测执行结果(如收款金额、账号余额、表记录、随机数计算结果等),当结果满足一定条件时调用 eosio_assert ,使得当前事务失败回滚。
  • 手法2:利用超级节点黑名单账号发起事务,欺骗普通节点做出响应,但此事务不会被打包。

漏洞示例

常见的有缺陷的模式:

  • 博弈类游戏下注随即开奖并转账,恶意合约可通过 inline_action 检测余额是否增加,从而回滚失败的开奖
  • 博弈类游戏下注随即将开奖结果写入表内,恶意合约可通过 inline_action 检测表中记录,从而回滚失败的开奖
  • 博弈类游戏开奖结果与游戏内奖券号相关联,恶意合约可通过同时发起多笔小额下注事务和一笔大额下注事务,当收到小额中奖时回滚事务,从而达到将可中奖的奖券号“转让”给大额下注的目的。
  • 博弈类游戏开奖事务与下注事务没有关联,攻击者可用黑名单账号或者恶意合约回滚下注事务

防御方法

  • 使用 defer action 转账和发送收据
  • 建立开奖依赖,如订单依赖,开奖的时候判断订单是否存在,就算在节点服务器上开奖成功,由于在 bp 上下注订单被回滚,所以相应的开奖记录也会被回滚。

真实案例

参考文献

致谢

  • 麒麟工作组
  • eosiofans
  • 荆凯(EOS42)
  • 星魂
  • 岛娘
  • 赵余(EOSLaoMao)
  • 字符