β

以太坊合约审计 CheckList 之“以太坊智能合约设计缺陷问题”影响分析报告

知道创宇 13 阅读

作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年8月22日
本系列上一篇: 《以太坊合约审计 CheckList 之“以太坊智能合约规范问题”影响分析报告》

一、 简介

在知道创宇404区块链安全研究团队整理输出的《知道创宇以太坊合约审计CheckList》中,把“条件竞争问题”、“循环DoS问题”等问题统一归类为“以太坊智能合约设计缺陷问题”。

“昊天塔(HaoTian)” 是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台。我们利用该平台针对上述提到的《知道创宇以太坊合约审计CheckList》中“以太坊智能合约设计缺陷”类问题在全网公开的智能合约代码做了扫描分析。详见下文:

二、漏洞详情

1、条件竞争

2016年11月29号,Mikhail Vladimirov和Dmitry Khovratovich公开了一篇 《ERC20 API: An Attack Vector on Approve/TransferFrom Methods》 ,在文章中提到了一个在ERC20标准中存在的隐患问题, 条件竞争

这里举一个approve函数中会出现的比较典型的例子,approve一般用于授权,比如授权别人可以取走自己的多少代币,整个流程是这样的:

  1. 用户A授权用户B 100代币的额度
  2. 用户A觉得100代币的额度太高了,再次调用approve试图把额度改为50
  3. 用户B在待交易处(打包前)看到了这笔交易
  4. 用户B构造一笔提取100代币的交易,通过条件竞争将这笔交易打包到了修改额度之前,成功提取了100代币
  5. 用户B发起了第二次交易,提取50代币,用户B成功拥有了150代币

想要理解上面这个条件竞争的原理,首先我们得对以太坊的打包交易逻辑有基础认识。

https://medium.com/blockchannel/life-cycle-of-an-ethereum-transaction-e5c66bae0f6e

简单来说就是

  1. 只有当交易被打包进区块时,他才是不可更改的
  2. 区块会优先打包gasprice更高的交易

所以当用户B在待打包处看到修改的交易时,可以通过构造更高gasprice的交易来竞争,将这笔交易打包到修改交易之前,就产生了问题。

以下代码就存在条件竞争的问题

<span class="kd">function</span> <span class="nx">approve</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_spender</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">){</span>
    <span class="nx">allowance</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">][</span><span class="nx">_spender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">_value</span><span class="p">;</span>
    <span class="k">return</span> <span class="kc">true</span>

2、循环DoS问题

在以太坊代码中,循环是一种很常见的结构,但由于以太坊智能合约的特殊性,在循环也有很多需要特别注意的点, 存在潜在的合约问题与安全隐患。

1) 循环消耗问题

在以太坊中,每一笔交易都会消耗一定的gas,而交易的复杂度越高,则该交易的gasprice越高。而在区块链上,每个区块又有最大gas消耗值限制,且在矿工最优化收益方案中,如果一个交易的gas消耗过大,就会倾向性把这个交易排除在区块外,从而导致交易失败。

所以,对于合约内的循环次数不宜过大,在循环中的代码不宜过于复杂。

<span class="nx">struct</span> <span class="nx">Payee</span> <span class="p">{</span>
    <span class="nx">address</span> <span class="nx">addr</span><span class="p">;</span>
    <span class="nx">uint256</span> <span class="nx">value</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">Payee</span> <span class="nx">payees</span><span class="p">[];</span>
<span class="nx">uint256</span> <span class="nx">nextPayeeIndex</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">payOut() {</span>
    <span class="nx">uint256</span> <span class="nx">i</span> <span class="o">=</span> <span class="nx">nextPayeeIndex</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="nx">i</span> <span class="o"><</span> <span class="nx">payees</span><span class="p">.</span><span class="nx">length</span> <span class="o">&&</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">gas</span> <span class="o">></span> <span class="mi">200000</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">payees</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">addr</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">payees</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">value</span><span class="p">);</span>
      <span class="nx">i</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nx">nextPayeeIndex</span> <span class="o">=</span> <span class="nx">i</span><span class="p">;</span>
<span class="p">}</span>

如果上述代码地址列表过长,就有可能导致交易失败。

2018年7月23日,Seebug Paper发表的 《首个区块链 token 的自动化薅羊毛攻击分析》 中攻击合约就提到了这种gas优化方式。

2) 循环安全问题

在以太坊中,应该尽量避免循环次数受到用户控制,攻击者可能会使用过大的循环来完成Dos攻击。

<span class="kd">function</span> <span class="nx">distribute</span><span class="p">(</span><span class="nx">address</span><span class="p">[]</span> <span class="nx">addresses</span><span class="p">)</span> <span class="nx">onlyOwner</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="nx">uint</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">addresses</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// transfer code</span>
    <span class="p">}</span>
<span class="p">}</span>

当攻击者通过不断添加address列表长度,来迫使该函数执行循环次数过多,导致合约无法正常维护,函数无法执行。

2016年,GovernMental合约代币被爆出 恶意攻击 ,导致地址列表过长无法执行,超过1100 ETH被困在了合约中。

三、漏洞影响范围

使用Haotian平台智能合约审计功能可以准确扫描到该类型问题。

基于Haotian平台智能合约审计功能规则,我们对全网的公开的共39548 个合约代码进行了扫描,其中共24791个合约涉及到这类问题。

1、 条件竞争

截止2018年8月10日为止,我们发现了22981个存在approve条件竞争的合约代码,其中15325个合约仍处于交易状态,其中交易量最高的10个合约情况如下:

2、 循环DoS问题

截止2018年8月10日为止,我们发现了1810个存在潜在循环dos问题的合约代码,其中1740个合约仍处于交易状态,其中交易量最高的10个合约情况如下:

四、修复方式

1)条件竞争

关于这个问题的修复方式讨论很多,由于这属于底层特性的问题,所以很难在智能合约层面做解决,在代码层面,我们建议在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;
    }

2)循环DoS问题

在面临循环DoS问题产生的场景中,最为常见的就是向多个用户转账这个功能。

这里推荐代码中尽量避免用户可以控制循环深度,如果无法避免的话,尽量使用类似withdrawFunds这种函数,循环中只分发用户提币的权限,让用户来提取属于自己的代币,通过这种操作可以大幅度节省花费的gas开支,也可以一定程度避免可能导致的问题。代码如下所示:

<span class="kd">function</span> <span class="nx">distribute</span><span class="p">(</span><span class="nx">address</span><span class="p">[]</span> <span class="nx">addresses</span><span class="p">)</span> <span class="nx">onlyOwner</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="nx">uint</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">addresses</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">address_claimed_tokens</span><span class="p">[</span><span class="nx">addresses</span><span class="p">[</span><span class="nx">i</span><span class="p">]]</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">balances</span><span class="p">[</span><span class="nx">owner</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">transferAmount</span><span class="p">;</span>
            <span class="nx">balances</span><span class="p">[</span><span class="nx">addresses</span><span class="p">[</span><span class="nx">i</span><span class="p">]]</span> <span class="o">+=</span> <span class="nx">transferAmount</span><span class="p">;</span>
            <span class="nx">address_claimed_tokens</span><span class="p">[</span><span class="nx">addresses</span><span class="p">[</span><span class="nx">i</span><span class="p">]]</span> <span class="o">+=</span> <span class="nx">transferAmount</span><span class="p">;</span>
            <span class="nx">Transfer</span><span class="p">(</span><span class="nx">owner</span><span class="p">,</span> <span class="nx">addresses</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">transferAmount</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

五、一些思考

在分析了许多智能合约已有的漏洞以及合约以后,我发现有一类问题比较特殊,这些问题的诞生根本原因都是因为以太坊智能合约本身的设计缺陷,再加上开发者对此没有清晰的认识,导致了合约本身的一些隐患。

文章中提到的条件竞争是个比较特殊的问题,这里的条件竞争涉及到了智能合约底层实现逻辑,本身打包逻辑存在条件竞争,我们无法在代码层面避免这个问题,但对于开发者来说,比起无缘无故的因为该问题丢失代币来说,更重要的是合约管理者可以监控到每一笔交易的结果,所以我们加入置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漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。

欢迎扫码咨询:

六、REF


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址: https://paper.seebug.org/679/

作者:知道创宇
更好更安全的互联网

发表评论