干货 | Solidity 安全:已知攻击方法和常见防御模式综合列表,Part-3

原文链接: https://blog.sigmaprime.io/solidity-security.html

作者: Dr Adrian Manning

翻译&校对: 爱上平顶山@慢雾安全团队 & keywolf@慢雾安全团队

致谢(校对):yudan、阿剑@EthFans

本文由慢雾安全团队翻译,这里是最新译文的 GitHub 地址:https://github.com/slowmist/Knowledge-Base/blob/master/solidity-security-comprehensive-list-of-known-attack-vectors-and-common-anti-patterns-chinese.md

默认可见性(Visibility)

Solidity 中的函数具有可见性说明符,它们会指定我们可以如何调用函数。可见性决定一个函数是否可以由用户或其他派生契约在外部调用、只允许内部调用或只允许外部调用。有四个可见性说明符,详情请参阅 Solidity 文档。为允许用户从外部调用函数,函数的可见性默认为 public 。正如本节将要讨论的,可见性说明符的不正确使用可能会导致智能合约中的一些资金流失。

漏洞

函数的可见性默认是 public 。因此,不指定任何可见性的函数就可以由用户在外部调用。当开发人员错误地忽略应该是私有的功能(或只能在合约本身内调用)的可见性说明符时,问题就出现了。 让我们快速浏览一个简单的例子。

contract HashForEther {
    
    function withdrawWinnings() {
        // Winner if the last 8 hex characters of the address are 0. 
        require(uint32(msg.sender) == 0);
        _sendWinnings();
     }
     
     function _sendWinnings() {
         msg.sender.transfer(this.balance);
     }
}

这个简单的合约被设计为充当赏金猜测游戏的地址。要赢得该合约的余额,用户必须生成一个以太坊地址,其最后 8 个十六进制字符为0。一旦获得,他们可以调用 WithdrawWinnings() 函数来获得赏金。 不幸的是,这些功能的可见性没有得到指定。特别是,因为 _sendWinnings() 函数的可见性是 public ,任何地址都可以调用该函数来窃取赏金。

预防技术

总是指定合约中所有功能的可见性、即便这些函数的可见性本就有意设计成 public ,这是一种很好的做法。最近版本的 Solidity 将在编译过程中为没有明确设置可见性的函数显示警告,以鼓励这种做法。

真实世界示例:Parity MultiSig Wallet(First Hack)

在 Parity 多签名钱包遭受的第一次黑客攻击中,约值 3100 万美元的 Ether 被盗,主要是三个钱包。Haseeb Qureshi 在这篇文章中给出了一个很好的回顾。

实质上,这些多签名钱包(可以在这里找到)是从一个基础的 Wallet 合约构建出来的,该基础合约调用包含核心功能的库合约(如真实世界中的例子:Parity Multisig(Second Hack)中所述)。库合约包含初始化钱包的代码,如以下代码片段所示

contract WalletLibrary is WalletEvents {
  
  ... 
  
  // METHODS

  ...
  
  // constructor is given number of sigs required to do protected "onlymanyowners" transactions
  // as well as the selection of addresses capable of confirming them.
  function initMultiowned(address[] _owners, uint _required) {
    m_numOwners = _owners.length + 1;
    m_owners[1] = uint(msg.sender);
    m_ownerIndex[uint(msg.sender)] = 1;
    for (uint i = 0; i < _owners.length; ++i)
    {
      m_owners[2 + i] = uint(_owners[i]);
      m_ownerIndex[uint(_owners[i])] = 2 + i;
    }
    m_required = _required;
  }

  ...

  // constructor - just pass on the owner array to the multiowned and
  // the limit to daylimit
  function initWallet(address[] _owners, uint _required, uint _daylimit) {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
  }
}

请注意,这两个函数都没有明确指定可见性。两个函数的可见性都默认为 public 。钱包构造函数会调用 initWallet() 函数,并设置多签名钱包的所有者,如 initMultiowned() 函数中所示。由于这些函数意外地设置为 public ,攻击者可以在部署的合约上调用这些功能,并将所有权重置为攻击者地址。作为主人,袭击者随后取走钱包中所有的 Ether,损失高达 3100 万美元。

随机数误区

以太坊区块链上的所有交易都是确定性的状态转换操作。这意味着每笔交易都会改变以太坊生态系统的全球状态,并且它以可计算的方式进行,没有不确定性。这最终意味着在区块链生态系统内不存在熵或随机性的来源。Solidity 中没有 rand() 功能。实现区中心化的熵源(随机性)是一个由来已久的问题,人们提出了很多想法来解决这个问题(例如,RanDAO,或是如 Vitalik 在这篇帖子中说的那样,使用哈希链)。

漏洞

以太坊平台上建立的首批合约中,有一些是围绕博彩的。从根本上讲,博彩需要不确定性(可以下注),这使得在区块链(一个确定性系统)上构建博彩系统变得相当困难。很明显,不确定性只能来自于区块链外部的来源。朋友之间怡情还是可以的(例如参见承诺揭示技术),然而,要让合约成为赌场(比如玩 21 点或是轮盘赌),则困难得多。一个常见的误区是使用未来的块变量,如区块哈希值,时间戳,区块高低或是 Gas 上限。与这些设计有关的问题是,这些量都是由挖矿的矿工控制的,因此并不是真正随机的。例如,考虑一个轮盘赌智能合约,其逻辑是如果下一个块哈希值以偶数结尾,则返回一个黑色数字。一个矿工(或矿池)可以在黑色上下注 100 万美元。如果他们挖出下一个块并发现块哈希值以奇数结尾,他们会高兴地不发布他们的块、继续挖矿、直到他们挖出一个块哈希值为偶数的块(假设区块奖励和费用低于 100 万美元)。Martin Swende 在其优秀的博客文章中指出,使用过去或现在的区块变量可能会更具破坏性。此外,仅使用块变量意味着伪随机数对于一个块中的所有交易都是相同的,所以攻击者可以通过在一个块内进行多次交易来使收益倍增(如果赌注有上限的话)。

预防技术

熵(随机性)的来源只能在区块链之外。在熟人之间,这可以通过使用诸如 commit-reveal之类的系统来解决,或通过将信任模型更改为一组参与者(例如 RanDAO)。这也可以通过一个中心化的实体来完成,这个实体充当一个随机性的预言机(Oracle)。区块变量(一般来说,有一些例外)不应该被用来提供熵,因为它们可以被矿工操纵。

真实世界示例:PRNG 合约

Arseny Reutov 分析了 3649 份使用某种伪随机数发生器(PRNG)的已上线智能合约,在发现 43 份可被利用的合约之后写了一篇博文。该文详细讨论了使用区块变量作为熵源的缺陷。