作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年8月22日
本系列上一篇:《以太坊合约审计 CheckList 之“以太坊智能合约规范问题”影响分析报告》
在知道创宇404区块链安全研究团队整理输出的《知道创宇以太坊合约审计CheckList》中,把“条件竞争问题”、“循环DoS问题”等问题统一归类为“以太坊智能合约设计缺陷问题”。
“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台。我们利用该平台针对上述提到的《知道创宇以太坊合约审计CheckList》中“以太坊智能合约设计缺陷”类问题在全网公开的智能合约代码做了扫描分析。详见下文:
2016年11月29号,Mikhail Vladimirov和Dmitry Khovratovich公开了一篇《ERC20 API: An Attack Vector on Approve/TransferFrom Methods》,在文章中提到了一个在ERC20标准中存在的隐患问题,条件竞争。
这里举一个approve函数中会出现的比较典型的例子,approve一般用于授权,比如授权别人可以取走自己的多少代币,整个流程是这样的:
想要理解上面这个条件竞争的原理,首先我们得对以太坊的打包交易逻辑有基础认识。
https://medium.com/blockchannel/life-cycle-of-an-ethereum-transaction-e5c66bae0f6e
简单来说就是
所以当用户B在待打包处看到修改的交易时,可以通过构造更高gasprice的交易来竞争,将这笔交易打包到修改交易之前,就产生了问题。
以下代码就存在条件竞争的问题
function approve(address _spender, uint256 _value) public returns (bool success){ allowance[msg.sender][_spender] = _value; return true
在以太坊代码中,循环是一种很常见的结构,但由于以太坊智能合约的特殊性,在循环也有很多需要特别注意的点, 存在潜在的合约问题与安全隐患。
在以太坊中,每一笔交易都会消耗一定的gas,而交易的复杂度越高,则该交易的gasprice越高。而在区块链上,每个区块又有最大gas消耗值限制,且在矿工最优化收益方案中,如果一个交易的gas消耗过大,就会倾向性把这个交易排除在区块外,从而导致交易失败。
所以,对于合约内的循环次数不宜过大,在循环中的代码不宜过于复杂。
struct Payee { address addr; uint256 value; } Payee payees[]; uint256 nextPayeeIndex; function payOut() { uint256 i = nextPayeeIndex; while (i < payees.length && msg.gas > 200000) { payees[i].addr.send(payees[i].value); i++; } nextPayeeIndex = i; }
如果上述代码地址列表过长,就有可能导致交易失败。
2018年7月23日,Seebug Paper发表的《首个区块链 token 的自动化薅羊毛攻击分析》中攻击合约就提到了这种gas优化方式。
在以太坊中,应该尽量避免循环次数受到用户控制,攻击者可能会使用过大的循环来完成Dos攻击。
function distribute(address[] addresses) onlyOwner { for (uint i = 0; i < addresses.length; i++) { // transfer code } }
当攻击者通过不断添加address列表长度,来迫使该函数执行循环次数过多,导致合约无法正常维护,函数无法执行。
2016年,GovernMental合约代币被爆出恶意攻击,导致地址列表过长无法执行,超过1100 ETH被困在了合约中。
使用Haotian平台智能合约审计功能可以准确扫描到该类型问题。
基于Haotian平台智能合约审计功能规则,我们对全网的公开的共39548 个合约代码进行了扫描,其中共24791个合约涉及到这类问题。
截止2018年8月10日为止,我们发现了22981个存在approve条件竞争的合约代码,其中15325个合约仍处于交易状态,其中交易量最高的10个合约情况如下:
截止2018年8月10日为止,我们发现了1810个存在潜在循环dos问题的合约代码,其中1740个合约仍处于交易状态,其中交易量最高的10个合约情况如下:
关于这个问题的修复方式讨论很多,由于这属于底层特性的问题,所以很难在智能合约层面做解决,在代码层面,我们建议在approve函数中加入
require((_value == 0) || (allowance[msg.sender][_spender] == 0));
将这个条件加入,在每次修改权限时,将额度修改为0,再将额度改为对应值。
在这种情况下,合约管理者可以通过日志或其他手段来判断是否有条件竞争发生,从风控的角度警醒合约管理者注意该问题的发生。范例代码如下:
function approve(address _spender, uint256 _value) isRunning validAddress returns (bool success) { require(_value == 0 || allowance[msg.sender][_spender] == 0); allowance[msg.sender][_spender] = _value; Approval(msg.sender, _spender, _value); return true; }
在面临循环DoS问题产生的场景中,最为常见的就是向多个用户转账这个功能。
这里推荐代码中尽量避免用户可以控制循环深度,如果无法避免的话,尽量使用类似withdrawFunds这种函数,循环中只分发用户提币的权限,让用户来提取属于自己的代币,通过这种操作可以大幅度节省花费的gas开支,也可以一定程度避免可能导致的问题。代码如下所示:
function distribute(address[] addresses) onlyOwner { for (uint i = 0; i < addresses.length; i++) { if (address_claimed_tokens[addresses[i]] == 0) { balances[owner] -= transferAmount; balances[addresses[i]] += transferAmount; address_claimed_tokens[addresses[i]] += transferAmount; Transfer(owner, addresses[i], transferAmount); } } }
在分析了许多智能合约已有的漏洞以及合约以后,我发现有一类问题比较特殊,这些问题的诞生根本原因都是因为以太坊智能合约本身的设计缺陷,再加上开发者对此没有清晰的认识,导致了合约本身的一些隐患。
文章中提到的条件竞争是个比较特殊的问题,这里的条件竞争涉及到了智能合约底层实现逻辑,本身打包逻辑存在条件竞争,我们无法在代码层面避免这个问题,但对于开发者来说,比起无缘无故的因为该问题丢失代币来说,更重要的是合约管理者可以监控到每一笔交易的结果,所以我们加入置0的操作来提醒合约管理者、代币持有者该问题,尽量避免这样的操作发生。
而循环Dos问题就是一个针对开发者的问题,每一次操作就是一次交易,每次交易就要花费gas,交易越复杂花费的gas越多,而在区块链上,每个区块又有最大gas消耗值限制,且在矿工最优化收益方案中,如果一个交易的gas消耗过大,就会倾向性把这个交易排除在区块外,从而导致交易失败。这也就直接导致了在交易中,我们需要尽可能的优化gas花费,避免交易失败。
我们在对全网公开的合约代码进行扫描和监控时容易发现,有很大一批开发人员并没有注意到这些问题,其中条件竞争问题甚至影响广泛,有超过一半以上的公开代码都受到影响。
这里我们建议所有的开发者重新审视自己的合约代码,检查是否存在设计缺陷问题,避免不必要的麻烦以及安全问题。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)
区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
ERC20 API: An Attack Vector on Approve/TransferFrom Methods https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit
Life Cycle of an Ethereum Transaction https://medium.com/blockchannel/life-cycle-of-an-ethereum-transaction-e5c66bae0f6e
首个区块链 token 的自动化薅羊毛攻击分析 https://paper.seebug.org/646/
GovernMental's 1100 ETH : https://www.reddit.com/r/ethereum/comments/4ghzhv/governmentals_1100_eth_jackpot_payout_is_stuck/