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

原文链接: 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。


拒绝服务(DOS)

这个类别非常广泛,但基本上用户可以在一段时间内(或在某些情况下,永久)使合约无法运行的攻击组成。这可以永远陷入这些契约中的以太,就像第二次奇偶MultiSig攻击一样

漏洞

合约可能有多种不可操作的方式。这里我只强调一些潜在的不太明显的区块链细微的Solidity编码模式,可能导致攻击者执行DOS攻击。

1.通过外部操纵映射或数组循环 - 在我的冒险中,我看到了这种模式的各种形式。通常情况下,它出现在owner希望在其投资者之间分配代币的情况下,并且distribute()可以在示例合约中看到类似功能的情况:

contract DistributeTokens {
    address public owner; // gets set somewhere
    address[] investors; // array of investors
    uint[] investorTokens; // the amount of tokens each investor gets
    
    // ... extra functionality, including transfertoken()
    
    function invest() public payable {
        investors.push(msg.sender);
        investorTokens.push(msg.value * 5); // 5 times the wei sent
        }
    
    function distribute() public {
        require(msg.sender == owner); // only owner
        for(uint i = 0; i < investors.length; i++) { 
            // here transferToken(to,amount) transfers "amount" of tokens to the address "to"
            transferToken(investors[i],investorTokens[i]); 
        }
    }
}

请注意,此合约中的循环遍历可能被人为夸大的数组。攻击者可以创建许多用户帐户,使investor阵列变大。原则上,可以这样做,即执行for循环所需的gas超过块gas极限,基本上使distribute()功能无法操作。

2.所有者操作 - 另一种常见模式是所有者在合约中具有特定权限,并且必须执行一些任务才能使合约进入下一个状态。例如,ICO合约要求所有者finalize()签订合约,然后允许令牌可以转让,即

bool public isFinalized = false;
address public owner; // gets set somewhere

function finalize() public {
    require(msg.sender == owner);
    isFinalized == true;
}

// ... extra ICO functionality

// overloaded transfer function
function transfer(address _to, uint _value) returns (bool) {
    require(isFinalized);
    super.transfer(_to,_value)
}

在这种情况下,如果特权用户丢失其私钥 或变为非活动状态,则整个令牌合约变得无法操作。在这种情况下,如果owner无法调用finalize()不可以转让代币,即令牌生态系统的整个操作取决于一个地址。

3.基于外部调用的进展状态 - 合约有时被编写成为了进入新的状态需要将以太网发送到某个地址,或者等待来自外部来源的某些输入。这些模式可能导致DOS攻击,当外部调用失败时,或由于外部原因而被阻止。在发送ether的例子中,用户可以创建一个不接受ether的契约。如果合约需要将ether送到这个地址才能进入新的状态,那么合约将永远不会达到新的状态,因为乙ether永远不会被送到合约。

预防技术

在第一个例子中,合约不应该循环通过可以被外部用户人为操纵的数据结构。建议撤销模式,每个投资者都会调用撤销函数来独立声明令牌。

在第二个例子中,要求特权用户改变合约的状态。在这样的例子中(只要有可能),如果无法使用故障安全装置,则可以使用故障安全装置owner。一种解决方案可能是建立owner一个多合约。另一种解决方案是使用一个时间段,其中线路[13]上的需求可以包括基于时间的机制,例如require(msg.sender == owner || now > unlockTime)允许任何用户在一段时间后完成,由指定unlockTime。这种缓解技术也可以在第三个例子中使用。如果需要进行外部呼叫才能进入新状态,请考虑其可能的失败情况,并且可能会添加基于时间的状态进度,以防止所需的呼叫不会到来。

注意:当然,这些建议可以集中替代,maintenanceUser如果需要的话,可以添加一个谁可以来解决基于DOS攻击向量的问题。通常,这类合约包含对这种实体的权力的信任问题,但这不是本节的对话。

真实的例子:GovernMental

GovernMental是一个古老的庞氏骗局,积累了相当多的以太。实际上,它曾经积累过一百一十万个以太。不幸的是,它很容易受到本节提到的DOS漏洞的影响。这个Reddit Post描述了合约如何删除一个大的映射以撤销以太。这个映射的删除有一个gas成本超过了当时的gas阻塞限制,因此不可能撤回1100ether。合约地址为0xF45717552f12Ef7cb65e95476F217Ea008167Ae3,您可以从交易0x0d80d67202bd9cb6773df8dd2020e7190a1b0793e8ec4fc105257e8128f0506b中看到1100ether最终通过使用2.5Mgas的交易获得。