區(qū)塊鏈安全—合約存儲(chǔ)機(jī)制安全分析

一、前言

作為不太成熟的編程語言,Solidity函數(shù)由于其運(yùn)行機(jī)制等問題目前能找到很多的安全問題。在之前的分析中,我們針對(duì)共識(shí)、合約等方向進(jìn)行過概括性的研究,而最近區(qū)塊鏈安全的研究熱也激起了研究者對(duì)以太坊的深入了解。

最近的幾次CTF比賽中,區(qū)塊鏈的題目出現(xiàn)的頻率也越來越高,也逐漸進(jìn)入大家的視野中。今天,我們就針對(duì)部分區(qū)塊鏈的CTF題目以及生產(chǎn)環(huán)境中的實(shí)例進(jìn)行一些相關(guān)技術(shù)分析,并帶領(lǐng)讀者一步一步模擬這些漏洞的出現(xiàn)情況。并在以太坊平臺(tái)上進(jìn)行相關(guān)合約部署,方便研究者更進(jìn)一步的研究。

在分析開始之前,我們先對(duì)智能合約有一個(gè)基礎(chǔ)的概念了解。

智能合約就是運(yùn)行在區(qū)塊鏈網(wǎng)絡(luò)上的程序,智能合約與合約執(zhí)行的結(jié)果都會(huì)儲(chǔ)存在區(qū)塊鏈上。在區(qū)塊鏈的背景下,智能合約不只是一個(gè)計(jì)算機(jī)程序:它自己就是一個(gè)參與者,對(duì)接收到的信息進(jìn)行回應(yīng)并在同時(shí)接受和存儲(chǔ)相應(yīng)的價(jià)值。除此之外,它也能同外部地址合約進(jìn)行交互,向外發(fā)送信息和價(jià)值。智能合約與一般程序的差異主要體現(xiàn)在以下四個(gè)方面:

  • 整合資金流程度

智能合約通過以太坊自帶的以太幣可以非常容易的整合資金流系統(tǒng)。

  • 部署以及后續(xù)費(fèi)用

一般程序部署在服務(wù)器上,程序部署成功后,除了需要花費(fèi)一些維護(hù)費(fèi)用外不需要其他的額外花費(fèi)。智能合約在部署的時(shí)候需要一筆費(fèi)用,這些費(fèi)用將分給參與交易驗(yàn)證的人。而在合約部署成功后,合約會(huì)作為不可更改的區(qū)塊鏈的一部分,分散地存儲(chǔ)在全球各地的以太坊節(jié)點(diǎn)上。因此,智能合約部署后,并不需要定期提供維持費(fèi)用,同時(shí)查詢已寫入?yún)^(qū)塊鏈的靜態(tài)數(shù)據(jù)時(shí)也不需要費(fèi)用,只有在每次通過智能合約寫入數(shù)據(jù)的時(shí)候才需要交易費(fèi)用。

例如:


上述圖片為查詢owner的信息,而此次查詢點(diǎn)擊即可獲得信息,并不需要支付交易費(fèi)用。

然而對(duì)于根據(jù)智能合約寫入信息來說,我們則需要進(jìn)行手續(xù)費(fèi)的提供。

image.png
  • 存儲(chǔ)成本不同

一般的應(yīng)用程序需要將數(shù)據(jù)存儲(chǔ)到服務(wù)器上,需要數(shù)據(jù)時(shí)需要從服務(wù)器上讀取。然而智能合約將數(shù)據(jù)存儲(chǔ)在區(qū)塊鏈上,存儲(chǔ)數(shù)據(jù)所需的成本相對(duì)比較昂貴,需要根據(jù)存儲(chǔ)數(shù)據(jù)的大小支付相當(dāng)?shù)馁M(fèi)用情況。

  • 部署后無法更改

一般的程序可以通過版本升級(jí)的方式進(jìn)行更改,而智能合約一旦部署到區(qū)塊鏈上后,就無法更改這個(gè)智能合約。

二、關(guān)鍵威脅函數(shù)分析

根據(jù)知道,在Solidity合約的書寫中,跨合約調(diào)用是經(jīng)常出現(xiàn)危險(xiǎn)的地方。而我們就要在這里對(duì)調(diào)用函數(shù)進(jìn)行一些詳細(xì)的分析。這里我們分別對(duì)call()以及delegatecall()函數(shù)進(jìn)行實(shí)驗(yàn)分析,之后對(duì)某些函數(shù)存在的上下文問題進(jìn)行深入的理論探討。

在實(shí)驗(yàn)中,我部署了

pragma solidity ^0.4.23;
contract subFun {

    address public addr;

    function subTest() public returns (address a){
        addr = address(this);
        
 
    }
}
contract callAndDelegatecall {
    address public b;
    
    address public testaddress;
    constructor(address _address) public {
        testaddress = _address;
    }
    function withcall() public {
        testaddress.call(bytes4(keccak256("subTest()")));
    }
    function withdelegatecall() public {
        testaddress.delegatecall(bytes4(keccak256("subTest()")));
    }
}

并以此代碼進(jìn)行實(shí)驗(yàn)。

1 call()函數(shù)

image.png

開始的時(shí)候,我們傳入地址信息并對(duì)此函數(shù)進(jìn)行部署。

image.png

由下圖,我們通過此函數(shù)部署了兩個(gè)contract。

image.png

此時(shí),我們查看兩個(gè)合約對(duì)應(yīng)的addr參數(shù)的值,我們知道初始時(shí)的值均為Ox0000000。

之后我們調(diào)用callAndDelegatecall合約中的withcall()函數(shù),將addr的值更改后我們進(jìn)行查看。

image.png

我們發(fā)現(xiàn)subFun合約中的地址被修改,而下面的地址仍然是0x00000。

這就可以很好地說明,調(diào)用call()時(shí),上下文環(huán)境是被調(diào)用的合約的環(huán)境。

2 delegatecall()函數(shù)

二者執(zhí)行代碼的上下文環(huán)境的不同,當(dāng)使用call調(diào)用其它合約的函數(shù)時(shí),代碼是在被調(diào)用的合約的環(huán)境里執(zhí)行,對(duì)應(yīng)的,使用delegatecall進(jìn)行函數(shù)調(diào)用時(shí)代碼則是在調(diào)用函數(shù)的合約的環(huán)境里執(zhí)行。

對(duì)于delegatecall()函數(shù)來說,我們同樣進(jìn)行試驗(yàn)。

image.png

點(diǎn)擊運(yùn)行函數(shù)后,我們發(fā)現(xiàn)了不同的現(xiàn)象。

image.png

我們下面的地址有了改變。我們根據(jù)代碼進(jìn)行分析:

由于我調(diào)用了

testaddress.delegatecall(bytes4(keccak256("subTest()")));

而這個(gè)函數(shù)遠(yuǎn)程調(diào)用了子合約中的函數(shù)。而我們之嚴(yán)重被改變的地址是父合約的。所以意味著碼則是在調(diào)用函數(shù)的合約的環(huán)境里執(zhí)行。

所以進(jìn)行總結(jié),我們得出:

  • call: 最常用的調(diào)用方式,調(diào)用后內(nèi)置變量 msg 的值會(huì)修改為調(diào)用者,執(zhí)行環(huán)境為被調(diào)用者的運(yùn)行環(huán)境(合約的 storage)。

  • delegatecall: 調(diào)用后內(nèi)置變量 msg 的值不會(huì)修改為調(diào)用者,但執(zhí)行環(huán)境為調(diào)用者的運(yùn)行環(huán)境。

三、實(shí)例分析

根據(jù)我們上述代碼的實(shí)驗(yàn)分析,我們知道由于delegatecall函數(shù)是在調(diào)用者環(huán)境中執(zhí)行代碼的,所以我們可以大膽的進(jìn)行設(shè)想:倘若有某個(gè)官方系統(tǒng)的合約代碼中存在某個(gè)接口能夠傳入?yún)?shù),并且擁有delegatecall函數(shù)的調(diào)用可能。那么我們是否可以通過此來進(jìn)行合約調(diào)用?(因?yàn)樗纳舷挛沫h(huán)境是在本機(jī))而下面,我們就要針對(duì)這個(gè)相關(guān)的問題進(jìn)行delegatecall函數(shù)的綜合利用。并根據(jù)EVM的機(jī)制漏洞來實(shí)驗(yàn)相關(guān)不安全代碼。

1 合約實(shí)例分析

pragma solidity ^0.4.23;
contract Subcontract {
    uint public start;
    uint public calculatedNumber;

    function setStart(uint _start) public {
        start = _start;
    }

    function setfun(uint n) public {
        calculatedNumber = test(n);
    }

    function test(uint n) internal returns (uint) {

        return start * n;

    }
}


contract Mastercontract {
    address public addr;
    uint public calculatedNumber = 1;
    uint public start = 1;
    uint public withdrawalCounter = 1;
    bytes4 constant fibSig = bytes4(keccak256("setfun(uint)"));

    constructor(address _fibonacciLibrary) public {
        fibonacciLibrary = _fibonacciLibrary;
    }

    function withdraw() public {
        withdrawalCounter += 1;
        require(addr.delegatecall(fibSig,withdrawalCounter),"something wrong");
        msg.sender.transfer(calculatedNumber * 1 ether);
    }


    }
}

分析上述合約,我們來看對(duì)應(yīng)的函數(shù)。

首先例子中存在一個(gè)Subcontract()合約,這個(gè)為子合約。而自合約中存在test函數(shù),而我們能夠看出來test函數(shù)中返回的值為傳入的n值與start的值的乘積。而在setfun()函數(shù)中,我們調(diào)用test()函數(shù)賦值給變量calculatedNumber。

而我們再看主合約。對(duì)于以太幣相關(guān)的東西,我們最應(yīng)該關(guān)注的地方就是轉(zhuǎn)賬函數(shù)。而在withdraw函數(shù)中,我們存在msg.sender.transfer(calculatedNumber * 1 ether);函數(shù)。而在此函數(shù)中,合約會(huì)向調(diào)用者轉(zhuǎn)賬calculatedNumber * 1個(gè)以太幣。所以倘若我們想增加轉(zhuǎn)賬數(shù)額,那么我們就需要提高calculatedNumber的值。

而在我們的合約中,我們發(fā)現(xiàn)轉(zhuǎn)賬參數(shù)只有1。所以轉(zhuǎn)賬的數(shù)額很少。

我們需要修改calculatedNumber的值,而我們并沒有在主函數(shù)中發(fā)現(xiàn)修改其值的地方。然而,這個(gè)代碼中卻存在著很嚴(yán)重的問題。

雖然我們不能直接修改calculatedNumber參數(shù)的值,但是我們發(fā)現(xiàn)了代碼中存在函數(shù)調(diào)用require(addr.delegatecall(fibSig,withdrawalCounter),"something wrong");。那我們能否在這個(gè)地方做手腳呢?

(此處是重點(diǎn)) 在子合約中我們定義了兩個(gè)uint的變量start 與 calculatedNumber。而在Solidity存儲(chǔ)機(jī)制中,他們兩個(gè)被分別存儲(chǔ)在slot[0]與slot[1]這兩個(gè)位置。(代表以太坊虛擬機(jī)的兩個(gè)空間)

類似的,在Mastercontract合約中,addr 與calculatedNumber也被存儲(chǔ)在slot[0]與slot[1]這兩個(gè)位置。而根據(jù)我們上面的測試內(nèi)容,delegatecall保留了合約的上下文,運(yùn)行環(huán)境其實(shí)為本合約。這意味著通過delegatecall的代碼將對(duì)主調(diào)用合約的狀態(tài)(如存儲(chǔ))產(chǎn)生作用。

也就是說,我使用delegatecall ()函數(shù)后由于是在主合約的上下文中,所以子合約將去尋找start,而在以太坊機(jī)制中,我們并不是通過名字來進(jìn)行值的獲取,而且根據(jù)位置了尋找。即庫合約中的start的存儲(chǔ)位置為slot[0],那么當(dāng)使用delegatecall時(shí),就是在主調(diào)用合約的slot[0]位置去找,但是在主調(diào)用合約中slot[0]位置的值為addr。也就是說,我們通過遠(yuǎn)程調(diào)用而改變了主函數(shù)中變量的值。

下面我們看具體的代碼實(shí)驗(yàn)。

首先我們部署子合約:

image.png

之后我們傳入子合約地址并部署master合約,之后得到

image.png

之后傳遞aaa的值為55555:

image.png

再次點(diǎn)擊aaa后,我們查看更新后的值:

image.png

發(fā)現(xiàn)我們的合約fibonacciLibrary1的值被更改了。

我們將代碼放于此:

pragma solidity ^0.4.23;
contract Subcontract {
    
    uint public calculatedNumber;
    // uint public start = 99;

    // function setStart(uint _start) public {
    //     start = _start;
    // }

    function setfun(uint n) public {
        calculatedNumber =  n;
    }

}


contract Mastercontract {
   
    // uint public withdrawalCounter = 20;
    address public fibonacciLibrary1;
    address public fibonacciLibrary;

    
    bytes4 constant fibSig = bytes4(keccak256("setfun(uint256)"));

    constructor(address _fibonacciLibrary) public {
        fibonacciLibrary = _fibonacciLibrary;
        
    }
    function aaa(uint Counter) public {
       
        fibonacciLibrary.delegatecall(fibSig,Counter);
    }

}

我們發(fā)現(xiàn)我們并沒有能夠更改fibonacciLibrary1參數(shù)的入口,但是它確實(shí)被更改了。也就意味著我們使用delegatecall函數(shù)成功了。

2 合約CTF題目分析

下面,我們看一道改編后的ctf題目。

在測試環(huán)境中,我們需要用到三個(gè)合約地址:

image.png

這三個(gè)合約地址分別部署子合約、父合約以及攻擊合約。

而下面我們看一下題目。

pragma solidity ^0.4.23;
import "github.com/Arachnid/solidity-stringutils/strings.sol";

contract Ttest {

  address public addr1;
  address public addr2;
  address public owner; 
  using strings for *;


  bytes4 constant setTimeSignature = bytes4(keccak256("set(uint256)"));

  

  constructor(address _a, address _b) public {
    addr1 = _a; 
    addr2 = _b; 
    owner = msg.sender;
  }


  function First(uint _timeStamp) public {
    addr1.delegatecall(setTimeSignature, _timeStamp);
  }


  function Second(uint _timeStamp) public {
    addr2.delegatecall(setTimeSignature, _timeStamp);
  }

  function attack(string name) public returns(string){
    require (owner == msg.sender);
    string memory c = "Congratulations  attacker !!";
    
    return c.toSlice().concat(name.toSlice());
  }
}


contract Library {


  uint first;  

  function set(uint _time) public {
    first = _time;
  }
}

主合約中共有三個(gè)函數(shù):First Second attack。而前兩個(gè)函數(shù)用于調(diào)用子合約中的set函數(shù)。我們在attack()函數(shù)中看到,在內(nèi)部需要require,即在執(zhí)行此函數(shù)的過程中需要將我們的owner身份驗(yàn)證為調(diào)用者。(也就是說我攻擊者需要將owner改成自己的地址才能攻擊)

所以我們根據(jù)上面提及的內(nèi)容,進(jìn)行分析。我們知道在First Second函數(shù)中存在delegatecall (),而我們知道這個(gè)函數(shù)是在運(yùn)行函數(shù)方的上下文中進(jìn)行的。所以我們根據(jù)上文提及的存儲(chǔ)漏洞來進(jìn)行合約攻擊。

首先部署好合約:

image.png

這里分別使用第一個(gè)地址與第二個(gè)地址部署。

之后我們使用第三個(gè)地址部署攻擊合約:

image.png

此時(shí)我們能夠看到目前addr1與addr2變量對(duì)應(yīng)的地址為子合約那個(gè)部分的地址。也就是說我現(xiàn)在調(diào)用函數(shù)會(huì)執(zhí)行自合約部分的set函數(shù)。

image.png

之后我使用存儲(chǔ)漏洞修改掉地址一。此時(shí)我們將attack部署在地址三上。然后傳入attack合約地址于First函數(shù)中。

image.png

運(yùn)行后查看得到:

image.png

此時(shí),我們addr1的地址已經(jīng)變成了部署attack的地方,也就是說此時(shí)倘若我運(yùn)行First()函數(shù),那么我們就會(huì)調(diào)用attack合約中的set()函數(shù)。而我們具體看一下set函數(shù)的內(nèi)容:

  function set (uint _time) public {
      owner = tx.origin;
  }

我們?nèi)我獾膫魅雲(yún)?shù),之后就會(huì)將owner更改為合約所有者——即attacker的地址。

image.png

此時(shí)我們調(diào)用了First函數(shù),之后我們再看owner的變化。

image.png

它從0x14.......變成了0x4B......。也就是說它變成了我們攻擊者的owner地址。

image.png

此時(shí)我們就可以調(diào)用Ttest合約中的attack()函數(shù)(因?yàn)橐呀?jīng)繞過了owner)。得到:

image.png

至此,我們的攻擊成功。

四、參考鏈接

本稿為原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)標(biāo)明出處。謝謝。

本文首發(fā)于先知社區(qū),地址為:https://xz.aliyun.com/t/3606
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容