全国大学生信息安全竞赛区块链题目分析

一、前言

前几天全国大学生信息安全竞赛初赛如期进行,在这次比赛中也看到了区块链题目的身影。所以我将题目拿来进行分析,并为后续的比赛赛题提供一些分析思路。

由于本次比赛我并没有参加,所以我并没有Flag等相关信息,但是我拿到了比赛中的相关文件以及合约地址并在此基础上进行的详细分析,希望能帮助到进行研究的同学。

二、题目分析

拿到题目后,我们只得到了两个内容,一个是合约的地址,一个是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,之后为构造函数,以及发送flag的事件。当我们调用payforflag函数并传入使用base64加密的邮件地址之后,需要满足当前账户的余额比10000多。

由这第一手信息我们可以进行一些简单的猜想。这道题目需要领自己的余额大于10000,只有这样才能购买flag。这也是很常见的题目类型。而这个题目十分设计的还是十分巧妙的,我们接着向下看。

根据上面的合约代码,我们并不能得到更多的有用信息。然而此时我们就需要利用合约地址来进一步分析。

此处合约地址为:0x455541c3e9179a6cd8C418142855d894e11A288c

我们访问公链信息看看是否能够访问到有价值的信息:

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

image.png

发现出题人并没有公开源代码,只有ABI码,此时我们只能根据此来进行合约逆向来寻找更有用的解题思路。

https://ethervm.io/decompile#func_profit

在此网站中进行逆向分析后,我们得到如下代码:

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函数   空投函数??
    function func_01DC() {
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    // 如果gift已经存在,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;
    }
    

    // 利润函数: 
    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的个数
        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])];
    }
}

之后我们针对此逆向后的代码进行分析。

我们经过分析发现了如下的public函数:

image.png

很明显这是代币合约,并且可以进行转账。而此代码中拥有两个转账函数。并且可以查看余额。

我们具体根据代码对函数详细分析:

image.png

首先我们分析编号为0x652e9d91func_01DC()函数。

首先合约将内存切换到0x01位置,此处为:mapping(address => uint) public gift;

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

即合约首先要判断该用户的gift是否为0,若不为0则revert(也就是说这个函数要保证只能领取一次)。

之后内存切换到mapping(address => uint) public balanceOf;

对此变量进行操作,也就是将用户的余额值+1。并将gift值加一。

profit()函数的分析如下:

image.png

根据函数的名称我们也知道,此函数为利润函数,其目的也很明显,根据我们的代币背景知识,我们猜测这个函数是用来赠送代币的。

函数要求balanceOf与gift必须==1,不然就会revert。当调用此函数时,当满足上述条件后就会给用户的余额+1,令用户余额为2 。

balanceOf()函数

这个函数很简单,就是返回用户的余额情况。

下面我们来看两个关键的转账函数:

transfer()

image.png

函数同样比较简单。

首先需要判断用户的余额是否小于1 。之后判断转账的金额(arg1)是否大于余额,如果用户余额不足以进行转账,那么就会revert。

之后将当前用户的账面上减掉arg1代币数量,将收款方arg0的账户上增加arg1代币数量。

我们可以适当还原此函数:

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()函数。

image.png

在看到这个函数前我就疑问为何一个代币中有两个转账函数?后来在分析了源码后我了解到第二个转账函数中就存在漏洞。具体如下:

开始时函数判断arg1需要>2,即转账数量要大于2. 。

之后判断用户余额需要大于等于2.

满足条件后需要令(余额 - arg1)大于零。即其本意是要用户余额大于转账金额。

之后进行转账后的余额更新。

我们分析该代码后将合约具体代码进行还原:

    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;

    }

不知用户是否发现,我们就看到了漏洞点了,这是一个典型的溢出漏洞。

image.png

根据作者给出的代码,我们发现其具体余额是使用uint定义的,由于uint的位数是有限的,并且其不支持负数。所以当其负数溢出时就会变成一个很大的正数。

而根据我们的transfer2函数内容,我们知道:require(balance(msg.sender) - arg1 >= 0);。此句进行判断的时候是将用户余额减去一个arg1来判断是否大于0的。而如果arg1设置一个比较大的数,那么balance(msg.sender) - arg1就会溢出为一个非常大的数,此时就成功绕过了检测并且转账大量的代币。

所以我们可以利用此处的整数溢出来进行题目求解,然而在分析的过程中我又发现了另一个解法。

如果做题人没有发现此处的漏洞点,我们可以利用常规做法来进行求解。

image.png

根据给出的flag函数我们知道,我们只需要余额>10000即可,那么我们可以发现,我们的profit函数可以给我们不断的新增钱。

根据我们的分析,我们需要令合约余额==1并且gitf==1,此时即可调用profit()来将余额++,调用后余额为2,gift为1 。这时候将余额转给第二个账户,余额就又变成1了,就又可以调用profit()函数。这样不断给第二个用户转账,转账10000次即可。(这里肯定是要用脚本去写,手动转账比较傻emmmm)

三、漏洞利用技巧

此处我们介绍漏洞利用的技巧。

首先我们需要拥有两个钱包地址(Addr1 Addr2)

  • 此时我们令Addr1调用func_01DC()函数领取1个代币以及1个gift。

  • 之后我们调用profit领取一个代币。此时余额为2,gift为1 。

由于transfer2需要余额大于2才能调用,所以我们首先令Addr2同样执行上面的两步。此时两个钱包均有余额为2 。

  • 这时候Adde1调用transfer给Addr2转账两个代币,此时Addr余额为0,Addr2为4 。

之后Addr2就可以调用transfer2给Adde1转账一个非常大的金额。达到溢出效果。此时Addr1与Addr2均拥有了大量的代币(Addr2为溢出得到,Addr1为转账得到)。任意地址均可以调用flag函数。

具体的交易日志如下:

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

此时flag就被调用发送到用户账户上了。

四、总结

本次题目非常巧妙,如果后面的同学想直接查看交易日志是非常难通过一个账户来进行跟踪的。并且本题目没有公布合约,所以考验逆向能力。但是只要逆出来后就是一道比较简单的题目,没有完全逆出来的同学也可以使用常规做法进行不断转账来使余额满足要求。希望本文对大家之后的研究有所帮助。欢迎讨论。

本稿为原创稿件,转载请标明出处。谢谢。

首发:[https://xz.aliyun.com/t/4982](https://xz.aliyun.com/t/4982)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容