區(qū)塊鏈安全—守株待兔的蜜罐合約(二)

一、前言

在前一篇的蜜罐合約中悠反,我們介紹并測(cè)試了部分由于繼承等問(wèn)題而搭建的蜜罐合約嗤军。蜜罐合約顧名思義注盈,就是利用了受害者的投機(jī)想法,從而另普通用戶自行進(jìn)行轉(zhuǎn)賬的行為叙赚。在我們文章中演示的相關(guān)合約對(duì)owner友好老客,即普通用戶很難從合約中獲得利益,所以讀者如果看到類(lèi)似的合約請(qǐng)不要輕易的使用以太幣進(jìn)行嘗試震叮。

而本文中胧砰,我們?cè)诿酃藓霞s之上分析由Solidity的結(jié)構(gòu)體產(chǎn)生的漏洞,而此漏洞危害性極大苇瓣,倘若合約開(kāi)發(fā)不到位會(huì)導(dǎo)致owner的篡改尉间,即普通用戶的提權(quán)操作。

二击罪、由合約漏洞而導(dǎo)致的蜜罐

1 蜜罐合約介紹

pragma solidity ^0.4.19;
/*
 * This is a distributed lottery that chooses random addresses as lucky addresses. If these
 * participate, they get the jackpot: 1.9 times the price of their bet.
 * Of course one address can only win once. The owner regularly reseeds the secret
 * seed of the contract (based on which the lucky addresses are chosen), so if you did not win,
 * just wait for a reseed and try again!
 *
 * Jackpot chance:   50%
 * Ticket price: Anything larger than (or equal to) 0.1 ETH
 * Jackpot size: 1.9 times the ticket price
 *
 * HOW TO PARTICIPATE: Just send any amount greater than (or equal to) 0.1 ETH to the contract's address
 * Keep in mind that your address can only win once
 *
 * If the contract doesn't have enough ETH to pay the jackpot, it sends the whole balance.
 *
 * Example: For each address, a random number is generated, either 0 or 1. This number is then compared
 * with the LuckyNumber - a constant 1. If they are equal, the contract will instantly send you the jackpot:
 * your bet multiplied by 1.9 (House edge of 0.1)
*/

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
        
    mapping (address => bool) winner; //keeping track of addresses that have already won
    
    function OpenAddressLottery() {
        owner = msg.sender;
        reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
    }
    
    function participate() payable {
        if(msg.value<0.1 ether)
            return; //verify ticket price
        
        // make sure he hasn't won already
        require(winner[msg.sender] == false);
        
        if(luckyNumberOfAddress(msg.sender) == LuckyNumber){ //check if it equals 1
            winner[msg.sender] = true; // every address can only win once
            
            uint win=(msg.value/10)*19; //win = 1.9 times the ticket price
            
            if(win>this.balance) //if the balance isnt sufficient...
                win=this.balance; //...send everything we've got
            msg.sender.transfer(win);
        }
        
        if(block.number-lastReseed>1000) //reseed if needed
            reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
    }
    
    function luckyNumberOfAddress(address addr) constant returns(uint n){
        // calculate the number of current address - 50% chance
        n = uint(keccak256(uint(addr), secretSeed)[0]) % 2; //mod 2 returns either 0 or 1
    }
    
    function reseed(SeedComponents components) internal {
        secretSeed = uint256(keccak256(
            components.component1,
            components.component2,
            components.component3,
            components.component4
        )); //hash the incoming parameters and use the hash to (re)initialize the seed
        lastReseed = block.number;
    }
    
    function kill() {
        require(msg.sender==owner);
        
        selfdestruct(msg.sender);
    }
    
    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
    }
    
    function () payable { //if someone sends money without any function call, just assume he wanted to participate
        if(msg.value>=0.1 ether && msg.sender!=owner) //owner can't participate, he can only fund the jackpot
            participate();
    }

}

下面我們簡(jiǎn)單的分析一下這個(gè)類(lèi)彩票合約哲嘲。

為何稱(chēng)這個(gè)合約為蜜罐合約么?我們根據(jù)合約內(nèi)容可以知道外邓,合約在起始時(shí)賦值LuckyNumber為1撤蚊,而在參與函數(shù)中根據(jù)參與者的地址生成隨機(jī)數(shù)0 or 1古掏,之后如果為1损话,那么就返還value * 1.9的賭金〔弁伲看似0.5的高概率丧枪,但是合約利用了一種以太坊的bug,從而導(dǎo)致用戶永遠(yuǎn)不可能取到錢(qián)庞萍。下面請(qǐng)看我們的分析拧烦。

首先,合約定義了一個(gè)結(jié)構(gòu)體钝计。(我認(rèn)為本來(lái)不需要結(jié)構(gòu)體這樣的類(lèi)型來(lái)進(jìn)行隨機(jī)數(shù)的生成恋博,所以我覺(jué)得這里的結(jié)構(gòu)體是為了觸發(fā)合約的漏洞)

    struct SeedComponents{
        uint component1;
        uint component2;
        uint component3;
        uint component4;
    }

之后定義了五個(gè)變量齐佳,分別代表合約的owner、隨機(jī)數(shù)種子债沮、上一次的記錄值炼吴、幸運(yùn)數(shù)、競(jìng)猜獲勝者集合疫衩。

    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
    mapping (address => bool) winner; //keeping track of addresses that have already won

而下一個(gè)部分是構(gòu)造函數(shù)硅蹦。

    function OpenAddressLottery() {
        owner = msg.sender;
        reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
    }

構(gòu)造函數(shù)將owner賦初值為合約創(chuàng)建者,之后調(diào)用reseed函數(shù)闷煤。而我們下面就看一看這個(gè)函數(shù)的作用童芹。

    function reseed(SeedComponents components) internal {
        secretSeed = uint256(keccak256(
            components.component1,
            components.component2,
            components.component3,
            components.component4
        )); //hash the incoming parameters and use the hash to (re)initialize the seed
        lastReseed = block.number;
    }

在這個(gè)函數(shù)中,我們會(huì)傳入components結(jié)構(gòu)體鲤拿,并使用keccak256 ()哈希函數(shù)更新secretSeed的值假褪,并初始化lastReseed

也就是說(shuō)皆愉,我們?cè)跇?gòu)造函數(shù)中調(diào)用此函數(shù)來(lái)更新secretSeed的值嗜价。

之后,我們來(lái)看participate()幕庐,此函數(shù)是用戶調(diào)用參與接口久锥,用于競(jìng)猜的環(huán)節(jié)。

    function participate() payable {
        if(msg.value<0.1 ether)
            return; //verify ticket price
        
        // make sure he hasn't won already
        require(winner[msg.sender] == false);
        
        if(luckyNumberOfAddress(msg.sender) == LuckyNumber){ //check if it equals 1
            winner[msg.sender] = true; // every address can only win once
            
            uint win=(msg.value/10)*19; //win = 1.9 times the ticket price
            
            if(win>this.balance) //if the balance isnt sufficient...
                win=this.balance; //...send everything we've got
            msg.sender.transfer(win);
        }
        
        if(block.number-lastReseed>1000) //reseed if needed
            reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
    }

在函數(shù)中异剥,我們看到用戶必須傳入value >= 0.1 eth瑟由,并且用戶還未贏得過(guò)獎(jiǎng)勵(lì)。之后合約會(huì)將LuckyNumberluckyNumberOfAddress(msg.sender)進(jìn)行比較冤寿。倘若兩者的值相等歹苦,那么記錄下該用戶的中獎(jiǎng)記錄并進(jìn)行【value * 1.9】的轉(zhuǎn)賬獎(jiǎng)勵(lì)(余額不足的將所有余額轉(zhuǎn)入)。

而我們?cè)诳?code>luckyNumberOfAddress函數(shù)督怜。

    function luckyNumberOfAddress(address addr) constant returns(uint n){
        // calculate the number of current address - 50% chance
        n = uint(keccak256(uint(addr), secretSeed)[0]) % 2; //mod 2 returns either 0 or 1
    }

傳入一個(gè)地址殴瘦,之后根據(jù)傳入的地址產(chǎn)生隨機(jī)數(shù),并%2号杠,得到1或0 蚪腋。

然后是一個(gè)測(cè)試函數(shù)forceReseed

    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
    }

合約創(chuàng)建者在這個(gè)函數(shù)后面添加了注釋//reseed initiated by the owner - for testing purposes姨蟋。表達(dá)用于測(cè)試的目的屉凯。

然而問(wèn)題就是出在這個(gè)地方。

整體來(lái)看眼溶,這個(gè)合約并沒(méi)有什么問(wèn)題悠砚。gamble的過(guò)程也十分清晰。

image.png

然而我們進(jìn)行一個(gè)合約測(cè)試堂飞。

2 攻擊手段分析

我們先看一個(gè)測(cè)試合約:

pragma solidity ^0.4.24;

contract test
{
    address public addr = 0xa;
    uint    public b    = 555;
    uint256 public c    = 666;
    bytes   public d    = "abcd";

    struct Seed{
        uint256 component1;
        uint256 component2;
        uint256 component3;
        uint256 component4;
    }

    function change() public{
        Seed s;
        s.component1 = 1;
        s.component2 = 2;
        s.component3 = 3;
        s.component4 = 4;
    }
}

在這個(gè)合約中灌旧,我們?cè)O(shè)置了4個(gè)變量绑咱,而這四個(gè)變量均有初始值。之后我們又設(shè)置了結(jié)構(gòu)體Seed枢泰。在這個(gè)結(jié)構(gòu)體中擁有四個(gè)變量羡玛,而我們?cè)?code>test()函數(shù)中初始化結(jié)構(gòu)體并賦初值,之后我們看看效果宗苍。

部署合約:


image.png

查看變量?jī)?nèi)容:

image.png

之后我們調(diào)用change函數(shù)稼稿。并查看,發(fā)現(xiàn)我們的變量被修改了讳窟,而修改的內(nèi)容就是結(jié)構(gòu)體中的內(nèi)容让歼。

image.png

這就是我們的漏洞所在。

我們的合約中并沒(méi)有修改變量的值丽啡,但是由于solidity機(jī)制的問(wèn)題而導(dǎo)致了變量修改問(wèn)題谋右。

而這個(gè)漏洞對(duì)我們上述介紹的蜜罐合約有什么影響呢?我們進(jìn)行一下測(cè)試补箍。

為了方便我們查看測(cè)試效果改执,我們?yōu)?code>LuckyNumber添加查看函數(shù)。

image.png
image.png

倘若此時(shí)owner不進(jìn)行任何操作坑雅,任憑用戶進(jìn)行下一步的賭博辈挂,那么用戶還是有很大的概率獲得獎(jiǎng)勵(lì)的。例如:(為了方便演示裹粤,我在函數(shù)中添加了event事件)

emit back(msg.sender,win,true);终蒂。

此時(shí)我們能夠看到,LuckyNumber是初始值1 遥诉。

image.png

之后拇泣,我們更換用戶進(jìn)行參與。我們投入1 eth進(jìn)行競(jìng)猜矮锈。

第一次:

image.png

沒(méi)有獲得獎(jiǎng)勵(lì)霉翔,所以1 eth賠進(jìn)去了。

繼續(xù)更換用戶參與:

image.png

直到最后一個(gè)用戶:

image.png

我們得到了獎(jiǎng)勵(lì)金1900000000000000000 wei苞笨,所以競(jìng)猜成功债朵。

而我們大致能夠發(fā)現(xiàn),其實(shí)我們是擁有很大的概率獲得獎(jiǎng)勵(lì)的猫缭。這個(gè)合約真的就是拼概率的傳統(tǒng)賭博合約嗎葱弟?然而事實(shí)并非如此壹店。

根據(jù)我們前文所測(cè)試的漏洞猜丹,這個(gè)合約中同樣存在惡意篡改的行為。我們發(fā)現(xiàn)了合約中其實(shí)存在著結(jié)構(gòu)體硅卢。

image.png

而這個(gè)結(jié)構(gòu)體在合約中存在修改函數(shù):

image.png

所以射窒,如果owner調(diào)用了此函數(shù)藏杖,那么會(huì)不會(huì)發(fā)起漏洞從而將競(jìng)猜值惡意修改呢?

我們更換地址為owner脉顿,并且調(diào)用此函數(shù)蝌麸。

image.png

我們驚奇的發(fā)現(xiàn),果然此時(shí)的競(jìng)猜值從1變成了7 艾疟。

而我們合約中的判斷條件是luckyNumberOfAddress(msg.sender) == LuckyNumber来吩。而我們函數(shù)中luckyNumberOfAddress(msg.sender)只能是0或者1兩種可能。這里的LuckNumber是7蔽莱,也就是說(shuō)無(wú)論我們?nèi)绾胃?jìng)猜弟疆,永遠(yuǎn)都不會(huì)成功。

image.png

三盗冷、賭博怠苔?莊家永遠(yuǎn)更勝一籌

在看完上述的高級(jí)蜜罐后,我們來(lái)看一下常規(guī)的蜜罐合約仪糖。

pragma solidity ^0.4.19;

// 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;
    }
    Game[] public gamesPlayed;

    function CryptoRoulette() public {
        ownerAddr = msg.sender;
        shuffle();
    }

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

    function() public payable { }
}

為什么說(shuō)蜜罐的owner更勝一籌呢柑司?我們?cè)陂喿x了合約的所有函數(shù)內(nèi)容后就知道,在合約中的shuffle()函數(shù)%20锅劝,也就意味著它最后的范圍是0~19攒驰,而用戶能夠傳入的數(shù)是多少呢?在play()函數(shù)中故爵,用戶需要傳入一個(gè)number讼育,而其規(guī)定值<=10。

image.png

其概率值相對(duì)來(lái)說(shuō)還是極低的稠集。并且在一天之后奶段,倘若用戶還未猜對(duì)那么owner便可以調(diào)用kill()函數(shù)進(jìn)行自殺操作。將余額轉(zhuǎn)入到自己的賬戶中剥纷。

四痹籍、參考鏈接

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市悠垛,隨后出現(xiàn)的幾起案子线定,更是在濱河造成了極大的恐慌,老刑警劉巖确买,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件斤讥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡湾趾,警方通過(guò)查閱死者的電腦和手機(jī)芭商,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)派草,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人铛楣,你說(shuō)我怎么就攤上這事近迁。” “怎么了簸州?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵鉴竭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我岸浑,道長(zhǎng)拓瞪,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任助琐,我火速辦了婚禮祭埂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘兵钮。我一直安慰自己蛆橡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布掘譬。 她就那樣靜靜地躺著泰演,像睡著了一般。 火紅的嫁衣襯著肌膚如雪葱轩。 梳的紋絲不亂的頭發(fā)上睦焕,一...
    開(kāi)封第一講書(shū)人閱讀 51,727評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音靴拱,去河邊找鬼垃喊。 笑死,一個(gè)胖子當(dāng)著我的面吹牛袜炕,可吹牛的內(nèi)容都是我干的本谜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼偎窘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼乌助!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起陌知,我...
    開(kāi)封第一講書(shū)人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤他托,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后仆葡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體赏参,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了登刺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嗡呼,死狀恐怖纸俭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情南窗,我是刑警寧澤揍很,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站万伤,受9級(jí)特大地震影響窒悔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜敌买,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一简珠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虹钮,春花似錦聋庵、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至春畔,卻和暖如春脱货,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背律姨。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工振峻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人择份。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓铺韧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親缓淹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哈打,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容