全國大學(xué)生信息安全競賽區(qū)塊鏈題目分析

一、前言

前幾天全國大學(xué)生信息安全競賽初賽如期進行,在這次比賽中也看到了區(qū)塊鏈題目的身影。所以我將題目拿來進行分析,并為后續(xù)的比賽賽題提供一些分析思路。

由于本次比賽我并沒有參加,所以我并沒有Flag等相關(guān)信息,但是我拿到了比賽中的相關(guān)文件以及合約地址并在此基礎(chǔ)上進行的詳細分析,希望能幫助到進行研究的同學(xué)。

二、題目分析

拿到題目后,我們只得到了兩個內(nèi)容,一個是合約的地址,一個是broken.so。

pragma solidity ^0.4.24;

contract DaysBank {
    mapping(address => uint) public balanceOf;
    mapping(address => uint) public gift;
    address owner;
        
    constructor()public{
        owner = msg.sender;
    }
    
    event SendFlag(uint256 flagnum, string b64email);
    function payforflag(string b64email) public {
        require(balanceOf[msg.sender] >= 10000);
        emit SendFlag(1,b64email);
    }

首先我們看這個合約文件。合約開始定義了兩個mapping變量——balanceOf 與gift,之后為構(gòu)造函數(shù),以及發(fā)送flag的事件。當(dāng)我們調(diào)用payforflag函數(shù)并傳入使用base64加密的郵件地址之后,需要滿足當(dāng)前賬戶的余額比10000多。

由這第一手信息我們可以進行一些簡單的猜想。這道題目需要領(lǐng)自己的余額大于10000,只有這樣才能購買flag。這也是很常見的題目類型。而這個題目十分設(shè)計的還是十分巧妙的,我們接著向下看。

根據(jù)上面的合約代碼,我們并不能得到更多的有用信息。然而此時我們就需要利用合約地址來進一步分析。

此處合約地址為:0x455541c3e9179a6cd8C418142855d894e11A288c

我們訪問公鏈信息看看是否能夠訪問到有價值的信息:

https://ropsten.etherscan.io/address/0x455541c3e9179a6cd8c418142855d894e11a288c#code

image.png

發(fā)現(xiàn)出題人并沒有公開源代碼,只有ABI碼,此時我們只能根據(jù)此來進行合約逆向來尋找更有用的解題思路。

https://ethervm.io/decompile#func_profit

在此網(wǎng)站中進行逆向分析后,我們得到如下代碼:

image.png
contract Contract {
    function main() {
        memory[0x40:0x60] = 0x80;
    
        if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }
    
        var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;
    
        if (var0 == 0x652e9d91) {
            // Dispatch table entry for 0x652e9d91 (unknown)
            var var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x009c;
            func_01DC();
            stop();
        } else if (var0 == 0x66d16cc3) {
            // Dispatch table entry for profit()
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x009c;
            profit();
            stop();
        } else if (var0 == 0x6bc344bc) {
            // Dispatch table entry for 0x6bc344bc (unknown)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var temp0 = memory[0x40:0x60];
            var temp1 = msg.data[0x04:0x24];
            var temp2 = msg.data[temp1 + 0x04:temp1 + 0x04 + 0x20];
            memory[0x40:0x60] = temp0 + (temp2 + 0x1f) / 0x20 * 0x20 + 0x20;
            memory[temp0:temp0 + 0x20] = temp2;
            var1 = 0x009c;
            memory[temp0 + 0x20:temp0 + 0x20 + temp2] = msg.data[temp1 + 0x24:temp1 + 0x24 + temp2];
            var var2 = temp0;
            func_0278(var2);
            stop();
        } else if (var0 == 0x70a08231) {
            // Dispatch table entry for balanceOf(address)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x013a;
            var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
            var2 = balanceOf(var2);
        
        label_013A:
            var temp3 = memory[0x40:0x60];
            memory[temp3:temp3 + 0x20] = var2;
            var temp4 = memory[0x40:0x60];
            return memory[temp4:temp4 + temp3 - temp4 + 0x20];
        } else if (var0 == 0x7ce7c990) {
            // Dispatch table entry for transfer2(address,uint256)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x009c;
            var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
            var var3 = msg.data[0x24:0x44];
            transfer2(var2, var3);
            stop();
        } else if (var0 == 0xa9059cbb) {
            // Dispatch table entry for transfer(address,uint256)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x009c;
            var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
            var3 = msg.data[0x24:0x44];
            transfer(var2, var3);
            stop();
        } else if (var0 == 0xcbfc4bce) {
            // Dispatch table entry for 0xcbfc4bce (unknown)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x013a;
            var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
            var2 = func_0417(var2);
            goto label_013A;
        } else { revert(memory[0x00:0x00]); }
    }
    //0x66d16cc3函數(shù)   空投函數(shù)??
    function func_01DC() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    // 如果gift已經(jīng)存在,revert
        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] + 0x01;
        memory[0x20:0x40] = 0x01;
        storage[keccak256(memory[0x00:0x40])] = 0x01;
    }
    

    // 利潤函數(shù): 
    function profit() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    
        if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + 0x01;
        memory[0x20:0x40] = 0x01;
        storage[keccak256(memory[0x00:0x40])] = 0x02;
    }
    
    function func_0278(var arg0) {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (0x2710 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        var var0 = 0xb1bc9a9c599feac73a94c3ba415fa0b75cbe44496bfda818a9b4a689efb7adba;
        var var1 = 0x01;
        var temp0 = arg0;
        var var2 = temp0;
        var temp1 = memory[0x40:0x60];
        var var3 = temp1;
        memory[var3:var3 + 0x20] = var1;
        var temp2 = var3 + 0x20;
        var var4 = temp2;
        var temp3 = var4 + 0x20;
        memory[var4:var4 + 0x20] = temp3 - var3;
        memory[temp3:temp3 + 0x20] = memory[var2:var2 + 0x20];
        var var5 = temp3 + 0x20;
        var var7 = memory[var2:var2 + 0x20];
        var var6 = var2 + 0x20;
        var var8 = var7;
        var var9 = var5;
        var var10 = var6;
        var var11 = 0x00;
    
        if (var11 >= var8) {
        label_02FD:
            var temp4 = var7;
            var5 = temp4 + var5;
            var6 = temp4 & 0x1f;
        
            if (!var6) {
                var temp5 = memory[0x40:0x60];
                log(memory[temp5:temp5 + var5 - temp5], [stack[-7]]);
                return;
            } else {
                var temp6 = var6;
                var temp7 = var5 - temp6;
                memory[temp7:temp7 + 0x20] = ~(0x0100 ** (0x20 - temp6) - 0x01) & memory[temp7:temp7 + 0x20];
                var temp8 = memory[0x40:0x60];
                log(memory[temp8:temp8 + (temp7 + 0x20) - temp8], [stack[-7]]);
                return;
            }
        } else {
        label_02EE:
            var temp9 = var11;
            memory[temp9 + var9:temp9 + var9 + 0x20] = memory[temp9 + var10:temp9 + var10 + 0x20];
            var11 = temp9 + 0x20;
        
            if (var11 >= var8) { goto label_02FD; }
            else { goto label_02EE; }
        }
    }
    
    function balanceOf(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x00;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }
    
    function transfer2(var arg0, var arg1) {
        if (arg1 <= 0x02) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (0x02 >= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (storage[keccak256(memory[0x00:0x40])] - arg1 <= 0x00) { 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];
    }
    
    function transfer(var arg0, var arg1) {
        if (arg1 <= 0x01) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    
        if (0x01 >= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
    // 如果arg1大于余額,revert
        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;
    // 地址arg0的余額增加arg1的個數(shù)
        memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
        var temp2 = keccak256(memory[0x00:0x40]);
        storage[temp2] = temp1 + storage[temp2];
    }
    
    function func_0417(var arg0) returns (var arg0) {
        memory[0x20:0x40] = 0x01;
        memory[0x00:0x20] = arg0;
        return storage[keccak256(memory[0x00:0x40])];
    }
}

之后我們針對此逆向后的代碼進行分析。

我們經(jīng)過分析發(fā)現(xiàn)了如下的public函數(shù):

image.png

很明顯這是代幣合約,并且可以進行轉(zhuǎn)賬。而此代碼中擁有兩個轉(zhuǎn)賬函數(shù)。并且可以查看余額。

我們具體根據(jù)代碼對函數(shù)詳細分析:

image.png

首先我們分析編號為0x652e9d91func_01DC()函數(shù)。

首先合約將內(nèi)存切換到0x01位置,此處為:mapping(address => uint) public gift;

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;

即合約首先要判斷該用戶的gift是否為0,若不為0則revert(也就是說這個函數(shù)要保證只能領(lǐng)取一次)。

之后內(nèi)存切換到mapping(address => uint) public balanceOf;。

對此變量進行操作,也就是將用戶的余額值+1。并將gift值加一。

profit()函數(shù)的分析如下:

image.png

根據(jù)函數(shù)的名稱我們也知道,此函數(shù)為利潤函數(shù),其目的也很明顯,根據(jù)我們的代幣背景知識,我們猜測這個函數(shù)是用來贈送代幣的。

函數(shù)要求balanceOf與gift必須==1,不然就會revert。當(dāng)調(diào)用此函數(shù)時,當(dāng)滿足上述條件后就會給用戶的余額+1,令用戶余額為2 。

balanceOf()函數(shù)

這個函數(shù)很簡單,就是返回用戶的余額情況。

下面我們來看兩個關(guān)鍵的轉(zhuǎn)賬函數(shù):

transfer()

image.png

函數(shù)同樣比較簡單。

首先需要判斷用戶的余額是否小于1 。之后判斷轉(zhuǎn)賬的金額(arg1)是否大于余額,如果用戶余額不足以進行轉(zhuǎn)賬,那么就會revert。

之后將當(dāng)前用戶的賬面上減掉arg1代幣數(shù)量,將收款方arg0的賬戶上增加arg1代幣數(shù)量。

我們可以適當(dāng)還原此函數(shù):

function transfer(var arg0, var arg1){

   if(arg1<=1) revert();
   if(balance(msg.sender)<=1) revert();
   if(balance(msg.sender)<arg1) revert();
   balance(msg.sender) = balance(msg.sender) - arg1;
   balance(arg0) = balance(arg0) + arg1; 

}

此時我們看transfer2()函數(shù)。

image.png

在看到這個函數(shù)前我就疑問為何一個代幣中有兩個轉(zhuǎn)賬函數(shù)?后來在分析了源碼后我了解到第二個轉(zhuǎn)賬函數(shù)中就存在漏洞。具體如下:

開始時函數(shù)判斷arg1需要>2,即轉(zhuǎn)賬數(shù)量要大于2. 。

之后判斷用戶余額需要大于等于2.

滿足條件后需要令(余額 - arg1)大于零。即其本意是要用戶余額大于轉(zhuǎn)賬金額。

之后進行轉(zhuǎn)賬后的余額更新。

我們分析該代碼后將合約具體代碼進行還原:

    function transfer2(var arg0, var arg1){
        require(arg1>2);
        require(balance(msg.sender) >= 2);
        require(balance(msg.sender) - arg1 >= 0);
        balance(msg.sender) = balance(msg.sender) - arg1;
        balance(arg0) = balance(arg0) + arg1;

    }

不知用戶是否發(fā)現(xiàn),我們就看到了漏洞點了,這是一個典型的溢出漏洞。

image.png

根據(jù)作者給出的代碼,我們發(fā)現(xiàn)其具體余額是使用uint定義的,由于uint的位數(shù)是有限的,并且其不支持負(fù)數(shù)。所以當(dāng)其負(fù)數(shù)溢出時就會變成一個很大的正數(shù)。

而根據(jù)我們的transfer2函數(shù)內(nèi)容,我們知道:require(balance(msg.sender) - arg1 >= 0);。此句進行判斷的時候是將用戶余額減去一個arg1來判斷是否大于0的。而如果arg1設(shè)置一個比較大的數(shù),那么balance(msg.sender) - arg1就會溢出為一個非常大的數(shù),此時就成功繞過了檢測并且轉(zhuǎn)賬大量的代幣。

所以我們可以利用此處的整數(shù)溢出來進行題目求解,然而在分析的過程中我又發(fā)現(xiàn)了另一個解法。

如果做題人沒有發(fā)現(xiàn)此處的漏洞點,我們可以利用常規(guī)做法來進行求解。

image.png

根據(jù)給出的flag函數(shù)我們知道,我們只需要余額>10000即可,那么我們可以發(fā)現(xiàn),我們的profit函數(shù)可以給我們不斷的新增錢。

根據(jù)我們的分析,我們需要令合約余額==1并且gitf==1,此時即可調(diào)用profit()來將余額++,調(diào)用后余額為2,gift為1 。這時候?qū)⒂囝~轉(zhuǎn)給第二個賬戶,余額就又變成1了,就又可以調(diào)用profit()函數(shù)。這樣不斷給第二個用戶轉(zhuǎn)賬,轉(zhuǎn)賬10000次即可。(這里肯定是要用腳本去寫,手動轉(zhuǎn)賬比較傻emmmm)

三、漏洞利用技巧

此處我們介紹漏洞利用的技巧。

首先我們需要擁有兩個錢包地址(Addr1 Addr2)。

  • 此時我們令Addr1調(diào)用func_01DC()函數(shù)領(lǐng)取1個代幣以及1個gift。

  • 之后我們調(diào)用profit領(lǐng)取一個代幣。此時余額為2,gift為1 。

由于transfer2需要余額大于2才能調(diào)用,所以我們首先令A(yù)ddr2同樣執(zhí)行上面的兩步。此時兩個錢包均有余額為2 。

  • 這時候Adde1調(diào)用transfer給Addr2轉(zhuǎn)賬兩個代幣,此時Addr余額為0,Addr2為4 。

之后Addr2就可以調(diào)用transfer2給Adde1轉(zhuǎn)賬一個非常大的金額。達到溢出效果。此時Addr1與Addr2均擁有了大量的代幣(Addr2為溢出得到,Addr1為轉(zhuǎn)賬得到)。任意地址均可以調(diào)用flag函數(shù)。

具體的交易日志如下:

image.png
image.png
image.png
image.png

此時flag就被調(diào)用發(fā)送到用戶賬戶上了。

四、總結(jié)

本次題目非常巧妙,如果后面的同學(xué)想直接查看交易日志是非常難通過一個賬戶來進行跟蹤的。并且本題目沒有公布合約,所以考驗?zāi)嫦蚰芰?。但是只要逆出來后就是一道比較簡單的題目,沒有完全逆出來的同學(xué)也可以使用常規(guī)做法進行不斷轉(zhuǎn)賬來使余額滿足要求。希望本文對大家之后的研究有所幫助。歡迎討論。

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

首發(fā):[https://xz.aliyun.com/t/4982](https://xz.aliyun.com/t/4982)

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

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

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