數(shù)字經(jīng)濟(jì)CTF-RISE區(qū)塊鏈題目詳解

一、前言

時隔這么多天終于有時間把當(dāng)時數(shù)字經(jīng)濟(jì)第二道區(qū)塊鏈題目拿來復(fù)現(xiàn)。感覺第二題更偏向邏輯方面的漏洞,說白了就是來考察做題人對合約的逆向能力??偨Y(jié)來說,以太坊的漏洞相對于其他類型問題來說還算是非常少的,所以逆合約是一個非常重要的手段,只要能完美的逆出來合約,剩下的就相對容易許多。

image.png

比賽包括兩道題目,這里我們分析一下第二道題目,第一題我們見https://xz.aliyun.com/t/6602。

二、題目描述

如上圖所述,拿到題目我們只能看到常規(guī)操作,即給了合約地址與發(fā)送flag的函數(shù)。為了加大難度,作者并沒有給合約的源碼信息,所以我們只能用最笨但是最有效的方法去逆合約。

讀者可以訪問這個網(wǎng)站來查詢逆向合約信息:https://ethervm.io/decompile/ropsten/0xc9B91F149d3699474a0E680D55da62FBD3a51485

我們這里拿到了合約的函數(shù)信息,現(xiàn)在我們查看具體的函數(shù)代碼,并嘗試逆出來合約函數(shù)的具體含義。

image.png

這里我們放出關(guān)鍵代碼:

    function func_0293(var arg0) {
        var var0 = 0x00;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
    
        if (storage[keccak256(memory[0x00:0x40])] <= var0) { revert(memory[0x00:0x00]); }
    
        var var1 = 0x0de0b6b3a7640000;
        var var2 = msg.value;
    
        if (!var1) { assert(); }
    
        var0 = var2 / var1;
    
        if (arg0 != storage[0x01]) {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            storage[keccak256(memory[0x00:0x40])] = 0x00;
            storage[0x02] = 0x01;
            return;
        } else {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            var temp0 = keccak256(memory[0x00:0x40]);
            storage[temp0] = storage[temp0] + var0 * storage[0x02];
            storage[0x02] = 0x01;
            return;
        }
    }
    
    function func_03B2(var arg0) {
        var var0 = 0x00;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
    
        if (storage[keccak256(memory[0x00:0x40])] <= var0) { revert(memory[0x00:0x00]); }
    
        if (arg0 & 0xffffffffffffffffffffffffffffffffffffffff == 0x00) {
            var temp0 = var0;
            var temp1 = temp0;
            storage[temp1] = msg.sender | (storage[temp1] & ~0xffffffffffffffffffffffffffffffffffffffff);
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            storage[temp0 + 0x01] = storage[keccak256(memory[0x00:0x40])];
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            storage[keccak256(memory[0x00:0x40])] = 0x00;
            return;
        } else {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            var temp2 = storage[keccak256(memory[0x00:0x40])];
            memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
            memory[0x20:0x40] = 0x03;
            storage[keccak256(memory[0x00:0x40])] = temp2;
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            storage[keccak256(memory[0x00:0x40])] = 0x00;
            return;
        }
    }
    
    function airdrop() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x04;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x00) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x04;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
    }
    
    function payforflag(var arg0) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
    
        if (storage[keccak256(memory[0x00:0x40])] <= 0x0f4240) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
        storage[keccak256(memory[0x00:0x40])] = 0x00;
        storage[0x02] = 0x01;
        var temp0 = address(address(this)).balance;
        var temp1 = memory[0x40:0x60];
        var temp2;
        temp2, memory[temp1:temp1 + 0x00] = address(storage[0x05] & 0xffffffffffffffffffffffffffffffffffffffff).call.gas(!temp0 * 0x08fc).value(temp0)(memory[temp1:temp1 + memory[0x40:0x60] - temp1]);
        var var0 = !temp2;
    
        if (!var0) {
            var0 = 0x7c2413bb49085e565f72ec50a1fb0460b69cf327e0b0d882980385b356239ea5;
            var temp3 = arg0;
            var var1 = temp3;
            var temp4 = memory[0x40:0x60];
            var var2 = temp4;
            var var3 = var2;
            var temp5 = var3 + 0x20;
            memory[var3:var3 + 0x20] = temp5 - var3;
            memory[temp5:temp5 + 0x20] = memory[var1:var1 + 0x20];
            var var4 = temp5 + 0x20;
            var var6 = memory[var1:var1 + 0x20];
            var var5 = var1 + 0x20;
            var var7 = var6;
            var var8 = var4;
            var var9 = var5;
            var var10 = 0x00;
        
            if (var10 >= var7) {
            label_0823:
                var temp6 = var6;
                var4 = temp6 + var4;
                var5 = temp6 & 0x1f;
            
                if (!var5) {
                    var temp7 = memory[0x40:0x60];
                    log(memory[temp7:temp7 + var4 - temp7], [stack[-6]]);
                    return;
                } else {
                    var temp8 = var5;
                    var temp9 = var4 - temp8;
                    memory[temp9:temp9 + 0x20] = ~(0x0100 ** (0x20 - temp8) - 0x01) & memory[temp9:temp9 + 0x20];
                    var temp10 = memory[0x40:0x60];
                    log(memory[temp10:temp10 + (temp9 + 0x20) - temp10], [stack[-6]]);
                    return;
                }
            } else {
            label_0811:
                var temp11 = var10;
                memory[var8 + temp11:var8 + temp11 + 0x20] = memory[var9 + temp11:var9 + temp11 + 0x20];
                var10 = temp11 + 0x20;
            
                if (var10 >= var7) { goto label_0823; }
                else { goto label_0811; }
            }
        } else {
            var temp12 = returndata.length;
            memory[0x00:0x00 + temp12] = returndata[0x00:0x00 + temp12];
            revert(memory[0x00:0x00 + returndata.length]);
        }
    }
    
    function func_0860(var arg0) {
        if (msg.sender != storage[0x05] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
    
        storage[0x01] = arg0;
    }
    
    function func_08C6(var arg0) {
        if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
    
        storage[0x02] = arg0;
    }
    
    function gift(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x04;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }
    
    function deposit() {
        var var0 = 0x00;
        var var1 = 0x0de0b6b3a7640000;
        var var2 = msg.value;
    
        if (!var1) { assert(); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + var2 / var1;
    }
    
    function balance(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x03;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }

上文為核心關(guān)鍵函數(shù)的具體代碼,我們?yōu)榱朔治鲱}目需要具體的看如何達(dá)到滿足flag調(diào)用函數(shù)的要求的。

 function payforflag(var arg0) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
    
        if (storage[keccak256(memory[0x00:0x40])] <= 0x0f4240) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
        storage[keccak256(memory[0x00:0x40])] = 0x00;
        storage[0x02] = 0x01;
        var temp0 = address(address(this)).balance;
        var temp1 = memory[0x40:0x60];

上述代碼作用為獲取flag。其中關(guān)鍵點(diǎn)為if (storage[keccak256(memory[0x00:0x40])] <= 0x0f4240) { revert(memory[0x00:0x00]); }。我們發(fā)現(xiàn)要想調(diào)用該函數(shù)的最關(guān)鍵部分為滿足memory[3]這個位置的書>0x0f4240,而0x0f4240為十進(jìn)制的1000000。

即我們獲得了我們的目標(biāo),即令我們的合約token>1000000即可。

三、解題步驟

我們對每個函數(shù)進(jìn)行詳細(xì)的分析。

首先我們來看:

    function func_0293(var arg0) {
        var var0 = 0x00;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
    
        if (storage[keccak256(memory[0x00:0x40])] <= var0) { revert(memory[0x00:0x00]); }
    
        var var1 = 0x0de0b6b3a7640000;
        var var2 = msg.value;
    
        if (!var1) { assert(); }
    
        var0 = var2 / var1;
    
        if (arg0 != storage[0x01]) {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            storage[keccak256(memory[0x00:0x40])] = 0x00;
            storage[0x02] = 0x01;
            return;
        } else {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            var temp0 = keccak256(memory[0x00:0x40]);
            storage[temp0] = storage[temp0] + var0 * storage[0x02];
            storage[0x02] = 0x01;
            return;
        }
    }

該函數(shù)需要滿足用戶的memory[3]的token>0,之后會對傳入的參數(shù)arg0進(jìn)行判定,如果該參數(shù)!=storage[1]的數(shù),則進(jìn)入,此時會賦予storage[3]為0,并將storage[2]為1 。

否則的話,storage[3]+=var0*storage[2](這里var0位傳入的以太幣數(shù)量)

我們下面看另一個函數(shù):

    function func_03B2(var arg0) {
        var var0 = 0x00;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
    
        if (storage[keccak256(memory[0x00:0x40])] <= var0) { revert(memory[0x00:0x00]); }
    
        if (arg0 & 0xffffffffffffffffffffffffffffffffffffffff == 0x00) {
            var temp0 = var0;
            var temp1 = temp0;
            storage[temp1] = msg.sender | (storage[temp1] & ~0xffffffffffffffffffffffffffffffffffffffff);
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            storage[temp0 + 0x01] = storage[keccak256(memory[0x00:0x40])];
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            storage[keccak256(memory[0x00:0x40])] = 0x00;
            return;
        } else {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            var temp2 = storage[keccak256(memory[0x00:0x40])];
            memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
            memory[0x20:0x40] = 0x03;
            storage[keccak256(memory[0x00:0x40])] = temp2;
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x03;
            storage[keccak256(memory[0x00:0x40])] = 0x00;
            return;
        }
    }

該函數(shù)同樣需要滿足用戶余額有錢(storage[3]>0),之后如果傳入?yún)?shù)0,則storage[0]賦值為msg.sender()、storage[1]= storage[3](將用戶token賦值給storage[1]);

或者使得storage[arg0] = storage[3],并還原storage[3]=0。

下面我們來看空投函數(shù)。一般空投函數(shù)都是用來給用戶送錢的。

    function airdrop() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x04;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x00) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x04;
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
    }

該函數(shù)要求用戶的storage[4]不等于0,而這里的storage[4]應(yīng)該就是記錄該用戶是否已經(jīng)調(diào)用過空投函數(shù)(畢竟用戶不能一直調(diào)用,否則不是薅羊毛了嗎hhh)。

然而往下看我們會發(fā)現(xiàn),調(diào)用了該函數(shù)后系統(tǒng)似乎并沒有對storage[4]初始化,而是用storage[3]覆蓋了storage[4],并且將storage[3]++。

這里其實有一個點(diǎn)可以利用,如果它沒有對storage[4]進(jìn)行操作,那么storage[4]就永遠(yuǎn)為0,此時該函數(shù)可以一直被調(diào)用,從而調(diào)用100000次令storage[3]=100000,從而獲得flag。不過這個方法太笨重了,非常不切實際,所以我們還是正常去做。時間花費(fèi)過多,難度很大。

    function func_0860(var arg0) {
        if (msg.sender != storage[0x05] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
        storage[0x01] = arg0;
    }

該函數(shù)判斷storage[5]是否為msg.sender,并將storage[1]任意賦值。

    function func_08C6(var arg0) {
        if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
    
        storage[0x02] = arg0;
    }

同上函數(shù),storage[0]需要==msg.sender,之后storage[2]賦值為任意值。

    function gift(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x04;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }

gift函數(shù)傳入arg0,這里arg0應(yīng)該是一個地址,然后就可以返回該地址對應(yīng)的storage[4]的值。

    function balance(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x03;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }

而balance返回對應(yīng)地址的storage[3]的值 。

    function deposit() {
        var var0 = 0x00;
        var var1 = 0x0de0b6b3a7640000;
        var var2 = msg.value;
    
        if (!var1) { assert(); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x03;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + var2 / var1;
    }

deposit函數(shù)令storage[3]+value,即給合約的token充錢。

那么我們怎么利用上述的函數(shù)來使得我們的合約token>1000000呢?

我們注意到里面唯一能大量修改代幣的函數(shù)為func_0293中的else函數(shù)。如下圖所示。

image.png

我們在這里給一個解決方案供讀者參考。

  • deposit() 傳入value=1 ether
  • func_03B2(0)
  • func_08C6(1000000)
  • deposit() 傳入value=2 ether
  • func_0293(1)
  • payforflag(b64email)

下面我們來走一遍相關(guān)函數(shù),并查看相關(guān)storage的數(shù)據(jù)變化情況。

首先初始化堆棧情況,如下圖所示:

首先調(diào)用deposit(),傳入1 ether(1000000000000000000):

image.png

之后調(diào)用 func_03B2(0),傳入?yún)?shù)0:

函數(shù)要求token>0,我們滿足,于是進(jìn)入函數(shù)。

arg0=0所以進(jìn)入第一個條件,最終得到:

image.png

之后為func_08C6(1000000)。

滿足條件,進(jìn)入函數(shù),得到:

image.png

調(diào)用deposit() 傳入value=2 ether:

image.png

最后調(diào)用:func_0293(1)

storage[0x01]=1,arg0參數(shù)=1,傳入value=2,所以進(jìn)行下面的條件語句:

所以storage[temp0] = storage[temp0] + var0 * storage[0x02]=storage[3] = storage[3] + 2 * 1000000;

即我們得到storage[3]=2000002>1000000。滿足題目條件,此時可以調(diào)用flag函數(shù)獲取flag了。

為了驗證自己是否真正調(diào)用獲取flag函數(shù),我們可以到event事件中查看是否調(diào)用成功:

https://ropsten.etherscan.io/address/0xc9b91f149d3699474a0e680d55da62fbd3a51485#events

image.png
image.png
?著作權(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)容