干货 | Solidity 安全:已知攻击方法和常见防御模式综合列表,Part-7:构造函数失控、未初始化的存储指针

Ajian   |     |   708 次阅读


Solidity 安全系列:

Part-1:可重入漏洞、算法上下溢出
Part-2:不期而至的 Ether、Delegatecall
Part-3:默认可见性、随机数误区
Part-4:外部合约引用、短地址攻击
Part-5:未检查的 Call 返回值,条件竞争
Part-6:拒绝服务、时间戳攻击


目录

1. 构造函数失控

构造函数(Constructors)是特殊函数,在初始化合约时经常执行关键的权限任务。在 solidity v0.4.22 以前,构造函数被定义为与所在合约同名的函数。因此,如果合约名称在开发过程中发生变化,而构造函数名称没有更改,它将变成正常的可调用函数。正如你可以想象的,这可以(并且已经)导致一些有趣的合约被黑。

为了进一步阅读,我建议读者尝试 Ethernaught 挑战(特别是 Fallout 层级)。

1.1 漏洞

如果合约名称被修改,或者在构造函数名称中存在拼写错误以致它不再与合约名称匹配,则构造函数的行为将与普通函数类似。这可能会导致可怕的后果,特别是如果构造函数正在执行有权限的操作。考虑以下合约:

contract OwnerWallet {
    address public owner;

    //constructor
    function ownerWallet(address _owner) public {
        owner = _owner;
    }

    // fallback. Collect ether.
    function () payable {} 

    function withdraw() public {
        require(msg.sender == owner); 
        msg.sender.transfer(this.balance);
    }
}

该合约储存 Ether,并只允许所有者通过调用 withdraw() 函数来取出所有 Ether。但由于构造函数的名称与合约名称不完全一致,这个合约会出问题。具体来说, ownerWalletOwnerWallet 不相同。因此,任何用户都可以调用 ownerWallet() 函数,将自己设置为所有者,然后通过调用 withdraw() 将合约中的所有 Ether 都取出来。

1.2 预防技术

这个问题在 Solidity 0.4.22 版本的编译器中已经基本得到了解决。该版本引入了一个关键词 constructor 来指定构造函数,而不是要求函数的名称与合约名称匹配。建议使用这个关键词来指定构造函数,以防止上面显示的命名问题。

1.3 真实世界的例子:Rubixi

Rubixi(合约代码)是另一个显现出这种漏洞的传销方案。合约中的构造函数一开始叫做 DynamicPyramid ,但合约名称在部署之前已改为 Rubixi 。构造函数的名字没有改变,因此任何用户都可以成为 creator 。这篇 Bitcoin Thread 中可以找到关于这个 bug 的一些有趣的讨论。总之,用户因为这个漏洞开始互相争夺 creator 身份,以从合约中获得金钱。关于这个特定 bug 的更多细节可以在这里找到。

2. 未初始化的存储指针

EVM 既用 storage 来存储,也用 memory 来存储。强烈建议开发合约时弄懂存储的方式和函数局部变量的默认类型。因为不恰当地初始化变量可能产生有漏洞的合约。

要了解更多关于的 EVM 中 storagememory 的内容,请看 Solidity Docs: Data LocationSolidity Docs: Layout of State Variables in StorageSolidity Docs: Layout in Memory

本节以 Sfan Beyer出色的文章为基础。关于这个话题的进一步阅读可以从 Sefan 的启发中找到,也就是个这篇 reddit 帖子

2.1 漏洞

函数内的局部变量根据它们的类型默认用 storagememory 存储。未初始化的局部 storage 变量可能会指向合约中的其他意外存储变量,从而导致有意(即,开发人员故意将它们放在那里进行攻击)或无意的漏洞。

我们来考虑以下相对简单的名称注册器合约:

// A Locked Name Registrar
contract NameRegistrar {

    bool public unlocked = false;  // registrar locked, no name updates

    struct NameRecord { // map hashes to addresses
        bytes32 name;  
        address mappedAddress;
    }

    mapping(address => NameRecord) public registeredNameRecord; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses

    function register(bytes32 _name, address _mappedAddress) public {
        // set up the new NameRecord
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 

        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 

        require(unlocked); // only allow registrations if contract is unlocked
    }
}

这个简单的名称注册器只有一个功能。当合约是 unlocked 状态时,任何用户都可以注册一个名称(以 bytes32 哈希值的形式)并将该名称映射到地址。不幸的是,这个注册器一开始是被锁定的,并且在 [23] 行的 require 函数禁止 register() 添加姓名记录。但是,这个合约中存在一个漏洞,让用户可以不管 unlocked 运行注册器。

为了讨论这个漏洞,首先我们需要了解存储(Storage)在 Solidity 中的工作方式。作为一个高度抽象的概述(没有任何适当的技术细节——我建议阅读 Solidity 文档以进行适当的审查),状态变量按它们出现在合约中的顺序存储在合约的 Slot 中(它们可以被组合在一起,但在本例中不可以,所以我们不用担心)。因此, unlocked 存在 slot 0 中, registeredNameRecord 存在 slot 1 中, resolveslot 2 中,等等。这些 slot 的大小是 32 字节(映射会让事情更加复杂,但我们暂时忽略)。如果 unlockedfalse ,其布尔值看起来会是 0x000...0 (64 个 0,不包括 0x );如果是 true ,则其布尔值会是 0x000...1 (63 个 0)。正如你所看到的,在这个特殊的例子中,存储上存在着很大的浪费。

我们需要的另一部分知识,是 Solidity 会在将复杂的数据类型,比如 structs ,初始化为局部变量时,默认使用 storage 来存储。因此,在 [16] 行中的 newRecord 默认为storage。合约的漏洞是由 newRecord 未初始化导致的。由于它默认为 storage,因此它成为指向 storage 的指针;并且由于它未初始化,它指向 slot 0(即 unlocked 的存储位置)。请注意,[17] 行和[18] 行中,我们将 _name 设为 nameRecord.name 、将 _mappedAddress 设为 nameRecord.mappedAddress 的操作,实际上改变了 slot 0 和 slot 1 的存储位置,也就是改变了 unlocked 和与 registeredNameRecord 相关联的 slot。

这意味着我们可以通过 register() 函数的 bytes32 _name 参数直接修改 unlocked 。因此,如果 _name 的最后一个字节为非零,它将修改 slot 0 的最后一个字节并直接将 unlocked 转为 true 。就在我们将 unlocked 设置为 true 之时,这样的 _name 值将传入 [23] 行的 require() 函数。在Remix中试试这个。注意如果你的 _name 使用下面形式,函数会通过: 0x0000000000000000000000000000000000000000000000000000000000000001

2.2 预防技术

Solidity 编译器会在出现未经初始化的存储变量时发出警告,因此开发人员在构建智能合约时应小心注意这些警告。当前版本的 mist(0.10)不允许编译这些合约。在处理复杂类型时,明确使用 memorystorage 以保证合约行为符合预期一般是很好的做法。

2.3 真实世界的例子:钓鱼:OpenAddressLottery 和 CryptoRoulette

有人部署了一个名为 OpenAddressLottery(合约代码)的钓鱼合约,它使用未初始化的存储变量以从一些可能的黑客手上吊取 ether。合约是相当深入的,所以我会把讨论留在这个 reddit 帖子中。我在里面很清楚地解释了这种攻击。

另一个钓鱼合约,CryptoRoulette(合约代码)也利用这个技巧尝试获得一些 Ether。如果您无法弄清楚攻击是如何进行的,请参阅对以太坊钓鱼合约的分析以获得对此合约和其他内容的概述。


Part-8:浮点和精度、Tx.Origin 用作身份验证


原文链接: https://blog.sigmaprime.io/solidity-security.html
作者: Dr Adrian Manning
翻译&校对: 爱上平顶山@慢雾安全团队 & keywolf@慢雾安全团队

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

EthFans 经授权转载。


你可能还会喜欢:

教程 | 【Ethereum 智能合约开发笔记】使用 Remix
通告 | 以太坊 Mist 浏览器的未来:分层节点及其它
教程 | 以太坊编程简单介绍 ,Part-1

 
1 人喜欢