Solidity 安全系列:

**
**

Part-1: 可重入漏洞、算法上下溢出
Part-2:不期而至的 Ether、Delegatecall
Part-3:默認可見性、隨機數誤區


**

**

1. 外部合約引用

**
**

以太坊全球計算機的好處之一是能夠重複使用代碼、與已部署在網絡上的合約進行交互。因此,大量合約引用外部合約,並且在一般運營中使用外部消息調用(External Message Call)來與這些合約交互。惡意行爲者的意圖可以隱藏在這些不起眼的外部消息調用之下,下面我們就來探討這些瞞天過海的方法。

1.1 漏洞

在 Solidity 中,任何地址都可以被當作合約,無論地址上的代碼是否表示需要用到合約類型。這可能是騙人的,特別是當合約的作者試圖隱藏惡意代碼時。讓我們以一個例子來說明這一點:

考慮一段代碼,它初步地實現了 Rot13 密碼。

Rot13Encryption.sol

[code]

//encryption contractcontract 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,0x7 B)) // 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(0x7 B, sub(char,0x61)) }                if iszero(eq(char, 0x20))                {mstore8(add(add(text,0x20), mul(i,1)), sub(char,13))}            }        }        emit Result(text);    }}

[/code]

得到一串字符(字母 a-z,沒有驗證)之後,上述代碼通過將每個字符向右移動 13 個位置(圍繞 \’z\’)來加密該字符串;即 \’a\’ 轉換爲 \’n\’,\’x\’ 轉換爲 \’k\’。這裏的集合並不重要,所以如果在這個階段看不出問題,不必焦躁。

考慮以下使用此代碼進行加密的合約,

[code]

import \"Rot13Encryption.sol\";// encrypt your top secret infocontract 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);     } }

[/code]

這個合約的問題是, encryptionLibrary 地址並不是公開的或保證不變的。因此,合約的配置人員可以在指向該合約的構造函數中給出一個地址:

[code]

//encryption contractcontract 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,0x7 B)) // 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(0x7 B, sub(char,0x61)) }                if iszero(eq(char, 0x20))                {mstore8(add(add(text,0x20), mul(i,1)), sub(char,26))}            }        }        emit Result(text);    }}

[/code]

它實現了 rot26 密碼(每個字母移動 26 個位置,明白了嗎(微笑臉))。再次強調,你不需要了解本合約中的程序集。部署人員也可以鏈接下列合約:

[code]

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

[/code]

如果這些合約中的任何一個的地址在構造函數中給出,那麼 encryptPrivateData() 函數只會產生一個打印出未加密私有數據的事件(Event)。儘管在這個例子中,在構造函數中設置了類似庫的合約,但是特權用戶(例如 owner )可以更改庫合約地址。如果被鏈接的合約不包含被調用的函數,則將執行回退函數。例如,對於行 encryptionLibrary.rot13Encrypt() ,如果指定的合約 encryptionLibrary 是:

[code]

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

[/code]

那麼會發出一個帶有“Here”文字的事件。因此,如果用戶可以更改合約庫,原則上可以讓用戶在不知不覺中運行任意代碼。

注意:不要使用這些加密合約,因爲智能合約的輸入參數在區塊鏈上可見。另外,Rot 密碼並不是推薦的加密技術:p

1.2 預防技術

如上所示,無漏洞合約可以(在某些情況下)以惡意行爲的方式部署。審計人員可以公開驗證合約並讓其所有者以惡意方式進行部署,從而產生具有漏洞或惡意的公開審計合約。

有許多技術可以防止這些情況發生。

一種技術是使用 new 關鍵詞來創建合約。在上面的例子中,構造函數可以寫成:

[code]

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

[/code]

這樣,引用合約的一個實例就會在部署時創建,並且部署者無法在不修改智能合約的情況下用其他任何東西替換 Rot13Encryption 合約。

另一個解決方案是如果已知外部合約地址的話,對所有外部合約地址進行硬編碼。

一般來說,應該仔細查看調用外部合約的代碼。作爲開發人員,在定義外部合約時,最好將合約地址公開(在 Honey-pot 的例子中就不是這樣),以便用戶輕鬆查看合約引用了哪些代碼。反過來說,如果合約具有私人變量合約地址,則它可能是某人惡意行爲的標誌(如現實示例中所示)。如果特權(或任何)用戶能夠更改用於調用外部函數的合約地址,(在去中心化系統的情境中)實現時間鎖定或投票機制就變得很重要,爲要允許用戶查看哪些代碼正在改變,或讓參與者有機會選擇加入 / 退出新的合約地址。

1.3 真實世界的例子:可重入釣魚合約

最近主網上出現了一些釣魚合約(Honey Pot)。這些合約試圖打敗那些想要利用合約漏洞的黑客,讓他們反過來在想要利用的合約中損失 Ether。一個例子是通過在構造函數中用惡意合約代替期望的合約來發動上述攻擊。代碼可以在這裏找到:

[code]

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);    }}

[/code]

一位 reddit 用戶發佈了這篇文章,解釋他們如何在他們想利用可重入漏洞的合約中失去 1 Ether。

2. 短地址 / 參數攻擊

**
**

這種攻擊並不是專門針對 Solidity 合約執行的,而是針對可能與之交互的第三方應用程序執行的。爲了完整性,我添加了這個攻擊,然後意識到了參數可以在合約中被操縱。

有關進一步閱讀,請參閱 ERC20 短地址攻擊說明,ICO 智能合約漏洞:短地址攻擊或這個 Reddit 帖子。

2.1 漏洞

將參數傳遞給智能合約時,參數將根據 ABI 規範進行編碼。可以發送比預期參數長度短的編碼參數(例如,發送只有 38 個十六進制字符(19 個字節)的地址而不是標準的 40 個十六進制字符(20 個字節))。在這種情況下,EVM 會將 0 填到編碼參數的末尾以補成預期的長度。

當第三方應用程序不驗證輸入時,這會成爲問題。最明顯的例子是當用戶請求提款時,交易所不驗證 ERC20 Token 的地址。Peter Venesses 的文章 “ERC20 短地址攻擊解釋”中詳細介紹了這個例子。

考慮一下標準的 ERC20 傳輸函數接口,注意參數的順序,

[code]

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

[/code]

現在考慮一下,一個交易所持有大量代(比方說 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 真實世界的例子:未知

我尚不知道真實世界中發生的此類攻擊的公開例子。


原文鏈接 :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: 地址

來源鏈接:None