数字经济CTF-RISE区块链题目详解

一、前言

时隔这么多天终于有时间把当时数字经济第二道区块链题目拿来复现。感觉第二题更偏向逻辑方面的漏洞,说白了就是来考察做题人对合约的逆向能力。总结来说,以太坊的漏洞相对于其他类型问题来说还算是非常少的,所以逆合约是一个非常重要的手段,只要能完美的逆出来合约,剩下的就相对容易许多。

image.png

比赛包括两道题目,这里我们分析一下第二道题目,第一题我们见https://xz.aliyun.com/t/6602

二、题目描述

如上图所述,拿到题目我们只能看到常规操作,即给了合约地址与发送flag的函数。为了加大难度,作者并没有给合约的源码信息,所以我们只能用最笨但是最有效的方法去逆合约。

读者可以访问这个网站来查询逆向合约信息:https://ethervm.io/decompile/ropsten/0xc9B91F149d3699474a0E680D55da62FBD3a51485

我们这里拿到了合约的函数信息,现在我们查看具体的函数代码,并尝试逆出来合约函数的具体含义。

image.png

这里我们放出关键代码:

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

上文为核心关键函数的具体代码,我们为了分析题目需要具体的看如何达到满足flag调用函数的要求的。

 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。其中关键点为if (storage[keccak256(memory[0x00:0x40])] <= 0x0f4240) { revert(memory[0x00:0x00]); }。我们发现要想调用该函数的最关键部分为满足memory[3]这个位置的书>0x0f4240,而0x0f4240为十进制的1000000。

即我们获得了我们的目标,即令我们的合约token>1000000即可。

三、解题步骤

我们对每个函数进行详细的分析。

首先我们来看:

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

该函数需要满足用户的memory[3]的token>0,之后会对传入的参数arg0进行判定,如果该参数!=storage[1]的数,则进入,此时会赋予storage[3]为0,并将storage[2]为1 。

否则的话,storage[3]+=var0*storage[2](这里var0位传入的以太币数量)

我们下面看另一个函数:

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

该函数同样需要满足用户余额有钱(storage[3]>0),之后如果传入参数0,则storage[0]赋值为msg.sender()、storage[1]= storage[3](将用户token赋值给storage[1]);

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

下面我们来看空投函数。一般空投函数都是用来给用户送钱的。

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

该函数要求用户的storage[4]不等于0,而这里的storage[4]应该就是记录该用户是否已经调用过空投函数(毕竟用户不能一直调用,否则不是薅羊毛了吗hhh)。

然而往下看我们会发现,调用了该函数后系统似乎并没有对storage[4]初始化,而是用storage[3]覆盖了storage[4],并且将storage[3]++。

这里其实有一个点可以利用,如果它没有对storage[4]进行操作,那么storage[4]就永远为0,此时该函数可以一直被调用,从而调用100000次令storage[3]=100000,从而获得flag。不过这个方法太笨重了,非常不切实际,所以我们还是正常去做。时间花费过多,难度很大。

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

该函数判断storage[5]是否为msg.sender,并将storage[1]任意赋值。

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

同上函数,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函数传入arg0,这里arg0应该是一个地址,然后就可以返回该地址对应的storage[4]的值。

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

而balance返回对应地址的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函数令storage[3]+value,即给合约的token充钱。

那么我们怎么利用上述的函数来使得我们的合约token>1000000呢?

我们注意到里面唯一能大量修改代币的函数为func_0293中的else函数。如下图所示。

image.png

我们在这里给一个解决方案供读者参考。

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

下面我们来走一遍相关函数,并查看相关storage的数据变化情况。

首先初始化堆栈情况,如下图所示:

首先调用deposit(),传入1 ether(1000000000000000000):

image.png

之后调用 func_03B2(0),传入参数0:

函数要求token>0,我们满足,于是进入函数。

arg0=0所以进入第一个条件,最终得到:

image.png

之后为func_08C6(1000000)。

满足条件,进入函数,得到:

image.png

调用deposit() 传入value=2 ether:

image.png

最后调用:func_0293(1)

storage[0x01]=1,arg0参数=1,传入value=2,所以进行下面的条件语句:

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

即我们得到storage[3]=2000002>1000000。满足题目条件,此时可以调用flag函数获取flag了。

为了验证自己是否真正调用获取flag函数,我们可以到event事件中查看是否调用成功:

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

image.png
image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容