一、前言
作為不太成熟的編程語言,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)的提供。

- 存儲(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ù)

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

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

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

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


我們發(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)。

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

我們下面的地址有了改變。我們根據(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)。
首先我們部署子合約:

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

之后傳遞aaa的值為55555:

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

發(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è)合約地址:

這三個(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)行合約攻擊。
首先部署好合約:

這里分別使用第一個(gè)地址與第二個(gè)地址部署。
之后我們使用第三個(gè)地址部署攻擊合約:

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

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

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

此時(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的地址。

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

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

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

至此,我們的攻擊成功。
四、參考鏈接
本稿為原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)標(biāo)明出處。謝謝。
本文首發(fā)于先知社區(qū),地址為:https://xz.aliyun.com/t/3606