干货 | Solidity 安全:已知攻击方法和常见防御模式综合列表,Part-4:外部合约引用、短地址攻击

Ajian   |     |   1420 次阅读


Solidity 安全系列:

Part-1:可重入漏洞、算法上下溢出
Part-2:不期而至的 Ether、Delegatecall
Part-3:默认可见性、随机数误区


目录


1. 外部合约引用

以太坊全球计算机的好处之一是能够重复使用代码、与已部署在网络上的合约进行交互。因此,大量合约引用外部合约,并且在一般运营中使用外部消息调用(External Message Call)来与这些合约交互。恶意行为者的意图可以隐藏在这些不起眼的外部消息调用之下,下面我们就来探讨这些瞒天过海的方法。

1.1 漏洞

在 Solidity 中,任何地址都可以被当作合约,无论地址上的代码是否表示需要用到合约类型。这可能是骗人的,特别是当合约的作者试图隐藏恶意代码时。让我们以一个例子来说明这一点:

考虑一段代码,它初步地实现了 Rot13 密码。

Rot13Encryption.sol

//encryption contract
contract Rot13Encryption {

   event Result(string convertedString);

    //rot13 encrypt a string
    function rot13Encrypt (string text) public {
        uint256 length = bytes(text).length;
        for (var i = 0; i < length; i++) {
            byte char = bytes(text)[i];
            //inline assembly to modify the string
            assembly {
                char := byte(0,char) // get the first byte
                if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping. 
                { char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z. 
                if iszero(eq(char, 0x20)) // ignore spaces
                {mstore8(add(add(text,0x20), mul(i,1)), add(char,13))} // add 13 to char. 
            }
        }
        emit Result(text);
    }

    // rot13 decrypt a string
    function rot13Decrypt (string text) public {
        uint256 length = bytes(text).length;
        for (var i = 0; i < length; i++) {
            byte char = bytes(text)[i];
            assembly {
                char := byte(0,char)
                if and(gt(char,0x60), lt(char,0x6E))
                { char:= add(0x7B, sub(char,0x61)) }
                if iszero(eq(char, 0x20))
                {mstore8(add(add(text,0x20), mul(i,1)), sub(char,13))}
            }
        }
        emit Result(text);
    }
}

得到一串字符(字母 a-z,没有验证)之后,上述代码通过将每个字符向右移动 13 个位置(围绕 'z')来加密该字符串;即 'a' 转换为 'n','x' 转换为 'k'。这里的集合并不重要,所以如果在这个阶段看不出问题,不必焦躁。

考虑以下使用此代码进行加密的合约,

import "Rot13Encryption.sol";

// encrypt your top secret info
contract EncryptionContract {
    // library for encryption
    Rot13Encryption encryptionLibrary;

    // constructor - initialise the library
    constructor(Rot13Encryption _encryptionLibrary) {
        encryptionLibrary = _encryptionLibrary;
    }

    function encryptPrivateData(string privateInfo) {
        // potentially do some operations here
        encryptionLibrary.rot13Encrypt(privateInfo);
     }
 }

这个合约的问题是, encryptionLibrary 地址并不是公开的或保证不变的。因此,合约的配置人员可以在指向该合约的构造函数中给出一个地址:

//encryption contract
contract Rot26Encryption {

   event Result(string convertedString);

    //rot13 encrypt a string
    function rot13Encrypt (string text) public {
        uint256 length = bytes(text).length;
        for (var i = 0; i < length; i++) {
            byte char = bytes(text)[i];
            //inline assembly to modify the string
            assembly {
                char := byte(0,char) // get the first byte
                if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping. 
                { char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z. 
                if iszero(eq(char, 0x20)) // ignore spaces
                {mstore8(add(add(text,0x20), mul(i,1)), add(char,26))} // add 13 to char. 
            }
        }
        emit Result(text);
    }

    // rot13 decrypt a string
    function rot13Decrypt (string text) public {
        uint256 length = bytes(text).length;
        for (var i = 0; i < length; i++) {
            byte char = bytes(text)[i];
            assembly {
                char := byte(0,char)
                if and(gt(char,0x60), lt(char,0x6E))
                { char:= add(0x7B, sub(char,0x61)) }
                if iszero(eq(char, 0x20))
                {mstore8(add(add(text,0x20), mul(i,1)), sub(char,26))}
            }
        }
        emit Result(text);
    }
}

它实现了 rot26 密码(每个字母移动 26 个位置,明白了吗(微笑脸))。再次强调,你不需要了解本合约中的程序集。部署人员也可以链接下列合约:

contract Print{
    event Print(string text);

    function rot13Encrypt(string text) public {
        emit Print(text);
    }
 }

如果这些合约中的任何一个的地址在构造函数中给出,那么 encryptPrivateData() 函数只会产生一个打印出未加密私有数据的事件(Event)。尽管在这个例子中,在构造函数中设置了类似库的合约,但是特权用户(例如 owner )可以更改库合约地址。如果被链接的合约不包含被调用的函数,则将执行回退函数。例如,对于行 encryptionLibrary.rot13Encrypt() ,如果指定的合约 encryptionLibrary 是:

 contract Blank {
     event Print(string text);
     function () {
         emit Print("Here");
         //put malicious code here and it will run
     }
 }

那么会发出一个带有“Here”文字的事件。因此,如果用户可以更改合约库,原则上可以让用户在不知不觉中运行任意代码。

注意:不要使用这些加密合约,因为智能合约的输入参数在区块链上可见。另外,Rot密码并不是推荐的加密技术:p

1.2 预防技术

如上所示,无漏洞合约可以(在某些情况下)以恶意行为的方式部署。审计人员可以公开验证合约并让其所有者以恶意方式进行部署,从而产生具有漏洞或恶意的公开审计合约。

有许多技术可以防止这些情况发生。

一种技术是使用 new 关键词来创建合约。在上面的例子中,构造函数可以写成:

    constructor(){
        encryptionLibrary =  new  Rot13Encryption();
    }

这样,引用合约的一个实例就会在部署时创建,并且部署者无法在不修改智能合约的情况下用其他任何东西替换 Rot13Encryption 合约。

另一个解决方案是如果已知外部合约地址的话,对所有外部合约地址进行硬编码。

一般来说,应该仔细查看调用外部合约的代码。作为开发人员,在定义外部合约时,最好将合约地址公开(在 Honey-pot 的例子中就不是这样),以便用户轻松查看合约引用了哪些代码。反过来说,如果合约具有私人变量合约地址,则它可能是某人恶意行为的标志(如现实示例中所示)。如果特权(或任何)用户能够更改用于调用外部函数的合约地址,(在去中心化系统的情境中)实现时间锁定或投票机制就变得很重要,为要允许用户查看哪些代码正在改变,或让参与者有机会选择加入/退出新的合约地址。

1.3 真实世界的例子:可重入钓鱼合约

最近主网上出现了一些钓鱼合约(Honey Pot)。这些合约试图打败那些想要利用合约漏洞的黑客,让他们反过来在想要利用的合约中损失 Ether。一个例子是通过在构造函数中用恶意合约代替期望的合约来发动上述攻击。代码可以在这里找到:

pragma solidity ^0.4.19;

contract Private_Bank
{
    mapping (address => uint) public balances;
    uint public MinDeposit = 1 ether;
    Log TransferLog;

    function Private_Bank(address _log)
    {
        TransferLog = Log(_log);
    }

    function Deposit()
    public
    payable
    {
        if(msg.value >= MinDeposit)
        {
            balances[msg.sender]+=msg.value;
            TransferLog.AddMessage(msg.sender,msg.value,"Deposit");
        }
    }

    function CashOut(uint _am)
    {
        if(_am<=balances[msg.sender])
        {
            if(msg.sender.call.value(_am)())
            {
                balances[msg.sender]-=_am;
                TransferLog.AddMessage(msg.sender,_am,"CashOut");
            }
        }
    }

    function() public payable{}    

}

contract Log 
{
    struct Message
    {
        address Sender;
        string  Data;
        uint Val;
        uint  Time;
    }

    Message[] public History;
    Message LastMsg;

    function AddMessage(address _adr,uint _val,string _data)
    public
    {
        LastMsg.Sender = _adr;
        LastMsg.Time = now;
        LastMsg.Val = _val;
        LastMsg.Data = _data;
        History.push(LastMsg);
    }
}

一位 reddit 用户发布了这篇文章,解释他们如何在他们想利用可重入漏洞的合约中失去 1 Ether。

2. 短地址/参数攻击

这种攻击并不是专门针对 Solidity 合约执行的,而是针对可能与之交互的第三方应用程序执行的。为了完整性,我添加了这个攻击,然后意识到了参数可以在合约中被操纵。

有关进一步阅读,请参阅 ERC20 短地址攻击说明ICO智能合约漏洞:短地址攻击这个 Reddit 帖子

2.1 漏洞

将参数传递给智能合约时,参数将根据 ABI 规范进行编码。可以发送比预期参数长度短的编码参数(例如,发送只有 38 个十六进制字符(19 个字节)的地址而不是标准的 40 个十六进制字符(20 个字节))。在这种情况下,EVM 会将 0 填到编码参数的末尾以补成预期的长度。

当第三方应用程序不验证输入时,这会成为问题。最明显的例子是当用户请求提款时,交易所不验证 ERC20 Token 的地址。Peter Venesses 的文章 “ERC20 短地址攻击解释”中详细介绍了这个例子。

考虑一下标准的 ERC20 传输函数接口,注意参数的顺序,

function transfer(address to, uint tokens) public returns (bool success);

现在考虑一下,一个交易所持有大量代(比方说 REP ),并且,某用户希望取回他们存储的100个代币。用户将提交他们的地址, 0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead 以及代币的数量 100 。交易所将根据 transfer() 函数指定的顺序对这些参数进行编码,即先是 address 然后是 tokens 。编码结果将是 a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000000000000000000000000000000000056bc75e2d63100000 。前四个字节(a9059cbb)是 transfer()  函数签名/选择器,第二个 32 字节是地址,最后 32 个字节是表示代币数量的 uint256 。请注意,最后的十六进制数 56bc75e2d63100000 对应于 100 个代币(包含 18 个小数位,这是由 REP 代币合约指定的)。

好的,现在让我们看看如果我们发送一个丢失 1 个字节(2 个十六进制数字)的地址会发生什么。具体而言,假设攻击者以 0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde 作为地址发送(缺少最后两位数字),并取回相同的 100 个代币。如果交易所没有验证这个输入,它将被编码为 a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeadde0000000000000000000000000000000000000000000000056bc75e2d6310000000 。差别是微妙的。请注意, 00 已被填充到编码的末尾,以补完发送的短地址。当它被发送到智能合约时, address 参数将被读为 0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde00 并且值将被读为 56bc75e2d6310000000 (注意两个额外的 0)。此值现在是 25600 个代币(值已被乘以 256 )。在这个例子中,如果交易所持有这么多的代币,用户会取出 25600 个代币(而交易所认为用户只是取出 100)到修改后的地址。很显然,在这个例子中攻击者不会拥有修改后的地址,但是如果攻击者产生了以 0 结尾的地址(很容易强制产生)并且使用了这个生成的地址,他们很容易从毫无防备的交易所中窃取代币。

2.2 预防技术

我想很明显,在将所有输入发送到区块链之前对其进行验证可以防止这些类型的攻击。还应该指出的是参数排序在这里起着重要的作用。由于填充只发生在字符串末尾,智能合约中参数的缜密排序可能会缓解此攻击的某些形式。

2.3 真实世界的例子:未知

我尚不知道真实世界中发生的此类攻击的公开例子。


Part-5:未检查的 Call 返回值,条件竞争
Part-6:拒绝服务、时间戳攻击
Part-7:构造函数失控、未初始化的存储指针
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 经授权转载。


你可能还会喜欢:

干货 | 以太坊智能合约安全
观点 | 批评 VB 的《一种权益证明设计哲学》,Part-1
教程 | 用 Go 构建一个区块链 -- Part 5: 地址

 
0 人喜欢