【慢雾出品】EOS 智能合约最佳安全开发指南

in eos •  6 years ago 

本文安全实践合集,由慢雾(SlowMist)同学主导,麒麟小组以及其他同学参与,结合近期爆发的安全热点问题进行实战说明,主体内容涵盖了安全准则以及常见已知漏洞(数值溢出、权限校验、apply 校验)说明,旨在为EOS社区积累良好安全的智能合约开发实践经验,为 EOS 智能合约开发人员提供一些智能合约的安全准则及已知漏洞分析。我们邀请社区对该文档提出修改或完善建议,欢迎各种合并请求(Pull Request)。若有相关的文章或博客的发表,也请将其加入到参考文献中。

中文版:
https://github.com/slowmist/eos-smart-contract-security-best-practices
英文版:
https://github.com/slowmist/eos-smart-contract-security-best-practices/blob/master/README_EN.md
目录

安全准则
已知漏洞
1. 数值溢出
1.1 漏洞示例
1.2 防御方法
1.3 真实案例
2. 权限校验
2.1 漏洞示例
2.2 防御方法
2.3 真实案例
3. apply 校验
3.1 漏洞示例
3.2 防御方法
3.3 真实案例
参考文献
致谢

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

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

对可能的错误有所准备。
任何有意义的智能合约或多或少都存在错误,因此你的代码必须能够正确的处理出现的 bug 和漏洞。需始终保证以下规则:
1.当智能合约出现错误时,停止合约
2.管理账户的资金风险,如限制(转账)速率、最大(转账)额度
3.有效的途径来进行 bug 修复和功能提升

谨慎发布智能合约。
尽量在正式发布智能合约之前发现并修复可能的 bug。
1.对智能合约进行彻底的测试,并在任何新的攻击手法被发现后及时的测试(包括已经发布的合约)
2.从 alpha 版本在麒麟测试网(CryptoKylin-Testnet)上发布开始便邀请专业安全审计机构进行审计,并提供漏洞赏金计划(Bug Bounty)
3.阶段性发布,每个阶段都提供足够的测试

保持智能合约的简洁。
复杂会增加出错的风险。
1.确保智能合约逻辑简洁
2.确保合约和函数模块化
3.使用已经被广泛使用的合约或工具(比如,不要自己写一个随机数生成器)
4.条件允许的话,清晰明了比性能更重要
5.只在你系统的去中心化部分使用区块链

保持更新。
通过公开资源来确保获取到最新的安全进展。
1.在任何新的漏洞被发现时检查你的智能合约
2.尽可能快的将使用到的库或者工具更新到最新
3.使用最新的安全技术

清楚区块链的特性。
尽管你先前所拥有的编程经验同样适用于智能合约开发,但这里仍然有些陷阱你需要留意:
require_recipient(account_name name) 可触发通知,调用name合约中的同名函数,官方文档

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

1.漏洞示例
存在缺陷的代码: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);

}

2.防御方法
尽可能使用 asset 结构体进行运算,而不是把 balance 提取出来进行运算。

3.真实案例
【EOS Fomo3D你千万别玩】狼人杀遭到溢出攻击, 已经凉凉
权限校验
在进行相关操作时,应严格判断函数入参和实际调用者是否一致,使用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 均满足关联要求。

1.漏洞示例
存在缺陷的代码:
// 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))

2.防御方法
使用
绑定每个关键 action 与 code 是否满足要求,避免异常调用

3.真实案例
EOSBet 黑客攻击事件复盘

参考文献
保管好私钥就安全了吗?注意隐藏在EOS DAPP中的安全隐患
漏洞详解|恶意 EOS 合约存在吞噬用户 RAM 的安全风险
How EOSBET attacked by aabbccddeefg
BET被黑客攻击始末,实锤还原作案现场和攻击手段
累计薅走数百万,EOS Dapps已成黑客提款机?

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

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!