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

原文:https://github.com/slowmist/eos-smart-contract-security-best-practices

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

目录

安全准则

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

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

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

已知漏洞

数值溢出

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

漏洞示例

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

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

void transfer(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 是否满足要求,避免异常调用。

真实案例

参考文献

致谢

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

EOS合约权限(二)- 如何检查 EOS 合约的安全性

开源,并能够验证开源代码与链上部署代码一致

比如 EOS.win 的代码就能够在 EOSpark 上查到,并已被 EOSpark 验证与链上部署代码一致
https://eospark.com/MainNet/contract/eosluckygame

慢雾科技也推出 EOS 合约验证平台,也可以做相应的查询和确认
https://eos.slowmist.io

代码安全性

EOS 代码安全性审计需要比较专业的背景知识,在合约层面一般的问题包括数值溢出漏洞、代码逻辑错误等。

权限

合约账户的权限能够控制对账户内金额转账,以及对合约进行升级,是非常重要的安全环节。目前主要有三种方法对权限进行限制。

方法一(低安全):多重签名账户

通过EOS账户体系的Weight和Threshold控制账户权限,实现由多人共同管理一个账户。假设该账户owner权限由5个不同的公钥控制,每个公钥的Weight为1,Threshold为3,表示需要这5个人中的3人进行签名,才能转账或修改合约代码,从而提升账户安全性。

参看 eosio.prods 的权限设置,就是使用这种方式 https://eospark.com/MainNet/account/eosio.prods
当然 eosio.prods 的参与者是所有当选的超级节点,公信力更高。

方法二(高安全):移交智能合约权限

修改合约权限为 eosio.prods 账户,意味着将修改合约的权限交给21个超级节点,如果需要转移资金或修改合约,需要申请节点仲裁。
使用 updateauth 命令可以进行权限的修改:

$ cleos push action eosio updateauth '{"account": "eosio", "permission": "owner", "parent": "", "auth": {"threshold": 1, "keys": [], "waits": [], "accounts": [{"weight": 1, "permission": {"actor": "eosio.prods", "permission": "active"}}]}}' -p eosio@owner
$ cleos push action eosio updateauth '{"account": "eosio", "permission": "active", "parent": "owner", "auth": {"threshold": 1, "keys": [], "waits": [], "accounts": [{"weight": 1, "permission": {"actor": "eosio.prods", "permission": "active"}}]}}' -p eosio@active

参看 eosio 的权限设置,就是使用这种方式 https://eospark.com/MainNet/account/eosio

方法三(高安全):设置账户权限到一个黑洞公钥

将owner和active权限设置为一个没有人知道私钥的公钥地址(黑洞),即可保证没有人可以获得账户的实际控制权。比如 EOS1111111111111111111111111111111114T1Anm,它的公钥是0值加检验数据生成,任何人都不知道它的私钥。

以下一些最近的热门游戏合约,供大家学习研究用:

狼人
https://eosflare.io/account/eosfoiowolfs

ITE
https://eosflare.io/account/itedeathstar

EOS Bet
https://eostracker.io/accounts/eosbetdice11

从导致SMT被黑的transferProxy函数看EOS体系的安全解决方案

下面这段就是导致SMT被黑客攻击导致价值归零、以及各大交易所纷纷停止提币的核心代码

    /*
     * Proxy transfer SmartMesh token. When some users of the ethereum account has no ether,
     * he or she can authorize the agent for broadcast transactions, and agents may charge agency fees
     * @param _from
     * @param _to
     * @param _value
     * @param feeSmt
     * @param _v
     * @param _r
     * @param _s
     */
    function transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt,
        uint8 _v,bytes32 _r, bytes32 _s) public transferAllowed(_from) returns (bool){

        if(balances[_from] < _feeSmt + _value) revert();

        uint256 nonce = nonces[_from];
        bytes32 h = keccak256(_from,_to,_value,_feeSmt,nonce);
        if(_from != ecrecover(h,_v,_r,_s)) revert();

        if(balances[_to] + _value < balances[_to]
            || balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
        balances[_to] += _value;
        Transfer(_from, _to, _value);

        balances[msg.sender] += _feeSmt;
        Transfer(_from, msg.sender, _feeSmt);

        balances[_from] -= _value + _feeSmt;
        nonces[_from] = nonce + 1;
        return true;
    }

这是黑客利用漏洞完成SMT无限增发的合约调用
https://etherscan.io/tx/0x1abab4c8db9a30e703114528e31dee129a3a758f7f8abc3b6494aad3d304e43f

方法涉及的角色:
角色1 需要转SMT、但地址里面没有ETH的人 – 合约中的_from
角色2 帮助角色1来转SMT,并支付ETH的gas费用 – 合约中的msg.sender,也是调用这个合约的人
角色3 SMT接收方

方法的目的:
角色1想要转SMT给角色3,但自己又没有ETH来支付手续费,于是角色1找到有ETH的角色2说:我给你一些SMT当做手续费,你来通过调用transferProxy来把我的SMT转给角色3,因为你有ETH

方法的漏洞:
漏洞利用前几天导致BEC归零类似的uint256溢出,绕过了if的判断,所以解决这类问题还是需要使用SafeMath来做加减乘除。

EOS体系的解决方案:

1 关于转账手续费
虽然EOS主网还没有上,但根据EOS白皮书的说法,EOS体系转账是需要手续费的,但EOS允许收款人支付。这样角色1可以把中间人角色2排除,直接与角色3交易。

2 关于智能合约漏洞
当“势不可挡的应用程序”以不可预知的方式发挥作用时,使用EOS.IO软件的区块链允许区块生产者(超级节点)替换账户的代码,而不分叉整个区块链。这类似冻结账户的过程,这个代码的替换需要选出的15/21的区块生产者投票。

3 权限管理
proxyTransfer方法里的角色B获得角色授权进行资金操作的情况,在EOS中可以通过权限映射+多签名阈值来实现,虽然没有ETH上这么灵活,但从编程友好和安全的角度是有优势的。

以太坊的智能合约的灵活性是很高的,但也对智能合约开发者提出了很高的要求,而EOS有针对性对这些做了自己的结构设计。